sommark 4.0.3 → 4.1.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.
@@ -0,0 +1,785 @@
1
+ import { quickJS } from "@sebastianwessel/quickjs";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import * as acorn from "acorn";
5
+ import SomMark, { registerHostCompile, registerHostSettings } from "./helpers/lib.js";
6
+
7
+ // Global tracker to ensure deep recursive Smark compilation never exceeds safe boundaries
8
+ let globalCompilationDepth = 0;
9
+
10
+ // Pure, top-level stateless adapters to avoid circular references and closures over EvaluatorState
11
+ const customFetchAdapter = async (input, init, security = {}) => {
12
+ const allowFetch = security?.allowFetch !== false;
13
+ if (!allowFetch) {
14
+ throw new Error("Fetch Error: fetch is disabled in this environment.");
15
+ }
16
+
17
+ const url = input.toString();
18
+ try {
19
+ const parsedUrl = new URL(url);
20
+ const protocol = parsedUrl.protocol.toLowerCase();
21
+ const hostname = parsedUrl.hostname.toLowerCase();
22
+
23
+ // 1. Enforce HTTPS (HTTP Blocked by default unless allowHttp is true)
24
+ const allowHttp = security?.allowHttp === true;
25
+ if (protocol === "http:" && !allowHttp) {
26
+ throw new Error("Fetch Security Error: HTTP requests are disabled. Use HTTPS instead.");
27
+ }
28
+ if (protocol !== "http:" && protocol !== "https:") {
29
+ throw new Error(`Fetch Security Error: Unsupported protocol '${protocol}'. Only HTTP/HTTPS is allowed.`);
30
+ }
31
+
32
+ // 2. SSRF Protection: Block localhost, loopbacks, link-local, and RFC 1918 private network IP ranges
33
+ const isLocal = hostname === "localhost" ||
34
+ hostname === "127.0.0.1" ||
35
+ hostname === "0.0.0.0" ||
36
+ hostname === "[::1]" ||
37
+ hostname === "::" ||
38
+ hostname.startsWith("127.") ||
39
+ hostname.startsWith("10.") ||
40
+ hostname.startsWith("192.168.") ||
41
+ hostname.startsWith("169.254.") ||
42
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname);
43
+
44
+ if (isLocal) {
45
+ throw new Error("SSRF Protection: Requests to local or private IP addresses are forbidden.");
46
+ }
47
+
48
+ // 3. Whitelisted Origins Check
49
+ const allowedOrigins = security?.allowedOrigins;
50
+ if (allowedOrigins && allowedOrigins.length > 0) {
51
+ const origin = parsedUrl.origin.toLowerCase();
52
+ const isOriginAllowed = allowedOrigins.some(allowed => {
53
+ try {
54
+ const allowedUrl = new URL(allowed);
55
+ return origin === allowedUrl.origin.toLowerCase();
56
+ } catch {
57
+ return hostname === allowed.toLowerCase() || hostname.endsWith("." + allowed.toLowerCase());
58
+ }
59
+ });
60
+ if (!isOriginAllowed) {
61
+ throw new Error(`Fetch Security Error: Origin '${origin}' is not whitelisted.`);
62
+ }
63
+ }
64
+
65
+ // 4. Whitelisted Extensions Check
66
+ const allowedExtensions = security?.allowedExtensions;
67
+ if (allowedExtensions && allowedExtensions.length > 0) {
68
+ const ext = path.extname(parsedUrl.pathname).toLowerCase();
69
+ if (!allowedExtensions.includes(ext)) {
70
+ throw new Error(`Fetch Security Error: Extension '${ext || "(none)"}' is not whitelisted.`);
71
+ }
72
+ }
73
+ } catch (e) {
74
+ throw new Error(e.message.startsWith("Fetch Security Error:") || e.message.startsWith("SSRF Protection:")
75
+ ? e.message
76
+ : "Fetch Security Error: " + e.message);
77
+ }
78
+
79
+ const res = await fetch(url, init);
80
+ const bodyText = await res.text();
81
+
82
+ const headers = {};
83
+ res.headers.forEach((val, key) => {
84
+ headers[key.toLowerCase()] = val;
85
+ });
86
+
87
+ return {
88
+ status: res.status,
89
+ ok: res.ok,
90
+ statusText: res.statusText,
91
+ url: res.url,
92
+ type: res.type,
93
+ redirected: res.redirected,
94
+ bodyText,
95
+ headers
96
+ };
97
+ };
98
+
99
+ const customCompileAdapter = async (src, options, parentSecurity = {}) => {
100
+ const maxDepth = parentSecurity?.maxDepth ?? 5;
101
+ if (globalCompilationDepth >= maxDepth) {
102
+ throw new Error(`Recursion Guard: Maximum Smark compilation depth exceeded (limit is ${maxDepth}).`);
103
+ }
104
+
105
+ globalCompilationDepth++;
106
+ try {
107
+ // Securely isolate and deep-clone options to strip parent VM proxies
108
+ const cleanOptions = JSON.parse(JSON.stringify(options || {}));
109
+ const { default: SomMarkCompiler } = await import("../index.js");
110
+ const compilerOptions = {
111
+ src,
112
+ format: cleanOptions.format || "html",
113
+ variables: cleanOptions.variables || {},
114
+ formatOption: cleanOptions.formatOption || {},
115
+ security: parentSecurity
116
+ };
117
+ const sm = new SomMarkCompiler(compilerOptions);
118
+ return await sm.transpile();
119
+ } finally {
120
+ globalCompilationDepth--;
121
+ }
122
+ };
123
+
124
+ // Register statically once at module loading
125
+ registerHostCompile(customCompileAdapter);
126
+
127
+ /**
128
+ * EvaluatorState
129
+ *
130
+ * Houses the actual state, scopes, and QuickJS VM instance for a single transpilation lifecycle.
131
+ */
132
+ class EvaluatorState {
133
+ constructor() {
134
+ this.runtime = null;
135
+ this.baseDir = process.cwd();
136
+ this.scopes = [{}];
137
+ this.dynamicTagsStack = [new Map()];
138
+ this.deadline = 0;
139
+ }
140
+
141
+ /**
142
+ * Initializes the QuickJS VM.
143
+ */
144
+ async init(baseDir = null, security = {}, settings = {}, mapperFile = null) {
145
+ if (baseDir) this.baseDir = baseDir;
146
+ this.scopes = [{}];
147
+ this.dynamicTagsStack = [new Map()];
148
+ this.security = security;
149
+ this.settings = settings;
150
+ this.mapperFile = mapperFile;
151
+ registerHostSettings(settings);
152
+
153
+ if (this.runtime) {
154
+ this.runtime.vm.expose({
155
+ __allowRaw: this.security.allowRaw !== false
156
+ });
157
+ return;
158
+ }
159
+
160
+ const { createRuntime } = await quickJS();
161
+
162
+ this.runtime = await createRuntime({
163
+ allowFetch: true,
164
+ fetchAdapter: async (input, init) => {
165
+ return await customFetchAdapter(input, init, this.security);
166
+ },
167
+ allowFs: true,
168
+ env: {}
169
+ });
170
+
171
+ this.deadline = 0;
172
+ if (this.runtime?.vm?.context?.runtime?.setInterruptHandler) {
173
+ this.runtime.vm.context.runtime.setInterruptHandler(() => {
174
+ return this.deadline > 0 && Date.now() > this.deadline;
175
+ });
176
+ }
177
+
178
+ // Expose standard library version & compile adapter, then construct the frozen global namespace inside the VM
179
+ this.runtime.vm.expose({
180
+ __hostSomMarkVersion: SomMark.version,
181
+ __hostSomMarkSettings: () => JSON.stringify(SomMark.settings),
182
+ __hostCompile: async (src, options) => {
183
+ return await customCompileAdapter(src, options, this.security);
184
+ },
185
+ __hostRegisterDynamicTag: (id, options) => {
186
+ this.registerDynamicTag(id, options);
187
+ },
188
+ __hostRemoveDynamicTag: (id) => {
189
+ const activeMap = this.dynamicTagsStack[this.dynamicTagsStack.length - 1];
190
+ activeMap.delete(id);
191
+ },
192
+ __hostGetTagInfo: (id) => {
193
+ if (!this.mapperFile) return null;
194
+ const target = this.mapperFile.get(id);
195
+ if (!target) return null;
196
+ return JSON.stringify({
197
+ options: target.options || {}
198
+ });
199
+ },
200
+ __hostCallTagRender: async (id, payloadStr) => {
201
+ if (!this.mapperFile) return "";
202
+ const target = this.mapperFile.get(id);
203
+ if (!target) return "";
204
+ const payload = JSON.parse(payloadStr);
205
+ return await target.render.call(this.mapperFile, payload);
206
+ },
207
+ __allowRaw: this.security.allowRaw !== false
208
+ });
209
+
210
+ await this.runtime.vm.evalCode(`
211
+ const __nativeFetch = globalThis.fetch;
212
+ class TagBuilder {
213
+ constructor(tagName) {
214
+ this.tagName = tagName;
215
+ this._children = "";
216
+ this._attr = [];
217
+ this._is_self_close = false;
218
+ }
219
+ attributes(obj) {
220
+ if (obj && typeof obj === "object") {
221
+ Object.entries(obj).forEach(([key, value]) => {
222
+ if (value === true) this._attr.push(key);
223
+ else if (value !== false && value !== null && value !== undefined) {
224
+ const esc = String(value)
225
+ .replace(/&/g, "&")
226
+ .replace(/</g, "&lt;")
227
+ .replace(/>/g, "&gt;")
228
+ .replace(/"/g, "&quot;")
229
+ .replace(/'/g, "&#39;");
230
+ this._attr.push(\`\${key}="\${esc}"\`);
231
+ }
232
+ });
233
+ }
234
+ return this;
235
+ }
236
+ body(nodes) {
237
+ if (nodes) {
238
+ this._children += (this._children ? " " : "") + nodes;
239
+ }
240
+ return this.builder();
241
+ }
242
+ selfClose() {
243
+ this._is_self_close = true;
244
+ return this.builder();
245
+ }
246
+ builder() {
247
+ const props = this._attr.length > 0 ? " " + this._attr.join(" ") : "";
248
+ if (this._is_self_close) {
249
+ return \`<\${this.tagName}\${props} />\`;
250
+ }
251
+ return \`<\${this.tagName}\${props}>\${this._children}</\${this.tagName}>\`;
252
+ }
253
+ }
254
+
255
+ const SomMark = {
256
+ version: __hostSomMarkVersion,
257
+ __dynamicTags: new Map(),
258
+ register: function(id, render, options = {}) {
259
+ if (typeof id !== "string") {
260
+ throw new Error("SomMark.register Error: Tag ID must be a string.");
261
+ }
262
+ if (typeof render !== "function") {
263
+ throw new Error("SomMark.register Error: Render function must be a function.");
264
+ }
265
+ this.__dynamicTags.set(id, { render, options });
266
+ __hostRegisterDynamicTag(id, options);
267
+ },
268
+ get: function(id) {
269
+ if (typeof id !== "string") {
270
+ throw new Error("SomMark.get Error: Tag ID must be a string.");
271
+ }
272
+ const local = this.__dynamicTags.get(id);
273
+ if (local) {
274
+ return {
275
+ options: local.options || {},
276
+ render: local.render
277
+ };
278
+ }
279
+ const hostInfoStr = __hostGetTagInfo(id);
280
+ if (hostInfoStr) {
281
+ const hostInfo = JSON.parse(hostInfoStr);
282
+ return {
283
+ options: hostInfo.options || {},
284
+ render: async function(payload) {
285
+ return await __hostCallTagRender(id, JSON.stringify(payload));
286
+ }
287
+ };
288
+ }
289
+ return null;
290
+ },
291
+ removeOutput: function(id) {
292
+ if (typeof id !== "string") {
293
+ throw new Error("SomMark.removeOutput Error: Tag ID must be a string.");
294
+ }
295
+ this.__dynamicTags.delete(id);
296
+ __hostRemoveDynamicTag(id);
297
+ },
298
+ includesId: function(ids) {
299
+ if (!Array.isArray(ids)) {
300
+ throw new Error("SomMark.includesId Error: Expected an array of IDs.");
301
+ }
302
+ if (ids.some(id => this.__dynamicTags.has(id))) {
303
+ return true;
304
+ }
305
+ return ids.some(id => __hostGetTagInfo(id) !== null);
306
+ },
307
+ tag: function(tagName) {
308
+ if (typeof tagName !== "string") {
309
+ throw new Error("SomMark.tag Error: Tag name must be a string.");
310
+ }
311
+ return new TagBuilder(tagName);
312
+ },
313
+ get settings() {
314
+ const parsed = JSON.parse(__hostSomMarkSettings() || "{}");
315
+ Object.defineProperty(parsed, "__raw", {
316
+ value: JSON.stringify(parsed),
317
+ enumerable: false,
318
+ writable: false,
319
+ configurable: false
320
+ });
321
+ return Object.freeze(parsed);
322
+ },
323
+ fetch: async (input, init) => {
324
+ const plainRes = await __nativeFetch(input, init);
325
+ return {
326
+ status: plainRes.status,
327
+ ok: plainRes.ok,
328
+ statusText: plainRes.statusText,
329
+ url: plainRes.url,
330
+ type: plainRes.type,
331
+ redirected: plainRes.redirected,
332
+ headers: {
333
+ get: (name) => plainRes.headers[name.toLowerCase()] || null,
334
+ forEach: (cb) => {
335
+ Object.keys(plainRes.headers).forEach(key => cb(plainRes.headers[key], key));
336
+ }
337
+ },
338
+ text: async () => plainRes.bodyText,
339
+ json: async () => JSON.parse(plainRes.bodyText),
340
+ clone: function() { return { ...this }; }
341
+ };
342
+ },
343
+ compile: async (src, options) => {
344
+ if (src === null || src === undefined) {
345
+ throw new Error("SomMark.compile Error: Template source cannot be null or undefined.");
346
+ }
347
+ if (typeof src === "function") {
348
+ throw new Error("SomMark.compile Error: Cannot pass a function as the template source. Did you forget to invoke/call it?");
349
+ }
350
+ if (src instanceof Promise || (typeof src === "object" && typeof src.then === "function")) {
351
+ throw new Error("SomMark.compile Error: Cannot pass a Promise as the template source. Did you forget to use 'await'?");
352
+ }
353
+ if (typeof src !== "string") {
354
+ throw new Error("SomMark.compile Error: Template source must be a string.");
355
+ }
356
+ return await __hostCompile(src, options);
357
+ },
358
+ raw: (html) => {
359
+ if (typeof __allowRaw !== "undefined" && !__allowRaw) {
360
+ throw new Error("Security Error: SomMark.raw is disabled in this environment.");
361
+ }
362
+ if (html === null || html === undefined) {
363
+ return { __raw: "" };
364
+ }
365
+ if (typeof html === "function") {
366
+ throw new Error("SomMark.raw Error: Cannot pass a function directly to SomMark.raw. Did you forget to invoke/call it?");
367
+ }
368
+ if (html instanceof Promise || (typeof html === "object" && typeof html.then === "function")) {
369
+ throw new Error("SomMark.raw Error: Cannot pass a Promise directly to SomMark.raw. Did you forget to use 'await'?");
370
+ }
371
+ if (typeof html === "object" && !html.__raw) {
372
+ throw new Error("SomMark.raw Error: Cannot render an object directly.");
373
+ }
374
+ return { __raw: String(html.__raw !== undefined ? html.__raw : html) };
375
+ },
376
+ static: (expr) => {
377
+ if (typeof expr !== "string") {
378
+ throw new Error("SomMark.static Error: Argument must be a string.");
379
+ }
380
+ return globalThis.eval(expr);
381
+ }
382
+ };
383
+
384
+ // Deep freeze the SomMark standard library to make it completely immutable
385
+ Object.freeze(SomMark);
386
+
387
+ // Establish the global SomMark constant (non-writable, non-configurable)
388
+ Object.defineProperty(globalThis, "SomMark", {
389
+ value: SomMark,
390
+ writable: false,
391
+ configurable: false
392
+ });
393
+
394
+ // Prevent direct/un-namespaced global fetch usage to enforce standard library architecture
395
+ delete globalThis.fetch;
396
+ delete globalThis.process;
397
+ `);
398
+
399
+ // Configure host-based module loader to support local imports perfectly
400
+ this.runtime.vm.context.runtime.setModuleLoader((moduleName) => {
401
+ try {
402
+ const isRaw = moduleName.endsWith("?raw");
403
+ const cleanModuleName = isRaw ? moduleName.slice(0, -4) : moduleName;
404
+ const resolvedPath = path.resolve(this.baseDir, cleanModuleName);
405
+ if (fs.existsSync(resolvedPath)) {
406
+ let source = fs.readFileSync(resolvedPath, "utf8");
407
+
408
+ if (isRaw) {
409
+ const escapedSource = source.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
410
+ return `export default \`${escapedSource}\`;`;
411
+ }
412
+
413
+ // Support JSON files
414
+ if (resolvedPath.endsWith(".json")) {
415
+ source = `export default ${source};`;
416
+ }
417
+
418
+ // Support Smark files
419
+ if (resolvedPath.endsWith(".smark")) {
420
+ const escapedSource = source.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
421
+ source = `
422
+ export default async (variables = {}) => {
423
+ return await SomMark.compile(\`${escapedSource}\`, { variables });
424
+ };
425
+ `;
426
+ }
427
+
428
+ return source; // MUST BE A STRING
429
+ }
430
+ throw new Error(`Module not found: ${moduleName}`);
431
+ } catch (err) {
432
+ throw err;
433
+ }
434
+ });
435
+ }
436
+
437
+ /**
438
+ * Pushes a new block scope level.
439
+ */
440
+ pushScope() {
441
+ this.scopes.push({});
442
+ this.dynamicTagsStack.push(new Map());
443
+ }
444
+
445
+ /**
446
+ * Pops the current block scope level, cleaning up VM globals and restoring parent scope variables.
447
+ */
448
+ async popScope() {
449
+ if (this.scopes.length > 1) {
450
+ const popped = this.scopes.pop();
451
+ this.dynamicTagsStack.pop();
452
+ const keysToDelete = Object.keys(popped);
453
+ if (keysToDelete.length > 0 && this.runtime) {
454
+ try {
455
+ const deleteCode = keysToDelete.map(k => `delete globalThis['${k}'];`).join(" ");
456
+ await this.runtime.vm.evalCode(deleteCode, "cleanup.js");
457
+ } catch (e) {
458
+ // ignore
459
+ }
460
+ }
461
+ // Restore parent scopes
462
+ if (this.runtime) {
463
+ const merged = {};
464
+ for (const scope of this.scopes) {
465
+ Object.assign(merged, scope);
466
+ }
467
+ this.runtime.vm.expose(merged);
468
+ }
469
+ }
470
+ }
471
+
472
+ hasDynamicTag(id) {
473
+ for (let i = this.dynamicTagsStack.length - 1; i >= 0; i--) {
474
+ if (this.dynamicTagsStack[i].has(id)) return true;
475
+ }
476
+ return false;
477
+ }
478
+
479
+ getDynamicTagOptions(id) {
480
+ for (let i = this.dynamicTagsStack.length - 1; i >= 0; i--) {
481
+ const entry = this.dynamicTagsStack[i].get(id);
482
+ if (entry) return entry.options;
483
+ }
484
+ return {};
485
+ }
486
+
487
+ registerDynamicTag(id, options = {}) {
488
+ const activeMap = this.dynamicTagsStack[this.dynamicTagsStack.length - 1];
489
+ activeMap.set(id, { options });
490
+ }
491
+
492
+ async executeDynamicTag(id, payload) {
493
+ if (!this.runtime) throw new Error("EvaluatorState not initialized");
494
+ this.runtime.vm.expose({
495
+ __activeTagPayload: () => JSON.stringify(payload)
496
+ });
497
+ const code = `
498
+ (() => {
499
+ const payload = JSON.parse(__activeTagPayload());
500
+ const tag = SomMark.__dynamicTags.get(${JSON.stringify(id)});
501
+ if (!tag) throw new Error("Tag not found inside VM: " + ${JSON.stringify(id)});
502
+ const res = tag.render({
503
+ args: payload.args,
504
+ content: payload.content,
505
+ textContent: payload.textContent,
506
+ nodeType: payload.nodeType,
507
+ isSelfClosing: payload.isSelfClosing
508
+ });
509
+ return res;
510
+ })()
511
+ `;
512
+ let result = await this.runtime.vm.evalCode(code, "render_tag.js");
513
+ if (result instanceof Promise || (result && typeof result === "object" && typeof result.then === "function")) {
514
+ result = await result;
515
+ }
516
+ return result;
517
+ }
518
+
519
+ /**
520
+ * Synchronizes changed VM global variables back to the scope stack.
521
+ */
522
+ async _syncScopes() {
523
+ if (!this.runtime) return;
524
+ const allKeysSet = new Set();
525
+ for (const scope of this.scopes) {
526
+ for (const key of Object.keys(scope)) {
527
+ allKeysSet.add(key);
528
+ }
529
+ }
530
+ const allKeys = Array.from(allKeysSet);
531
+ if (allKeys.length > 0) {
532
+ try {
533
+ const getValuesCode = `export default { ${allKeys.map(k => `${JSON.stringify(k)}: globalThis['${k}']`).join(", ")} };`;
534
+ const valuesRes = await this.runtime.vm.evalCode(getValuesCode, "sync.js", { type: 'module' });
535
+ if (valuesRes && typeof valuesRes === 'object' && 'default' in valuesRes) {
536
+ const syncedValues = valuesRes.default;
537
+ for (const [key, val] of Object.entries(syncedValues)) {
538
+ for (let s = this.scopes.length - 1; s >= 0; s--) {
539
+ if (key in this.scopes[s]) {
540
+ this.scopes[s][key] = val;
541
+ break;
542
+ }
543
+ }
544
+ }
545
+ }
546
+ } catch (err) {
547
+ // ignore
548
+ }
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Injects variables safely into the sandbox.
554
+ */
555
+ inject(vars) {
556
+ if (!this.runtime) return;
557
+ const currentScope = this.scopes[this.scopes.length - 1];
558
+ Object.assign(currentScope, vars);
559
+ this.runtime.vm.expose(vars);
560
+ }
561
+
562
+ /**
563
+ * Executes code asynchronously and returns resolved result.
564
+ */
565
+ async execute(code) {
566
+ if (!this.runtime) throw new Error("Evaluator not initialized");
567
+
568
+ const timeout = this.security?.timeout ?? 5000;
569
+ this.deadline = Date.now() + timeout; // Dynamic timeout safety safeguard
570
+
571
+ // Keep QuickJS event loop alive in the background during execution
572
+ const interval = setInterval(() => {
573
+ try {
574
+ this.runtime.vm.context.runtime.executePendingJobs();
575
+ } catch (err) {
576
+ // ignore
577
+ }
578
+ }, 1);
579
+
580
+ try {
581
+ // Detect top-level declarations for Auto-Export
582
+ let autoExportedNames = [];
583
+ let hasExplicitExports = false;
584
+ try {
585
+ const ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true });
586
+ for (const node of ast.body) {
587
+ if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
588
+ hasExplicitExports = true;
589
+ }
590
+ if (node.type === 'VariableDeclaration') {
591
+ for (const decl of node.declarations) {
592
+ if (decl.id.type === 'Identifier') autoExportedNames.push(decl.id.name);
593
+ else if (decl.id.type === 'ObjectPattern') {
594
+ for (const prop of decl.id.properties) {
595
+ if (prop.value.type === 'Identifier') autoExportedNames.push(prop.value.name);
596
+ }
597
+ }
598
+ }
599
+ } else if (node.type === 'FunctionDeclaration') {
600
+ if (node.id) autoExportedNames.push(node.id.name);
601
+ } else if (node.type === 'ImportDeclaration') {
602
+ for (const spec of node.specifiers) {
603
+ autoExportedNames.push(spec.local.name);
604
+ }
605
+ }
606
+ }
607
+ } catch (e) {
608
+ // If it fails to parse as module, it might be a simple expression, ignore
609
+ }
610
+
611
+ const hasImportExport = hasExplicitExports || /\bimport\b/.test(code);
612
+ const hasAwait = /\bawait\b/.test(code);
613
+
614
+ let finalCode = code;
615
+
616
+ // Rewrite the last expression statement to be export default so we automatically return its value
617
+ try {
618
+ const ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true });
619
+ const lastNode = ast.body[ast.body.length - 1];
620
+ if (lastNode && lastNode.type === 'ExpressionStatement') {
621
+ const start = lastNode.start;
622
+ finalCode = code.slice(0, start) + "export default " + code.slice(start);
623
+ } else if (lastNode && lastNode.type === 'ReturnStatement') {
624
+ const start = lastNode.start;
625
+ if (lastNode.argument) {
626
+ const argumentCode = code.slice(lastNode.argument.start, lastNode.argument.end);
627
+ finalCode = code.slice(0, start) + `export default (${argumentCode});` + code.slice(lastNode.end);
628
+ } else {
629
+ finalCode = code.slice(0, start) + "export default undefined;" + code.slice(lastNode.end);
630
+ }
631
+ }
632
+ } catch (err) {
633
+ // Ignore parsing errors and fallback to raw code
634
+ }
635
+
636
+ if (autoExportedNames.length > 0 && !hasExplicitExports) {
637
+ finalCode += `\nexport { ${autoExportedNames.join(', ')} };`;
638
+ }
639
+
640
+ const isModule = hasImportExport || hasAwait || autoExportedNames.length > 0 || finalCode.includes("export default");
641
+
642
+ let result;
643
+ if (isModule) {
644
+ // Evaluate as module using Arena
645
+ const evalPromise = this.runtime.vm.evalCode(finalCode, "main.js", {
646
+ strict: true,
647
+ strip: true,
648
+ backtraceBarrier: true,
649
+ type: 'module'
650
+ });
651
+
652
+ const res = await evalPromise;
653
+
654
+ // Move exports directly to global scope in the VM
655
+ if (res && typeof res === 'object') {
656
+ const currentScope = this.scopes[this.scopes.length - 1];
657
+ for (const [key, val] of Object.entries(res)) {
658
+ if (key !== 'default') {
659
+ currentScope[key] = val;
660
+ this.runtime.vm.expose({ [key]: val });
661
+ }
662
+ }
663
+ if ('default' in res) {
664
+ result = res.default;
665
+ } else {
666
+ result = undefined;
667
+ }
668
+ } else {
669
+ result = res;
670
+ }
671
+ } else {
672
+ result = await this.runtime.vm.evalCode(code, "main.js");
673
+ }
674
+
675
+ if (result instanceof Promise || (result && typeof result === "object" && typeof result.then === "function")) {
676
+ result = await result;
677
+ }
678
+
679
+ await this._syncScopes();
680
+ return result;
681
+ } catch (error) {
682
+ // Try to extract line/col from stack trace
683
+ const stack = error.stack || "";
684
+ const match = stack.match(/main\.js:(\d+):(\d+)/) || stack.match(/:(\d+):(\d+)/);
685
+
686
+ const err = new Error(error.message || error);
687
+ if (match) {
688
+ err.line = parseInt(match[1]);
689
+ err.column = parseInt(match[2]);
690
+ }
691
+ throw err;
692
+ } finally {
693
+ this.deadline = 0;
694
+ clearInterval(interval);
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Disposal.
700
+ */
701
+ destroy() {
702
+ if (this.runtime) {
703
+ try {
704
+ // Execute any lingering jobs & trigger the QuickJS garbage collector
705
+ if (this.runtime.vm?.context?.runtime) {
706
+ this.runtime.vm.context.runtime.executePendingJobs();
707
+ this.runtime.vm.context.runtime.gc();
708
+ }
709
+ } catch (e) { }
710
+
711
+ try {
712
+ this.runtime.dispose();
713
+ } catch (e) {
714
+ // Graceful logging for minor Emscripten reference delays
715
+ console.warn("<$yellow:Warning:$> Safe context disposal warning: " + e.message);
716
+ }
717
+ this.runtime = null;
718
+ }
719
+ }
720
+ }
721
+
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
+ class Evaluator {
729
+ constructor() {
730
+ this.instances = [];
731
+ }
732
+
733
+ /**
734
+ * Get the active logic engine state instance at the top of the stack.
735
+ */
736
+ get active() {
737
+ if (this.instances.length === 0) {
738
+ throw new Error("No active EvaluatorState instance. Did you call init()?");
739
+ }
740
+ return this.instances[this.instances.length - 1];
741
+ }
742
+
743
+ async init(baseDir = null, security = {}, settings = {}, mapperFile = null) {
744
+ const state = new EvaluatorState();
745
+ await state.init(baseDir, security, settings, mapperFile);
746
+ this.instances.push(state);
747
+ }
748
+
749
+ destroy() {
750
+ if (this.instances.length > 0) {
751
+ const state = this.instances.pop();
752
+ state.destroy();
753
+ }
754
+ }
755
+
756
+ pushScope() {
757
+ this.active.pushScope();
758
+ }
759
+
760
+ async popScope() {
761
+ await this.active.popScope();
762
+ }
763
+
764
+ inject(vars) {
765
+ this.active.inject(vars);
766
+ }
767
+
768
+ async execute(code) {
769
+ return await this.active.execute(code);
770
+ }
771
+
772
+ hasDynamicTag(id) {
773
+ return this.active.hasDynamicTag(id);
774
+ }
775
+
776
+ getDynamicTagOptions(id) {
777
+ return this.active.getDynamicTagOptions(id);
778
+ }
779
+
780
+ async executeDynamicTag(id, payload) {
781
+ return await this.active.executeDynamicTag(id, payload);
782
+ }
783
+ }
784
+
785
+ export default new Evaluator();