Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ jobs:
- run: pnpm install

- name: Lint
run: pnpm exec oxlint -c .oxlintrc.json core/src/ cli/src/ durable-streams/ durable-effects/ runtime/
run: pnpm exec oxlint -c .oxlintrc.json core/src/ cli/src/ durable-streams/ durable-effects/ runtime/ testing/

- name: Format
run: pnpm exec oxfmt --check core/src/ cli/src/ durable-streams/ durable-effects/ runtime/ vendor/ test-support/
run: pnpm exec oxfmt --check core/src/ cli/src/ durable-streams/ durable-effects/ runtime/ testing/ vendor/ test-support/

test-deno:
runs-on: ubuntu-latest
Expand All @@ -47,6 +47,25 @@ jobs:
- name: Test
run: deno task test

smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.1

- name: Build the xmd binary
run: deno task build

- name: Smoke test the compiled binary
run: |
./dist/xmd test smoke-test/README.md \
--component-dir smoke-test \
--component-dir core/components \
--raw

site:
runs-on: ubuntu-latest
steps:
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/publish-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Validate the manifests declare this version
run: |
VERSION="${{ steps.resolve.outputs.value }}"
for f in durable-streams/deno.json runtime/deno.json core/deno.json cli/deno.json packages/code-review-agent/deno.json; do
for f in durable-streams/deno.json runtime/deno.json core/deno.json testing/deno.json cli/deno.json packages/code-review-agent/deno.json; do
declared="$(jq -r .version "$f")"
if [ "$declared" != "$VERSION" ]; then
echo "::error::$f declares $declared, not $VERSION — the tag does not match the manifests"
Expand Down Expand Up @@ -82,9 +82,16 @@ jobs:
package: core
version: ${{ needs.version.outputs.value }}

cli:
testing:
needs: [version, core, durable-streams]
uses: ./.github/workflows/publish-one.yml
with:
package: testing
version: ${{ needs.version.outputs.value }}

cli:
needs: [version, core, durable-streams, testing]
uses: ./.github/workflows/publish-one.yml
with:
package: cli
version: ${{ needs.version.outputs.value }}
Expand Down
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
strict-peer-dependencies=false
@jsr:registry=https://npm.jsr.io
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Do not commit if any check fails. Fix the issue first, then re-run all three.
require changes to specs/release-process-spec.md to match.
9. Prefer stateless generators - use a function when calling a function that
returns an operation; Do not do this function*(arg) { return yield* generator(arg) }
10. Structure source through names and modules. Do not use decorative section-divider comments.

## PR Process

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ Each invocation requires a new path. If the path already exists, `xmd` exits wit

## Project layout

- `core/src/run-document.ts` - document entrypoint and durable import pipeline.
- `core/src/execute.ts` - document entrypoint and durable import pipeline.
- `core/src/scanner.ts` - boundary scanner for components and executable fences.
- `core/src/` - component expansion, eval/exec handling, modifiers, and sampling helpers.
- `core/components/` - reusable provider and demo components.
Expand Down
30 changes: 25 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[test]
tsconfig-override = "tsconfig.node.json"

[install.scopes]
"@jsr" = "https://npm.jsr.io"
3 changes: 2 additions & 1 deletion cli/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"exports": "./src/cli.ts",
"imports": {
"@executablemd/core": "../core/mod.ts",
"zod": "npm:zod@^4.3.6"
"zod": "npm:zod@^4.3.6",
"@executablemd/testing": "../testing/mod.ts"
}
}
1 change: 1 addition & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@effectionx/stream-helpers": "0.8.3",
"@executablemd/core": "workspace:*",
"@executablemd/durable-streams": "workspace:*",
"@executablemd/testing": "workspace:*",
"configliere": "^0.2.3",
"effection": "4.1.0-alpha.7",
"zod": "^4.3.6"
Expand Down
80 changes: 66 additions & 14 deletions cli/src/cli.ts
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import { inspect } from "node:util";
import process from "node:process";
import { program, object, field, cli, commands, type Mods } from "configliere";
import { z } from "zod";
import { runDocument, useNormalizedOutput, useTerminalOutput } from "@executablemd/core";
import { execute, useNormalizedOutput, useTerminalOutput } from "@executablemd/core";
import { installTestingVocabulary, TestFailureError, useTesting } from "@executablemd/testing";
import { FileStream } from "./file-stream.ts";
import denoJson from "../deno.json" with { type: "json" };

Expand Down Expand Up @@ -66,10 +67,35 @@ const runConfig = object({
},
});

const testConfig = object({
docPath: {
description: "markdown document to test",
...field(z.string(), cli.argument()),
},
componentDir: {
description: "component search directory",
...field(z.array(z.string()), defaults(["components", "."]), field.array()),
},
verbose: {
description: "log journal entries to stderr",
aliases: ["-V"],
...field(z.boolean(), defaults(false)),
},
journal: {
description: "write a diagnostic JSONL trace (path must not exist)",
aliases: ["-j"],
...field(z.string().optional()),
},
raw: {
description: "output raw markdown without normalization or terminal formatting",
...field(z.boolean(), defaults(false)),
},
});

const xmd = program({
name: "xmd",
version: denoJson.version,
config: commands({ run: runConfig }, { default: "run" }),
config: commands({ run: runConfig, test: testConfig }, { default: "run" }),
});

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -142,13 +168,16 @@ function* createJournalFile(filePath: string): Operation<void> {
yield* until(handle.close());
}

function* run(config: {
docPath: string;
componentDir: string[];
verbose: boolean;
journal: string | undefined;
raw: boolean;
}): Operation<void> {
function* run(
config: {
docPath: string;
componentDir: string[];
verbose: boolean;
journal: string | undefined;
raw: boolean;
},
mode: { testing: boolean },
): Operation<void> {
const { docPath, componentDir, verbose, journal, raw } = config;

// Every CLI invocation starts from an empty stream. --journal writes
Expand Down Expand Up @@ -188,7 +217,7 @@ function* run(config: {
// Output middleware (spec §9).
//
// Middleware is installed on the DocumentOutput Api via Api.around() before
// runDocument is called. runDocument owns the channel internally —
// execute is called. execute owns the output stream internally —
// the CLI just installs transformations and consumes the returned stream.
// ---------------------------------------------------------------------------

Expand All @@ -200,9 +229,17 @@ function* run(config: {
yield* useTerminalOutput();
}

// Run the document — returns a DocumentExecution.
// yield* execution waits for completion. execution.output streams chunks.
const execution = yield* runDocument({
// Compose testing around the single core execution entrypoint: both
// commands register the vocabulary (assertions work in regular documents,
// explicit <Testing> boundaries affect the outcome), while `xmd test`
// additionally activates root testing through a useTesting() session.
Comment thread
taras marked this conversation as resolved.
if (mode.testing) {
yield* useTesting({ verbose });
} else {
yield* installTestingVocabulary({ verbose });
}

const execution = yield* execute({
docPath,
stream,
componentDirs: componentDir,
Expand All @@ -227,6 +264,18 @@ function* run(config: {
signal.close();
yield* writer;
}

// Inspect the completion Result AFTER the report finished streaming:
// test failures, assertion aborts, and any document abort exit nonzero.
Comment thread
taras marked this conversation as resolved.
const result = yield* execution;
if (!result.ok) {
if (result.error instanceof TestFailureError) {
console.error(`\ntests failed: ${result.error.message}`);
} else {
console.error(result.error.message);
}
yield* exit(1);
}
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -254,7 +303,10 @@ await main(function* (args) {
}
switch (parsed.value.name) {
case "run":
yield* run(parsed.value.config);
yield* run(parsed.value.config, { testing: false });
break;
case "test":
yield* run(parsed.value.config, { testing: true });
break;
}
}
Expand Down
Loading
Loading