@executablemd/testing 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
package/esm/core/src/execute.js
DELETED
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entry point — execute (spec §7).
|
|
3
|
-
*
|
|
4
|
-
* Wires together the boundary scanner, component import, expansion engine,
|
|
5
|
-
* modifier system, and journal infrastructure. `execute` is delivered
|
|
6
|
-
* through the Execution Api so extensions can decorate the execution
|
|
7
|
-
* lifecycle as middleware.
|
|
8
|
-
*
|
|
9
|
-
* Journal runtime integration is concentrated at execution boundaries.
|
|
10
|
-
* See DEC-005 in specs/decisions.md.
|
|
11
|
-
*/
|
|
12
|
-
import { Err, Ok, scoped, spawn, withResolvers, until } from "effection";
|
|
13
|
-
import { createApi } from "@effectionx/context-api";
|
|
14
|
-
import { durableRun, createDurableOperation, ephemeral, } from "@executablemd/durable-streams";
|
|
15
|
-
import { exec, readTextFile, stat, cwd } from "@executablemd/runtime";
|
|
16
|
-
import { createReplayStream } from "./replay-stream.ts";
|
|
17
|
-
import { useDenoCompiler } from "./deno-compiler.ts";
|
|
18
|
-
import { useTempFileCompiler } from "./temp-file-compiler.ts";
|
|
19
|
-
import { scanSegments } from "./scanner.ts";
|
|
20
|
-
import { parseFrontmatter } from "./frontmatter.ts";
|
|
21
|
-
import { expandSegments, expandBody, bodyHasOutput, validateOutputPlacement, createBlockCounter, } from "./expand.ts";
|
|
22
|
-
import { Component, importComponent } from "./component-api.ts";
|
|
23
|
-
import { renderSegment } from "./render.ts";
|
|
24
|
-
import { DocumentOutput } from "./api.ts";
|
|
25
|
-
import { composeModifierChain, buildCommand, createModifierRegistry, useCodeBlock, } from "./modifiers.ts";
|
|
26
|
-
import { evalFactory } from "./eval-handler.ts";
|
|
27
|
-
import { persistFactory } from "./modifiers/persist.ts";
|
|
28
|
-
import { timeoutFactory } from "./modifiers/timeout.ts";
|
|
29
|
-
import { daemonFactory } from "./modifiers/daemon.ts";
|
|
30
|
-
import { useEvalScope } from "@effectionx/scope-eval";
|
|
31
|
-
import { Stdio } from "@effectionx/process";
|
|
32
|
-
// Re-export gray-matter — we use it for YAML frontmatter extraction
|
|
33
|
-
import matter from "gray-matter";
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// durableImportComponent (spec §4.3)
|
|
36
|
-
//
|
|
37
|
-
// This is a Workflow<ComponentDefinition | FunctionComponentDefinition> —
|
|
38
|
-
// it yields durable import effects and returns either markdown or function
|
|
39
|
-
// component definitions.
|
|
40
|
-
// See DEC-001, DEC-004 in specs/decisions.md.
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
function* durableImportComponent(name, rootDocPath, searchPaths) {
|
|
43
|
-
const result = (yield createDurableOperation({ type: "import_component", name }, function* () {
|
|
44
|
-
// Resolve the path — runs inside Operation context
|
|
45
|
-
let path;
|
|
46
|
-
if (name === "__root__" && rootDocPath) {
|
|
47
|
-
path = rootDocPath;
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
path = yield* resolveComponentPath(name, searchPaths);
|
|
51
|
-
}
|
|
52
|
-
const content = yield* readTextFile(path);
|
|
53
|
-
return { path, content };
|
|
54
|
-
}));
|
|
55
|
-
// Function component: .ts file — import() the module
|
|
56
|
-
if (result.path.endsWith(".ts")) {
|
|
57
|
-
// Resolve to absolute path for dynamic import
|
|
58
|
-
const currentDir = yield* ephemeral(cwd());
|
|
59
|
-
const absolutePath = result.path.startsWith("/") ? result.path : `${currentDir}/${result.path}`;
|
|
60
|
-
const mod = (yield* ephemeral(until(import(`file://${absolutePath}`))));
|
|
61
|
-
const fn = mod.default;
|
|
62
|
-
if (typeof fn !== "function") {
|
|
63
|
-
throw new Error(`Function component "${name}" at ${result.path} must have a default export that is a generator function`);
|
|
64
|
-
}
|
|
65
|
-
const typedFn = fn;
|
|
66
|
-
const inputs = (mod.inputs ?? {});
|
|
67
|
-
return {
|
|
68
|
-
kind: "function",
|
|
69
|
-
name,
|
|
70
|
-
path: result.path,
|
|
71
|
-
inputs,
|
|
72
|
-
fn: typedFn,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
// Markdown component: parse at runtime — deterministic from content
|
|
76
|
-
const parsed = matter(result.content);
|
|
77
|
-
const { meta, inputs } = parseFrontmatter(parsed.data);
|
|
78
|
-
// The markdown body is a verbatim suffix of the raw file, so the body start
|
|
79
|
-
// is computed by length — never by content search, which could false-match
|
|
80
|
-
// body text repeated inside frontmatter. The invariant check turns any
|
|
81
|
-
// gray-matter normalization surprise into a loud error instead of silently
|
|
82
|
-
// wrong source positions.
|
|
83
|
-
const bodyStart = result.content.length - parsed.content.length;
|
|
84
|
-
if (result.content.slice(bodyStart) !== parsed.content) {
|
|
85
|
-
throw new Error(`frontmatter parse did not preserve the markdown body verbatim: ${result.path}`);
|
|
86
|
-
}
|
|
87
|
-
let baseLine = 1;
|
|
88
|
-
for (let i = 0; i < bodyStart; i++) {
|
|
89
|
-
if (result.content[i] === "\n") {
|
|
90
|
-
baseLine++;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const bodySegments = scanSegments(parsed.content, {
|
|
94
|
-
path: result.path,
|
|
95
|
-
baseOffset: bodyStart,
|
|
96
|
-
baseLine,
|
|
97
|
-
});
|
|
98
|
-
return {
|
|
99
|
-
kind: "markdown",
|
|
100
|
-
name,
|
|
101
|
-
path: result.path,
|
|
102
|
-
meta,
|
|
103
|
-
inputs,
|
|
104
|
-
bodySegments,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
// Component path resolution — runs inside Operation context (not Workflow)
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
function* resolveComponentPath(name, searchPaths) {
|
|
111
|
-
const baseName = name.replace(/\./g, "/");
|
|
112
|
-
for (const dir of searchPaths) {
|
|
113
|
-
// Try {dir}/{Name}.md (backward compat — .md wins over .ts)
|
|
114
|
-
const mdCandidate = normalizePath(dir === "." ? `${baseName}.md` : `${dir}/${baseName}.md`);
|
|
115
|
-
const mdStat = yield* stat(mdCandidate);
|
|
116
|
-
if (mdStat.exists && mdStat.isFile) {
|
|
117
|
-
return mdCandidate;
|
|
118
|
-
}
|
|
119
|
-
// Try {dir}/{Name}.ts (function component)
|
|
120
|
-
const tsCandidate = normalizePath(dir === "." ? `${baseName}.ts` : `${dir}/${baseName}.ts`);
|
|
121
|
-
const tsStat = yield* stat(tsCandidate);
|
|
122
|
-
if (tsStat.exists && tsStat.isFile) {
|
|
123
|
-
return tsCandidate;
|
|
124
|
-
}
|
|
125
|
-
// Try {dir}/{Name}/index.md
|
|
126
|
-
const indexMdCandidate = normalizePath(dir === "." ? `${baseName}/index.md` : `${dir}/${baseName}/index.md`);
|
|
127
|
-
const indexMdStat = yield* stat(indexMdCandidate);
|
|
128
|
-
if (indexMdStat.exists && indexMdStat.isFile) {
|
|
129
|
-
return indexMdCandidate;
|
|
130
|
-
}
|
|
131
|
-
// Try {dir}/{Name}/index.ts
|
|
132
|
-
const indexTsCandidate = normalizePath(dir === "." ? `${baseName}/index.ts` : `${dir}/${baseName}/index.ts`);
|
|
133
|
-
const indexTsStat = yield* stat(indexTsCandidate);
|
|
134
|
-
if (indexTsStat.exists && indexTsStat.isFile) {
|
|
135
|
-
return indexTsCandidate;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
throw new Error(`Cannot resolve component: ${name} (searched: ${searchPaths.join(", ")})`);
|
|
139
|
-
}
|
|
140
|
-
/** Strip leading ./ from paths for workspace-relative normalization. */
|
|
141
|
-
function normalizePath(path) {
|
|
142
|
-
return path.replace(/^\.\//, "");
|
|
143
|
-
}
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
// Built-in modifier handlers (spec §3.3)
|
|
146
|
-
//
|
|
147
|
-
// Modifier handlers return Workflow<CodeBlockResult> since terminal
|
|
148
|
-
// handlers (exec) yield DurableEffects. See DEC-003 in specs/decisions.md.
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
const execFactory = (_params) => (_args, _next) => (function* () {
|
|
151
|
-
const context = yield* useCodeBlock();
|
|
152
|
-
const command = buildCommand(context.language, context.content);
|
|
153
|
-
const result = (yield createDurableOperation({
|
|
154
|
-
type: "exec",
|
|
155
|
-
name: `exec:${context.content.slice(0, 40).replace(/\n/g, " ")}`,
|
|
156
|
-
command: command,
|
|
157
|
-
}, function* () {
|
|
158
|
-
const execResult = yield* exec({
|
|
159
|
-
command,
|
|
160
|
-
timeout: 30_000,
|
|
161
|
-
});
|
|
162
|
-
return execResult;
|
|
163
|
-
}));
|
|
164
|
-
return {
|
|
165
|
-
output: result.stdout,
|
|
166
|
-
exitCode: result.exitCode,
|
|
167
|
-
stderr: result.stderr,
|
|
168
|
-
};
|
|
169
|
-
})();
|
|
170
|
-
const silentFactory = (_params) => (_args, next) => (function* () {
|
|
171
|
-
yield* next(); // inner chain runs — exec journals its result
|
|
172
|
-
return { output: "", exitCode: 0, stderr: "" };
|
|
173
|
-
})();
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
// Document workflow (spec §7.1)
|
|
176
|
-
//
|
|
177
|
-
// This is a Workflow<string> — it yields DurableEffects (via
|
|
178
|
-
// durableImportComponent, exec handler). Non-journaled work
|
|
179
|
-
// (interpolation, validation, parsing) runs as synchronous function
|
|
180
|
-
// calls inside the expansion engine. See DEC-002.
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
function* documentWorkflow() {
|
|
183
|
-
// Import root — same pipeline as any component. The provider middleware
|
|
184
|
-
// installed by execute maps "__root__" to the document path. The
|
|
185
|
-
// ephemeral() wrapper bridges typing only — the import inside remains a
|
|
186
|
-
// durable, journaled operation.
|
|
187
|
-
const root = yield* ephemeral(importComponent("__root__"));
|
|
188
|
-
if (root.kind === "function") {
|
|
189
|
-
throw new Error("Root document must be a markdown file, not a function component");
|
|
190
|
-
}
|
|
191
|
-
const rootEnv = { values: {} };
|
|
192
|
-
// Per-root-segment emission loop (spec §9).
|
|
193
|
-
// Mutable counter preserves deterministic blockIds across
|
|
194
|
-
// per-segment expansion calls (see spec §6.1).
|
|
195
|
-
const counter = createBlockCounter();
|
|
196
|
-
// The root binding environment is installed as scope-local middleware
|
|
197
|
-
// around the entire loop so all segments share it. Resources spawned by
|
|
198
|
-
// `persist` blocks are retained in the eval scope until expansion
|
|
199
|
-
// completes, then torn down.
|
|
200
|
-
const scopedExpansion = scoped(function* () {
|
|
201
|
-
yield* Component.around({ env: () => rootEnv }, { at: "min" });
|
|
202
|
-
// Structural preflight (spec §6.9): a root with misplaced <Output>
|
|
203
|
-
// executes no body side effects; the aggregate diagnostic renders as a
|
|
204
|
-
// comment (root policy is "collect").
|
|
205
|
-
const placementError = validateOutputPlacement(root.bodySegments);
|
|
206
|
-
if (placementError) {
|
|
207
|
-
const text = renderSegment(placementError);
|
|
208
|
-
yield* ephemeral(DocumentOutput.operations.output(text));
|
|
209
|
-
return text;
|
|
210
|
-
}
|
|
211
|
-
// A root declaring top-level <Output> buffers completely (spec §5.4):
|
|
212
|
-
// execute the whole body, then emit the selected regions only after
|
|
213
|
-
// successful completion. A documentation failure throws before any emit,
|
|
214
|
-
// so no partial output is produced.
|
|
215
|
-
if (bodyHasOutput(root.bodySegments)) {
|
|
216
|
-
const expanded = yield* expandBody(root.bodySegments, [], root.meta, {}, new Set(), counter, undefined);
|
|
217
|
-
const text = expanded.map(renderSegment).join("");
|
|
218
|
-
// An empty buffered root emits no output event.
|
|
219
|
-
if (text) {
|
|
220
|
-
yield* ephemeral(DocumentOutput.operations.output(text));
|
|
221
|
-
}
|
|
222
|
-
return text;
|
|
223
|
-
}
|
|
224
|
-
// Per-root-segment emission loop for roots without <Output> (spec §5.4).
|
|
225
|
-
const chunks = [];
|
|
226
|
-
for (const segment of root.bodySegments) {
|
|
227
|
-
const expanded = yield* expandSegments([segment], root.meta, {}, new Set(), counter);
|
|
228
|
-
for (const resolved of expanded) {
|
|
229
|
-
const text = renderSegment(resolved);
|
|
230
|
-
if (text) {
|
|
231
|
-
// Emit through the Document Output Api (spec §9).
|
|
232
|
-
// ephemeral() bridges from Workflow (durable) to Operation
|
|
233
|
-
// (non-durable) — output emission is a derived side effect,
|
|
234
|
-
// not journaled.
|
|
235
|
-
yield* ephemeral(DocumentOutput.operations.output(text));
|
|
236
|
-
chunks.push(text);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return chunks.join("");
|
|
241
|
-
});
|
|
242
|
-
return yield* ephemeral(scopedExpansion);
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Execute a markdown document as a durable workflow.
|
|
246
|
-
*
|
|
247
|
-
* Returns a `DocumentExecution` — an operation you can `yield*` for the
|
|
248
|
-
* completion `Result<string>`, with a `.output` stream for chunk-by-chunk
|
|
249
|
-
* consumption. Once the handle exists, completion never throws: every
|
|
250
|
-
* later failure — document, infrastructure, or policy middleware — closes
|
|
251
|
-
* `output` (with the complete or partial rendered text) and resolves
|
|
252
|
-
* `Err(error)`.
|
|
253
|
-
*
|
|
254
|
-
* Simple — get the outcome:
|
|
255
|
-
*
|
|
256
|
-
* ```ts
|
|
257
|
-
* const execution = yield* execute(options);
|
|
258
|
-
* const result = yield* execution;
|
|
259
|
-
* if (result.ok) {
|
|
260
|
-
* console.log(result.value);
|
|
261
|
-
* }
|
|
262
|
-
* ```
|
|
263
|
-
*
|
|
264
|
-
* Streaming — consume chunks as they arrive:
|
|
265
|
-
*
|
|
266
|
-
* ```ts
|
|
267
|
-
* const execution = yield* execute(options);
|
|
268
|
-
* const output = yield* forEach(function* (chunk) {
|
|
269
|
-
* process.stdout.write(chunk);
|
|
270
|
-
* }, execution.output);
|
|
271
|
-
* ```
|
|
272
|
-
*/
|
|
273
|
-
function* executeDocument(options) {
|
|
274
|
-
const { docPath, stream, componentDirs = ["components", "."], modifiers: customModifiers = {}, } = options;
|
|
275
|
-
// Build modifier registry — pure data, no scope side effects.
|
|
276
|
-
const registry = createModifierRegistry();
|
|
277
|
-
registry.set("exec", execFactory);
|
|
278
|
-
registry.set("silent", silentFactory);
|
|
279
|
-
registry.set("eval", evalFactory);
|
|
280
|
-
registry.set("persist", persistFactory);
|
|
281
|
-
registry.set("timeout", timeoutFactory);
|
|
282
|
-
registry.set("daemon", daemonFactory);
|
|
283
|
-
for (const [name, handler] of Object.entries(customModifiers)) {
|
|
284
|
-
registry.set(name, handler);
|
|
285
|
-
}
|
|
286
|
-
// ---------------------------------------------------------------------------
|
|
287
|
-
// Document execution.
|
|
288
|
-
//
|
|
289
|
-
// The workflow runs in a spawned child scope that contains all
|
|
290
|
-
// execution state: eval context/scope and the
|
|
291
|
-
// DocumentOutput→channel bridge. Nothing leaks onto the caller's scope.
|
|
292
|
-
//
|
|
293
|
-
// withResolvers captures the completion result so `yield* execution`
|
|
294
|
-
// can wait for it without needing to await the spawned Task directly
|
|
295
|
-
// (which would propagate errors through scope teardown).
|
|
296
|
-
// ---------------------------------------------------------------------------
|
|
297
|
-
// Replay-safe transport: late subscribers receive every chunk and the
|
|
298
|
-
// close value, so subscription readiness before first emission is never
|
|
299
|
-
// required (spec §9; see replay-stream.ts).
|
|
300
|
-
const channel = createReplayStream();
|
|
301
|
-
const { operation, resolve } = withResolvers();
|
|
302
|
-
yield* spawn(function* () {
|
|
303
|
-
let emitted = false;
|
|
304
|
-
let emittedText = "";
|
|
305
|
-
// The ENTIRE setup and workflow sit inside one error boundary: once the
|
|
306
|
-
// handle exists, any failure closes output (with whatever rendered so
|
|
307
|
-
// far) and resolves Err — completion never throws.
|
|
308
|
-
try {
|
|
309
|
-
// Install platform-appropriate compiler middleware
|
|
310
|
-
// deno-lint-ignore no-explicit-any
|
|
311
|
-
if (typeof globalThis.Deno !== "undefined") {
|
|
312
|
-
yield* useDenoCompiler();
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
yield* useTempFileCompiler();
|
|
316
|
-
}
|
|
317
|
-
// DocumentOutput → channel bridge (innermost middleware — output flows
|
|
318
|
-
// through caller-installed normalize/terminal middleware first, then here).
|
|
319
|
-
yield* DocumentOutput.around({
|
|
320
|
-
*output([text]) {
|
|
321
|
-
emitted = true;
|
|
322
|
-
emittedText += text;
|
|
323
|
-
yield* channel.send(text);
|
|
324
|
-
},
|
|
325
|
-
});
|
|
326
|
-
yield* Stdio.around({
|
|
327
|
-
*stdout() { },
|
|
328
|
-
*stderr() { },
|
|
329
|
-
});
|
|
330
|
-
// Create per-document eval scope (spec §3.1).
|
|
331
|
-
// Created in the same scope as durableRun so that DurableCtx
|
|
332
|
-
// (set by durableRun) is visible to eval code that calls
|
|
333
|
-
// renderChildren → importComponent → createDurableOperation.
|
|
334
|
-
const rootEvalScope = yield* useEvalScope();
|
|
335
|
-
// Install the document's runtime Component providers before durableRun
|
|
336
|
-
// so the workflow inherits them: component import, modifier execution,
|
|
337
|
-
// and the root eval scope.
|
|
338
|
-
yield* Component.around({
|
|
339
|
-
*importComponent([name], _next) {
|
|
340
|
-
return yield* durableImportComponent(name, name === "__root__" ? docPath : undefined, componentDirs);
|
|
341
|
-
},
|
|
342
|
-
*applyModifiers([modifiers, context], _next) {
|
|
343
|
-
const chain = composeModifierChain(modifiers, context, registry);
|
|
344
|
-
return yield* chain();
|
|
345
|
-
},
|
|
346
|
-
evalScope: () => rootEvalScope,
|
|
347
|
-
}, { at: "min" });
|
|
348
|
-
const output = yield* durableRun(() => documentWorkflow(), {
|
|
349
|
-
stream,
|
|
350
|
-
});
|
|
351
|
-
// Preserve output for any synchronous completion path that did not emit
|
|
352
|
-
// through the streaming API.
|
|
353
|
-
if (!emitted && output) {
|
|
354
|
-
yield* DocumentOutput.operations.output(output);
|
|
355
|
-
}
|
|
356
|
-
yield* channel.close(output);
|
|
357
|
-
resolve(Ok(output));
|
|
358
|
-
}
|
|
359
|
-
catch (error) {
|
|
360
|
-
// Close with everything already emitted — diagnostics produced before
|
|
361
|
-
// an abort stay visible to consumers of the close value.
|
|
362
|
-
yield* channel.close(emittedText);
|
|
363
|
-
resolve(Err(error instanceof Error ? error : new Error(String(error))));
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
return {
|
|
367
|
-
*[Symbol.iterator]() {
|
|
368
|
-
return yield* operation;
|
|
369
|
-
},
|
|
370
|
-
output: channel,
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
export const Execution = createApi("Execution", {
|
|
374
|
-
*execute(options) {
|
|
375
|
-
return yield* executeDocument(options);
|
|
376
|
-
},
|
|
377
|
-
});
|
|
378
|
-
export const { execute } = Execution.operations;
|