@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
|
@@ -46,18 +46,12 @@ function requireRegExp(value) {
|
|
|
46
46
|
}
|
|
47
47
|
throw new AssertionError("match assertions require a RegExp through the expected prop — use expected={/pattern/}");
|
|
48
48
|
}
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// Live expression evaluation
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
49
|
const IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
53
|
-
function evaluateExpression(expression, values) {
|
|
50
|
+
export function evaluateExpression(expression, values) {
|
|
54
51
|
const names = Object.keys(values).filter((name) => IDENTIFIER_RE.test(name));
|
|
55
52
|
const fn = new Function(...names, `return (${expression});`);
|
|
56
53
|
return fn(...names.map((name) => values[name]));
|
|
57
54
|
}
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Guarded value formatting — runs only AFTER the assertion outcome is fixed
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
55
|
function safeFormat(value) {
|
|
62
56
|
try {
|
|
63
57
|
if (typeof value === "string") {
|
|
@@ -81,9 +75,6 @@ function safeFormat(value) {
|
|
|
81
75
|
}
|
|
82
76
|
}
|
|
83
77
|
}
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// Diagnostic-carrying assertion error
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
78
|
/**
|
|
88
79
|
* An assertion failure enriched with its Markdown diagnostic. Still an
|
|
89
80
|
* `AssertionError`, so containment and classification treat it as the
|
|
@@ -100,9 +91,6 @@ export class AssertionDiagnostic extends AssertionError {
|
|
|
100
91
|
this.cause = cause;
|
|
101
92
|
}
|
|
102
93
|
}
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
// expandAssertion
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
94
|
const KIND_PROPS = {
|
|
107
95
|
"unary-truthy": { allowed: ["expr", "msg"], required: ["expr"] },
|
|
108
96
|
"unary-exists": { allowed: ["actual", "msg"], required: ["actual"] },
|
|
@@ -111,9 +99,20 @@ const KIND_PROPS = {
|
|
|
111
99
|
match: { allowed: ["actual", "expected", "msg"], required: ["actual", "expected"] },
|
|
112
100
|
numeric: { allowed: ["actual", "expected", "msg"], required: ["actual", "expected"] },
|
|
113
101
|
};
|
|
114
|
-
function validationError(name, message) {
|
|
102
|
+
export function validationError(name, message) {
|
|
115
103
|
return { type: "error", message: `<${name}> ${message}`, source: name };
|
|
116
104
|
}
|
|
105
|
+
// Outside a test the throw would abort before the segment could render, so
|
|
106
|
+
// emit the visible diagnostic first.
|
|
107
|
+
export function* failVisiblyThenThrow(name, msg, detail, failure) {
|
|
108
|
+
const diagnostic = buildDiagnostic(name, "failed", msg, detail, failure);
|
|
109
|
+
const visible = (yield* testing) || (yield* verbose);
|
|
110
|
+
const inTestScope = yield* inTest;
|
|
111
|
+
if (visible && !inTestScope) {
|
|
112
|
+
yield* DocumentOutput.operations.output(diagnostic);
|
|
113
|
+
}
|
|
114
|
+
throw new AssertionDiagnostic(failure, diagnostic, detail);
|
|
115
|
+
}
|
|
117
116
|
/**
|
|
118
117
|
* Expand one assertion component: validate props, resolve raw values, run the
|
|
119
118
|
* @std/assert function, then build the diagnostic. Returns diagnostic text on
|
|
@@ -215,15 +214,7 @@ export function* expandAssertion(assertion, invocation, ctx) {
|
|
|
215
214
|
}
|
|
216
215
|
}
|
|
217
216
|
if (failure) {
|
|
218
|
-
|
|
219
|
-
const visible = (yield* testing) || (yield* verbose);
|
|
220
|
-
const inTestScope = yield* inTest;
|
|
221
|
-
if (visible && !inTestScope) {
|
|
222
|
-
// Outside a test the throw below aborts expansion before the segment
|
|
223
|
-
// could render — emit the diagnostic directly so it reaches the output.
|
|
224
|
-
yield* DocumentOutput.operations.output(diagnostic);
|
|
225
|
-
}
|
|
226
|
-
throw new AssertionDiagnostic(failure, diagnostic, detail);
|
|
217
|
+
yield* failVisiblyThenThrow(assertion.name, values.msg, detail, failure);
|
|
227
218
|
}
|
|
228
219
|
const visible = (yield* testing) || (yield* verbose);
|
|
229
220
|
if (!visible) {
|
|
@@ -231,7 +222,7 @@ export function* expandAssertion(assertion, invocation, ctx) {
|
|
|
231
222
|
}
|
|
232
223
|
return [{ type: "text", content: buildDiagnostic(assertion.name, "passed", values.msg, detail) }];
|
|
233
224
|
}
|
|
234
|
-
function buildDiagnostic(name, outcome, msg, detail, failure) {
|
|
225
|
+
export function buildDiagnostic(name, outcome, msg, detail, failure) {
|
|
235
226
|
const icon = outcome === "passed" ? "✅" : "❌";
|
|
236
227
|
const lines = [`> ${icon} **${name}** ${outcome}${msg ? ` — ${msg}` : ""}`];
|
|
237
228
|
if (detail.actual !== undefined) {
|
|
@@ -9,13 +9,10 @@ import { ensure, scoped, spawn, suspend, withResolvers } from "effection";
|
|
|
9
9
|
import { timebox } from "@effectionx/timebox";
|
|
10
10
|
import { unbox, useEvalScope } from "@effectionx/scope-eval";
|
|
11
11
|
import { AssertionError } from "../deps/jsr.io/@std/assert/1.0.19/mod.js";
|
|
12
|
-
import { Component, env, evalScope } from "@executablemd/core";
|
|
13
|
-
import { Test, boundary, inTest, record, testing } from "./test-api.js";
|
|
14
|
-
import { AssertionDiagnostic, expandAssertion } from "./assertions.js";
|
|
12
|
+
import { Component, env, evalScope, validateBindingName } from "@executablemd/core";
|
|
13
|
+
import { Test, boundary, inTest, record, testing, verbose } from "./test-api.js";
|
|
14
|
+
import { AssertionDiagnostic, buildDiagnostic, evaluateExpression, expandAssertion, failVisiblyThenThrow, validationError, } from "./assertions.js";
|
|
15
15
|
import { persistBoundaryOutcome, persistTestResult } from "./journal.js";
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Contained-failure carriers
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
16
|
/** An ErrorSegment raised anywhere inside a test body. */
|
|
20
17
|
class RaisedSegmentError extends Error {
|
|
21
18
|
name = "RaisedSegmentError";
|
|
@@ -25,6 +22,14 @@ class RaisedSegmentError extends Error {
|
|
|
25
22
|
this.segment = segment;
|
|
26
23
|
}
|
|
27
24
|
}
|
|
25
|
+
class CapturedRaise extends Error {
|
|
26
|
+
name = "CapturedRaise";
|
|
27
|
+
segment;
|
|
28
|
+
constructor(segment) {
|
|
29
|
+
super(segment.message);
|
|
30
|
+
this.segment = segment;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
28
33
|
/** A failure while dismantling an established test scope or lease. */
|
|
29
34
|
class TeardownError extends Error {
|
|
30
35
|
name = "TeardownError";
|
|
@@ -188,11 +193,136 @@ export function createTestHandlers(options) {
|
|
|
188
193
|
}
|
|
189
194
|
return testOutput;
|
|
190
195
|
}
|
|
191
|
-
|
|
196
|
+
function* expandAssertThrows(invocation, ctx) {
|
|
197
|
+
for (const propName of [
|
|
198
|
+
...Object.keys(invocation.props),
|
|
199
|
+
...Object.keys(invocation.expressions),
|
|
200
|
+
]) {
|
|
201
|
+
if (propName !== "message" && propName !== "as") {
|
|
202
|
+
return [
|
|
203
|
+
validationError("AssertThrows", `does not accept a "${propName}" prop (allowed: message, as).`),
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!("message" in invocation.props) && !("message" in invocation.expressions)) {
|
|
208
|
+
return [validationError("AssertThrows", 'requires a "message" prop.')];
|
|
209
|
+
}
|
|
210
|
+
if ("as" in invocation.expressions) {
|
|
211
|
+
return [
|
|
212
|
+
validationError("AssertThrows", 'the "as" prop must be a string literal, not an expression.'),
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
const currentEnv = yield* env;
|
|
216
|
+
const merged = {
|
|
217
|
+
...(ctx.projectedEnv?.values ?? {}),
|
|
218
|
+
...(currentEnv?.values ?? {}),
|
|
219
|
+
};
|
|
220
|
+
let matcher;
|
|
221
|
+
if ("message" in invocation.expressions) {
|
|
222
|
+
let evaluated;
|
|
223
|
+
try {
|
|
224
|
+
evaluated = evaluateExpression(invocation.expressions["message"], merged);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
return [
|
|
228
|
+
validationError("AssertThrows", `failed to evaluate the "message" expression: ${error instanceof Error ? error.message : String(error)}`),
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
if (typeof evaluated === "string" || evaluated instanceof RegExp) {
|
|
232
|
+
matcher = evaluated;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
return [
|
|
236
|
+
validationError("AssertThrows", `the "message" expression must evaluate to a string or RegExp, got ${typeof evaluated}.`),
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const literal = invocation.props["message"];
|
|
242
|
+
if (typeof literal !== "string") {
|
|
243
|
+
return [validationError("AssertThrows", 'the "message" prop must be a string.')];
|
|
244
|
+
}
|
|
245
|
+
matcher = literal;
|
|
246
|
+
}
|
|
247
|
+
let binding;
|
|
248
|
+
if ("as" in invocation.props) {
|
|
249
|
+
if (!currentEnv) {
|
|
250
|
+
return [
|
|
251
|
+
validationError("AssertThrows", 'binding with "as" requires an eval scope in context.'),
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
const parsed = validateBindingName(invocation.props["as"]);
|
|
255
|
+
if (!parsed.ok) {
|
|
256
|
+
return [validationError("AssertThrows", `the "as" prop ${parsed.error}`)];
|
|
257
|
+
}
|
|
258
|
+
if (parsed.value === undefined) {
|
|
259
|
+
return [validationError("AssertThrows", 'the "as" prop must be a non-empty string.')];
|
|
260
|
+
}
|
|
261
|
+
binding = parsed.value;
|
|
262
|
+
}
|
|
263
|
+
// Install a scope-local raise interceptor and expand the body. Inside a
|
|
264
|
+
// <Test> the enclosing interceptor is installed in an outer scope, and
|
|
265
|
+
// outer middleware runs first (see @effectionx/context-api ordering), so
|
|
266
|
+
// it throws RaisedSegmentError before this hook is reached; outside a test
|
|
267
|
+
// this hook is the only interceptor and throws CapturedRaise. Catching
|
|
268
|
+
// both makes capture behave identically inside and outside <Test>. The
|
|
269
|
+
// first raised error stops expansion, so later children never execute.
|
|
270
|
+
let captured;
|
|
271
|
+
try {
|
|
272
|
+
yield* scoped(function* () {
|
|
273
|
+
yield* Component.around({
|
|
274
|
+
// deno-lint-ignore require-yield
|
|
275
|
+
*raise([segment]) {
|
|
276
|
+
throw new CapturedRaise(segment);
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
yield* ctx.expand(invocation.children);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (error instanceof CapturedRaise || error instanceof RaisedSegmentError) {
|
|
284
|
+
captured = error.segment;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!captured) {
|
|
291
|
+
yield* failAssertThrows(matcher, undefined);
|
|
292
|
+
}
|
|
293
|
+
else if (!matchesMessage(matcher, captured.message)) {
|
|
294
|
+
yield* failAssertThrows(matcher, captured.message);
|
|
295
|
+
}
|
|
296
|
+
if (binding !== undefined && currentEnv) {
|
|
297
|
+
currentEnv.values[binding] = captured;
|
|
298
|
+
}
|
|
299
|
+
const visible = (yield* testing) || (yield* verbose);
|
|
300
|
+
if (!visible) {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
return [
|
|
304
|
+
{
|
|
305
|
+
type: "text",
|
|
306
|
+
content: buildDiagnostic("AssertThrows", "passed", describeMatcher(matcher), {}),
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
return { expandTesting, expandTest, expandAssertion, expandAssertThrows };
|
|
311
|
+
}
|
|
312
|
+
function* failAssertThrows(matcher, actual) {
|
|
313
|
+
const expected = describeMatcher(matcher);
|
|
314
|
+
const failure = new AssertionError(actual === undefined
|
|
315
|
+
? `AssertThrows: expected the body to raise an error matching ${expected}, but none was raised`
|
|
316
|
+
: `AssertThrows: raised error ${JSON.stringify(actual)} did not match ${expected}`);
|
|
317
|
+
yield* failVisiblyThenThrow("AssertThrows", undefined, { expected, actual: actual ?? "(none)" }, failure);
|
|
318
|
+
throw failure;
|
|
319
|
+
}
|
|
320
|
+
function matchesMessage(matcher, message) {
|
|
321
|
+
return matcher instanceof RegExp ? matcher.test(message) : message.includes(matcher);
|
|
322
|
+
}
|
|
323
|
+
function describeMatcher(matcher) {
|
|
324
|
+
return matcher instanceof RegExp ? String(matcher) : `substring ${JSON.stringify(matcher)}`;
|
|
192
325
|
}
|
|
193
|
-
// ---------------------------------------------------------------------------
|
|
194
|
-
// Result classification and diagnostics
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
326
|
function formatLocation(invocation) {
|
|
197
327
|
const position = invocation.position;
|
|
198
328
|
if (!position) {
|
|
@@ -39,6 +39,9 @@ export function* installHandlers(handlers, options) {
|
|
|
39
39
|
if (invocation.name === "Test") {
|
|
40
40
|
return { segments: yield* handlers.expandTest(invocation, ctx) };
|
|
41
41
|
}
|
|
42
|
+
if (invocation.name === "AssertThrows") {
|
|
43
|
+
return { segments: yield* handlers.expandAssertThrows(invocation, ctx) };
|
|
44
|
+
}
|
|
42
45
|
const assertion = ASSERTIONS.get(invocation.name);
|
|
43
46
|
if (assertion) {
|
|
44
47
|
return { segments: yield* handlers.expandAssertion(assertion, invocation, ctx) };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@executablemd/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Testing vocabulary for executable.md documents: <Testing>, <Test>, and assertion components.",
|
|
5
5
|
"homepage": "https://executable.md",
|
|
6
6
|
"repository": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"@effectionx/context-api": "0.6.0",
|
|
27
27
|
"@effectionx/scope-eval": "0.1.3",
|
|
28
28
|
"@effectionx/timebox": "0.4.3",
|
|
29
|
-
"@executablemd/core": "^0.
|
|
30
|
-
"@executablemd/durable-streams": "^0.
|
|
29
|
+
"@executablemd/core": "^0.4.0",
|
|
30
|
+
"@executablemd/durable-streams": "^0.4.0",
|
|
31
31
|
"effection": "4.1.0-alpha.7",
|
|
32
32
|
"@std/assert": "npm:@jsr/std__assert@^1.0.17"
|
|
33
33
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { AssertionError } from "../deps/jsr.io/@std/assert/1.0.19/mod.js";
|
|
15
15
|
import type { Operation } from "effection";
|
|
16
|
-
import type { ComponentInvocation, InvocationContext, Segment } from "@executablemd/core";
|
|
16
|
+
import type { ComponentInvocation, ErrorSegment, InvocationContext, Segment } from "@executablemd/core";
|
|
17
17
|
type AssertionKind = "unary-truthy" | "unary-exists" | "binary-eq" | "string-includes" | "match" | "numeric";
|
|
18
18
|
export interface AssertionEntry {
|
|
19
19
|
name: string;
|
|
@@ -29,6 +29,7 @@ interface ResolvedValues {
|
|
|
29
29
|
msg?: string;
|
|
30
30
|
}
|
|
31
31
|
export declare const ASSERTIONS: Map<string, AssertionEntry>;
|
|
32
|
+
export declare function evaluateExpression(expression: string, values: Record<string, unknown>): unknown;
|
|
32
33
|
/**
|
|
33
34
|
* An assertion failure enriched with its Markdown diagnostic. Still an
|
|
34
35
|
* `AssertionError`, so containment and classification treat it as the
|
|
@@ -46,10 +47,19 @@ export declare class AssertionDiagnostic extends AssertionError {
|
|
|
46
47
|
expected?: string;
|
|
47
48
|
});
|
|
48
49
|
}
|
|
50
|
+
export declare function validationError(name: string, message: string): ErrorSegment;
|
|
51
|
+
export declare function failVisiblyThenThrow(name: string, msg: string | undefined, detail: {
|
|
52
|
+
actual?: string;
|
|
53
|
+
expected?: string;
|
|
54
|
+
}, failure: Error): Operation<never>;
|
|
49
55
|
/**
|
|
50
56
|
* Expand one assertion component: validate props, resolve raw values, run the
|
|
51
57
|
* @std/assert function, then build the diagnostic. Returns diagnostic text on
|
|
52
58
|
* a visible pass; throws `AssertionDiagnostic` on failure.
|
|
53
59
|
*/
|
|
54
60
|
export declare function expandAssertion(assertion: AssertionEntry, invocation: ComponentInvocation, ctx: InvocationContext): Operation<Segment[]>;
|
|
61
|
+
export declare function buildDiagnostic(name: string, outcome: "passed" | "failed", msg: string | undefined, detail: {
|
|
62
|
+
actual?: string;
|
|
63
|
+
expected?: string;
|
|
64
|
+
}, failure?: Error): string;
|
|
55
65
|
export {};
|
package/types/src/handlers.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface TestHandlers {
|
|
|
12
12
|
expandTesting(invocation: ComponentInvocation, ctx: InvocationContext): Operation<Segment[]>;
|
|
13
13
|
expandTest(invocation: ComponentInvocation, ctx: InvocationContext): Operation<Segment[]>;
|
|
14
14
|
expandAssertion(assertion: AssertionEntry, invocation: ComponentInvocation, ctx: InvocationContext): Operation<Segment[]>;
|
|
15
|
+
expandAssertThrows(invocation: ComponentInvocation, ctx: InvocationContext): Operation<Segment[]>;
|
|
15
16
|
}
|
|
16
17
|
export declare function createTestHandlers(options: {
|
|
17
18
|
timeoutMs: number;
|
package/esm/core/mod.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
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";
|
package/esm/core/src/api.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
});
|
package/esm/core/src/collect.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
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;
|
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
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
|
-
}
|
package/esm/core/src/errors.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
}
|