sommark 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -4
- package/core/evaluator.js +408 -132
- package/core/helpers/config-loader.js +8 -8
- package/core/helpers/lib.js +1 -4
- package/core/helpers/preprocessor.js +23 -6
- package/core/helpers/url.js +12 -0
- package/core/modules.js +16 -14
- package/core/transpiler.js +23 -19
- package/helpers/fetch-fs.js +37 -0
- package/helpers/spinner.js +7 -1
- package/helpers/virtual-fs.js +29 -0
- package/index.browser.js +87 -0
- package/index.js +23 -419
- package/index.shared.js +443 -0
- package/package.json +8 -4
package/core/evaluator.js
CHANGED
|
@@ -1,12 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import path from "node:path";
|
|
1
|
+
import { getQuickJS } from "quickjs-emscripten";
|
|
2
|
+
import path from "pathe";
|
|
4
3
|
import * as acorn from "acorn";
|
|
5
4
|
import SomMark, { registerHostCompile, registerHostSettings } from "./helpers/lib.js";
|
|
5
|
+
import { formatMessage } from "./errors.js";
|
|
6
6
|
|
|
7
7
|
// Global tracker to ensure deep recursive Smark compilation never exceeds safe boundaries
|
|
8
8
|
let globalCompilationDepth = 0;
|
|
9
9
|
|
|
10
|
+
async function prefetchImports(code, baseDir, fsImpl) {
|
|
11
|
+
if (!fsImpl?.readFile) return;
|
|
12
|
+
let ast;
|
|
13
|
+
try { ast = acorn.parse(code, { ecmaVersion: "latest", sourceType: "module" }); }
|
|
14
|
+
catch { return; }
|
|
15
|
+
|
|
16
|
+
for (const node of ast.body) {
|
|
17
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
18
|
+
const importPath = node.source.value;
|
|
19
|
+
const resolved = /^https?:\/\//.test(baseDir)
|
|
20
|
+
? new URL(importPath, baseDir.endsWith("/") ? baseDir : baseDir + "/").href
|
|
21
|
+
: path.resolve(baseDir, importPath);
|
|
22
|
+
|
|
23
|
+
if (fsImpl.existsSync(resolved)) continue; // already cached
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const content = await fsImpl.readFile(resolved);
|
|
27
|
+
if (resolved.endsWith(".js")) {
|
|
28
|
+
const nextBase = /^https?:\/\//.test(resolved)
|
|
29
|
+
? resolved.slice(0, resolved.lastIndexOf("/") + 1)
|
|
30
|
+
: path.dirname(resolved);
|
|
31
|
+
await prefetchImports(content, nextBase, fsImpl);
|
|
32
|
+
}
|
|
33
|
+
} catch { /* let QuickJS surface the error */ }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let compilerClass = null;
|
|
38
|
+
|
|
39
|
+
export function setCompilerClass(cls) {
|
|
40
|
+
compilerClass = cls;
|
|
41
|
+
}
|
|
42
|
+
|
|
10
43
|
// Pure, top-level stateless adapters to avoid circular references and closures over EvaluatorState
|
|
11
44
|
const customFetchAdapter = async (input, init, security = {}) => {
|
|
12
45
|
const allowFetch = security?.allowFetch !== false;
|
|
@@ -104,17 +137,17 @@ const customCompileAdapter = async (src, options, parentSecurity = {}) => {
|
|
|
104
137
|
|
|
105
138
|
globalCompilationDepth++;
|
|
106
139
|
try {
|
|
107
|
-
// Securely isolate and deep-clone options to strip parent VM proxies
|
|
108
140
|
const cleanOptions = JSON.parse(JSON.stringify(options || {}));
|
|
109
|
-
|
|
141
|
+
if (!compilerClass) {
|
|
142
|
+
throw new Error("Compiler class is not registered in the evaluator.");
|
|
143
|
+
}
|
|
110
144
|
const compilerOptions = {
|
|
145
|
+
...cleanOptions,
|
|
111
146
|
src,
|
|
112
147
|
format: cleanOptions.format || "html",
|
|
113
|
-
variables: cleanOptions.variables || {},
|
|
114
|
-
formatOption: cleanOptions.formatOption || {},
|
|
115
148
|
security: parentSecurity
|
|
116
149
|
};
|
|
117
|
-
const sm = new
|
|
150
|
+
const sm = new compilerClass(compilerOptions);
|
|
118
151
|
return await sm.transpile();
|
|
119
152
|
} finally {
|
|
120
153
|
globalCompilationDepth--;
|
|
@@ -124,25 +157,124 @@ const customCompileAdapter = async (src, options, parentSecurity = {}) => {
|
|
|
124
157
|
// Register statically once at module loading
|
|
125
158
|
registerHostCompile(customCompileAdapter);
|
|
126
159
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
160
|
+
let defaultFs = null;
|
|
161
|
+
let quickJSInstance = null;
|
|
162
|
+
async function getQuickJSModule() {
|
|
163
|
+
if (!quickJSInstance) {
|
|
164
|
+
quickJSInstance = await getQuickJS();
|
|
165
|
+
}
|
|
166
|
+
return quickJSInstance;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function objectToHandle(context, obj) {
|
|
170
|
+
if (obj === undefined) {
|
|
171
|
+
return context.undefined;
|
|
172
|
+
}
|
|
173
|
+
const jsonStr = JSON.stringify(obj);
|
|
174
|
+
const stringHandle = context.newString(jsonStr);
|
|
175
|
+
const jsonHandle = context.getProp(context.global, "JSON");
|
|
176
|
+
const parseHandle = context.getProp(jsonHandle, "parse");
|
|
177
|
+
const result = context.callFunction(parseHandle, jsonHandle, stringHandle);
|
|
178
|
+
stringHandle.dispose();
|
|
179
|
+
parseHandle.dispose();
|
|
180
|
+
jsonHandle.dispose();
|
|
181
|
+
return result.unwrap();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function expose(context, vars, pendingDeferreds) {
|
|
185
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
186
|
+
let handle;
|
|
187
|
+
if (typeof value === "function") {
|
|
188
|
+
handle = context.newFunction(key, (...args) => {
|
|
189
|
+
try {
|
|
190
|
+
const jsArgs = args.map(arg => context.dump(arg));
|
|
191
|
+
const res = value(...jsArgs);
|
|
192
|
+
if (res instanceof Promise || (res && typeof res === "object" && typeof res.then === "function")) {
|
|
193
|
+
const deferred = context.newPromise();
|
|
194
|
+
if (pendingDeferreds) {
|
|
195
|
+
pendingDeferreds.add(deferred);
|
|
196
|
+
}
|
|
197
|
+
res.then(
|
|
198
|
+
(resolvedVal) => {
|
|
199
|
+
try {
|
|
200
|
+
if (!context.alive) return;
|
|
201
|
+
if (resolvedVal === undefined) {
|
|
202
|
+
deferred.resolve();
|
|
203
|
+
} else {
|
|
204
|
+
const valHandle = objectToHandle(context, resolvedVal);
|
|
205
|
+
deferred.resolve(valHandle);
|
|
206
|
+
valHandle.dispose();
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
if (context.alive) {
|
|
210
|
+
const errHandle = context.newError(e.message || String(e));
|
|
211
|
+
deferred.reject(errHandle);
|
|
212
|
+
errHandle.dispose();
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
if (pendingDeferreds) {
|
|
216
|
+
pendingDeferreds.delete(deferred);
|
|
217
|
+
}
|
|
218
|
+
if (context.alive) {
|
|
219
|
+
deferred.dispose();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
(rejectedErr) => {
|
|
224
|
+
try {
|
|
225
|
+
if (!context.alive) return;
|
|
226
|
+
const errHandle = context.newError(rejectedErr.message || String(rejectedErr));
|
|
227
|
+
deferred.reject(errHandle);
|
|
228
|
+
errHandle.dispose();
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// ignore
|
|
231
|
+
} finally {
|
|
232
|
+
if (pendingDeferreds) {
|
|
233
|
+
pendingDeferreds.delete(deferred);
|
|
234
|
+
}
|
|
235
|
+
if (context.alive) {
|
|
236
|
+
deferred.dispose();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
return deferred.handle.dup();
|
|
242
|
+
} else if (res === undefined) {
|
|
243
|
+
return;
|
|
244
|
+
} else {
|
|
245
|
+
return objectToHandle(context, res);
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
throw context.newError(err.message || String(err));
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
handle = objectToHandle(context, value);
|
|
253
|
+
}
|
|
254
|
+
context.setProp(context.global, key, handle);
|
|
255
|
+
handle.dispose();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
132
259
|
class EvaluatorState {
|
|
133
260
|
constructor() {
|
|
134
261
|
this.runtime = null;
|
|
135
|
-
this.
|
|
262
|
+
this.context = null;
|
|
263
|
+
this.baseDir = "/";
|
|
136
264
|
this.scopes = [{}];
|
|
137
265
|
this.dynamicTagsStack = [new Map()];
|
|
138
266
|
this.deadline = 0;
|
|
267
|
+
this.pendingDeferreds = new Set();
|
|
139
268
|
}
|
|
140
269
|
|
|
141
|
-
/**
|
|
142
|
-
* Initializes the QuickJS VM.
|
|
143
|
-
*/
|
|
144
270
|
async init(baseDir = null, security = {}, settings = {}, mapperFile = null) {
|
|
145
|
-
if (baseDir)
|
|
271
|
+
if (baseDir) {
|
|
272
|
+
this.baseDir = baseDir;
|
|
273
|
+
} else if (settings?.instance?.cwd) {
|
|
274
|
+
this.baseDir = settings.instance.cwd;
|
|
275
|
+
} else {
|
|
276
|
+
this.baseDir = "/";
|
|
277
|
+
}
|
|
146
278
|
this.scopes = [{}];
|
|
147
279
|
this.dynamicTagsStack = [new Map()];
|
|
148
280
|
this.security = security;
|
|
@@ -150,38 +282,39 @@ class EvaluatorState {
|
|
|
150
282
|
this.mapperFile = mapperFile;
|
|
151
283
|
registerHostSettings(settings);
|
|
152
284
|
|
|
153
|
-
|
|
154
|
-
|
|
285
|
+
this.nodeFs = defaultFs;
|
|
286
|
+
|
|
287
|
+
if (this.context) {
|
|
288
|
+
this.expose({
|
|
155
289
|
__allowRaw: this.security.allowRaw !== false
|
|
156
290
|
});
|
|
157
291
|
return;
|
|
158
292
|
}
|
|
159
293
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
this.
|
|
163
|
-
allowFetch: true,
|
|
164
|
-
fetchAdapter: async (input, init) => {
|
|
165
|
-
return await customFetchAdapter(input, init, this.security);
|
|
166
|
-
},
|
|
167
|
-
allowFs: true,
|
|
168
|
-
env: {}
|
|
169
|
-
});
|
|
294
|
+
const QuickJS = await getQuickJSModule();
|
|
295
|
+
this.runtime = QuickJS.newRuntime();
|
|
296
|
+
this.context = this.runtime.newContext();
|
|
170
297
|
|
|
171
298
|
this.deadline = 0;
|
|
172
|
-
|
|
173
|
-
this.
|
|
174
|
-
|
|
175
|
-
});
|
|
176
|
-
}
|
|
299
|
+
this.runtime.setInterruptHandler(() => {
|
|
300
|
+
return this.deadline > 0 && Date.now() > this.deadline;
|
|
301
|
+
});
|
|
177
302
|
|
|
178
|
-
|
|
179
|
-
this.runtime.vm.expose({
|
|
303
|
+
this.expose({
|
|
180
304
|
__hostSomMarkVersion: SomMark.version,
|
|
181
|
-
__hostSomMarkSettings: () =>
|
|
305
|
+
__hostSomMarkSettings: () => {
|
|
306
|
+
const clean = { ...SomMark.settings };
|
|
307
|
+
delete clean.instance;
|
|
308
|
+
delete clean.fs;
|
|
309
|
+
return JSON.stringify(clean);
|
|
310
|
+
},
|
|
182
311
|
__hostCompile: async (src, options) => {
|
|
183
312
|
return await customCompileAdapter(src, options, this.security);
|
|
184
313
|
},
|
|
314
|
+
__hostFetch: async (input, initStr) => {
|
|
315
|
+
const init = initStr ? JSON.parse(initStr) : undefined;
|
|
316
|
+
return await customFetchAdapter(input, init, this.security);
|
|
317
|
+
},
|
|
185
318
|
__hostRegisterDynamicTag: (id, options) => {
|
|
186
319
|
this.registerDynamicTag(id, options);
|
|
187
320
|
},
|
|
@@ -207,7 +340,8 @@ class EvaluatorState {
|
|
|
207
340
|
__allowRaw: this.security.allowRaw !== false
|
|
208
341
|
});
|
|
209
342
|
|
|
210
|
-
|
|
343
|
+
// Setup standard library and namespace
|
|
344
|
+
const setupRes = this.context.evalCode(`
|
|
211
345
|
const __nativeFetch = globalThis.fetch;
|
|
212
346
|
class TagBuilder {
|
|
213
347
|
constructor(tagName) {
|
|
@@ -321,7 +455,7 @@ class EvaluatorState {
|
|
|
321
455
|
return Object.freeze(parsed);
|
|
322
456
|
},
|
|
323
457
|
fetch: async (input, init) => {
|
|
324
|
-
const plainRes = await
|
|
458
|
+
const plainRes = await __hostFetch(input.toString(), init ? JSON.stringify(init) : "");
|
|
325
459
|
return {
|
|
326
460
|
status: plainRes.status,
|
|
327
461
|
ok: plainRes.ok,
|
|
@@ -381,41 +515,51 @@ class EvaluatorState {
|
|
|
381
515
|
}
|
|
382
516
|
};
|
|
383
517
|
|
|
384
|
-
// Deep freeze the SomMark standard library to make it completely immutable
|
|
385
518
|
Object.freeze(SomMark);
|
|
386
519
|
|
|
387
|
-
// Establish the global SomMark constant (non-writable, non-configurable)
|
|
388
520
|
Object.defineProperty(globalThis, "SomMark", {
|
|
389
521
|
value: SomMark,
|
|
390
522
|
writable: false,
|
|
391
523
|
configurable: false
|
|
392
524
|
});
|
|
393
525
|
|
|
394
|
-
// Prevent direct/un-namespaced global fetch usage to enforce standard library architecture
|
|
395
526
|
delete globalThis.fetch;
|
|
396
527
|
delete globalThis.process;
|
|
397
528
|
`);
|
|
398
529
|
|
|
399
|
-
|
|
400
|
-
|
|
530
|
+
if (setupRes.error) {
|
|
531
|
+
const err = this.context.dump(setupRes.error);
|
|
532
|
+
setupRes.error.dispose();
|
|
533
|
+
throw new Error("VM initialization failed: " + JSON.stringify(err));
|
|
534
|
+
}
|
|
535
|
+
setupRes.value.dispose();
|
|
536
|
+
|
|
537
|
+
// Configure module loader using virtual FS implementation
|
|
538
|
+
this.runtime.setModuleLoader((moduleName) => {
|
|
401
539
|
try {
|
|
402
540
|
const isRaw = moduleName.endsWith("?raw");
|
|
403
541
|
const cleanModuleName = isRaw ? moduleName.slice(0, -4) : moduleName;
|
|
404
|
-
const resolvedPath =
|
|
405
|
-
|
|
406
|
-
|
|
542
|
+
const resolvedPath = /^https?:\/\//.test(this.baseDir)
|
|
543
|
+
? new URL(cleanModuleName, this.baseDir.endsWith("/") ? this.baseDir : this.baseDir + "/").href
|
|
544
|
+
: path.resolve(this.baseDir, cleanModuleName);
|
|
545
|
+
|
|
546
|
+
const fsImpl = this.settings?.fs || this.settings?.instance?.fs || this.nodeFs;
|
|
547
|
+
if (!fsImpl) {
|
|
548
|
+
throw new Error("No filesystem implementation available.");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (fsImpl.existsSync(resolvedPath)) {
|
|
552
|
+
let source = fsImpl.readFileSync(resolvedPath, "utf8");
|
|
407
553
|
|
|
408
554
|
if (isRaw) {
|
|
409
555
|
const escapedSource = source.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
|
|
410
556
|
return `export default \`${escapedSource}\`;`;
|
|
411
557
|
}
|
|
412
558
|
|
|
413
|
-
// Support JSON files
|
|
414
559
|
if (resolvedPath.endsWith(".json")) {
|
|
415
560
|
source = `export default ${source};`;
|
|
416
561
|
}
|
|
417
562
|
|
|
418
|
-
// Support Smark files
|
|
419
563
|
if (resolvedPath.endsWith(".smark")) {
|
|
420
564
|
const escapedSource = source.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
|
|
421
565
|
source = `
|
|
@@ -425,7 +569,7 @@ class EvaluatorState {
|
|
|
425
569
|
`;
|
|
426
570
|
}
|
|
427
571
|
|
|
428
|
-
return source;
|
|
572
|
+
return source;
|
|
429
573
|
}
|
|
430
574
|
throw new Error(`Module not found: ${moduleName}`);
|
|
431
575
|
} catch (err) {
|
|
@@ -434,37 +578,37 @@ class EvaluatorState {
|
|
|
434
578
|
});
|
|
435
579
|
}
|
|
436
580
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
581
|
+
expose(vars) {
|
|
582
|
+
if (!this.context) return;
|
|
583
|
+
expose(this.context, vars, this.pendingDeferreds);
|
|
584
|
+
}
|
|
585
|
+
|
|
440
586
|
pushScope() {
|
|
441
587
|
this.scopes.push({});
|
|
442
588
|
this.dynamicTagsStack.push(new Map());
|
|
443
589
|
}
|
|
444
590
|
|
|
445
|
-
/**
|
|
446
|
-
* Pops the current block scope level, cleaning up VM globals and restoring parent scope variables.
|
|
447
|
-
*/
|
|
448
591
|
async popScope() {
|
|
449
592
|
if (this.scopes.length > 1) {
|
|
450
593
|
const popped = this.scopes.pop();
|
|
451
594
|
this.dynamicTagsStack.pop();
|
|
452
595
|
const keysToDelete = Object.keys(popped);
|
|
453
|
-
if (keysToDelete.length > 0 && this.
|
|
596
|
+
if (keysToDelete.length > 0 && this.context) {
|
|
454
597
|
try {
|
|
455
598
|
const deleteCode = keysToDelete.map(k => `delete globalThis['${k}'];`).join(" ");
|
|
456
|
-
|
|
599
|
+
const deleteRes = this.context.evalCode(deleteCode, "cleanup.js");
|
|
600
|
+
if (deleteRes.value) deleteRes.value.dispose();
|
|
601
|
+
if (deleteRes.error) deleteRes.error.dispose();
|
|
457
602
|
} catch (e) {
|
|
458
603
|
// ignore
|
|
459
604
|
}
|
|
460
605
|
}
|
|
461
|
-
|
|
462
|
-
if (this.runtime) {
|
|
606
|
+
if (this.context) {
|
|
463
607
|
const merged = {};
|
|
464
608
|
for (const scope of this.scopes) {
|
|
465
609
|
Object.assign(merged, scope);
|
|
466
610
|
}
|
|
467
|
-
this.
|
|
611
|
+
this.expose(merged);
|
|
468
612
|
}
|
|
469
613
|
}
|
|
470
614
|
}
|
|
@@ -490,8 +634,8 @@ class EvaluatorState {
|
|
|
490
634
|
}
|
|
491
635
|
|
|
492
636
|
async executeDynamicTag(id, payload) {
|
|
493
|
-
if (!this.
|
|
494
|
-
this.
|
|
637
|
+
if (!this.context) throw new Error("EvaluatorState not initialized");
|
|
638
|
+
this.expose({
|
|
495
639
|
__activeTagPayload: () => JSON.stringify(payload)
|
|
496
640
|
});
|
|
497
641
|
const code = `
|
|
@@ -509,18 +653,43 @@ class EvaluatorState {
|
|
|
509
653
|
return res;
|
|
510
654
|
})()
|
|
511
655
|
`;
|
|
512
|
-
|
|
513
|
-
if (
|
|
514
|
-
|
|
656
|
+
const evalRes = this.context.evalCode(code, "render_tag.js");
|
|
657
|
+
if (evalRes.error) {
|
|
658
|
+
const err = this.context.dump(evalRes.error);
|
|
659
|
+
evalRes.error.dispose();
|
|
660
|
+
throw err;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
let resultHandle = evalRes.unwrap();
|
|
664
|
+
const state = this.context.getPromiseState(resultHandle);
|
|
665
|
+
if (state && state.type === "pending") {
|
|
666
|
+
while (true) {
|
|
667
|
+
this.runtime.executePendingJobs();
|
|
668
|
+
const curState = this.context.getPromiseState(resultHandle);
|
|
669
|
+
if (curState.type !== "pending") {
|
|
670
|
+
if (curState.type === "fulfilled") {
|
|
671
|
+
resultHandle.dispose();
|
|
672
|
+
resultHandle = curState.value;
|
|
673
|
+
} else {
|
|
674
|
+
const errHandle = curState.error;
|
|
675
|
+
const err = this.context.dump(errHandle);
|
|
676
|
+
errHandle.dispose();
|
|
677
|
+
resultHandle.dispose();
|
|
678
|
+
throw err;
|
|
679
|
+
}
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
683
|
+
}
|
|
515
684
|
}
|
|
685
|
+
|
|
686
|
+
const result = this.context.dump(resultHandle);
|
|
687
|
+
resultHandle.dispose();
|
|
516
688
|
return result;
|
|
517
689
|
}
|
|
518
690
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
*/
|
|
522
|
-
async _syncScopes() {
|
|
523
|
-
if (!this.runtime) return;
|
|
691
|
+
_syncScopes() {
|
|
692
|
+
if (!this.context) return;
|
|
524
693
|
const allKeysSet = new Set();
|
|
525
694
|
for (const scope of this.scopes) {
|
|
526
695
|
for (const key of Object.keys(scope)) {
|
|
@@ -531,17 +700,23 @@ class EvaluatorState {
|
|
|
531
700
|
if (allKeys.length > 0) {
|
|
532
701
|
try {
|
|
533
702
|
const getValuesCode = `export default { ${allKeys.map(k => `${JSON.stringify(k)}: globalThis['${k}']`).join(", ")} };`;
|
|
534
|
-
const valuesRes =
|
|
535
|
-
if (valuesRes
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
703
|
+
const valuesRes = this.context.evalCode(getValuesCode, "sync.js", { type: 'module' });
|
|
704
|
+
if (valuesRes.value) {
|
|
705
|
+
const syncedValuesObj = this.context.dump(valuesRes.value);
|
|
706
|
+
valuesRes.value.dispose();
|
|
707
|
+
if (syncedValuesObj && typeof syncedValuesObj === 'object' && 'default' in syncedValuesObj) {
|
|
708
|
+
const syncedValues = syncedValuesObj.default;
|
|
709
|
+
for (const [key, val] of Object.entries(syncedValues)) {
|
|
710
|
+
for (let s = this.scopes.length - 1; s >= 0; s--) {
|
|
711
|
+
if (key in this.scopes[s]) {
|
|
712
|
+
this.scopes[s][key] = val;
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
542
715
|
}
|
|
543
716
|
}
|
|
544
717
|
}
|
|
718
|
+
} else if (valuesRes.error) {
|
|
719
|
+
valuesRes.error.dispose();
|
|
545
720
|
}
|
|
546
721
|
} catch (err) {
|
|
547
722
|
// ignore
|
|
@@ -549,36 +724,28 @@ class EvaluatorState {
|
|
|
549
724
|
}
|
|
550
725
|
}
|
|
551
726
|
|
|
552
|
-
/**
|
|
553
|
-
* Injects variables safely into the sandbox.
|
|
554
|
-
*/
|
|
555
727
|
inject(vars) {
|
|
556
|
-
if (!this.
|
|
728
|
+
if (!this.context) return;
|
|
557
729
|
const currentScope = this.scopes[this.scopes.length - 1];
|
|
558
730
|
Object.assign(currentScope, vars);
|
|
559
|
-
this.
|
|
731
|
+
this.expose(vars);
|
|
560
732
|
}
|
|
561
733
|
|
|
562
|
-
/**
|
|
563
|
-
* Executes code asynchronously and returns resolved result.
|
|
564
|
-
*/
|
|
565
734
|
async execute(code) {
|
|
566
|
-
if (!this.
|
|
735
|
+
if (!this.context) throw new Error("Evaluator not initialized");
|
|
567
736
|
|
|
568
737
|
const timeout = this.security?.timeout ?? 5000;
|
|
569
|
-
this.deadline = Date.now() + timeout;
|
|
738
|
+
this.deadline = Date.now() + timeout;
|
|
570
739
|
|
|
571
|
-
// Keep QuickJS event loop alive in the background during execution
|
|
572
740
|
const interval = setInterval(() => {
|
|
573
741
|
try {
|
|
574
|
-
this.runtime.
|
|
742
|
+
this.runtime.executePendingJobs();
|
|
575
743
|
} catch (err) {
|
|
576
744
|
// ignore
|
|
577
745
|
}
|
|
578
746
|
}, 1);
|
|
579
747
|
|
|
580
748
|
try {
|
|
581
|
-
// Detect top-level declarations for Auto-Export
|
|
582
749
|
let autoExportedNames = [];
|
|
583
750
|
let hasExplicitExports = false;
|
|
584
751
|
try {
|
|
@@ -605,7 +772,7 @@ class EvaluatorState {
|
|
|
605
772
|
}
|
|
606
773
|
}
|
|
607
774
|
} catch (e) {
|
|
608
|
-
//
|
|
775
|
+
// Ignore parsing errors for simple expression fragments
|
|
609
776
|
}
|
|
610
777
|
|
|
611
778
|
const hasImportExport = hasExplicitExports || /\bimport\b/.test(code);
|
|
@@ -613,7 +780,6 @@ class EvaluatorState {
|
|
|
613
780
|
|
|
614
781
|
let finalCode = code;
|
|
615
782
|
|
|
616
|
-
// Rewrite the last expression statement to be export default so we automatically return its value
|
|
617
783
|
try {
|
|
618
784
|
const ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true });
|
|
619
785
|
const lastNode = ast.body[ast.body.length - 1];
|
|
@@ -639,29 +805,113 @@ class EvaluatorState {
|
|
|
639
805
|
|
|
640
806
|
const isModule = hasImportExport || hasAwait || autoExportedNames.length > 0 || finalCode.includes("export default");
|
|
641
807
|
|
|
808
|
+
const fsImpl = this.settings?.fs || this.settings?.instance?.fs || this.nodeFs;
|
|
809
|
+
if (isModule) await prefetchImports(finalCode, this.baseDir, fsImpl);
|
|
810
|
+
|
|
642
811
|
let result;
|
|
643
812
|
if (isModule) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
813
|
+
const evalRes = this.context.evalCode(finalCode, "main.js", { type: 'module' });
|
|
814
|
+
if (evalRes.error) {
|
|
815
|
+
const err = this.context.dump(evalRes.error);
|
|
816
|
+
evalRes.error.dispose();
|
|
817
|
+
throw err;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
let resultHandle = evalRes.unwrap();
|
|
821
|
+
const state = this.context.getPromiseState(resultHandle);
|
|
822
|
+
if (state && state.type === "pending") {
|
|
823
|
+
while (true) {
|
|
824
|
+
this.runtime.executePendingJobs();
|
|
825
|
+
const curState = this.context.getPromiseState(resultHandle);
|
|
826
|
+
if (curState.type !== "pending") {
|
|
827
|
+
if (curState.type === "fulfilled") {
|
|
828
|
+
resultHandle.dispose();
|
|
829
|
+
resultHandle = curState.value;
|
|
830
|
+
} else {
|
|
831
|
+
const errHandle = curState.error;
|
|
832
|
+
const err = this.context.dump(errHandle);
|
|
833
|
+
errHandle.dispose();
|
|
834
|
+
resultHandle.dispose();
|
|
835
|
+
throw err;
|
|
836
|
+
}
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
let defaultHandle = this.context.getProp(resultHandle, "default");
|
|
844
|
+
let resolvedDefaultHandle = defaultHandle;
|
|
845
|
+
let isPromise = false;
|
|
846
|
+
|
|
847
|
+
const defaultState = this.context.getPromiseState(defaultHandle);
|
|
848
|
+
if (defaultState && !defaultState.notAPromise) {
|
|
849
|
+
isPromise = true;
|
|
850
|
+
if (defaultState.type === "pending") {
|
|
851
|
+
while (true) {
|
|
852
|
+
this.runtime.executePendingJobs();
|
|
853
|
+
const curState = this.context.getPromiseState(defaultHandle);
|
|
854
|
+
if (curState.type !== "pending") {
|
|
855
|
+
if (curState.type === "fulfilled") {
|
|
856
|
+
resolvedDefaultHandle = curState.value;
|
|
857
|
+
} else {
|
|
858
|
+
const errHandle = curState.error;
|
|
859
|
+
const err = this.context.dump(errHandle);
|
|
860
|
+
errHandle.dispose();
|
|
861
|
+
defaultHandle.dispose();
|
|
862
|
+
resultHandle.dispose();
|
|
863
|
+
throw err;
|
|
864
|
+
}
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
868
|
+
}
|
|
869
|
+
} else if (defaultState.type === "fulfilled") {
|
|
870
|
+
resolvedDefaultHandle = defaultState.value;
|
|
871
|
+
} else if (defaultState.type === "rejected") {
|
|
872
|
+
const errHandle = defaultState.error;
|
|
873
|
+
const err = this.context.dump(errHandle);
|
|
874
|
+
errHandle.dispose();
|
|
875
|
+
defaultHandle.dispose();
|
|
876
|
+
resultHandle.dispose();
|
|
877
|
+
throw err;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const defaultValue = this.context.dump(resolvedDefaultHandle);
|
|
882
|
+
|
|
883
|
+
if (isPromise) {
|
|
884
|
+
resolvedDefaultHandle.dispose();
|
|
885
|
+
}
|
|
886
|
+
defaultHandle.dispose();
|
|
887
|
+
|
|
888
|
+
const res = this.context.dump(resultHandle);
|
|
651
889
|
|
|
652
|
-
|
|
890
|
+
this.context.setProp(this.context.global, "__tempModule", resultHandle);
|
|
891
|
+
const copyRes = this.context.evalCode(`
|
|
892
|
+
for (const key of Object.keys(__tempModule)) {
|
|
893
|
+
if (key !== "default") {
|
|
894
|
+
globalThis[key] = __tempModule[key];
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
delete globalThis.__tempModule;
|
|
898
|
+
`);
|
|
899
|
+
if (copyRes.error) {
|
|
900
|
+
copyRes.error.dispose();
|
|
901
|
+
} else {
|
|
902
|
+
copyRes.value.dispose();
|
|
903
|
+
}
|
|
904
|
+
resultHandle.dispose();
|
|
653
905
|
|
|
654
|
-
// Move exports directly to global scope in the VM
|
|
655
906
|
if (res && typeof res === 'object') {
|
|
656
907
|
const currentScope = this.scopes[this.scopes.length - 1];
|
|
657
908
|
for (const [key, val] of Object.entries(res)) {
|
|
658
909
|
if (key !== 'default') {
|
|
659
910
|
currentScope[key] = val;
|
|
660
|
-
this.runtime.vm.expose({ [key]: val });
|
|
661
911
|
}
|
|
662
912
|
}
|
|
663
913
|
if ('default' in res) {
|
|
664
|
-
result =
|
|
914
|
+
result = defaultValue;
|
|
665
915
|
} else {
|
|
666
916
|
result = undefined;
|
|
667
917
|
}
|
|
@@ -669,17 +919,41 @@ class EvaluatorState {
|
|
|
669
919
|
result = res;
|
|
670
920
|
}
|
|
671
921
|
} else {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
922
|
+
const evalRes = this.context.evalCode(code, "main.js");
|
|
923
|
+
if (evalRes.error) {
|
|
924
|
+
const err = this.context.dump(evalRes.error);
|
|
925
|
+
evalRes.error.dispose();
|
|
926
|
+
throw err;
|
|
927
|
+
}
|
|
928
|
+
let resultHandle = evalRes.unwrap();
|
|
929
|
+
const state = this.context.getPromiseState(resultHandle);
|
|
930
|
+
if (state && state.type === "pending") {
|
|
931
|
+
while (true) {
|
|
932
|
+
this.runtime.executePendingJobs();
|
|
933
|
+
const curState = this.context.getPromiseState(resultHandle);
|
|
934
|
+
if (curState.type !== "pending") {
|
|
935
|
+
if (curState.type === "fulfilled") {
|
|
936
|
+
resultHandle.dispose();
|
|
937
|
+
resultHandle = curState.value;
|
|
938
|
+
} else {
|
|
939
|
+
const errHandle = curState.error;
|
|
940
|
+
const err = this.context.dump(errHandle);
|
|
941
|
+
errHandle.dispose();
|
|
942
|
+
resultHandle.dispose();
|
|
943
|
+
throw err;
|
|
944
|
+
}
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
result = this.context.dump(resultHandle);
|
|
951
|
+
resultHandle.dispose();
|
|
677
952
|
}
|
|
678
953
|
|
|
679
954
|
await this._syncScopes();
|
|
680
955
|
return result;
|
|
681
956
|
} catch (error) {
|
|
682
|
-
// Try to extract line/col from stack trace
|
|
683
957
|
const stack = error.stack || "";
|
|
684
958
|
const match = stack.match(/main\.js:(\d+):(\d+)/) || stack.match(/:(\d+):(\d+)/);
|
|
685
959
|
|
|
@@ -695,44 +969,46 @@ class EvaluatorState {
|
|
|
695
969
|
}
|
|
696
970
|
}
|
|
697
971
|
|
|
698
|
-
/**
|
|
699
|
-
* Disposal.
|
|
700
|
-
*/
|
|
701
972
|
destroy() {
|
|
702
973
|
if (this.runtime) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
974
|
+
if (this.pendingDeferreds) {
|
|
975
|
+
for (const deferred of this.pendingDeferreds) {
|
|
976
|
+
try {
|
|
977
|
+
if (deferred.alive) {
|
|
978
|
+
deferred.dispose();
|
|
979
|
+
}
|
|
980
|
+
} catch (e) {}
|
|
708
981
|
}
|
|
709
|
-
|
|
982
|
+
this.pendingDeferreds.clear();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
try {
|
|
986
|
+
this.runtime.executePendingJobs();
|
|
987
|
+
} catch (e) {}
|
|
710
988
|
|
|
711
989
|
try {
|
|
990
|
+
if (this.context) {
|
|
991
|
+
this.context.dispose();
|
|
992
|
+
}
|
|
712
993
|
this.runtime.dispose();
|
|
713
994
|
} catch (e) {
|
|
714
|
-
|
|
715
|
-
console.warn("<$yellow:Warning:$> Safe context disposal warning: " + e.message);
|
|
995
|
+
console.warn(formatMessage("<$yellow:Warning:$> Safe context disposal warning: " + e.message));
|
|
716
996
|
}
|
|
717
997
|
this.runtime = null;
|
|
998
|
+
this.context = null;
|
|
718
999
|
}
|
|
719
1000
|
}
|
|
720
1001
|
}
|
|
721
1002
|
|
|
722
|
-
/**
|
|
723
|
-
* Evaluator
|
|
724
|
-
*
|
|
725
|
-
* Acts as a router/proxy singleton that routes VM calls to a stack of active isolated runtimes.
|
|
726
|
-
* This guarantees concurrent and recursive safety across all compiler runs.
|
|
727
|
-
*/
|
|
728
1003
|
class Evaluator {
|
|
729
1004
|
constructor() {
|
|
730
1005
|
this.instances = [];
|
|
731
1006
|
}
|
|
732
1007
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1008
|
+
setDefaultFs(fs) {
|
|
1009
|
+
defaultFs = fs;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
736
1012
|
get active() {
|
|
737
1013
|
if (this.instances.length === 0) {
|
|
738
1014
|
throw new Error("No active EvaluatorState instance. Did you call init()?");
|