@executablemd/testing 0.3.1 → 0.4.1
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/esm/{testing/npm/src/src → src}/assertions.js +15 -24
- package/esm/{testing/npm/src/src → src}/handlers.js +140 -10
- package/esm/{testing/npm/src/src → src}/vocabulary.js +3 -0
- package/package.json +3 -3
- package/types/src/assertions.d.ts +11 -1
- package/types/src/handlers.d.ts +1 -0
- package/esm/core/mod.js +0 -55
- package/esm/core/src/api.js +0 -31
- package/esm/core/src/collect.js +0 -24
- package/esm/core/src/component-api.js +0 -47
- package/esm/core/src/content-context.js +0 -23
- package/esm/core/src/deno-compiler.js +0 -51
- package/esm/core/src/errors.js +0 -13
- package/esm/core/src/eval-context.js +0 -13
- package/esm/core/src/eval-handler.js +0 -79
- package/esm/core/src/eval-interpolate.js +0 -63
- package/esm/core/src/eval-transform.js +0 -378
- package/esm/core/src/execute.js +0 -378
- package/esm/core/src/expand.js +0 -988
- package/esm/core/src/frontmatter.js +0 -98
- package/esm/core/src/heal.js +0 -29
- package/esm/core/src/interpolate.js +0 -39
- package/esm/core/src/modifiers/daemon.js +0 -60
- package/esm/core/src/modifiers/persist.js +0 -35
- package/esm/core/src/modifiers/timeout.js +0 -55
- package/esm/core/src/modifiers.js +0 -113
- package/esm/core/src/output/normalize.js +0 -33
- package/esm/core/src/output/terminal.js +0 -20
- package/esm/core/src/render.js +0 -30
- package/esm/core/src/replay-stream.js +0 -58
- package/esm/core/src/sample-api.js +0 -45
- package/esm/core/src/scanner.js +0 -632
- package/esm/core/src/temp-file-compiler.js +0 -68
- package/esm/core/src/types.js +0 -7
- package/esm/core/src/validate.js +0 -82
- package/esm/durable-streams/combinators.js +0 -227
- package/esm/durable-streams/context.js +0 -13
- package/esm/durable-streams/divergence.js +0 -56
- package/esm/durable-streams/each.js +0 -193
- package/esm/durable-streams/effect.js +0 -273
- package/esm/durable-streams/ephemeral.js +0 -80
- package/esm/durable-streams/errors.js +0 -92
- package/esm/durable-streams/http-stream.js +0 -207
- package/esm/durable-streams/mod.js +0 -34
- package/esm/durable-streams/operations.js +0 -88
- package/esm/durable-streams/replay-guard.js +0 -87
- package/esm/durable-streams/replay-index.js +0 -132
- package/esm/durable-streams/run.js +0 -137
- package/esm/durable-streams/serialize.js +0 -65
- package/esm/durable-streams/stream.js +0 -56
- package/esm/durable-streams/types.js +0 -10
- package/esm/runtime/apis.js +0 -237
- package/esm/runtime/find-free-port.js +0 -33
- package/esm/runtime/mod.js +0 -20
- /package/esm/{testing/npm/src/_dnt.polyfills.js → _dnt.polyfills.js} +0 -0
- /package/esm/{testing/npm/src/_dnt.shims.js → _dnt.shims.js} +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/almost_equals.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/array_includes.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assert.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assertion_error.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equal.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equals.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/exists.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/fail.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/false.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater_or_equal.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/instance_of.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/is_error.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less_or_equal.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/match.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/mod.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_equals.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_instance_of.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_match.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_strict_equals.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/object_match.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/rejects.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/strict_equals.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/string_includes.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/throws.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unimplemented.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unreachable.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/build_message.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff_str.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/format.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/styles.js +0 -0
- /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/types.js +0 -0
- /package/esm/{testing/npm/src/mod.js → mod.js} +0 -0
- /package/esm/{testing/npm/src/src → src}/journal.js +0 -0
- /package/esm/{testing/npm/src/src → src}/test-api.js +0 -0
- /package/esm/{testing/npm/src/src → src}/use-testing.js +0 -0
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { createDurableOperation, ephemeral } from "@executablemd/durable-streams";
|
|
2
|
-
import { unbox } from "@effectionx/scope-eval";
|
|
3
|
-
import { useCodeBlock } from "./modifiers.ts";
|
|
4
|
-
import { env, evalScope, persistent } from "./component-api.ts";
|
|
5
|
-
import { compileBlock } from "./eval-context.ts";
|
|
6
|
-
import { transformBlock, serializeExports } from "./eval-transform.ts";
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// evalFactory — terminal modifier (spec §6.2)
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
export const evalFactory = (_params) => (_args, _next) => (function* () {
|
|
11
|
-
const ctx = yield* useCodeBlock();
|
|
12
|
-
const evalEnv = yield* ephemeral(env);
|
|
13
|
-
if (!evalEnv) {
|
|
14
|
-
throw new Error(`eval block "${ctx.blockId}" requires a binding environment; none is in scope.`);
|
|
15
|
-
}
|
|
16
|
-
const persist = yield* ephemeral(persistent);
|
|
17
|
-
// Inject output() function into env so eval blocks can produce
|
|
18
|
-
// rendered output. The function is a plain synchronous call:
|
|
19
|
-
// output("some text")
|
|
20
|
-
// The mutable ref is block-local; serializeExports silently
|
|
21
|
-
// omits non-JSON values (functions), so output won't pollute
|
|
22
|
-
// the journal. The output text itself is journaled alongside
|
|
23
|
-
// exports as __output.
|
|
24
|
-
const outputRef = { text: "" };
|
|
25
|
-
evalEnv.values.output = (text) => {
|
|
26
|
-
outputRef.text = String(text);
|
|
27
|
-
};
|
|
28
|
-
const transformed = transformBlock(ctx.content, ctx.blockId, Object.keys(evalEnv.values));
|
|
29
|
-
const bindings = serializeExports(evalEnv.values, transformed.imports);
|
|
30
|
-
const result = (yield createDurableOperation({
|
|
31
|
-
type: "eval",
|
|
32
|
-
name: `eval:${ctx.blockId}`,
|
|
33
|
-
...(ctx.language ? { language: ctx.language } : {}),
|
|
34
|
-
}, function* () {
|
|
35
|
-
// Merge incoming bindings snapshot into env before execution
|
|
36
|
-
Object.assign(evalEnv.values, bindings);
|
|
37
|
-
// Compile the eval block via data: URI module import.
|
|
38
|
-
// compileBlock is async (returns Operation) — it generates a
|
|
39
|
-
// TypeScript module and imports it via data: URI.
|
|
40
|
-
const fn = yield* compileBlock(transformed.code, transformed.userImports ?? []);
|
|
41
|
-
if (persist) {
|
|
42
|
-
// Persist mode: run the compiled block inside the eval scope
|
|
43
|
-
// so spawned resources are retained in the persistent EvalScope.
|
|
44
|
-
const scope = yield* evalScope;
|
|
45
|
-
if (!scope) {
|
|
46
|
-
throw new Error(`persist eval block "${ctx.blockId}" requires a component eval scope; none is in scope.`);
|
|
47
|
-
}
|
|
48
|
-
const blockResult = yield* scope.eval(() => fn(evalEnv.values));
|
|
49
|
-
const returnValue = unbox(blockResult);
|
|
50
|
-
if (!outputRef.text && returnValue != null) {
|
|
51
|
-
outputRef.text = String(returnValue);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
// Normal mode: run the compiled block in the current scope.
|
|
56
|
-
// Resources are torn down when this operation completes.
|
|
57
|
-
const returnValue = yield* fn(evalEnv.values);
|
|
58
|
-
if (!outputRef.text && returnValue != null) {
|
|
59
|
-
outputRef.text = String(returnValue);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
const exports = serializeExports(evalEnv.values, transformed.exports);
|
|
63
|
-
if (outputRef.text) {
|
|
64
|
-
exports.__output = outputRef.text;
|
|
65
|
-
}
|
|
66
|
-
return { value: exports };
|
|
67
|
-
}));
|
|
68
|
-
if (result.value && typeof result.value === "object") {
|
|
69
|
-
const restored = result.value;
|
|
70
|
-
// Extract __output before merging into env
|
|
71
|
-
if (typeof restored.__output === "string") {
|
|
72
|
-
outputRef.text = restored.__output;
|
|
73
|
-
}
|
|
74
|
-
// Remove __output from exports before assigning to env
|
|
75
|
-
const { __output: _, ...exports } = restored;
|
|
76
|
-
Object.assign(evalEnv.values, exports);
|
|
77
|
-
}
|
|
78
|
-
return { output: outputRef.text, exitCode: 0, stderr: "" };
|
|
79
|
-
})();
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Eval binding interpolation (spec §6.6).
|
|
3
|
-
*
|
|
4
|
-
* Substitutes `{name}` and `{name.path.chain}` references in content
|
|
5
|
-
* with values from the eval binding environment (`env.values`). This
|
|
6
|
-
* runs in the expansion engine for both code block content and text
|
|
7
|
-
* segments.
|
|
8
|
-
*
|
|
9
|
-
* References use JavaScript identifier syntax with optional dot paths:
|
|
10
|
-
* /\{([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\}/g
|
|
11
|
-
*
|
|
12
|
-
* The first segment of a dotted path must be a key in `env.values`.
|
|
13
|
-
* Subsequent segments traverse nested properties. If any intermediate
|
|
14
|
-
* value is null/undefined, the reference is left verbatim.
|
|
15
|
-
*
|
|
16
|
-
* No collision with `{meta.key}` / `{props.key}`: those are consumed
|
|
17
|
-
* by the `interpolate()` pass which runs first. By the time this
|
|
18
|
-
* function runs, namespaced references are already resolved.
|
|
19
|
-
*
|
|
20
|
-
* If `env.values` has no key matching the root reference, it is left
|
|
21
|
-
* verbatim. Non-string values are converted via `String()`.
|
|
22
|
-
*
|
|
23
|
-
* Escaped braces (`\{name}`) are preserved as literal `{name}` in the
|
|
24
|
-
* output — the backslash is consumed. This is consistent with
|
|
25
|
-
* `interpolate()` which handles `\{meta.key}` the same way.
|
|
26
|
-
*/
|
|
27
|
-
const BARE_BINDING_RE = /\{([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\}/g;
|
|
28
|
-
/**
|
|
29
|
-
* Unicode private-use placeholder for escaped opening braces.
|
|
30
|
-
* Used to protect `\{` from interpolation, then restored as `{`.
|
|
31
|
-
*/
|
|
32
|
-
const ESCAPED_BRACE_PLACEHOLDER = "\uE000";
|
|
33
|
-
/**
|
|
34
|
-
* Replace bare `{name}` references with values from the binding environment.
|
|
35
|
-
* Respects `\{` escaping — `\{name}` is left as literal `{name}`.
|
|
36
|
-
*
|
|
37
|
-
* @param content - The content to interpolate
|
|
38
|
-
* @param bindings - The current eval binding environment (env.values)
|
|
39
|
-
* @returns Content with resolved bindings substituted
|
|
40
|
-
*/
|
|
41
|
-
export function interpolateEvalBindings(content, bindings) {
|
|
42
|
-
// Protect escaped braces: \{ → placeholder
|
|
43
|
-
const escaped = content.replaceAll("\\{", ESCAPED_BRACE_PLACEHOLDER);
|
|
44
|
-
// Run interpolation on the protected content.
|
|
45
|
-
// Supports dotted paths: {pr.meta.number} traverses bindings.pr.meta.number
|
|
46
|
-
const interpolated = escaped.replace(BARE_BINDING_RE, (match, key) => {
|
|
47
|
-
const parts = key.split(".");
|
|
48
|
-
if (!(parts[0] in bindings))
|
|
49
|
-
return match;
|
|
50
|
-
let value = bindings;
|
|
51
|
-
for (let i = 0; i < parts.length; i++) {
|
|
52
|
-
if (value == null || typeof value !== "object")
|
|
53
|
-
return match;
|
|
54
|
-
const obj = value;
|
|
55
|
-
if (i < parts.length - 1 && !(parts[i] in obj))
|
|
56
|
-
return match;
|
|
57
|
-
value = obj[parts[i]];
|
|
58
|
-
}
|
|
59
|
-
return String(value);
|
|
60
|
-
});
|
|
61
|
-
// Restore escaped braces: placeholder → literal {
|
|
62
|
-
return interpolated.replaceAll(ESCAPED_BRACE_PLACEHOLDER, "{");
|
|
63
|
-
}
|
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Source transform for generator eval blocks (spec §4).
|
|
3
|
-
*
|
|
4
|
-
* Rewrites top-level declarations to export their values to a shared
|
|
5
|
-
* binding environment, injects a preamble for importing bindings from
|
|
6
|
-
* previous blocks, and detects execution mode from the AST.
|
|
7
|
-
*
|
|
8
|
-
* Uses acorn for parsing and magic-string for string mutations.
|
|
9
|
-
*/
|
|
10
|
-
import { parse } from "acorn";
|
|
11
|
-
import MagicString from "magic-string";
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Main transform function (spec §4.3)
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
/**
|
|
16
|
-
* Transform an eval block's source code for execution.
|
|
17
|
-
*
|
|
18
|
-
* Pipeline:
|
|
19
|
-
* 1. Parse with acorn (ecmaVersion: "latest", sourceType: "module")
|
|
20
|
-
* 2. Detect mode from top-level yield/await
|
|
21
|
-
* 3. Collect exports from top-level declarations
|
|
22
|
-
* 4. Collect imports — free variable references in currentEnvKeys
|
|
23
|
-
* 5. Build preamble — const { a, b } = env; for each imported name
|
|
24
|
-
* 6. Append env-writes — env.x = x; after each top-level declaration
|
|
25
|
-
* 7. Append sourceURL for debugger identification
|
|
26
|
-
* 8. Generate source map
|
|
27
|
-
*/
|
|
28
|
-
export function transformBlock(source, blockId, currentEnvKeys) {
|
|
29
|
-
// 1. Parse — wrap in an async generator function to allow both
|
|
30
|
-
// top-level yield and await. Eval blocks are generator function bodies,
|
|
31
|
-
// so yield* is valid syntax. We use `async function*` so that `await`
|
|
32
|
-
// is also parseable (for mode detection — we detect and reject mixed
|
|
33
|
-
// yield+await later).
|
|
34
|
-
//
|
|
35
|
-
// `allowImportExportEverywhere` lets acorn parse `import` declarations
|
|
36
|
-
// inside the function body. These are extracted as userImports and
|
|
37
|
-
// hoisted to module top level by compileBlock.
|
|
38
|
-
//
|
|
39
|
-
// To handle TypeScript `import type { X }` syntax (which acorn doesn't
|
|
40
|
-
// understand), we normalize it to `import { X }` (spaces, same
|
|
41
|
-
// length) before parsing. This preserves AST positions so we can
|
|
42
|
-
// extract the original source text including `import type`.
|
|
43
|
-
const WRAPPER_PREFIX = "(async function*() {\n";
|
|
44
|
-
const WRAPPER_SUFFIX = "\n})";
|
|
45
|
-
const normalizedSource = source.replace(/^(import\s+)type(\s+)/gm, (_, pre, post) => pre + " ".repeat("type".length) + post);
|
|
46
|
-
const wrappedSource = `${WRAPPER_PREFIX}${normalizedSource}${WRAPPER_SUFFIX}`;
|
|
47
|
-
const wrappedAst = parse(wrappedSource, {
|
|
48
|
-
ecmaVersion: "latest",
|
|
49
|
-
sourceType: "module",
|
|
50
|
-
allowReturnOutsideFunction: true,
|
|
51
|
-
allowImportExportEverywhere: true,
|
|
52
|
-
});
|
|
53
|
-
// Extract the body of the inner async generator function.
|
|
54
|
-
// The wrapped AST is: ExpressionStatement > FunctionExpression > body
|
|
55
|
-
// AST node positions are offset by WRAPPER_PREFIX.length relative to
|
|
56
|
-
// the original source.
|
|
57
|
-
// deno-lint-ignore no-explicit-any
|
|
58
|
-
const exprStmt = wrappedAst.body[0];
|
|
59
|
-
const funcExpr = exprStmt?.expression;
|
|
60
|
-
const allNodes = funcExpr?.body?.body ?? [];
|
|
61
|
-
const offset = WRAPPER_PREFIX.length;
|
|
62
|
-
// 1b. Separate ImportDeclaration nodes from body nodes.
|
|
63
|
-
// Import nodes are extracted as userImports (hoisted to module level
|
|
64
|
-
// by compileBlock). Body nodes proceed through the existing pipeline.
|
|
65
|
-
const importNodes = [];
|
|
66
|
-
const bodyNodes = [];
|
|
67
|
-
for (const node of allNodes) {
|
|
68
|
-
if (node.type === "ImportDeclaration") {
|
|
69
|
-
importNodes.push(node);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
bodyNodes.push(node);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const userImports = importNodes.map((node) => source.slice(node.start - offset, node.end - offset));
|
|
76
|
-
// Build AST with only body nodes for mode detection, export collection, etc.
|
|
77
|
-
// deno-lint-ignore no-explicit-any
|
|
78
|
-
const ast = { body: bodyNodes, type: "Program" };
|
|
79
|
-
// 2. Detect mode
|
|
80
|
-
const mode = detectMode(ast);
|
|
81
|
-
// 3. Collect exports from top-level declarations
|
|
82
|
-
const exports = collectExports(ast);
|
|
83
|
-
// 4. Collect imports — free variables present in env
|
|
84
|
-
const envKeySet = new Set(currentEnvKeys);
|
|
85
|
-
const declaredNames = new Set(exports);
|
|
86
|
-
const freeVars = collectFreeVariables(ast, declaredNames);
|
|
87
|
-
const imports = freeVars.filter((name) => envKeySet.has(name));
|
|
88
|
-
// 5–7. Build transformed code
|
|
89
|
-
const s = new MagicString(source);
|
|
90
|
-
// 5a. Remove import declarations from the body.
|
|
91
|
-
for (const node of importNodes) {
|
|
92
|
-
let start = node.start - offset;
|
|
93
|
-
let end = node.end - offset;
|
|
94
|
-
if (end < source.length && source[end] === "\n") {
|
|
95
|
-
end++;
|
|
96
|
-
}
|
|
97
|
-
if (start > 0 && source[start - 1] === "\n") {
|
|
98
|
-
start--;
|
|
99
|
-
}
|
|
100
|
-
s.remove(start, end);
|
|
101
|
-
}
|
|
102
|
-
if (imports.length > 0) {
|
|
103
|
-
const preamble = `const { ${imports.join(", ")} } = env;\n`;
|
|
104
|
-
s.prepend(preamble);
|
|
105
|
-
}
|
|
106
|
-
// 6. Append env-writes after each top-level declaration.
|
|
107
|
-
// AST positions are relative to the wrapped source, so we subtract
|
|
108
|
-
// the wrapper prefix length to get positions in the original source.
|
|
109
|
-
for (const node of ast.body) {
|
|
110
|
-
const names = extractDeclaredNames(node);
|
|
111
|
-
if (names.length > 0) {
|
|
112
|
-
const envWrites = names.map((name) => ` env.${name} = ${name};`).join("");
|
|
113
|
-
s.appendLeft(node.end - offset, envWrites);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
// 7. Append sourceURL
|
|
117
|
-
s.append(`\n//# sourceURL=eval:${blockId}`);
|
|
118
|
-
// 8. Generate source map
|
|
119
|
-
const map = s.generateMap({ source: blockId, hires: true });
|
|
120
|
-
return {
|
|
121
|
-
code: s.toString(),
|
|
122
|
-
map: map.toString(),
|
|
123
|
-
exports,
|
|
124
|
-
imports,
|
|
125
|
-
mode,
|
|
126
|
-
userImports,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Detect execution mode from top-level yield/await in ast.body.
|
|
131
|
-
*
|
|
132
|
-
* Only direct children of ast.body are inspected. yield/await inside
|
|
133
|
-
* nested function bodies do NOT count.
|
|
134
|
-
*/
|
|
135
|
-
function detectMode(ast) {
|
|
136
|
-
let hasYield = false;
|
|
137
|
-
let hasAwait = false;
|
|
138
|
-
for (const node of ast.body) {
|
|
139
|
-
walkTopLevel(node, (n) => {
|
|
140
|
-
if (n.type === "YieldExpression")
|
|
141
|
-
hasYield = true;
|
|
142
|
-
if (n.type === "AwaitExpression")
|
|
143
|
-
hasAwait = true;
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
if (hasYield && hasAwait) {
|
|
147
|
-
throw new Error(`Cannot mix \`yield*\` and \`await\` at the top level. ` +
|
|
148
|
-
`Use \`yield* call(async () => { ... })\` to bridge async code into a generator block.`);
|
|
149
|
-
}
|
|
150
|
-
if (hasYield)
|
|
151
|
-
return "generator";
|
|
152
|
-
if (hasAwait)
|
|
153
|
-
return "async";
|
|
154
|
-
return "sync";
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Walk a top-level statement, visiting expressions but NOT descending
|
|
158
|
-
* into nested function/class/arrow bodies.
|
|
159
|
-
*/
|
|
160
|
-
function walkTopLevel(node, visitor) {
|
|
161
|
-
if (!node || typeof node !== "object")
|
|
162
|
-
return;
|
|
163
|
-
visitor(node);
|
|
164
|
-
// Do not descend into nested function/class/arrow bodies
|
|
165
|
-
if (node.type === "FunctionDeclaration" ||
|
|
166
|
-
node.type === "FunctionExpression" ||
|
|
167
|
-
node.type === "ArrowFunctionExpression" ||
|
|
168
|
-
node.type === "ClassDeclaration" ||
|
|
169
|
-
node.type === "ClassExpression" ||
|
|
170
|
-
node.type === "MethodDefinition") {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
for (const key of Object.keys(node)) {
|
|
174
|
-
if (key === "type" || key === "start" || key === "end")
|
|
175
|
-
continue;
|
|
176
|
-
const child = node[key];
|
|
177
|
-
if (Array.isArray(child)) {
|
|
178
|
-
for (const item of child) {
|
|
179
|
-
if (item && typeof item === "object" && item.type) {
|
|
180
|
-
walkTopLevel(item, visitor);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
else if (child && typeof child === "object" && child.type) {
|
|
185
|
-
walkTopLevel(child, visitor);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
// Export collection (spec §4.2)
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
/**
|
|
193
|
-
* Collect names exported by top-level declarations.
|
|
194
|
-
*
|
|
195
|
-
* Only direct ast.body children are considered. Nested declarations
|
|
196
|
-
* (inside if/for/etc.) are NOT exported.
|
|
197
|
-
*/
|
|
198
|
-
function collectExports(ast) {
|
|
199
|
-
const names = [];
|
|
200
|
-
for (const node of ast.body) {
|
|
201
|
-
names.push(...extractDeclaredNames(node));
|
|
202
|
-
}
|
|
203
|
-
return names;
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Extract names declared by a top-level statement node.
|
|
207
|
-
*/
|
|
208
|
-
function extractDeclaredNames(node) {
|
|
209
|
-
if (!node)
|
|
210
|
-
return [];
|
|
211
|
-
switch (node.type) {
|
|
212
|
-
case "VariableDeclaration": {
|
|
213
|
-
const names = [];
|
|
214
|
-
for (const decl of node.declarations) {
|
|
215
|
-
if (decl.id) {
|
|
216
|
-
names.push(...extractPatternNames(decl.id));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return names;
|
|
220
|
-
}
|
|
221
|
-
case "FunctionDeclaration":
|
|
222
|
-
return node.id?.name ? [node.id.name] : [];
|
|
223
|
-
case "ClassDeclaration":
|
|
224
|
-
return node.id?.name ? [node.id.name] : [];
|
|
225
|
-
default:
|
|
226
|
-
return [];
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Recursively extract bound names from a destructuring pattern.
|
|
231
|
-
*/
|
|
232
|
-
function extractPatternNames(pattern) {
|
|
233
|
-
if (!pattern)
|
|
234
|
-
return [];
|
|
235
|
-
switch (pattern.type) {
|
|
236
|
-
case "Identifier":
|
|
237
|
-
return [pattern.name];
|
|
238
|
-
case "ObjectPattern": {
|
|
239
|
-
const names = [];
|
|
240
|
-
for (const prop of pattern.properties) {
|
|
241
|
-
if (prop.type === "RestElement") {
|
|
242
|
-
names.push(...extractPatternNames(prop.argument));
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
names.push(...extractPatternNames(prop.value));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return names;
|
|
249
|
-
}
|
|
250
|
-
case "ArrayPattern": {
|
|
251
|
-
const names = [];
|
|
252
|
-
for (const elem of pattern.elements) {
|
|
253
|
-
if (elem) {
|
|
254
|
-
names.push(...extractPatternNames(elem));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return names;
|
|
258
|
-
}
|
|
259
|
-
case "RestElement":
|
|
260
|
-
return extractPatternNames(pattern.argument);
|
|
261
|
-
case "AssignmentPattern":
|
|
262
|
-
return extractPatternNames(pattern.left);
|
|
263
|
-
default:
|
|
264
|
-
return [];
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
// Free variable collection (spec §4.2)
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
|
-
/**
|
|
271
|
-
* Collect free variable references in the AST.
|
|
272
|
-
*
|
|
273
|
-
* A "free variable" is an Identifier that appears in a read position
|
|
274
|
-
* and is not declared within the block itself. Only names that are in
|
|
275
|
-
* the current env keys set will be injected as imports.
|
|
276
|
-
*/
|
|
277
|
-
function collectFreeVariables(ast, declaredNames) {
|
|
278
|
-
const references = new Set();
|
|
279
|
-
const localDecls = new Set(declaredNames);
|
|
280
|
-
// Also collect names from all top-level variable declarations
|
|
281
|
-
for (const node of ast.body) {
|
|
282
|
-
const names = extractDeclaredNames(node);
|
|
283
|
-
for (const name of names) {
|
|
284
|
-
localDecls.add(name);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
for (const node of ast.body) {
|
|
288
|
-
collectReferences(node, references, localDecls);
|
|
289
|
-
}
|
|
290
|
-
return [...references];
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Walk the AST collecting identifier references that are not local declarations.
|
|
294
|
-
*/
|
|
295
|
-
function collectReferences(node, references, localDecls) {
|
|
296
|
-
if (!node || typeof node !== "object")
|
|
297
|
-
return;
|
|
298
|
-
if (node.type === "Identifier") {
|
|
299
|
-
if (!localDecls.has(node.name)) {
|
|
300
|
-
references.add(node.name);
|
|
301
|
-
}
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
// Skip property names in member expressions (obj.prop — prop is not a reference)
|
|
305
|
-
if (node.type === "MemberExpression" && !node.computed) {
|
|
306
|
-
collectReferences(node.object, references, localDecls);
|
|
307
|
-
// Skip node.property — it's a property name, not a variable reference
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
// Skip key names in object properties (not computed)
|
|
311
|
-
if (node.type === "Property" && !node.computed) {
|
|
312
|
-
// Only visit the value, not the key
|
|
313
|
-
collectReferences(node.value, references, localDecls);
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
// For variable declarations, skip the declared name(s) and visit init
|
|
317
|
-
if (node.type === "VariableDeclarator") {
|
|
318
|
-
// Don't visit the id (it's a declaration, not a reference)
|
|
319
|
-
collectReferences(node.init, references, localDecls);
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
for (const key of Object.keys(node)) {
|
|
323
|
-
if (key === "type" || key === "start" || key === "end")
|
|
324
|
-
continue;
|
|
325
|
-
const child = node[key];
|
|
326
|
-
if (Array.isArray(child)) {
|
|
327
|
-
for (const item of child) {
|
|
328
|
-
if (item && typeof item === "object" && item.type) {
|
|
329
|
-
collectReferences(item, references, localDecls);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
else if (child && typeof child === "object" && child.type) {
|
|
334
|
-
collectReferences(child, references, localDecls);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
// ---------------------------------------------------------------------------
|
|
339
|
-
// Serialization helpers (spec §6.3)
|
|
340
|
-
// ---------------------------------------------------------------------------
|
|
341
|
-
/**
|
|
342
|
-
* Check if a value is JSON-serializable.
|
|
343
|
-
*/
|
|
344
|
-
export function isJson(value) {
|
|
345
|
-
if (value === null)
|
|
346
|
-
return true;
|
|
347
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
348
|
-
return true;
|
|
349
|
-
}
|
|
350
|
-
if (Array.isArray(value)) {
|
|
351
|
-
return value.every(isJson);
|
|
352
|
-
}
|
|
353
|
-
if (typeof value === "object" && value !== null) {
|
|
354
|
-
const proto = Object.getPrototypeOf(value);
|
|
355
|
-
if (proto !== Object.prototype && proto !== null)
|
|
356
|
-
return false;
|
|
357
|
-
return Object.values(value).every(isJson);
|
|
358
|
-
}
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Extract the JSON-serializable subset of exports from the env.
|
|
363
|
-
*
|
|
364
|
-
* Non-serializable values are silently omitted. They remain in
|
|
365
|
-
* env.values as live references during this run but are absent from
|
|
366
|
-
* the diagnostic journal.
|
|
367
|
-
*/
|
|
368
|
-
export function serializeExports(env, names) {
|
|
369
|
-
const result = {};
|
|
370
|
-
for (const name of names) {
|
|
371
|
-
const value = env[name];
|
|
372
|
-
if (isJson(value)) {
|
|
373
|
-
result[name] = value;
|
|
374
|
-
}
|
|
375
|
-
// Non-serializable values silently omitted.
|
|
376
|
-
}
|
|
377
|
-
return result;
|
|
378
|
-
}
|