-
Notifications
You must be signed in to change notification settings - Fork 125
/
Copy pathscript.ts
222 lines (178 loc) · 6.91 KB
/
script.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
import ScriptContext from "./script_context.js";
import cls from "./cls.js";
import log from "./log.js";
import becca from "../becca/becca.js";
import type BNote from "../becca/entities/bnote.js";
import type { ApiParams } from "./backend_script_api_interface.js";
interface Bundle {
note?: BNote;
noteId?: string;
script: string;
html: string;
allNotes?: BNote[];
allNoteIds?: string[];
}
type ScriptParams = any[];
function executeNote(note: BNote, apiParams: ApiParams) {
if (!note.isJavaScript() || note.getScriptEnv() !== "backend" || !note.isContentAvailable()) {
log.info(`Cannot execute note ${note.noteId} "${note.title}", note must be of type "Code: JS backend"`);
return;
}
const bundle = getScriptBundle(note, true, "backend");
if (!bundle) {
throw new Error("Unable to determine bundle.");
}
return executeBundle(bundle, apiParams);
}
function executeNoteNoException(note: BNote, apiParams: ApiParams) {
try {
executeNote(note, apiParams);
} catch (e) {
// just swallow, exception is logged already in executeNote
}
}
function executeBundle(bundle: Bundle, apiParams: ApiParams = {}) {
if (!apiParams.startNote) {
// this is the default case, the only exception is when we want to preserve frontend startNote
apiParams.startNote = bundle.note;
}
const originalComponentId = cls.get("componentId");
cls.set("componentId", "script");
cls.set("bundleNoteId", bundle.note?.noteId);
// last \r\n is necessary if the script contains line comment on its last line
const script = `function() {\r
${bundle.script}\r
}`;
const ctx = new ScriptContext(bundle.allNotes || [], apiParams);
try {
return execute(ctx, script);
} catch (e: any) {
log.error(`Execution of script "${bundle.note?.title}" (${bundle.note?.noteId}) failed with error: ${e.message}`);
throw e;
} finally {
cls.set("componentId", originalComponentId);
}
}
/**
* THIS METHOD CAN'T BE ASYNC, OTHERWISE TRANSACTION WRAPPER WON'T BE EFFECTIVE AND WE WILL BE LOSING THE
* ENTITY CHANGES IN CLS.
*
* This method preserves frontend startNode - that's why we start execution from currentNote and override
* bundle's startNote.
*/
function executeScript(script: string, params: ScriptParams, startNoteId: string, currentNoteId: string, originEntityName: string, originEntityId: string) {
const startNote = becca.getNote(startNoteId);
const currentNote = becca.getNote(currentNoteId);
const originEntity = becca.getEntity(originEntityName, originEntityId);
if (!currentNote) {
throw new Error("Cannot find note.");
}
// we're just executing an excerpt of the original frontend script in the backend context, so we must
// override normal note's content, and it's mime type / script environment
const overrideContent = `return (${script}\r\n)(${getParams(params)})`;
const bundle = getScriptBundle(currentNote, true, "backend", [], overrideContent);
if (!bundle) {
throw new Error("Unable to determine script bundle.");
}
return executeBundle(bundle, { startNote, originEntity });
}
function execute(ctx: ScriptContext, script: string) {
return function () {
return eval(`const apiContext = this;\r\n(${script}\r\n)()`);
}.call(ctx);
}
function getParams(params?: ScriptParams) {
if (!params) {
return params;
}
return params
.map((p) => {
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
return p.substr(13);
} else {
return JSON.stringify(p);
}
})
.join(",");
}
function getScriptBundleForFrontend(note: BNote, script?: string, params?: ScriptParams) {
let overrideContent = null;
if (script) {
overrideContent = `return (${script}\r\n)(${getParams(params)})`;
}
const bundle = getScriptBundle(note, true, "frontend", [], overrideContent);
if (!bundle) {
return;
}
// for frontend, we return just noteIds because frontend needs to use its own entity instances
bundle.noteId = bundle.note?.noteId;
delete bundle.note;
bundle.allNoteIds = bundle.allNotes?.map((note) => note.noteId);
delete bundle.allNotes;
return bundle;
}
function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: string | null = null, includedNoteIds: string[] = [], overrideContent: string | null = null): Bundle | undefined {
if (!note.isContentAvailable()) {
return;
}
if (!note.isJavaScript() && !note.isHtml()) {
return;
}
if (!root && note.hasOwnedLabel("disableInclusion")) {
return;
}
if (note.type !== "file" && !root && scriptEnv !== note.getScriptEnv()) {
return;
}
const bundle: Bundle = {
note: note,
script: "",
html: "",
allNotes: [note]
};
if (includedNoteIds.includes(note.noteId)) {
return bundle;
}
includedNoteIds.push(note.noteId);
const modules = [];
for (const child of note.getChildNotes()) {
const childBundle = getScriptBundle(child, false, scriptEnv, includedNoteIds);
if (childBundle) {
if (childBundle.note) {
modules.push(childBundle.note);
}
bundle.script += childBundle.script;
bundle.html += childBundle.html;
if (bundle.allNotes && childBundle.allNotes) {
bundle.allNotes = bundle.allNotes.concat(childBundle.allNotes);
}
}
}
const moduleNoteIds = modules.map((mod) => mod.noteId);
// only frontend scripts are async. Backend cannot be async because of transaction management.
const isFrontend = scriptEnv === "frontend";
if (note.isJavaScript()) {
bundle.script += `
apiContext.modules['${note.noteId}'] = { exports: {} };
${root ? "return " : ""}${isFrontend ? "await" : ""} ((${isFrontend ? "async" : ""} function(exports, module, require, api${modules.length > 0 ? ", " : ""}${modules.map((child) => sanitizeVariableName(child.title)).join(", ")}) {
try {
${overrideContent || note.getContent()};
} catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); }
for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];
return module.exports;
}).call({}, {}, apiContext.modules['${note.noteId}'], apiContext.require(${JSON.stringify(moduleNoteIds)}), apiContext.apis['${note.noteId}']${modules.length > 0 ? ", " : ""}${modules.map((mod) => `apiContext.modules['${mod.noteId}'].exports`).join(", ")}));
`;
} else if (note.isHtml()) {
bundle.html += note.getContent();
}
return bundle;
}
function sanitizeVariableName(str: string) {
return str.replace(/[^a-z0-9_]/gim, "");
}
export default {
executeNote,
executeNoteNoException,
executeScript,
getScriptBundleForFrontend
};