@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Taras Mankovski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Executable MDX — public API.
3
+ *
4
+ * Treats markdown documents as durable workflows: text is emitted immediately,
5
+ * component references are resolved and expanded recursively, and code blocks
6
+ * marked as executable are run via durableExec.
7
+ */
8
+ export { ephemeral } from "@executablemd/durable-streams";
9
+ export { healSegment } from "./src/heal.ts";
10
+ export { combine } from "@effectionx/middleware";
11
+ export { useCodeBlock } from "./src/modifiers.ts";
12
+ export { Component, importComponent, applyModifiers, raise, env, evalScope, expandInvocation, codeBlock, persistent, content, } from "./src/component-api.ts";
13
+ // ---------------------------------------------------------------------------
14
+ // Rendering
15
+ // ---------------------------------------------------------------------------
16
+ export { renderSegments } from "./src/render.ts";
17
+ // ---------------------------------------------------------------------------
18
+ // Replay-safe stream
19
+ // ---------------------------------------------------------------------------
20
+ export { createReplayStream } from "./src/replay-stream.ts";
21
+ export { compileBlock } from "./src/eval-context.ts";
22
+ // ---------------------------------------------------------------------------
23
+ // executable.md Globals (for generated eval modules)
24
+ // ---------------------------------------------------------------------------
25
+ export { useContent } from "./src/content-context.ts";
26
+ export { Sample } from "./src/sample-api.ts";
27
+ export { evalFactory } from "./src/eval-handler.ts";
28
+ export { persistFactory } from "./src/modifiers/persist.ts";
29
+ export { timeoutFactory, parseDuration } from "./src/modifiers/timeout.ts";
30
+ export { daemonFactory } from "./src/modifiers/daemon.ts";
31
+ // ---------------------------------------------------------------------------
32
+ // Eval binding interpolation
33
+ // ---------------------------------------------------------------------------
34
+ export { interpolateEvalBindings } from "./src/eval-interpolate.ts";
35
+ // ---------------------------------------------------------------------------
36
+ // Port allocation
37
+ // ---------------------------------------------------------------------------
38
+ export { findFreePort } from "@executablemd/runtime";
39
+ export { transformBlock, serializeExports, isJson } from "./src/eval-transform.ts";
40
+ // ---------------------------------------------------------------------------
41
+ // Output Api & middleware
42
+ // ---------------------------------------------------------------------------
43
+ export { DocumentOutput } from "./src/api.ts";
44
+ export { useNormalizedOutput } from "./src/output/normalize.ts";
45
+ export { useTerminalOutput } from "./src/output/terminal.ts";
46
+ // ---------------------------------------------------------------------------
47
+ // Document runner
48
+ // ---------------------------------------------------------------------------
49
+ export { execute, Execution } from "./src/execute.ts";
50
+ export { useDenoCompiler } from "./src/deno-compiler.ts";
51
+ export { useTempFileCompiler } from "./src/temp-file-compiler.ts";
52
+ // ---------------------------------------------------------------------------
53
+ // Stream consumption
54
+ // ---------------------------------------------------------------------------
55
+ export { collect } from "./src/collect.ts";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Document Output Api — Effection Api for streaming document output (spec §9).
3
+ *
4
+ * A single Api with one operation: `output`. The core handler is a no-op.
5
+ * Behavior comes from middleware installed via `yield* DocumentOutput.around(...)`
6
+ * and channel delivery.
7
+ *
8
+ * Call sites use `DocumentOutput.operations.output(text)` inside `yield* ephemeral(...)`.
9
+ */
10
+ import { createApi } from "@effectionx/context-api";
11
+ /**
12
+ * The Document Output Api instance.
13
+ *
14
+ * Usage in middleware:
15
+ * ```js
16
+ * yield* DocumentOutput.around({
17
+ * *output([text], next) {
18
+ * const transformed = transform(text);
19
+ * yield* next(transformed);
20
+ * },
21
+ * });
22
+ * ```
23
+ *
24
+ * Usage in the emission loop:
25
+ * ```js
26
+ * yield* ephemeral(DocumentOutput.operations.output(text));
27
+ * ```
28
+ */
29
+ export const DocumentOutput = createApi("DocumentOutput", {
30
+ *output(_text) { },
31
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * collect — wait for a document execution to complete and return the output.
3
+ *
4
+ * Convenience wrapper that unwraps the completion `Result<string>`:
5
+ * the output string on `Ok`, a throw on `Err`.
6
+ *
7
+ * ```ts
8
+ * const output = yield* collect(yield* execute(options));
9
+ * ```
10
+ */
11
+ /**
12
+ * Wait for a document execution to complete and return the full output.
13
+ * Throws the failure when the execution completed with `Err`.
14
+ *
15
+ * @param execution - A `DocumentExecution` as returned by `execute`.
16
+ * @returns The full output string.
17
+ */
18
+ export function* collect(execution) {
19
+ const result = yield* execution;
20
+ if (!result.ok) {
21
+ throw result.error;
22
+ }
23
+ return result.value;
24
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Component Api — contextual operations for component expansion.
3
+ *
4
+ * One public Api replaces the former dependency container (ExpansionContext)
5
+ * and raw Effection context keys. Context-dependent behavior is installed as
6
+ * scope-local middleware via `Component.around(...)`:
7
+ *
8
+ * - Runtime implementations (document import, modifier execution, component
9
+ * state) install at `{ at: "min" }`. Middleware installed in a nested scope
10
+ * runs before inherited middleware, so a component that installs its own
11
+ * `env` shadows its ancestors without leaking into siblings — install
12
+ * inside `scoped()` for automatic removal.
13
+ * - Caller instrumentation and overrides wrap at the default `"max"`.
14
+ */
15
+ import { createApi } from "@effectionx/context-api";
16
+ export const Component = createApi("Component", {
17
+ // deno-lint-ignore require-yield
18
+ *importComponent(name) {
19
+ throw new Error(`Component.importComponent("${name}") has no provider. Install one with ` +
20
+ `Component.around({ importComponent }, { at: "min" }) before expansion.`);
21
+ },
22
+ // deno-lint-ignore require-yield
23
+ *applyModifiers(_modifiers, block) {
24
+ throw new Error(`Component.applyModifiers() has no provider for block "${block.blockId}". Install one ` +
25
+ `with Component.around({ applyModifiers }, { at: "min" }) before expansion.`);
26
+ },
27
+ // deno-lint-ignore require-yield
28
+ *raise(error) {
29
+ return error;
30
+ },
31
+ env: undefined,
32
+ evalScope: undefined,
33
+ // deno-lint-ignore require-yield
34
+ *expandInvocation() {
35
+ return undefined;
36
+ },
37
+ // deno-lint-ignore require-yield
38
+ *codeBlock() {
39
+ throw new Error("Component.codeBlock() has no provider: no code block is executing in this scope.");
40
+ },
41
+ persistent: false,
42
+ // deno-lint-ignore require-yield
43
+ *content(_slot) {
44
+ throw new Error("Component.content() has no provider: not inside a function component invocation.");
45
+ },
46
+ });
47
+ export const { importComponent, applyModifiers, raise, env, evalScope, expandInvocation, codeBlock, persistent, content, } = Component.operations;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * useContent() — the function component equivalent of `<Content />` in
3
+ * markdown components. Ergonomic alias for the Component `content()`
4
+ * operation; the expansion engine installs the slot-rendering middleware
5
+ * around each function component invocation.
6
+ */
7
+ import { content } from "./component-api.ts";
8
+ /**
9
+ * Render children content from the invoking component.
10
+ *
11
+ * @param slotName - Optional slot name. If provided, renders only the
12
+ * content assigned to that slot (matching `<Content slot="name" />`).
13
+ * If omitted, renders the default slot.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const body = yield* useContent();
18
+ * const header = yield* useContent("header");
19
+ * ```
20
+ */
21
+ export function useContent(slotName) {
22
+ return content(slotName);
23
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Deno-specific eval block compiler middleware.
3
+ *
4
+ * Compiles eval block source into generator functions via data: URIs.
5
+ * Standard imports (Effection, executable.md APIs) are captured in the middleware
6
+ * closure — they are not part of the Compiler API interface.
7
+ *
8
+ * Install via `yield* useDenoCompiler()` inside a document execution
9
+ * scope before any eval blocks are processed.
10
+ */
11
+ import { call } from "effection";
12
+ import { API } from "@executablemd/runtime";
13
+ // STANDARD_IMPORTS below resolve at runtime from generated eval modules;
14
+ // without these static anchors, `deno compile --exclude-unused-npm` prunes
15
+ // the packages from the binary and every eval block using them fails.
16
+ import "@effectionx/converge";
17
+ import "@effectionx/fetch";
18
+ /**
19
+ * Standard import statements prepended to every generated eval module.
20
+ * Captured in the middleware closure — not exposed on the public API.
21
+ */
22
+ const STANDARD_IMPORTS = [
23
+ 'import { sleep, spawn, call, resource, useScope, createChannel, each, suspend, createSignal } from "effection";',
24
+ 'import { when } from "@effectionx/converge";',
25
+ 'import { fetch } from "@effectionx/fetch";',
26
+ 'import { useContent, Sample } from "@executablemd/core";',
27
+ 'import { findFreePort } from "@executablemd/runtime";',
28
+ ];
29
+ /**
30
+ * Install the Deno data: URI compiler as middleware on the current scope.
31
+ *
32
+ * Must be called inside an Effection scope (e.g., inside `execute`'s
33
+ * spawned task) before any eval blocks execute.
34
+ */
35
+ export function* useDenoCompiler() {
36
+ yield* API.Compiler.around({
37
+ *compile([source, options], next) {
38
+ void next; // terminal middleware — does not delegate
39
+ const userImports = options?.imports ?? [];
40
+ const allImports = [...STANDARD_IMPORTS, ...userImports];
41
+ const importLines = allImports.join("\n");
42
+ const moduleSource = [importLines, `export default function*(env) {`, source, `}`].join("\n");
43
+ const dataUri = `data:application/typescript,${encodeURIComponent(moduleSource)}`;
44
+ const mod = yield* call(() => import(dataUri));
45
+ if (typeof mod.default !== "function") {
46
+ throw new Error(`useDenoCompiler: expected default export to be a generator function, got ${typeof mod.default}`);
47
+ }
48
+ return mod.default;
49
+ },
50
+ });
51
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Thrown by suppressed-documentation raise middleware (spec §6.9). Generic
3
+ * catches in the engine rethrow it instead of converting it into an
4
+ * ErrorSegment, so documentation fail-fast is never swallowed.
5
+ */
6
+ export class DocumentationError extends Error {
7
+ segment;
8
+ constructor(segment) {
9
+ super(segment.message);
10
+ this.name = "DocumentationError";
11
+ this.segment = segment;
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Eval block compilation (spec §5).
3
+ */
4
+ import { compile as runtimeCompile } from "@executablemd/runtime";
5
+ /**
6
+ * Compile transformed source code into a generator function.
7
+ *
8
+ * Delegates to `@executablemd/runtime` so platform-specific
9
+ * compilation can be provided via API.Compiler middleware.
10
+ */
11
+ export function compileBlock(transformedBodyCode, userImports) {
12
+ return runtimeCompile(transformedBodyCode, { imports: userImports });
13
+ }
@@ -0,0 +1,79 @@
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
+ })();
@@ -0,0 +1,63 @@
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
+ }