@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.
Files changed (94) hide show
  1. package/esm/{testing/npm/src/src → src}/assertions.js +15 -24
  2. package/esm/{testing/npm/src/src → src}/handlers.js +140 -10
  3. package/esm/{testing/npm/src/src → src}/vocabulary.js +3 -0
  4. package/package.json +3 -3
  5. package/types/src/assertions.d.ts +11 -1
  6. package/types/src/handlers.d.ts +1 -0
  7. package/esm/core/mod.js +0 -55
  8. package/esm/core/src/api.js +0 -31
  9. package/esm/core/src/collect.js +0 -24
  10. package/esm/core/src/component-api.js +0 -47
  11. package/esm/core/src/content-context.js +0 -23
  12. package/esm/core/src/deno-compiler.js +0 -51
  13. package/esm/core/src/errors.js +0 -13
  14. package/esm/core/src/eval-context.js +0 -13
  15. package/esm/core/src/eval-handler.js +0 -79
  16. package/esm/core/src/eval-interpolate.js +0 -63
  17. package/esm/core/src/eval-transform.js +0 -378
  18. package/esm/core/src/execute.js +0 -378
  19. package/esm/core/src/expand.js +0 -988
  20. package/esm/core/src/frontmatter.js +0 -98
  21. package/esm/core/src/heal.js +0 -29
  22. package/esm/core/src/interpolate.js +0 -39
  23. package/esm/core/src/modifiers/daemon.js +0 -60
  24. package/esm/core/src/modifiers/persist.js +0 -35
  25. package/esm/core/src/modifiers/timeout.js +0 -55
  26. package/esm/core/src/modifiers.js +0 -113
  27. package/esm/core/src/output/normalize.js +0 -33
  28. package/esm/core/src/output/terminal.js +0 -20
  29. package/esm/core/src/render.js +0 -30
  30. package/esm/core/src/replay-stream.js +0 -58
  31. package/esm/core/src/sample-api.js +0 -45
  32. package/esm/core/src/scanner.js +0 -632
  33. package/esm/core/src/temp-file-compiler.js +0 -68
  34. package/esm/core/src/types.js +0 -7
  35. package/esm/core/src/validate.js +0 -82
  36. package/esm/durable-streams/combinators.js +0 -227
  37. package/esm/durable-streams/context.js +0 -13
  38. package/esm/durable-streams/divergence.js +0 -56
  39. package/esm/durable-streams/each.js +0 -193
  40. package/esm/durable-streams/effect.js +0 -273
  41. package/esm/durable-streams/ephemeral.js +0 -80
  42. package/esm/durable-streams/errors.js +0 -92
  43. package/esm/durable-streams/http-stream.js +0 -207
  44. package/esm/durable-streams/mod.js +0 -34
  45. package/esm/durable-streams/operations.js +0 -88
  46. package/esm/durable-streams/replay-guard.js +0 -87
  47. package/esm/durable-streams/replay-index.js +0 -132
  48. package/esm/durable-streams/run.js +0 -137
  49. package/esm/durable-streams/serialize.js +0 -65
  50. package/esm/durable-streams/stream.js +0 -56
  51. package/esm/durable-streams/types.js +0 -10
  52. package/esm/runtime/apis.js +0 -237
  53. package/esm/runtime/find-free-port.js +0 -33
  54. package/esm/runtime/mod.js +0 -20
  55. /package/esm/{testing/npm/src/_dnt.polyfills.js → _dnt.polyfills.js} +0 -0
  56. /package/esm/{testing/npm/src/_dnt.shims.js → _dnt.shims.js} +0 -0
  57. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/almost_equals.js +0 -0
  58. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/array_includes.js +0 -0
  59. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assert.js +0 -0
  60. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assertion_error.js +0 -0
  61. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equal.js +0 -0
  62. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equals.js +0 -0
  63. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/exists.js +0 -0
  64. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/fail.js +0 -0
  65. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/false.js +0 -0
  66. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater.js +0 -0
  67. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater_or_equal.js +0 -0
  68. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/instance_of.js +0 -0
  69. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/is_error.js +0 -0
  70. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less.js +0 -0
  71. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less_or_equal.js +0 -0
  72. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/match.js +0 -0
  73. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/mod.js +0 -0
  74. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_equals.js +0 -0
  75. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_instance_of.js +0 -0
  76. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_match.js +0 -0
  77. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_strict_equals.js +0 -0
  78. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/object_match.js +0 -0
  79. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/rejects.js +0 -0
  80. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/strict_equals.js +0 -0
  81. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/string_includes.js +0 -0
  82. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/throws.js +0 -0
  83. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unimplemented.js +0 -0
  84. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unreachable.js +0 -0
  85. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/build_message.js +0 -0
  86. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff.js +0 -0
  87. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff_str.js +0 -0
  88. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/format.js +0 -0
  89. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/styles.js +0 -0
  90. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/types.js +0 -0
  91. /package/esm/{testing/npm/src/mod.js → mod.js} +0 -0
  92. /package/esm/{testing/npm/src/src → src}/journal.js +0 -0
  93. /package/esm/{testing/npm/src/src → src}/test-api.js +0 -0
  94. /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
- }