@executablemd/testing 0.3.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 (137) hide show
  1. package/LICENSE +21 -0
  2. package/esm/core/mod.js +55 -0
  3. package/esm/core/src/api.js +31 -0
  4. package/esm/core/src/collect.js +24 -0
  5. package/esm/core/src/component-api.js +47 -0
  6. package/esm/core/src/content-context.js +23 -0
  7. package/esm/core/src/deno-compiler.js +51 -0
  8. package/esm/core/src/errors.js +13 -0
  9. package/esm/core/src/eval-context.js +13 -0
  10. package/esm/core/src/eval-handler.js +79 -0
  11. package/esm/core/src/eval-interpolate.js +63 -0
  12. package/esm/core/src/eval-transform.js +378 -0
  13. package/esm/core/src/execute.js +378 -0
  14. package/esm/core/src/expand.js +988 -0
  15. package/esm/core/src/frontmatter.js +98 -0
  16. package/esm/core/src/heal.js +29 -0
  17. package/esm/core/src/interpolate.js +39 -0
  18. package/esm/core/src/modifiers/daemon.js +60 -0
  19. package/esm/core/src/modifiers/persist.js +35 -0
  20. package/esm/core/src/modifiers/timeout.js +55 -0
  21. package/esm/core/src/modifiers.js +113 -0
  22. package/esm/core/src/output/normalize.js +33 -0
  23. package/esm/core/src/output/terminal.js +20 -0
  24. package/esm/core/src/render.js +30 -0
  25. package/esm/core/src/replay-stream.js +58 -0
  26. package/esm/core/src/sample-api.js +45 -0
  27. package/esm/core/src/scanner.js +632 -0
  28. package/esm/core/src/temp-file-compiler.js +68 -0
  29. package/esm/core/src/types.js +7 -0
  30. package/esm/core/src/validate.js +82 -0
  31. package/esm/durable-streams/combinators.js +227 -0
  32. package/esm/durable-streams/context.js +13 -0
  33. package/esm/durable-streams/divergence.js +56 -0
  34. package/esm/durable-streams/each.js +193 -0
  35. package/esm/durable-streams/effect.js +273 -0
  36. package/esm/durable-streams/ephemeral.js +80 -0
  37. package/esm/durable-streams/errors.js +92 -0
  38. package/esm/durable-streams/http-stream.js +207 -0
  39. package/esm/durable-streams/mod.js +34 -0
  40. package/esm/durable-streams/operations.js +88 -0
  41. package/esm/durable-streams/replay-guard.js +87 -0
  42. package/esm/durable-streams/replay-index.js +132 -0
  43. package/esm/durable-streams/run.js +137 -0
  44. package/esm/durable-streams/serialize.js +65 -0
  45. package/esm/durable-streams/stream.js +56 -0
  46. package/esm/durable-streams/types.js +10 -0
  47. package/esm/package.json +3 -0
  48. package/esm/runtime/apis.js +237 -0
  49. package/esm/runtime/find-free-port.js +33 -0
  50. package/esm/runtime/mod.js +20 -0
  51. package/esm/testing/npm/src/_dnt.polyfills.js +1 -0
  52. package/esm/testing/npm/src/_dnt.shims.js +57 -0
  53. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/almost_equals.js +47 -0
  54. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/array_includes.js +65 -0
  55. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/assert.js +23 -0
  56. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/assertion_error.js +30 -0
  57. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/equal.js +194 -0
  58. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/equals.js +61 -0
  59. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/exists.js +27 -0
  60. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/fail.js +20 -0
  61. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/false.js +23 -0
  62. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/greater.js +29 -0
  63. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.js +29 -0
  64. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/instance_of.js +52 -0
  65. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/is_error.js +53 -0
  66. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/less.js +28 -0
  67. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/less_or_equal.js +29 -0
  68. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/match.js +26 -0
  69. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/mod.js +45 -0
  70. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/not_equals.js +39 -0
  71. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/not_instance_of.js +29 -0
  72. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/not_match.js +26 -0
  73. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.js +32 -0
  74. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/object_match.js +177 -0
  75. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/rejects.js +53 -0
  76. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/strict_equals.js +58 -0
  77. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/string_includes.js +26 -0
  78. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/throws.js +44 -0
  79. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/unimplemented.js +20 -0
  80. package/esm/testing/npm/src/deps/jsr.io/@std/assert/1.0.19/unreachable.js +20 -0
  81. package/esm/testing/npm/src/deps/jsr.io/@std/internal/1.0.14/build_message.js +116 -0
  82. package/esm/testing/npm/src/deps/jsr.io/@std/internal/1.0.14/diff.js +277 -0
  83. package/esm/testing/npm/src/deps/jsr.io/@std/internal/1.0.14/diff_str.js +181 -0
  84. package/esm/testing/npm/src/deps/jsr.io/@std/internal/1.0.14/format.js +70 -0
  85. package/esm/testing/npm/src/deps/jsr.io/@std/internal/1.0.14/styles.js +207 -0
  86. package/esm/testing/npm/src/deps/jsr.io/@std/internal/1.0.14/types.js +3 -0
  87. package/esm/testing/npm/src/mod.js +27 -0
  88. package/esm/testing/npm/src/src/assertions.js +250 -0
  89. package/esm/testing/npm/src/src/handlers.js +245 -0
  90. package/esm/testing/npm/src/src/journal.js +152 -0
  91. package/esm/testing/npm/src/src/test-api.js +33 -0
  92. package/esm/testing/npm/src/src/use-testing.js +79 -0
  93. package/esm/testing/npm/src/src/vocabulary.js +111 -0
  94. package/package.json +35 -0
  95. package/types/_dnt.polyfills.d.ts +6 -0
  96. package/types/_dnt.shims.d.ts +1 -0
  97. package/types/deps/jsr.io/@std/assert/1.0.19/almost_equals.d.ts +29 -0
  98. package/types/deps/jsr.io/@std/assert/1.0.19/array_includes.d.ts +28 -0
  99. package/types/deps/jsr.io/@std/assert/1.0.19/assert.d.ts +16 -0
  100. package/types/deps/jsr.io/@std/assert/1.0.19/assertion_error.d.ts +25 -0
  101. package/types/deps/jsr.io/@std/assert/1.0.19/equal.d.ts +24 -0
  102. package/types/deps/jsr.io/@std/assert/1.0.19/equals.d.ts +36 -0
  103. package/types/deps/jsr.io/@std/assert/1.0.19/exists.d.ts +17 -0
  104. package/types/deps/jsr.io/@std/assert/1.0.19/fail.d.ts +14 -0
  105. package/types/deps/jsr.io/@std/assert/1.0.19/false.d.ts +18 -0
  106. package/types/deps/jsr.io/@std/assert/1.0.19/greater.d.ts +19 -0
  107. package/types/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.d.ts +19 -0
  108. package/types/deps/jsr.io/@std/assert/1.0.19/instance_of.d.ts +22 -0
  109. package/types/deps/jsr.io/@std/assert/1.0.19/is_error.d.ts +24 -0
  110. package/types/deps/jsr.io/@std/assert/1.0.19/less.d.ts +18 -0
  111. package/types/deps/jsr.io/@std/assert/1.0.19/less_or_equal.d.ts +19 -0
  112. package/types/deps/jsr.io/@std/assert/1.0.19/match.d.ts +17 -0
  113. package/types/deps/jsr.io/@std/assert/1.0.19/mod.d.ts +43 -0
  114. package/types/deps/jsr.io/@std/assert/1.0.19/not_equals.d.ts +26 -0
  115. package/types/deps/jsr.io/@std/assert/1.0.19/not_instance_of.d.ts +19 -0
  116. package/types/deps/jsr.io/@std/assert/1.0.19/not_match.d.ts +17 -0
  117. package/types/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.d.ts +22 -0
  118. package/types/deps/jsr.io/@std/assert/1.0.19/object_match.d.ts +28 -0
  119. package/types/deps/jsr.io/@std/assert/1.0.19/rejects.d.ts +41 -0
  120. package/types/deps/jsr.io/@std/assert/1.0.19/strict_equals.d.ts +23 -0
  121. package/types/deps/jsr.io/@std/assert/1.0.19/string_includes.d.ts +17 -0
  122. package/types/deps/jsr.io/@std/assert/1.0.19/throws.d.ts +44 -0
  123. package/types/deps/jsr.io/@std/assert/1.0.19/unimplemented.d.ts +14 -0
  124. package/types/deps/jsr.io/@std/assert/1.0.19/unreachable.d.ts +14 -0
  125. package/types/deps/jsr.io/@std/internal/1.0.14/build_message.d.ts +79 -0
  126. package/types/deps/jsr.io/@std/internal/1.0.14/diff.d.ts +139 -0
  127. package/types/deps/jsr.io/@std/internal/1.0.14/diff_str.d.ts +98 -0
  128. package/types/deps/jsr.io/@std/internal/1.0.14/format.d.ts +28 -0
  129. package/types/deps/jsr.io/@std/internal/1.0.14/styles.d.ts +158 -0
  130. package/types/deps/jsr.io/@std/internal/1.0.14/types.d.ts +24 -0
  131. package/types/mod.d.ts +29 -0
  132. package/types/src/assertions.d.ts +55 -0
  133. package/types/src/handlers.d.ts +18 -0
  134. package/types/src/journal.d.ts +29 -0
  135. package/types/src/test-api.d.ts +54 -0
  136. package/types/src/use-testing.d.ts +25 -0
  137. package/types/src/vocabulary.d.ts +34 -0
@@ -0,0 +1,378 @@
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
+ }