From 26f60d8c3cab3ce5fdfdf6352354babcf67d645c Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 30 May 2026 00:02:23 +0300 Subject: [PATCH 01/12] =?UTF-8?q?chore(release):=20prep=20v0.1.0=20?= =?UTF-8?q?=E2=80=94=20npm=20scope=20@mdscript,=20crates=20metadata,=20CI/?= =?UTF-8?q?CD,=20examples=20Consolidates=20the=20v0.1.0=20release=20prepar?= =?UTF-8?q?ation:=20-=20npm=20scope=20rename=20@mds/*=20->=20@mdscript/*?= =?UTF-8?q?=20-=20crates.io=20metadata=20+=20pinned=20path-dep=20versions?= =?UTF-8?q?=20-=20CHANGELOG=20consolidated=20to=20[0.1.0];=20spec=20status?= =?UTF-8?q?;=20README=20install=20lines=20-=20ci.yml=20+=20partial=20relea?= =?UTF-8?q?se.yml=20-=20examples/=20promoted=20from=20test=5Fplayground;?= =?UTF-8?q?=20rustfmt=20cleanup=20-=20cross-dir=20scanner=20fixtures=20(ne?= =?UTF-8?q?eded=20by=20U-SM8=20on=20clean=20clone)=20Baselines:=20590=20Ru?= =?UTF-8?q?st=20tests,=20224=20JS=20tests,=20clippy=20-D=20warnings,=20fmt?= =?UTF-8?q?=20all=20green.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 95 +++++ .github/workflows/release.yml | 128 +++++++ .gitignore | 4 + CHANGELOG.md | 101 ++--- Cargo.toml | 2 +- README.md | 29 +- crates/mds-cli/Cargo.toml | 2 +- crates/mds-cli/tests/errors.rs | 26 +- crates/mds-cli/tests/language.rs | 231 +++++++++--- crates/mds-core/src/evaluator.rs | 14 +- crates/mds-core/src/parser.rs | 19 +- crates/mds-napi/Cargo.toml | 2 +- crates/mds-napi/index.js | 14 +- crates/mds-wasm/Cargo.toml | 4 +- examples/README.md | 50 +++ examples/ai-agent/multi-turn-prompt.mds | 69 ++++ examples/ai-agent/system-prompt.mds | 74 ++++ examples/ai-agent/tool-instructions.mds | 17 + examples/api-docs/endpoint-template.mds | 103 ++++++ examples/api-docs/response-schemas.mds | 31 ++ examples/blog-generator/post-template.mds | 74 ++++ examples/edge-cases/01_empty_arrays.mds | 12 + examples/edge-cases/02_nested_objects.mds | 27 ++ examples/edge-cases/03_shadowing.mds | 22 ++ examples/edge-cases/04_escaped_and_code.mds | 28 ++ .../edge-cases/05_deeply_nested_logic.mds | 53 +++ examples/edge-cases/06_object_iteration.mds | 14 + examples/edge-cases/07_falsy_values.mds | 61 ++++ examples/edge-cases/08_runtime_vars.mds | 30 ++ examples/edge-cases/09_reexport_chain.mds | 8 + examples/edge-cases/10_consume_reexports.mds | 11 + examples/edge-cases/11_include_directive.mds | 17 + examples/edge-cases/12_selective_import.mds | 14 + .../edge-cases/13_function_composition.mds | 27 ++ examples/edge-cases/14_merge_import.mds | 14 + .../edge-cases/15_frontmatter_passthrough.mds | 17 + examples/edge-cases/vars.json | 6 + examples/node-api-test.mjs | 153 ++++++++ examples/prompt-library/examples.mds | 16 + examples/prompt-library/formatting.mds | 44 +++ examples/prompt-library/guardrails.mds | 50 +++ examples/prompt-library/personas.mds | 25 ++ examples/rollup-app/package.json | 19 + examples/rollup-app/rollup.config.mjs | 14 + examples/rollup-app/src/main.ts | 43 +++ .../rollup-app/src/prompts/claude-code.mds | 40 ++ examples/rollup-app/src/prompts/shared.mds | 17 + examples/stress-test/agents/code-reviewer.mds | 43 +++ examples/stress-test/agents/data-analyst.mds | 78 ++++ examples/stress-test/agents/orchestrator.mds | 62 ++++ .../stress-test/edge/code-passthrough.mds | 36 ++ examples/stress-test/edge/deep-nesting.mds | 50 +++ .../stress-test/edge/empty-collections.mds | 37 ++ examples/stress-test/edge/escaped-braces.mds | 14 + examples/stress-test/edge/falsy-matrix.mds | 102 ++++++ .../stress-test/edge/shadowing-stress.mds | 40 ++ .../stress-test/errors/bad-arity.mds | 2 +- .../stress-test/errors/bad-circular-a.mds | 3 + .../stress-test/errors/bad-circular-b.mds | 3 + examples/stress-test/errors/bad-type.mds | 7 + examples/stress-test/errors/bad-undefined.mds | 5 + examples/stress-test/lib/examples.mds | 16 + examples/stress-test/lib/formatting.mds | 42 +++ examples/stress-test/lib/guardrails.mds | 36 ++ examples/stress-test/lib/index.mds | 4 + examples/stress-test/lib/personas.mds | 31 ++ examples/stress-test/main.mds | 74 ++++ examples/stress-test/node-api-stress-test.mjs | 345 ++++++++++++++++++ .../stress-test/shared/chain-consumer.mds | 13 + .../stress-test/shared/output-formats.mds | 21 ++ .../stress-test/shared/reexport-chain.mds | 3 + examples/stress-test/shared/tool-registry.mds | 34 ++ examples/stress-test/vars-minimal.json | 3 + examples/stress-test/vars.json | 6 + examples/vite-app/package.json | 19 + examples/vite-app/src/main.ts | 19 + examples/vite-app/src/prompts/reviewer.mds | 31 ++ examples/vite-app/src/prompts/rules.mds | 26 ++ examples/vite-app/src/prompts/system.mds | 23 ++ examples/vite-app/src/stress.ts | 46 +++ examples/vite-app/src/test-metadata.ts | 10 + examples/vite-app/tsconfig.json | 13 + examples/vite-app/vite.config.ts | 17 + examples/webpack-app/package.json | 18 + examples/webpack-app/src/index.js | 38 ++ .../webpack-app/src/prompts/assistant.mds | 26 ++ examples/webpack-app/webpack.config.mjs | 30 ++ package-lock.json | 46 +-- packages/bundler-utils/README.md | 22 +- .../__test__/cjs-compat.spec.mjs | 2 +- .../__test__/integration.spec.mjs | 4 +- packages/bundler-utils/package.json | 14 +- packages/bundler-utils/src/transform.ts | 2 +- packages/bundler-utils/src/types.ts | 6 +- packages/mds/README.md | 10 +- packages/mds/__test__/backend.spec.mjs | 2 +- packages/mds/__test__/browser.spec.mjs | 2 +- packages/mds/__test__/check.spec.mjs | 2 +- packages/mds/__test__/compile.spec.mjs | 2 +- packages/mds/__test__/compileFile.spec.mjs | 2 +- packages/mds/__test__/error.spec.mjs | 2 +- .../__test__/fixtures/cross-dir/app/entry.mds | 7 + .../fixtures/cross-dir/lib/helpers.mds | 9 + packages/mds/__test__/helpers.mjs | 2 +- packages/mds/__test__/native-backend.spec.mjs | 2 +- packages/mds/__test__/perf.spec.mjs | 2 +- packages/mds/__test__/scanner.spec.mjs | 2 +- packages/mds/__test__/wasm-backend.spec.mjs | 2 +- .../mds/__test__/wasm-compileFile.spec.mjs | 2 +- packages/mds/package.json | 12 +- packages/mds/src/backend/wasm.ts | 14 +- packages/mds/src/browser.ts | 2 +- packages/mds/src/node.ts | 8 +- packages/rollup-plugin/README.md | 10 +- .../rollup-plugin/__test__/plugin.spec.mjs | 2 +- packages/rollup-plugin/package.json | 16 +- packages/rollup-plugin/src/index.ts | 8 +- packages/vite-plugin/README.md | 10 +- packages/vite-plugin/__test__/plugin.spec.mjs | 2 +- packages/vite-plugin/package.json | 16 +- packages/vite-plugin/src/index.ts | 8 +- packages/webpack-loader/README.md | 10 +- .../__test__/cjs-compat.spec.mjs | 2 +- .../webpack-loader/__test__/loader.spec.mjs | 2 +- packages/webpack-loader/package.json | 16 +- packages/webpack-loader/src/index.ts | 16 +- packages/webpack-loader/tsconfig.cjs.json | 4 +- spec.md | 6 +- test_playground/01_basic.mds | 6 - test_playground/02_conditional.mds | 12 - test_playground/03_loop.mds | 10 - test_playground/04_function.mds | 20 - test_playground/05_import_alias.mds | 9 - test_playground/06_import_merge.mds | 11 - test_playground/07_import_selective.mds | 9 - test_playground/08_include.mds | 11 - test_playground/09_complex_prompt.mds | 35 -- test_playground/10_nested.mds | 19 - test_playground/11_escaped.mds | 9 - test_playground/12_code_block.mds | 13 - test_playground/13_reexport.mds | 2 - test_playground/14_reexport_consumer.mds | 5 - test_playground/15_runtime_vars.mds | 10 - test_playground/err_circular_a.mds | 1 - test_playground/err_circular_b.mds | 1 - test_playground/err_undefined.mds | 1 - test_playground/hello.mds | 11 - test_playground/lib/footer.mds | 6 - test_playground/lib/format.mds | 15 - test_playground/lib/rules.mds | 17 - test_playground/output.md | 1 - test_playground/output/01_basic.md | 1 - test_playground/output/02_conditional.md | 3 - test_playground/output/03_loop.md | 7 - test_playground/output/04_function.md | 7 - test_playground/output/05_import_alias.md | 3 - test_playground/output/06_import_merge.md | 5 - test_playground/output/07_import_selective.md | 3 - test_playground/output/08_include.md | 6 - test_playground/output/09_complex_prompt.md | 24 -- test_playground/output/10_nested.md | 13 - test_playground/output/11_escaped.md | 5 - test_playground/output/12_code_block.md | 9 - .../output/14_reexport_consumer.md | 6 - test_playground/output/15_runtime_vars.md | 5 - test_playground/vars.json | 5 - 166 files changed, 3344 insertions(+), 599 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 examples/README.md create mode 100644 examples/ai-agent/multi-turn-prompt.mds create mode 100644 examples/ai-agent/system-prompt.mds create mode 100644 examples/ai-agent/tool-instructions.mds create mode 100644 examples/api-docs/endpoint-template.mds create mode 100644 examples/api-docs/response-schemas.mds create mode 100644 examples/blog-generator/post-template.mds create mode 100644 examples/edge-cases/01_empty_arrays.mds create mode 100644 examples/edge-cases/02_nested_objects.mds create mode 100644 examples/edge-cases/03_shadowing.mds create mode 100644 examples/edge-cases/04_escaped_and_code.mds create mode 100644 examples/edge-cases/05_deeply_nested_logic.mds create mode 100644 examples/edge-cases/06_object_iteration.mds create mode 100644 examples/edge-cases/07_falsy_values.mds create mode 100644 examples/edge-cases/08_runtime_vars.mds create mode 100644 examples/edge-cases/09_reexport_chain.mds create mode 100644 examples/edge-cases/10_consume_reexports.mds create mode 100644 examples/edge-cases/11_include_directive.mds create mode 100644 examples/edge-cases/12_selective_import.mds create mode 100644 examples/edge-cases/13_function_composition.mds create mode 100644 examples/edge-cases/14_merge_import.mds create mode 100644 examples/edge-cases/15_frontmatter_passthrough.mds create mode 100644 examples/edge-cases/vars.json create mode 100644 examples/node-api-test.mjs create mode 100644 examples/prompt-library/examples.mds create mode 100644 examples/prompt-library/formatting.mds create mode 100644 examples/prompt-library/guardrails.mds create mode 100644 examples/prompt-library/personas.mds create mode 100644 examples/rollup-app/package.json create mode 100644 examples/rollup-app/rollup.config.mjs create mode 100644 examples/rollup-app/src/main.ts create mode 100644 examples/rollup-app/src/prompts/claude-code.mds create mode 100644 examples/rollup-app/src/prompts/shared.mds create mode 100644 examples/stress-test/agents/code-reviewer.mds create mode 100644 examples/stress-test/agents/data-analyst.mds create mode 100644 examples/stress-test/agents/orchestrator.mds create mode 100644 examples/stress-test/edge/code-passthrough.mds create mode 100644 examples/stress-test/edge/deep-nesting.mds create mode 100644 examples/stress-test/edge/empty-collections.mds create mode 100644 examples/stress-test/edge/escaped-braces.mds create mode 100644 examples/stress-test/edge/falsy-matrix.mds create mode 100644 examples/stress-test/edge/shadowing-stress.mds rename test_playground/err_arity.mds => examples/stress-test/errors/bad-arity.mds (57%) create mode 100644 examples/stress-test/errors/bad-circular-a.mds create mode 100644 examples/stress-test/errors/bad-circular-b.mds create mode 100644 examples/stress-test/errors/bad-type.mds create mode 100644 examples/stress-test/errors/bad-undefined.mds create mode 100644 examples/stress-test/lib/examples.mds create mode 100644 examples/stress-test/lib/formatting.mds create mode 100644 examples/stress-test/lib/guardrails.mds create mode 100644 examples/stress-test/lib/index.mds create mode 100644 examples/stress-test/lib/personas.mds create mode 100644 examples/stress-test/main.mds create mode 100644 examples/stress-test/node-api-stress-test.mjs create mode 100644 examples/stress-test/shared/chain-consumer.mds create mode 100644 examples/stress-test/shared/output-formats.mds create mode 100644 examples/stress-test/shared/reexport-chain.mds create mode 100644 examples/stress-test/shared/tool-registry.mds create mode 100644 examples/stress-test/vars-minimal.json create mode 100644 examples/stress-test/vars.json create mode 100644 examples/vite-app/package.json create mode 100644 examples/vite-app/src/main.ts create mode 100644 examples/vite-app/src/prompts/reviewer.mds create mode 100644 examples/vite-app/src/prompts/rules.mds create mode 100644 examples/vite-app/src/prompts/system.mds create mode 100644 examples/vite-app/src/stress.ts create mode 100644 examples/vite-app/src/test-metadata.ts create mode 100644 examples/vite-app/tsconfig.json create mode 100644 examples/vite-app/vite.config.ts create mode 100644 examples/webpack-app/package.json create mode 100644 examples/webpack-app/src/index.js create mode 100644 examples/webpack-app/src/prompts/assistant.mds create mode 100644 examples/webpack-app/webpack.config.mjs create mode 100644 packages/mds/__test__/fixtures/cross-dir/app/entry.mds create mode 100644 packages/mds/__test__/fixtures/cross-dir/lib/helpers.mds delete mode 100644 test_playground/01_basic.mds delete mode 100644 test_playground/02_conditional.mds delete mode 100644 test_playground/03_loop.mds delete mode 100644 test_playground/04_function.mds delete mode 100644 test_playground/05_import_alias.mds delete mode 100644 test_playground/06_import_merge.mds delete mode 100644 test_playground/07_import_selective.mds delete mode 100644 test_playground/08_include.mds delete mode 100644 test_playground/09_complex_prompt.mds delete mode 100644 test_playground/10_nested.mds delete mode 100644 test_playground/11_escaped.mds delete mode 100644 test_playground/12_code_block.mds delete mode 100644 test_playground/13_reexport.mds delete mode 100644 test_playground/14_reexport_consumer.mds delete mode 100644 test_playground/15_runtime_vars.mds delete mode 100644 test_playground/err_circular_a.mds delete mode 100644 test_playground/err_circular_b.mds delete mode 100644 test_playground/err_undefined.mds delete mode 100644 test_playground/hello.mds delete mode 100644 test_playground/lib/footer.mds delete mode 100644 test_playground/lib/format.mds delete mode 100644 test_playground/lib/rules.mds delete mode 100644 test_playground/output.md delete mode 100644 test_playground/output/01_basic.md delete mode 100644 test_playground/output/02_conditional.md delete mode 100644 test_playground/output/03_loop.md delete mode 100644 test_playground/output/04_function.md delete mode 100644 test_playground/output/05_import_alias.md delete mode 100644 test_playground/output/06_import_merge.md delete mode 100644 test_playground/output/07_import_selective.md delete mode 100644 test_playground/output/08_include.md delete mode 100644 test_playground/output/09_complex_prompt.md delete mode 100644 test_playground/output/10_nested.md delete mode 100644 test_playground/output/11_escaped.md delete mode 100644 test_playground/output/12_code_block.md delete mode 100644 test_playground/output/14_reexport_consumer.md delete mode 100644 test_playground/output/15_runtime_vars.md delete mode 100644 test_playground/vars.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..76611698 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,95 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +# Cancel superseded runs on the same ref to save CI minutes. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + rust: + name: Rust — fmt, clippy, test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - name: Format check + run: cargo fmt --all --check + - name: Clippy (warnings are errors) + run: cargo clippy --workspace --all-targets -- -D warnings + - name: Test + run: cargo test --workspace + + msrv: + name: MSRV (Rust 1.88) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # rust-version = 1.88 is declared workspace-wide; verify the published + # crates still compile on it. napi/wasm bindings are checked on stable + # in their own jobs since their toolchain needs can differ. + - uses: dtolnay/rust-toolchain@1.88 + - uses: Swatinem/rust-cache@v2 + - run: cargo check -p mds-core -p mds-cli + + wasm: + name: WASM — build & test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - uses: jetli/wasm-pack-action@v0.4.0 + - name: Build (nodejs + web targets) + run: | + wasm-pack build crates/mds-wasm --target nodejs --out-dir pkg + wasm-pack build crates/mds-wasm --target web --out-dir pkg-web + - name: Test (node) + run: wasm-pack test --node crates/mds-wasm + + js: + name: JS packages — build & test + # Run on every host OS so the native addon path is exercised everywhere. + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + # Build the native addon for the host so @mdscript/mds resolves a backend. + # --no-js preserves the hand-maintained crates/mds-napi/index.js loader. + - name: Build native addon (host) + working-directory: crates/mds-napi + run: npx napi build --platform --release --no-js + # Build the WASM pkg so the WASM fallback (and wasm-backend tests) work. + - uses: jetli/wasm-pack-action@v0.4.0 + - name: Build WASM (nodejs) + run: wasm-pack build crates/mds-wasm --target nodejs --out-dir pkg + - name: Build TS packages + run: npm run build --workspaces --if-present + - name: Test + run: npm test --workspaces --if-present diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..5e08087b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,128 @@ +name: Release + +# Triggered by pushing a version tag, e.g. `git tag v0.1.0 && git push --tags`. +on: + push: + tags: + - "v*" + +permissions: + contents: write # create the GitHub Release + +env: + CARGO_TERM_COLOR: always + +jobs: + # --------------------------------------------------------------------------- + # crates.io — READY. Publishes the library then the CLI (CLI depends on the + # library, so order and index-propagation matter). + # Requires repo secret: CARGO_REGISTRY_TOKEN + # --------------------------------------------------------------------------- + publish-crates: + name: Publish to crates.io + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Verify tag matches workspace version + run: | + TAG="${GITHUB_REF_NAME#v}" + CRATE_VER="$(cargo metadata --no-deps --format-version 1 \ + | grep -o '"name":"mds-core","version":"[^"]*"' \ + | grep -o '[0-9][^"]*' | head -1)" + echo "tag=$TAG crate=$CRATE_VER" + test "$TAG" = "$CRATE_VER" || { echo "::error::tag v$TAG != crate version $CRATE_VER"; exit 1; } + - name: Publish mds-core + run: cargo publish -p mds-core --token "${{ secrets.CARGO_REGISTRY_TOKEN }}" + - name: Wait for crates.io index to update + run: sleep 30 + - name: Publish mds-cli + run: cargo publish -p mds-cli --token "${{ secrets.CARGO_REGISTRY_TOKEN }}" + + # --------------------------------------------------------------------------- + # GitHub Release — READY. Cuts a release with notes once crates publish. + # --------------------------------------------------------------------------- + github-release: + name: GitHub Release + needs: [publish-crates] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Create release + run: gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --generate-notes + env: + GH_TOKEN: ${{ github.token }} + + # =========================================================================== + # npm publishing (native addon + universal + bundler packages) — NOT YET + # WIRED. Blocked on the "napi packaging" task: + # 1. Populate the `napi` config in crates/mds-napi/package.json with the + # target triples (currently `"triples": {}`) so the per-platform + # `@mdscript/mds-` packages can be generated and published. + # 2. Add the NPM_TOKEN repo secret and register the @mdscript org. + # 3. Validate the cross-compile matrix on a real CI run (musl/arm/windows + # cross toolchains are environment-sensitive). + # + # The job below is the canonical napi-rs build matrix, left commented until + # the packaging config above exists — publishing it half-configured would + # ship broken packages. See README/CHANGELOG for the intended package set. + # =========================================================================== + # + # build-napi: + # strategy: + # fail-fast: false + # matrix: + # settings: + # - { host: macos-latest, target: aarch64-apple-darwin } + # - { host: macos-latest, target: x86_64-apple-darwin } + # - { host: ubuntu-latest, target: x86_64-unknown-linux-gnu } + # - { host: ubuntu-latest, target: x86_64-unknown-linux-musl } + # - { host: ubuntu-latest, target: aarch64-unknown-linux-gnu } + # - { host: ubuntu-latest, target: aarch64-unknown-linux-musl } + # - { host: windows-latest, target: x86_64-pc-windows-msvc } + # runs-on: ${{ matrix.settings.host }} + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-node@v4 + # with: { node-version: 22, cache: npm } + # - uses: dtolnay/rust-toolchain@stable + # with: { targets: ${{ matrix.settings.target }} } + # - uses: Swatinem/rust-cache@v2 + # - run: npm ci + # - name: Build addon + # working-directory: crates/mds-napi + # run: npx napi build --platform --release --target ${{ matrix.settings.target }} --no-js + # - uses: actions/upload-artifact@v4 + # with: + # name: bindings-${{ matrix.settings.target }} + # path: crates/mds-napi/*.node + # + # publish-npm: + # needs: [build-napi] + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-node@v4 + # with: { node-version: 22, cache: npm, registry-url: "https://registry.npmjs.org" } + # - run: npm ci + # - uses: actions/download-artifact@v4 + # with: { path: crates/mds-napi/artifacts } + # - name: Stage platform packages & publish addon + # working-directory: crates/mds-napi + # run: | + # npx napi create-npm-dirs + # npx napi artifacts --output-dir . + # npx napi prepublish -t npm + # env: + # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + # - name: Build & publish universal + bundler packages + # run: | + # npm run build --workspaces --if-present + # npm publish -w @mdscript/mds --access public + # npm publish -w @mdscript/bundler-utils --access public + # npm publish -w @mdscript/vite-plugin --access public + # npm publish -w @mdscript/rollup-plugin --access public + # npm publish -w @mdscript/webpack-loader --access public + # env: + # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 57277bad..9ef55e23 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ crates/mds-napi/index.d.ts crates/mds-napi/package-lock.json packages/*/dist/ packages/*/dist-cjs/ + +# Example app build output (sources are tracked, builds are not) +examples/*/dist/ +examples/*/package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5821ed72..ebde2745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,46 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - -### Added - -- **Negation in `@if` conditions** — prefix a dot-path with `!` to negate it (`@if !feature_enabled`). -- **Equality comparisons in `@if`/`@elseif`** — compare a dot-path against a string, number, boolean, or null literal using `==` or `!=` (`@if role == "admin"`, `@if count != 0`). -- **`@elseif` directive** — chain multiple conditional branches without nesting (`@if`/`@elseif`/`@else`/`@end`). -- **NaN and Infinity rejection** — numeric literals that parse to `NaN` or infinite values are now rejected at parse time with a clear error. -- **CJS build for `@mds/webpack-loader`** — Webpack 5 compatibility; the package now ships both ESM and CommonJS entry points. -- **`LazyInit` utility in `@mds/bundler-utils`** — concurrency-safe lazy initialization with deduplication of concurrent factory calls, retry-on-reject semantics, and a TOCTOU-safe `reset()`. Extracted from the webpack-loader for shared use across bundler plugins (#32). -- **API surface tests and non-UTF-8 rejection tests** — `api_surface.rs` covers the new `&str` signatures for `resolve_path` and `resolve_source`; Unix-only tests verify that non-UTF-8 `OsStr` paths are rejected at the boundary with a clear error (#12). -- **Bundler integration packages** — import `.mds` templates natively in JavaScript/TypeScript bundlers - - `@mds/bundler-utils` — shared transform, frontmatter detection, and error formatting utilities - - `@mds/vite-plugin` — Vite transform hook with HMR support (`vite ^5 || ^6`) - - `@mds/rollup-plugin` — Rollup 3/4 transform hook - - `@mds/webpack-loader` — Webpack 5 async loader - - All plugins accept `{ vars?: Record }` for template variables - - TypeScript module declarations via `@mds/bundler-utils/mds` -- **`@mds/mds` npm package** — universal JavaScript/TypeScript bindings for the MDS compiler - - Node.js entry auto-selects the native addon with WASM fallback - - Browser entry via WASM; requires `init()` before use - - API: `compile`, `check`, `compileFile`, `checkFile`, `getBackend`, `init`, `isMdsError` - - `MDS_BACKEND` environment variable to force `native` or `wasm` backend - - Full TypeScript types with JSDoc - -### Changed +## [0.1.0] — 2026-05-29 -- **`MAX_NESTING_DEPTH` reduced from 256 to 64** — the maximum nesting depth for `@if`/`@for`/`@define` blocks is now 64 (down from 256). This prevents stack overflow on crafted adversarial inputs while remaining generous for real templates. Consumers relying on nesting deeper than 64 levels must flatten their templates. (`mds-core` crate breaking change.) -- **`isMdsError()` stricter identification** — the function now requires the `code` property to start with `"mds::"` in addition to being an `Error` instance with a string `code`. Consumers who previously created synthetic error objects with arbitrary `code` strings and relied on `isMdsError()` returning `true` must prefix their codes with `"mds::"` or use a separate check. -- **`ModuleCache::resolve_path` and `ModuleCache::resolve_source` accept `&str` instead of `&Path`** — eliminates silent UTF-8 corruption on non-UTF-8 paths; non-UTF-8 paths now fail with an explicit error at the public API boundary rather than producing garbled output. Rust library consumers calling these methods must pass `&str`; this is a breaking change for direct users of the `mds-core` crate (#23, #12). +Initial public release of the MDS (Markdown Script) compiler. -## [0.1.0] — 2026-05-15 +### Language features -Initial release of the MDS (Markdown Script) compiler. - -### Added - -**Language features** - Variable interpolation from YAML frontmatter (`{name}`) -- `@if`/`@else`/`@end` conditionals with full MDS truthiness rules +- `@if`/`@elseif`/`@else`/`@end` conditionals with full MDS truthiness rules, + negation (`@if !feature_enabled`), and equality/inequality comparisons against + string, number, boolean, or null literals (`@if role == "admin"`, `@if count != 0`) - `@for item in list:` loops over arrays - `@define` function definitions with parameters and lexical scoping - `@import` directives: alias (`as ns`), merge, and selective (`{ a, b }`) @@ -52,9 +22,11 @@ Initial release of the MDS (Markdown Script) compiler. - `@include ns` to inline the prompt body of an imported module - Escaped braces (`\{` produces `{`) - Frontmatter `type: mds` marker to allow `.md` files as MDS sources -- String literal arguments with single-quote delimiters +- String literal arguments with single- and double-quote delimiters +- `NaN` and `Infinity` numeric literals are rejected at parse time with a clear error + +### Compiler pipeline -**Compiler pipeline** - Lexer with token types for all MDS syntax elements - Recursive-descent parser producing a typed AST - Module resolver with `Arc` caching and cycle detection @@ -62,27 +34,60 @@ Initial release of the MDS (Markdown Script) compiler. - Evaluator with `EvalContext` threading (call stack, iteration counting, warnings) - `mds.json` project config with `build.output_dir` -**CLI** (`mds` binary) +### CLI (`mds` binary) + - `mds build` — compile `.mds` to Markdown with auto-detection, `--out-dir`, `--set`, `--vars` - `mds check` — validate without rendering - `mds init` — create a starter template - Stdin mode (`mds build -`) -- Categorized exit codes (0/1/2/3) +- Categorized exit codes (0 success / 1 template error / 2 I/O error / 3 resource limit) - Rich miette diagnostics with source spans +- Global `--quiet` flag + +### Security & resource limits -**Security** - Path traversal prevention for imports and config `output_dir` - Symlink rejection in import paths - File size limits (10 MB per file, 1 MB for `mds.json`) -- Resource limits: call depth (128), loop iterations (100 K per loop, 1 M total), output size (50 MB), warnings (1000) +- Resource limits: call depth (128), loop iterations (100 K per loop, 1 M total), + output size (50 MB), warnings (1000) +- Block nesting depth limit of 64 for `@if`/`@for`/`@define` (guards against + stack overflow on adversarial input) - YAML/JSON value nesting depth limit (64 levels) +- Non-UTF-8 paths are rejected at the public API boundary with an explicit error + rather than producing corrupted output -**Library API** (`mds` crate) -- `compile()`, `compile_str()`, `compile_str_with()`, `compile_file()` — render to String +### Library API (`mds-core` crate, imported as `mds`) + +- `compile()`, `compile_str()`, `compile_str_with()`, `compile_file()` — render to `String` - `check()`, `check_str()`, `check_str_with()` — validate without rendering -- `compile_collecting_warnings()`, `compile_str_collecting_warnings()` — render and return `(String, Vec)` for caller-controlled warning output -- `check_collecting_warnings()`, `check_str_collecting_warnings()` — validate and return `((), Vec)` for caller-controlled warning output +- `compile_collecting_warnings()`, `compile_str_collecting_warnings()` — render and + return `(String, Vec)` for caller-controlled warning output +- `check_collecting_warnings()`, `check_str_collecting_warnings()` — validate and + return `((), Vec)` for caller-controlled warning output - `load_vars_file()` — load runtime variables from JSON +- `#[non_exhaustive]` on the public `MdsError` and `Value` enums + +### JavaScript / TypeScript packages + +- **`@mdscript/mds`** — universal bindings for the MDS compiler + - Node.js entry auto-selects the native addon (`mds-napi`) with WASM fallback + - Browser entry via WASM; requires `init()` before use + - API: `compile`, `check`, `compileFile`, `checkFile`, `getBackend`, `init`, `isMdsError` + - `isMdsError()` identifies MDS errors by an `Error` instance whose `code` starts with `"mds::"` + - `MDS_BACKEND` environment variable to force the `native` or `wasm` backend + - Full TypeScript types with JSDoc +- **Bundler integration** — import `.mds` templates natively in JS/TS bundlers + - `@mdscript/bundler-utils` — shared transform, frontmatter detection, error + formatting, and a concurrency-safe `LazyInit` utility + - `@mdscript/vite-plugin` — Vite transform hook with HMR support (`vite ^5 || ^6`) + - `@mdscript/rollup-plugin` — Rollup 3/4 transform hook + - `@mdscript/webpack-loader` — Webpack 5 async loader (ships ESM + CommonJS) + - All plugins accept `{ vars?: Record }` for template variables + - TypeScript module declarations (`.mds` → `string`) via `@mdscript/bundler-utils/mds` + +### Tests + +- 292 Rust tests (integration, unit, and doc-tests) plus the JavaScript package suites -**Tests** -- 292 tests covering integration, unit, and doc-tests +[0.1.0]: https://github.com/dean0x/mds/releases/tag/v0.1.0 diff --git a/Cargo.toml b/Cargo.toml index d80fedd2..21fa9880 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" rust-version = "1.88" # napi 3.9.0 sets rust-version = "1.88" in its published Cargo.toml license = "MIT" readme = "README.md" -repository = "https://github.com/deanshrn/mdl" +repository = "https://github.com/dean0x/mds" keywords = ["markdown", "template", "llm", "prompt", "compiler"] [workspace.dependencies] diff --git a/README.md b/README.md index 8b7ab092..3ebc42a0 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,24 @@ MDS is a template language for composable LLM prompt engineering. Write prompts ## Quick Start -**Install** (from source): +**Install the CLI:** + +```bash +cargo install mds-cli # installs the `mds` binary from crates.io +``` + +Or build from source: ```bash cargo install --path crates/mds-cli ``` +**Use from JavaScript/TypeScript** (Node or browser): + +```bash +npm install @mdscript/mds +``` + **Create your first template** (`hello.mds`): ``` @@ -78,13 +90,13 @@ import systemPrompt from './prompts/system.mds'; | Package | Bundler | Version | |---------|---------|---------| -| [`@mds/vite-plugin`](packages/vite-plugin/README.md) | Vite | ^5 \|\| ^6 | -| [`@mds/rollup-plugin`](packages/rollup-plugin/README.md) | Rollup | ^3 \|\| ^4 | -| [`@mds/webpack-loader`](packages/webpack-loader/README.md) | Webpack | ^5 | +| [`@mdscript/vite-plugin`](packages/vite-plugin/README.md) | Vite | ^5 \|\| ^6 | +| [`@mdscript/rollup-plugin`](packages/rollup-plugin/README.md) | Rollup | ^3 \|\| ^4 | +| [`@mdscript/webpack-loader`](packages/webpack-loader/README.md) | Webpack | ^5 | -All plugins require `@mds/mds` as a peer dependency and accept `{ vars?: Record }` for runtime template variables. See each package README for configuration details. +All plugins require `@mdscript/mds` as a peer dependency and accept `{ vars?: Record }` for runtime template variables. See each package README for configuration details. -TypeScript module declarations (`.mds` → `string`) are provided by `@mds/bundler-utils/mds`. +TypeScript module declarations (`.mds` → `string`) are provided by `@mdscript/bundler-utils/mds`. ## Library Usage @@ -103,6 +115,11 @@ let output = mds::compile(Path::new("template.mds"), Some(vars))?; mds::check(Path::new("template.mds"), None)?; ``` +## Examples + +Runnable templates, a Node.js API demo, and Vite/Rollup/Webpack integration apps +live in [`examples/`](examples/). + ## Language Reference See [spec.md](spec.md) for the full MDS v0.1 language specification. diff --git a/crates/mds-cli/Cargo.toml b/crates/mds-cli/Cargo.toml index e3af865f..1a45bc0b 100644 --- a/crates/mds-cli/Cargo.toml +++ b/crates/mds-cli/Cargo.toml @@ -15,7 +15,7 @@ name = "mds" path = "src/main.rs" [dependencies] -mds = { package = "mds-core", path = "../mds-core" } +mds = { package = "mds-core", path = "../mds-core", version = "0.1.0" } clap = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/mds-cli/tests/errors.rs b/crates/mds-cli/tests/errors.rs index 7cf60943..92b72c31 100644 --- a/crates/mds-cli/tests/errors.rs +++ b/crates/mds-cli/tests/errors.rs @@ -254,8 +254,15 @@ fn if_negation_supported() { // With premium=true, the negated condition is false → else branch. let source = "---\npremium: true\n---\n@if !premium:\nnegated_yes\n@else:\nnegated_no\n@end\n"; let result = mds::compile_str(source); - assert!(result.is_ok(), "@if with negation must succeed, got: {:?}", result); - assert!(result.unwrap().contains("negated_no"), "negation of true must take else branch"); + assert!( + result.is_ok(), + "@if with negation must succeed, got: {:?}", + result + ); + assert!( + result.unwrap().contains("negated_no"), + "negation of true must take else branch" + ); } #[test] @@ -388,7 +395,10 @@ fn elseif_after_else_is_parse_error() { // accepts @end as a terminator so @elseif is an unknown directive there. let source = "---\nx: true\n---\n@if x:\nyes\n@else:\nno\n@elseif x:\nbad\n@end\n"; let result = mds::compile_str(source); - assert!(result.is_err(), "@elseif after @else: must be a parse error"); + assert!( + result.is_err(), + "@elseif after @else: must be a parse error" + ); let err = format!("{}", result.unwrap_err()); assert!( err.contains("@elseif") || err.contains("unknown directive"), @@ -427,7 +437,10 @@ fn if_eq_undefined_variable_is_error() { // `@if missing == "x":` — undefined variable in equality let source = "---\nvar: x\n---\n@if missing == \"x\":\nyes\n@end\n"; let result = mds::compile_str(source); - assert!(result.is_err(), "undefined variable in equality must be an error"); + assert!( + result.is_err(), + "undefined variable in equality must be an error" + ); let err = format!("{}", result.unwrap_err()); assert!( err.contains("missing") || err.contains("undefined"), @@ -440,7 +453,10 @@ fn if_negation_undefined_variable_is_error() { // `@if !missing:` — undefined variable in negation let source = "---\nvar: x\n---\n@if !missing:\nyes\n@end\n"; let result = mds::compile_str(source); - assert!(result.is_err(), "undefined variable in negation must be an error"); + assert!( + result.is_err(), + "undefined variable in negation must be an error" + ); let err = format!("{}", result.unwrap_err()); assert!( err.contains("missing") || err.contains("undefined"), diff --git a/crates/mds-cli/tests/language.rs b/crates/mds-cli/tests/language.rs index 761a0be8..e3a8e6bb 100644 --- a/crates/mds-cli/tests/language.rs +++ b/crates/mds-cli/tests/language.rs @@ -637,7 +637,10 @@ fn if_negation_truthy_variable_skips_then_body() { // `@if !premium:` with premium=true → else branch executes let source = "---\npremium: true\n---\n@if !premium:\nfree_tier\n@else:\npaid_tier\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("paid_tier"), "negation of true must take else branch"); + assert!( + result.contains("paid_tier"), + "negation of true must take else branch" + ); assert!(!result.contains("free_tier"), "then body must be skipped"); } @@ -646,7 +649,10 @@ fn if_negation_falsy_variable_enters_then_body() { // `@if !premium:` with premium=false → then branch executes let source = "---\npremium: false\n---\n@if !premium:\nfree_tier\n@else:\npaid_tier\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("free_tier"), "negation of false must take then branch"); + assert!( + result.contains("free_tier"), + "negation of false must take then branch" + ); assert!(!result.contains("paid_tier"), "else body must be skipped"); } @@ -655,7 +661,10 @@ fn if_negation_zero_is_truthy_branch() { // `@if !count:` with count=0 → 0 is falsy, so !0 is truthy let source = "---\ncount: 0\n---\n@if !count:\nzero_branch\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("zero_branch"), "negation of 0 must enter then branch"); + assert!( + result.contains("zero_branch"), + "negation of 0 must enter then branch" + ); } #[test] @@ -663,7 +672,10 @@ fn if_negation_empty_string_is_truthy_branch() { // `@if !name:` with name="" → empty string is falsy, so !name is truthy let source = "---\nname: \"\"\n---\n@if !name:\nempty_branch\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("empty_branch"), "negation of empty string must enter then branch"); + assert!( + result.contains("empty_branch"), + "negation of empty string must enter then branch" + ); } #[test] @@ -671,7 +683,10 @@ fn if_negation_null_is_truthy_branch() { // `@if !val:` with val=null → null is falsy, so !val is truthy let source = "---\nval: null\n---\n@if !val:\nnull_branch\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("null_branch"), "negation of null must enter then branch"); + assert!( + result.contains("null_branch"), + "negation of null must enter then branch" + ); } #[test] @@ -679,7 +694,10 @@ fn if_negation_dot_path() { // `@if !config.debug:` with config.debug=false → enters then branch let source = "---\nconfig:\n debug: false\n---\n@if !config.debug:\ndebug_off_branch\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("debug_off_branch"), "negation of config.debug=false must enter then branch"); + assert!( + result.contains("debug_off_branch"), + "negation of config.debug=false must enter then branch" + ); } // ── @if negation with undefined variable tests ─────────────────────────────── @@ -691,7 +709,8 @@ fn if_negation_undefined_variable_is_error() { let result = mds::compile_str(source); assert!( result.is_err(), - "negation of undefined variable must produce an error, got: {:?}", result + "negation of undefined variable must produce an error, got: {:?}", + result ); let err = format!("{}", result.unwrap_err()); assert!( @@ -705,35 +724,51 @@ fn if_negation_undefined_variable_is_error() { #[test] fn if_eq_string_match_enters_then_body() { // `@if role == "admin":` with role=admin → then branch - let source = "---\nrole: admin\n---\n@if role == \"admin\":\nadmin_yes\n@else:\nadmin_no\n@end\n"; + let source = + "---\nrole: admin\n---\n@if role == \"admin\":\nadmin_yes\n@else:\nadmin_no\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("admin_yes"), "exact string match must enter then branch"); + assert!( + result.contains("admin_yes"), + "exact string match must enter then branch" + ); assert!(!result.contains("admin_no"), "else body must not appear"); } #[test] fn if_eq_string_no_match_enters_else_body() { // `@if role == "admin":` with role=user → else branch - let source = "---\nrole: user\n---\n@if role == \"admin\":\nadmin_yes\n@else:\nadmin_no\n@end\n"; + let source = + "---\nrole: user\n---\n@if role == \"admin\":\nadmin_yes\n@else:\nadmin_no\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("admin_no"), "string mismatch must take else branch"); + assert!( + result.contains("admin_no"), + "string mismatch must take else branch" + ); assert!(!result.contains("admin_yes"), "then body must not appear"); } #[test] fn if_eq_number_match() { // `@if count == 42:` with count=42 → then branch - let source = "---\ncount: 42\n---\n@if count == 42:\ncount_match\n@else:\ncount_nomatch\n@end\n"; + let source = + "---\ncount: 42\n---\n@if count == 42:\ncount_match\n@else:\ncount_nomatch\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("count_match") && !result.contains("count_nomatch"), "number equality must match"); + assert!( + result.contains("count_match") && !result.contains("count_nomatch"), + "number equality must match" + ); } #[test] fn if_eq_bool_true_match() { // `@if active == true:` with active=true → then branch - let source = "---\nactive: true\n---\n@if active == true:\nactive_on\n@else:\nactive_off\n@end\n"; + let source = + "---\nactive: true\n---\n@if active == true:\nactive_on\n@else:\nactive_off\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("active_on") && !result.contains("active_off"), "bool true equality must match"); + assert!( + result.contains("active_on") && !result.contains("active_off"), + "bool true equality must match" + ); } #[test] @@ -749,16 +784,23 @@ fn if_eq_strict_no_type_coercion_number_vs_string() { // `@if x == "3":` with x=3 (number) → strict, no coercion → else branch let source = "---\nx: 3\n---\n@if x == \"3\":\ncoercion_yes\n@else:\nstrict_types\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("strict_types"), "number 3 must not equal string \"3\""); + assert!( + result.contains("strict_types"), + "number 3 must not equal string \"3\"" + ); assert!(!result.contains("coercion_yes"), "coercion must not happen"); } #[test] fn if_eq_strict_no_type_coercion_bool_vs_string() { // `@if x == "true":` with x=true (bool) → strict, no coercion → else branch - let source = "---\nx: true\n---\n@if x == \"true\":\ncoercion_yes\n@else:\nstrict_types\n@end\n"; + let source = + "---\nx: true\n---\n@if x == \"true\":\ncoercion_yes\n@else:\nstrict_types\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("strict_types"), "bool true must not equal string \"true\""); + assert!( + result.contains("strict_types"), + "bool true must not equal string \"true\"" + ); assert!(!result.contains("coercion_yes"), "coercion must not happen"); } @@ -767,7 +809,10 @@ fn if_eq_single_quoted_rhs() { // `@if role == 'admin':` with role=admin → then branch (single quotes) let source = "---\nrole: admin\n---\n@if role == 'admin':\nadmin_singlequote\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("admin_singlequote"), "single-quoted RHS must work"); + assert!( + result.contains("admin_singlequote"), + "single-quoted RHS must work" + ); } #[test] @@ -775,7 +820,10 @@ fn if_eq_empty_string_rhs() { // `@if name == "":` with name="" → then branch let source = "---\nname: \"\"\n---\n@if name == \"\":\nempty_name_branch\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("empty_name_branch"), "empty string equality must match"); + assert!( + result.contains("empty_name_branch"), + "empty string equality must match" + ); } #[test] @@ -784,8 +832,14 @@ fn if_eq_operator_in_string_rhs() { // The `==` inside the string must not be mistaken for the operator let source = "---\nmsg: \"a == b\"\n---\n@if msg == \"a == b\":\nop_inside_string_match\n@else:\nop_inside_string_nomatch\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("op_inside_string_match"), "operator inside string must not confuse the parser"); - assert!(!result.contains("op_inside_string_nomatch"), "nomatch branch must not appear"); + assert!( + result.contains("op_inside_string_match"), + "operator inside string must not confuse the parser" + ); + assert!( + !result.contains("op_inside_string_nomatch"), + "nomatch branch must not appear" + ); } #[test] @@ -793,7 +847,10 @@ fn if_eq_negative_number() { // `@if temp == -5:` with temp=-5 → then branch let source = "---\ntemp: -5\n---\n@if temp == -5:\ncold_branch\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("cold_branch"), "negative number equality must match"); + assert!( + result.contains("cold_branch"), + "negative number equality must match" + ); } #[test] @@ -811,7 +868,10 @@ fn if_neq_string_no_match_enters_then_body() { // `@if role != "admin":` with role=user → then branch let source = "---\nrole: user\n---\n@if role != \"admin\":\nnot_admin_branch\n@else:\nis_admin_branch\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("not_admin_branch"), "inequality with non-matching value must enter then branch"); + assert!( + result.contains("not_admin_branch"), + "inequality with non-matching value must enter then branch" + ); assert!(!result.contains("is_admin_branch"), "else must not appear"); } @@ -820,16 +880,26 @@ fn if_neq_string_match_enters_else_body() { // `@if role != "admin":` with role=admin → else branch let source = "---\nrole: admin\n---\n@if role != \"admin\":\nnot_admin_branch\n@else:\nis_admin_branch\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("is_admin_branch"), "inequality with matching value must take else branch"); - assert!(!result.contains("not_admin_branch"), "then branch must not appear"); + assert!( + result.contains("is_admin_branch"), + "inequality with matching value must take else branch" + ); + assert!( + !result.contains("not_admin_branch"), + "then branch must not appear" + ); } #[test] fn if_neq_cross_type_always_true() { // `@if x != "3":` with x=3 (number) → types differ, always true - let source = "---\nx: 3\n---\n@if x != \"3\":\ndiff_type_branch\n@else:\nsame_type_branch\n@end\n"; + let source = + "---\nx: 3\n---\n@if x != \"3\":\ndiff_type_branch\n@else:\nsame_type_branch\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("diff_type_branch"), "cross-type != must be true"); + assert!( + result.contains("diff_type_branch"), + "cross-type != must be true" + ); assert!(!result.contains("same_type_branch"), "else must not appear"); } @@ -840,7 +910,10 @@ fn elseif_first_branch_matches() { // tier=enterprise → first branch matches let source = "---\ntier: enterprise\n---\n@if tier == \"enterprise\":\nenterprise_body\n@elseif tier == \"pro\":\npro_body\n@else:\nfree_body\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("enterprise_body"), "enterprise branch must match"); + assert!( + result.contains("enterprise_body"), + "enterprise branch must match" + ); assert!(!result.contains("pro_body"), "pro branch must not appear"); assert!(!result.contains("free_body"), "else branch must not appear"); } @@ -851,7 +924,10 @@ fn elseif_second_branch_matches() { let source = "---\ntier: pro\n---\n@if tier == \"enterprise\":\nenterprise_body\n@elseif tier == \"pro\":\npro_body\n@else:\nfree_body\n@end\n"; let result = mds::compile_str(source).unwrap(); assert!(result.contains("pro_body"), "pro branch must match"); - assert!(!result.contains("enterprise_body"), "enterprise branch must not appear"); + assert!( + !result.contains("enterprise_body"), + "enterprise branch must not appear" + ); assert!(!result.contains("free_body"), "else branch must not appear"); } @@ -860,8 +936,14 @@ fn elseif_falls_through_to_else() { // tier=starter → falls through to @else let source = "---\ntier: starter\n---\n@if tier == \"enterprise\":\nenterprise_body\n@elseif tier == \"pro\":\npro_body\n@else:\nfree_body\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("free_body"), "unmatched tiers must fall through to else"); - assert!(!result.contains("enterprise_body"), "enterprise branch must not appear"); + assert!( + result.contains("free_body"), + "unmatched tiers must fall through to else" + ); + assert!( + !result.contains("enterprise_body"), + "enterprise branch must not appear" + ); assert!(!result.contains("pro_body"), "pro branch must not appear"); } @@ -870,8 +952,14 @@ fn elseif_no_match_no_else_empty_output() { // No matching branch and no @else → only frontmatter in output let source = "---\ntier: starter\n---\n@if tier == \"enterprise\":\nenterprise_body\n@elseif tier == \"pro\":\npro_body\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(!result.contains("enterprise_body"), "unmatched must produce no enterprise output"); - assert!(!result.contains("pro_body"), "unmatched must produce no pro output"); + assert!( + !result.contains("enterprise_body"), + "unmatched must produce no enterprise output" + ); + assert!( + !result.contains("pro_body"), + "unmatched must produce no pro output" + ); } #[test] @@ -880,9 +968,18 @@ fn elseif_five_branches_matches_fourth() { let source = "---\nval: delta\n---\n@if val == \"alpha\":\nalpha_out\n@elseif val == \"bravo\":\nbravo_out\n@elseif val == \"charlie\":\ncharlie_out\n@elseif val == \"delta\":\ndelta_out\n@elseif val == \"echo\":\necho_out\n@else:\nother_out\n@end\n"; let result = mds::compile_str(source).unwrap(); assert!(result.contains("delta_out"), "fourth branch must match"); - assert!(!result.contains("alpha_out"), "alpha branch must not appear"); - assert!(!result.contains("bravo_out"), "bravo branch must not appear"); - assert!(!result.contains("charlie_out"), "charlie branch must not appear"); + assert!( + !result.contains("alpha_out"), + "alpha branch must not appear" + ); + assert!( + !result.contains("bravo_out"), + "bravo branch must not appear" + ); + assert!( + !result.contains("charlie_out"), + "charlie branch must not appear" + ); assert!(!result.contains("echo_out"), "echo branch must not appear"); assert!(!result.contains("other_out"), "else branch must not appear"); } @@ -901,7 +998,10 @@ fn elseif_with_negation() { // `@if flag_x:` / `@elseif !flag_y:` — negation in elseif let source = "---\nflag_x: false\nflag_y: false\n---\n@if flag_x:\nbranch_x\n@elseif !flag_y:\nbranch_not_y\n@else:\nbranch_none\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("branch_not_y"), "negation in elseif must work"); + assert!( + result.contains("branch_not_y"), + "negation in elseif must work" + ); assert!(!result.contains("branch_x"), "x branch must not appear"); } @@ -911,7 +1011,10 @@ fn elseif_with_equality() { let source = "---\nrole: mod\n---\n@if role == \"admin\":\nadmin_body\n@elseif role == \"mod\":\nmod_body\n@else:\nother_body\n@end\n"; let result = mds::compile_str(source).unwrap(); assert!(result.contains("mod_body"), "equality in elseif must work"); - assert!(!result.contains("admin_body"), "admin branch must not appear"); + assert!( + !result.contains("admin_body"), + "admin branch must not appear" + ); } #[test] @@ -919,8 +1022,14 @@ fn elseif_nested_if_in_body() { // Nested @if inside @elseif body let source = "---\ntier: pro\nextra: true\n---\n@if tier == \"enterprise\":\nenterprise_body\n@elseif tier == \"pro\":\n@if extra:\npro_plus_body\n@else:\npro_basic_body\n@end\n@else:\nfree_body\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("pro_plus_body"), "nested @if inside elseif body must work"); - assert!(!result.contains("enterprise_body"), "enterprise must not appear"); + assert!( + result.contains("pro_plus_body"), + "nested @if inside elseif body must work" + ); + assert!( + !result.contains("enterprise_body"), + "enterprise must not appear" + ); } #[test] @@ -933,9 +1042,18 @@ fn elseif_mixed_equality_and_inequality_in_chain() { @else:\nanon_visitor\n\ @end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("registered_user"), "mod != visitor so != branch must match"); - assert!(!result.contains("admin_only"), "admin branch must not appear"); - assert!(!result.contains("anon_visitor"), "else branch must not appear"); + assert!( + result.contains("registered_user"), + "mod != visitor so != branch must match" + ); + assert!( + !result.contains("admin_only"), + "admin branch must not appear" + ); + assert!( + !result.contains("anon_visitor"), + "else branch must not appear" + ); // role=visitor → first branch (==) fails, second branch (!=) is false → else branch let source2 = "---\nrole: visitor\n---\n\ @@ -944,9 +1062,18 @@ fn elseif_mixed_equality_and_inequality_in_chain() { @else:\nanon_visitor\n\ @end\n"; let result2 = mds::compile_str(source2).unwrap(); - assert!(result2.contains("anon_visitor"), "visitor == visitor so != is false, must fall to else"); - assert!(!result2.contains("registered_user"), "registered branch must not appear"); - assert!(!result2.contains("admin_only"), "admin branch must not appear"); + assert!( + result2.contains("anon_visitor"), + "visitor == visitor so != is false, must fall to else" + ); + assert!( + !result2.contains("registered_user"), + "registered branch must not appear" + ); + assert!( + !result2.contains("admin_only"), + "admin branch must not appear" + ); } #[test] @@ -954,6 +1081,12 @@ fn elseif_short_circuit_only_matched_branch_evaluates() { // Short-circuit: once a branch matches, subsequent @elseif are not evaluated. let source = "---\nval: first\n---\n@if val == \"first\":\nfirst_match_body\n@elseif val == \"first\":\nduplicate_match_body\n@end\n"; let result = mds::compile_str(source).unwrap(); - assert!(result.contains("first_match_body"), "first matching branch must win"); - assert!(!result.contains("duplicate_match_body"), "subsequent branches must be skipped"); + assert!( + result.contains("first_match_body"), + "first matching branch must win" + ); + assert!( + !result.contains("duplicate_match_body"), + "subsequent branches must be skipped" + ); } diff --git a/crates/mds-core/src/evaluator.rs b/crates/mds-core/src/evaluator.rs index 6397df93..75a801bb 100644 --- a/crates/mds-core/src/evaluator.rs +++ b/crates/mds-core/src/evaluator.rs @@ -356,12 +356,14 @@ fn evaluate_condition(condition: &Condition, scope: &Scope) -> Result Ok(resolve_condition_value(condition, scope)?.is_truthy()), Condition::Not(_) => Ok(!resolve_condition_value(condition, scope)?.is_truthy()), - Condition::Eq(_, expected) => { - Ok(values_equal(&resolve_condition_value(condition, scope)?, expected)) - } - Condition::NotEq(_, expected) => { - Ok(!values_equal(&resolve_condition_value(condition, scope)?, expected)) - } + Condition::Eq(_, expected) => Ok(values_equal( + &resolve_condition_value(condition, scope)?, + expected, + )), + Condition::NotEq(_, expected) => Ok(!values_equal( + &resolve_condition_value(condition, scope)?, + expected, + )), } } diff --git a/crates/mds-core/src/parser.rs b/crates/mds-core/src/parser.rs index 1046d1d2..923d016a 100644 --- a/crates/mds-core/src/parser.rs +++ b/crates/mds-core/src/parser.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use crate::ast::{ - Arg, Condition, CondValue, DefineBlock, ExportDirective, Expr, ForBlock, Frontmatter, IfBlock, + Arg, CondValue, Condition, DefineBlock, ExportDirective, Expr, ForBlock, Frontmatter, IfBlock, ImportDirective, IncludeDirective, Interpolation, Module, Node, TextNode, MAX_ELSEIF_BRANCHES, }; use crate::error::MdsError; @@ -214,9 +214,7 @@ impl Parser<'_> { // Give targeted hints for @elseif used outside an @if block if trimmed.starts_with("@elseif ") || trimmed == "@elseif" { - return Err(MdsError::syntax( - "@elseif must appear inside an @if block", - )); + return Err(MdsError::syntax("@elseif must appear inside an @if block")); } // Give a targeted hint for @elseif: (missing condition after the colon) @@ -617,9 +615,7 @@ fn parse_condition(s: &str) -> Result { let rhs = s[rhs_start..].trim(); if rhs.is_empty() { - return Err(MdsError::syntax(format!( - "expected value after '{op}'" - ))); + return Err(MdsError::syntax(format!("expected value after '{op}'"))); } let path = parse_dot_path(lhs)?; @@ -641,9 +637,7 @@ fn parse_condition(s: &str) -> Result { let after = &s[eq_pos + 1..]; // Bare `=` (not `==` and not `!=`) if !after.starts_with('=') && !before.ends_with('!') { - return Err(MdsError::syntax( - "use '==' for comparison, not '='", - )); + return Err(MdsError::syntax("use '==' for comparison, not '='")); } } @@ -1745,7 +1739,10 @@ mod tests { fn condition_value_escaped_quote_in_string() { // @if var == "say \"hi\"": — inner escaped quote must be unescaped let result = parse_cond_value(r#""say \"hi\"""#); - assert!(result.is_ok(), "escaped quote in condition value must parse"); + assert!( + result.is_ok(), + "escaped quote in condition value must parse" + ); if let Ok(CondValue::String(s)) = result { assert_eq!(s, r#"say "hi""#, "escaped quote must be unescaped"); } else { diff --git a/crates/mds-napi/Cargo.toml b/crates/mds-napi/Cargo.toml index c7660f7b..dea65484 100644 --- a/crates/mds-napi/Cargo.toml +++ b/crates/mds-napi/Cargo.toml @@ -20,7 +20,7 @@ debug-panics = [] crate-type = ["cdylib"] [dependencies] -mds = { package = "mds-core", path = "../mds-core" } +mds = { package = "mds-core", path = "../mds-core", version = "0.1.0" } napi = { workspace = true } napi-derive = { workspace = true } serde_json = { workspace = true } diff --git a/crates/mds-napi/index.js b/crates/mds-napi/index.js index c5fa8b2c..d430d7ba 100644 --- a/crates/mds-napi/index.js +++ b/crates/mds-napi/index.js @@ -12,13 +12,13 @@ function isMusl() { function loadBinding() { const errors = []; const triples = { - 'darwin-arm64': ['mds-napi.darwin-arm64.node', '@mds/mds-darwin-arm64'], - 'darwin-x64': ['mds-napi.darwin-x64.node', '@mds/mds-darwin-x64'], - 'linux-x64-gnu': ['mds-napi.linux-x64-gnu.node', '@mds/mds-linux-x64-gnu'], - 'linux-x64-musl': ['mds-napi.linux-x64-musl.node', '@mds/mds-linux-x64-musl'], - 'linux-arm64-gnu': ['mds-napi.linux-arm64-gnu.node', '@mds/mds-linux-arm64-gnu'], - 'linux-arm64-musl': ['mds-napi.linux-arm64-musl.node', '@mds/mds-linux-arm64-musl'], - 'win32-x64': ['mds-napi.win32-x64-msvc.node', '@mds/mds-win32-x64-msvc'], + 'darwin-arm64': ['mds-napi.darwin-arm64.node', '@mdscript/mds-darwin-arm64'], + 'darwin-x64': ['mds-napi.darwin-x64.node', '@mdscript/mds-darwin-x64'], + 'linux-x64-gnu': ['mds-napi.linux-x64-gnu.node', '@mdscript/mds-linux-x64-gnu'], + 'linux-x64-musl': ['mds-napi.linux-x64-musl.node', '@mdscript/mds-linux-x64-musl'], + 'linux-arm64-gnu': ['mds-napi.linux-arm64-gnu.node', '@mdscript/mds-linux-arm64-gnu'], + 'linux-arm64-musl': ['mds-napi.linux-arm64-musl.node', '@mdscript/mds-linux-arm64-musl'], + 'win32-x64': ['mds-napi.win32-x64-msvc.node', '@mdscript/mds-win32-x64-msvc'], }; let key; diff --git a/crates/mds-wasm/Cargo.toml b/crates/mds-wasm/Cargo.toml index c97b1929..1da5b4e3 100644 --- a/crates/mds-wasm/Cargo.toml +++ b/crates/mds-wasm/Cargo.toml @@ -20,7 +20,7 @@ debug-panics = [] crate-type = ["cdylib", "rlib"] [dependencies] -mds = { package = "mds-core", path = "../mds-core" } +mds = { package = "mds-core", path = "../mds-core", version = "0.1.0" } wasm-bindgen = { workspace = true } serde-wasm-bindgen = { workspace = true } serde = { workspace = true } @@ -28,7 +28,7 @@ serde_json = { workspace = true } js-sys = { workspace = true } [dev-dependencies] -mds = { package = "mds-core", path = "../mds-core" } +mds = { package = "mds-core", path = "../mds-core", version = "0.1.0" } wasm-bindgen-test = { workspace = true } [package.metadata.wasm-pack.profile.release] diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..29d18393 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,50 @@ +# MDS Examples + +Runnable examples demonstrating the MDS language and its integrations. Compile +any `.mds` file with the CLI: + +```bash +mds build examples/ai-agent/system-prompt.mds -o - +``` + +## Templates + +| Directory | What it shows | +|-----------|---------------| +| [`ai-agent/`](ai-agent/) | System prompts, multi-turn conversations, and tool instructions for LLM agents | +| [`api-docs/`](api-docs/) | Generating API documentation from endpoint and response-schema templates | +| [`blog-generator/`](blog-generator/) | A blog post template driven by frontmatter variables | +| [`prompt-library/`](prompt-library/) | A reusable prompt library using `@export`/`@import` (personas, formatting, guardrails) | +| [`edge-cases/`](edge-cases/) | Numbered walkthrough of language features — loops, conditionals, imports, escaping, re-exports, runtime vars | +| [`stress-test/`](stress-test/) | A large, deeply-composed template tree exercising the resolver and evaluator | + +Some examples take runtime variables — pass the accompanying `vars.json`: + +```bash +mds build examples/edge-cases/08_runtime_vars.mds --vars examples/edge-cases/vars.json -o - +``` + +## Node.js API + +[`node-api-test.mjs`](node-api-test.mjs) demonstrates compiling templates from +JavaScript via `@mdscript/mds`. + +## Bundler integrations + +Each app imports `.mds` files directly through the bundler plugin and resolves +the MDS packages from this monorepo (`file:` dependencies): + +| App | Plugin | +|-----|--------| +| [`vite-app/`](vite-app/) | `@mdscript/vite-plugin` | +| [`rollup-app/`](rollup-app/) | `@mdscript/rollup-plugin` | +| [`webpack-app/`](webpack-app/) | `@mdscript/webpack-loader` | + +To run one (after building the workspace packages from the repo root with +`npm install && npm run build --workspaces`): + +```bash +cd examples/vite-app +npm install +npm run build +``` diff --git a/examples/ai-agent/multi-turn-prompt.mds b/examples/ai-agent/multi-turn-prompt.mds new file mode 100644 index 00000000..42defb00 --- /dev/null +++ b/examples/ai-agent/multi-turn-prompt.mds @@ -0,0 +1,69 @@ +--- +assistant_name: CodeHelper +supported_languages: + - name: TypeScript + frameworks: + - React + - Next.js + - Express + - name: Python + frameworks: + - FastAPI + - Django + - Flask + - name: Rust + frameworks: + - Actix + - Axum + - Rocket +review_checklist: + - Check for security vulnerabilities + - Verify error handling patterns + - Ensure test coverage + - Review naming conventions + - Check for performance issues +max_context_lines: 500 +--- + +@import "../prompt-library/guardrails.mds" as guard +@import "../prompt-library/personas.mds" as persona +@import "../prompt-library/formatting.mds" as fmt + +# {assistant_name} — Code Review Assistant + +{guard.safety_rules()} + +{guard.tone_professional()} + +{persona.code_reviewer("multi-language")} + +## Supported Languages & Frameworks + +@for lang in supported_languages: +### {lang.name} + +Frameworks: +@for fw in lang.frameworks: +- {fw} +@end + +@end + +## Review Process + +When reviewing code, follow this checklist: + +@for item in review_checklist: +1. {item} +@end + +## Context Limits + +Maximum context window: **{max_context_lines} lines**. If the code exceeds +this limit, ask the user to share specific sections. + +{guard.output_markdown()} + +{fmt.separator()} + +{fmt.blockquote("Ready to review your code. Paste a snippet or describe what you need help with.")} diff --git a/examples/ai-agent/system-prompt.mds b/examples/ai-agent/system-prompt.mds new file mode 100644 index 00000000..5cd4124c --- /dev/null +++ b/examples/ai-agent/system-prompt.mds @@ -0,0 +1,74 @@ +--- +agent_name: DataBot +version: "2.1" +model: claude-sonnet +capabilities: + - data_analysis + - visualization + - sql_generation + - report_writing +max_tokens: 4096 +temperature: 0.3 +tools: + - name: run_sql + description: Execute SQL queries against the data warehouse + - name: create_chart + description: Generate charts from data + - name: export_csv + description: Export results to CSV +constraints: + max_rows: 10000 + timeout_seconds: 30 + allowed_schemas: + - analytics + - reporting + - public +--- + +@import "./tool-instructions.mds" as tools +@import "../prompt-library/guardrails.mds" as guard +@import "../prompt-library/formatting.mds" as fmt + +# {agent_name} v{version} + +You are **{agent_name}**, an AI data analysis assistant powered by {model}. + +{guard.safety_rules()} + +## Your Capabilities + +@for cap in capabilities: +- {fmt.badge("Skill", cap)} +@end + +## Available Tools + +@for tool in tools: +### {tool.name} + +{tool.description} + +@end + +{tools.usage_guidelines()} + +## Constraints + +- Maximum result rows: **{constraints.max_rows}** +- Query timeout: **{constraints.timeout_seconds}s** +- Allowed database schemas: +@for schema in constraints.allowed_schemas: + - `{schema}` +@end + +{guard.output_markdown()} + +{guard.tone_professional()} + +## Interaction Pattern + +1. Understand the user's data question +2. Determine which tools are needed +3. Execute queries step by step +4. Present results with visualizations when helpful +5. Summarize findings in plain language diff --git a/examples/ai-agent/tool-instructions.mds b/examples/ai-agent/tool-instructions.mds new file mode 100644 index 00000000..acbe9a97 --- /dev/null +++ b/examples/ai-agent/tool-instructions.mds @@ -0,0 +1,17 @@ +--- +type: mds +--- + +@define usage_guidelines(): +## Tool Usage Guidelines + +When using tools, follow these rules: + +1. **Always explain** what you're about to do before calling a tool +2. **Validate inputs** — check that column names and table names exist +3. **Limit results** — use LIMIT clauses to avoid overwhelming output +4. **Handle errors** — if a query fails, explain why and suggest fixes +5. **Chain tools** — you can use multiple tools in sequence to answer complex questions +@end + +@export usage_guidelines diff --git a/examples/api-docs/endpoint-template.mds b/examples/api-docs/endpoint-template.mds new file mode 100644 index 00000000..f9d07feb --- /dev/null +++ b/examples/api-docs/endpoint-template.mds @@ -0,0 +1,103 @@ +--- +api_name: User Management API +base_url: "https://api.example.com/v2" +auth_type: Bearer Token +endpoints: + - method: GET + path: /users + description: List all users with pagination + params: + - name: page + type: integer + required: false + default: "1" + - name: limit + type: integer + required: false + default: "20" + response_code: 200 + - method: POST + path: /users + description: Create a new user + params: + - name: email + type: string + required: true + default: "" + - name: name + type: string + required: true + default: "" + - name: role + type: string + required: false + default: member + response_code: 201 + - method: GET + path: "/users/{id}" + description: Get user by ID + params: + - name: id + type: string + required: true + default: "" + response_code: 200 + - method: DELETE + path: "/users/{id}" + description: Delete a user + params: + - name: id + type: string + required: true + default: "" + response_code: 204 +rate_limit: + requests_per_minute: 100 + burst: 20 +--- + +@import "../prompt-library/formatting.mds" as fmt +@import "./response-schemas.mds" as schemas + +# {api_name} + +Base URL: `{base_url}` + +Authentication: **{auth_type}** + +Rate Limit: {rate_limit.requests_per_minute} req/min (burst: {rate_limit.burst}) + +{fmt.separator()} + +## Endpoints + +@for endpoint in endpoints: +### {endpoint.method} `{endpoint.path}` + +{endpoint.description} + +@if endpoint.params: +#### Parameters + +| Name | Type | Required | Default | +|------|------|----------|---------| +@for param in endpoint.params: +| `{param.name}` | {param.type} | {param.required} | {param.default} | +@end +@end + +**Response**: `{endpoint.response_code}` + +{fmt.separator()} + +@end + +{schemas.error_format()} + +## Authentication + +Include the following header with every request: + +``` +Authorization: Bearer YOUR_API_TOKEN +``` diff --git a/examples/api-docs/response-schemas.mds b/examples/api-docs/response-schemas.mds new file mode 100644 index 00000000..4c87ea0c --- /dev/null +++ b/examples/api-docs/response-schemas.mds @@ -0,0 +1,31 @@ +--- +type: mds +--- + +@define error_format(): +## Error Response Format + +All errors follow this structure: + +```json +{ + "error": { + "code": "ERROR_CODE", + "message": "Human-readable message", + "details": {} + } +} +``` + +Common error codes: + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `UNAUTHORIZED` | 401 | Missing or invalid token | +| `FORBIDDEN` | 403 | Insufficient permissions | +| `NOT_FOUND` | 404 | Resource not found | +| `RATE_LIMITED` | 429 | Too many requests | +| `INTERNAL` | 500 | Server error | +@end + +@export error_format diff --git a/examples/blog-generator/post-template.mds b/examples/blog-generator/post-template.mds new file mode 100644 index 00000000..fbfcff41 --- /dev/null +++ b/examples/blog-generator/post-template.mds @@ -0,0 +1,74 @@ +--- +title: "Building Reliable Distributed Systems" +author: "Jane Chen" +date: "2026-05-20" +tags: + - distributed-systems + - reliability + - engineering +audience: senior engineers +sections: + - title: Introduction + key_points: + - Why distributed systems fail + - The fallacies of distributed computing + - title: Core Patterns + key_points: + - Circuit breakers + - Retry with backoff + - Bulkheads + - title: Observability + key_points: + - Distributed tracing + - Structured logging + - SLO-based alerting + - title: Conclusion + key_points: + - Start simple + - Measure everything +--- + +@import "../prompt-library/formatting.mds" as fmt +@import "../prompt-library/guardrails.mds" as guard + +{guard.tone_professional()} + +You are a technical blog writer. Generate a detailed blog post based on the +following specification. + +{fmt.separator()} + +# {title} + +{fmt.badge("Author", author)} | {fmt.badge("Date", date)} | {fmt.badge("Audience", audience)} + +## Tags + +@for tag in tags: +- `#{tag}` +@end + +{fmt.separator()} + +## Article Structure + +@for section in sections: +### {section.title} + +Cover the following key points in this section: + +@for point in section.key_points: +- {point} +@end + +@end + +{fmt.separator()} + +## Writing Guidelines + +- Use concrete examples and code snippets where appropriate +- Include diagrams described in Mermaid syntax +- Target {audience} — assume familiarity with basic concepts +- Keep paragraphs concise — max 4 sentences each +- End with actionable takeaways diff --git a/examples/edge-cases/01_empty_arrays.mds b/examples/edge-cases/01_empty_arrays.mds new file mode 100644 index 00000000..22132380 --- /dev/null +++ b/examples/edge-cases/01_empty_arrays.mds @@ -0,0 +1,12 @@ +--- +items: [] +name: test +--- + +Before loop. + +@for item in items: +- {item} +@end + +After loop. Name is {name}. diff --git a/examples/edge-cases/02_nested_objects.mds b/examples/edge-cases/02_nested_objects.mds new file mode 100644 index 00000000..d2010d80 --- /dev/null +++ b/examples/edge-cases/02_nested_objects.mds @@ -0,0 +1,27 @@ +--- +config: + database: + host: localhost + port: 5432 + name: myapp + cache: + enabled: true + ttl: 3600 + features: + - name: dark_mode + enabled: true + - name: beta_api + enabled: false +--- + +## Configuration Report + +Database: `{config.database.host}:{config.database.port}/{config.database.name}` + +Cache enabled: {config.cache.enabled} (TTL: {config.cache.ttl}s) + +### Feature Flags + +@for feature in config.features: +- **{feature.name}**: {feature.enabled} +@end diff --git a/examples/edge-cases/03_shadowing.mds b/examples/edge-cases/03_shadowing.mds new file mode 100644 index 00000000..88f0e76d --- /dev/null +++ b/examples/edge-cases/03_shadowing.mds @@ -0,0 +1,22 @@ +--- +name: outer +items: + - first + - second +--- + +@define greet(name): +Hello {name} from function! +@end + +Outer name: {name} + +{greet("inner")} + +After function, outer name: {name} + +@for name in items: +Loop name: {name} +@end + +After loop, outer name: {name} diff --git a/examples/edge-cases/04_escaped_and_code.mds b/examples/edge-cases/04_escaped_and_code.mds new file mode 100644 index 00000000..36a7bc77 --- /dev/null +++ b/examples/edge-cases/04_escaped_and_code.mds @@ -0,0 +1,28 @@ +--- +language: Python +--- + +## Working with {language} + +Here's how to use string formatting in {language}: + +Escaped braces: \{variable\} should appear literally. + +```python +name = "World" +print(f"Hello {name}!") # This should NOT be interpolated +result = {"key": "value"} # This should NOT be interpolated +``` + +After the code block, interpolation resumes: {language} is great! + +Another code block with template syntax: + +``` +@if condition: +This is literal text inside a code fence. +@end +{this_is_also_literal} +``` + +Back to normal: {language} rocks. diff --git a/examples/edge-cases/05_deeply_nested_logic.mds b/examples/edge-cases/05_deeply_nested_logic.mds new file mode 100644 index 00000000..e8720170 --- /dev/null +++ b/examples/edge-cases/05_deeply_nested_logic.mds @@ -0,0 +1,53 @@ +--- +users: + - name: Alice + role: admin + projects: + - name: Alpha + active: true + tasks: + - Deploy v2 + - Update docs + - name: Beta + active: false + tasks: + - Fix bug + - name: Bob + role: member + projects: + - name: Gamma + active: true + tasks: + - Write tests + - Review PR +show_paused: false +--- + +# Project Report + +@for user in users: +## {user.name} ({user.role}) + +@for project in user.projects: +@if project.active: +### {project.name} — Active + +Tasks: +@for task in project.tasks: +- [ ] {task} +@end + +@else: +@if show_paused: +### {project.name} — Paused + +Tasks: +@for task in project.tasks: +- [x] {task} (on hold) +@end + +@end +@end +@end + +@end diff --git a/examples/edge-cases/06_object_iteration.mds b/examples/edge-cases/06_object_iteration.mds new file mode 100644 index 00000000..cd971bf0 --- /dev/null +++ b/examples/edge-cases/06_object_iteration.mds @@ -0,0 +1,14 @@ +--- +env: + NODE_ENV: production + PORT: "3000" + DATABASE_URL: "postgres://localhost/app" + LOG_LEVEL: info + CACHE_TTL: "300" +--- + +## Environment Variables + +@for key, value in env: +- `{key}` = `{value}` +@end diff --git a/examples/edge-cases/07_falsy_values.mds b/examples/edge-cases/07_falsy_values.mds new file mode 100644 index 00000000..de3ad228 --- /dev/null +++ b/examples/edge-cases/07_falsy_values.mds @@ -0,0 +1,61 @@ +--- +empty_string: "" +zero: 0 +false_val: false +empty_array: [] +empty_object: {} +truthy_string: "hello" +truthy_number: 42 +truthy_array: + - one +--- + +Testing truthiness: + +@if empty_string: +FAIL: empty string is truthy +@else: +PASS: empty string is falsy +@end + +@if zero: +FAIL: zero is truthy +@else: +PASS: zero is falsy +@end + +@if false_val: +FAIL: false is truthy +@else: +PASS: false is falsy +@end + +@if empty_array: +FAIL: empty array is truthy +@else: +PASS: empty array is falsy +@end + +@if empty_object: +FAIL: empty object is truthy +@else: +PASS: empty object is falsy +@end + +@if truthy_string: +PASS: non-empty string is truthy +@else: +FAIL: non-empty string is falsy +@end + +@if truthy_number: +PASS: non-zero number is truthy +@else: +FAIL: non-zero number is falsy +@end + +@if truthy_array: +PASS: non-empty array is truthy +@else: +FAIL: non-empty array is falsy +@end diff --git a/examples/edge-cases/08_runtime_vars.mds b/examples/edge-cases/08_runtime_vars.mds new file mode 100644 index 00000000..9a0bb47a --- /dev/null +++ b/examples/edge-cases/08_runtime_vars.mds @@ -0,0 +1,30 @@ +--- +app_name: MyApp +is_production: false +is_development: true +debug: false +--- + +# {app_name} + +@if debug: +## Debug Mode Enabled + +Verbose logging is ON. All queries will be traced. +@end + +@if is_production: +## Production Checklist + +- Ensure all secrets are rotated +- Verify CDN configuration +- Check rate limiting +@end + +@if is_development: +## Development Notes + +- Using local database +- Hot reload enabled +- Mock services active +@end diff --git a/examples/edge-cases/09_reexport_chain.mds b/examples/edge-cases/09_reexport_chain.mds new file mode 100644 index 00000000..03722374 --- /dev/null +++ b/examples/edge-cases/09_reexport_chain.mds @@ -0,0 +1,8 @@ +--- +type: mds +--- + +@export bold from "../prompt-library/formatting.mds" +@export italic from "../prompt-library/formatting.mds" +@export teacher from "../prompt-library/personas.mds" +@export safety_rules from "../prompt-library/guardrails.mds" diff --git a/examples/edge-cases/10_consume_reexports.mds b/examples/edge-cases/10_consume_reexports.mds new file mode 100644 index 00000000..809dd273 --- /dev/null +++ b/examples/edge-cases/10_consume_reexports.mds @@ -0,0 +1,11 @@ +--- +subject: Mathematics +--- + +@import "./09_reexport_chain.mds" as lib + +{lib.safety_rules()} + +{lib.teacher(subject)} + +Here is a {lib.bold("key concept")} in {lib.italic(subject)}. diff --git a/examples/edge-cases/11_include_directive.mds b/examples/edge-cases/11_include_directive.mds new file mode 100644 index 00000000..ae682c03 --- /dev/null +++ b/examples/edge-cases/11_include_directive.mds @@ -0,0 +1,17 @@ +--- +name: Test +--- + +@import "../prompt-library/guardrails.mds" as guard + +# Header + +This is the main content for {name}. + +## Included Guardrails Module + +@include guard + +## Back to Main + +This is after the include. diff --git a/examples/edge-cases/12_selective_import.mds b/examples/edge-cases/12_selective_import.mds new file mode 100644 index 00000000..23b41715 --- /dev/null +++ b/examples/edge-cases/12_selective_import.mds @@ -0,0 +1,14 @@ +--- +name: World +--- + +@import { bold, badge } from "../prompt-library/formatting.mds" +@import { teacher } from "../prompt-library/personas.mds" + +# Welcome + +Hello {bold(name)}! + +{badge("Status", "Active")} + +{teacher("Physics")} diff --git a/examples/edge-cases/13_function_composition.mds b/examples/edge-cases/13_function_composition.mds new file mode 100644 index 00000000..6b5fd883 --- /dev/null +++ b/examples/edge-cases/13_function_composition.mds @@ -0,0 +1,27 @@ +--- +items: + - name: Alpha + is_high: true + - name: Beta + is_high: false + - name: Gamma + is_high: true +--- + +@define format_priority(is_high): +@if is_high: +HIGH +@else: +LOW +@end +@end + +@define task_line(task): +- **{task.name}**: {format_priority(task.is_high)} +@end + +# Task List + +@for item in items: +{task_line(item)} +@end diff --git a/examples/edge-cases/14_merge_import.mds b/examples/edge-cases/14_merge_import.mds new file mode 100644 index 00000000..c222ae2b --- /dev/null +++ b/examples/edge-cases/14_merge_import.mds @@ -0,0 +1,14 @@ +--- +topic: Rust Ownership +--- + +@import "../prompt-library/formatting.mds" +@import "../prompt-library/personas.mds" + +# {bold(topic)} + +{teacher("Rust")} + +Key concepts to cover: + +{badge("Topic", topic)} diff --git a/examples/edge-cases/15_frontmatter_passthrough.mds b/examples/edge-cases/15_frontmatter_passthrough.mds new file mode 100644 index 00000000..7b66768f --- /dev/null +++ b/examples/edge-cases/15_frontmatter_passthrough.mds @@ -0,0 +1,17 @@ +--- +title: My Document +author: Dean +version: 1 +tags: + - test + - frontmatter +--- + +# {title} + +By {author}, version {version}. + +Tags: +@for tag in tags: +`{tag}` +@end diff --git a/examples/edge-cases/vars.json b/examples/edge-cases/vars.json new file mode 100644 index 00000000..859597e6 --- /dev/null +++ b/examples/edge-cases/vars.json @@ -0,0 +1,6 @@ +{ + "is_production": true, + "is_development": false, + "debug": true, + "app_name": "ProductionApp" +} diff --git a/examples/node-api-test.mjs b/examples/node-api-test.mjs new file mode 100644 index 00000000..065b8c7a --- /dev/null +++ b/examples/node-api-test.mjs @@ -0,0 +1,153 @@ +import * as mds from '../packages/mds/dist/node.js'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +await mds.init(); +console.log(`Backend: ${mds.getBackend()}`); + +const tests = []; +let passed = 0; +let failed = 0; + +function test(name, fn) { + tests.push({ name, fn }); +} + +function assert(condition, msg) { + if (!condition) throw new Error(`Assertion failed: ${msg}`); +} + +// ─── Test: compile simple string ───────────────────────────────── +test('compile simple string', () => { + const result = mds.compile('---\nname: World\n---\nHello {name}!\n'); + assert(result.output.includes('Hello World!'), 'should interpolate variable'); + assert(result.warnings.length === 0, 'should have no warnings'); + assert(result.dependencies.length === 0, 'string compile has no deps'); +}); + +// ─── Test: compile with runtime vars ───────────────────────────── +test('compile with vars override', () => { + const result = mds.compile('---\nenv: dev\n---\nEnv: {env}\n', { + vars: { env: 'production' }, + }); + assert(result.output.includes('Env: production'), 'vars should override frontmatter'); +}); + +// ─── Test: compile file with imports ───────────────────────────── +test('compileFile with imports', async () => { + const result = await mds.compileFile( + resolve(__dirname, 'ai-agent/system-prompt.mds'), + ); + assert(result.output.includes('DataBot'), 'should contain agent name'); + assert(result.output.includes('Safety Guidelines'), 'should include imported guardrails'); + assert(result.dependencies.length === 3, `expected 3 deps, got ${result.dependencies.length}`); +}); + +// ─── Test: compileFile with vars ───────────────────────────────── +test('compileFile with vars', async () => { + const result = await mds.compileFile( + resolve(__dirname, 'edge-cases/08_runtime_vars.mds'), + { vars: { is_production: true, is_development: false, debug: true } }, + ); + assert(result.output.includes('Debug Mode Enabled'), 'debug should be enabled via vars'); + assert(result.output.includes('Production Checklist'), 'should show production section'); + assert(!result.output.includes('Development Notes'), 'should NOT show dev section'); +}); + +// ─── Test: check valid file ────────────────────────────────────── +test('check valid file', async () => { + const result = await mds.checkFile( + resolve(__dirname, 'ai-agent/multi-turn-prompt.mds'), + ); + assert(result.warnings.length === 0, 'should have no warnings'); +}); + +// ─── Test: error handling with isMdsError ──────────────────────── +test('error handling', () => { + try { + mds.compile('Hello {undefined_var}!'); + assert(false, 'should have thrown'); + } catch (err) { + assert(mds.isMdsError(err), 'should be MDS error'); + assert(err.code === 'mds::undefined_var', `expected mds::undefined_var, got ${err.code}`); + } +}); + +// ─── Test: complex template with nested data ───────────────────── +test('complex nested data', () => { + const source = `--- +users: + - name: Alice + active: true + - name: Bob + active: false +--- +@for user in users: +@if user.active: +- {user.name} (active) +@end +@end +`; + const result = mds.compile(source); + assert(result.output.includes('Alice (active)'), 'should include active user'); + const body = result.output.split('---\n').slice(2).join('---\n'); + assert(!body.includes('Bob'), 'body should exclude inactive user'); +}); + +// ─── Test: function definition and call ────────────────────────── +test('function definition and call', () => { + const source = `--- +--- +@define greet(name, role): +Hello {name}, you are a {role}! +@end + +{greet("Alice", "developer")} +`; + const result = mds.compile(source); + assert(result.output.includes('Hello Alice, you are a developer!'), 'should expand function'); +}); + +// ─── Test: code block passthrough ──────────────────────────────── +test('code block passthrough', () => { + const source = '---\nlang: Python\n---\n\n```python\nx = {\"key\": \"value\"}\n```\n\nLanguage: {lang}\n'; + const result = mds.compile(source); + assert(result.output.includes('{"key": "value"}'), 'braces in code block should be literal'); + assert(result.output.includes('Language: Python'), 'var outside code block should interpolate'); +}); + +// ─── Test: escaped braces ──────────────────────────────────────── +test('escaped braces', () => { + const source = '---\nname: test\n---\nLiteral: \\{name\\} Interpolated: {name}\n'; + const result = mds.compile(source); + assert(result.output.includes('Literal: {name}'), 'escaped braces should be literal'); + assert(result.output.includes('Interpolated: test'), 'non-escaped should interpolate'); +}); + +// ─── Test: empty array loop ────────────────────────────────────── +test('empty array loop', () => { + const source = '---\nitems: []\n---\nBefore\n@for item in items:\n{item}\n@end\nAfter\n'; + const result = mds.compile(source); + assert(result.output.includes('Before'), 'should have content before loop'); + assert(result.output.includes('After'), 'should have content after loop'); + assert(!result.output.includes('undefined'), 'should not have undefined'); +}); + +// ─── Run all tests ─────────────────────────────────────────────── +console.log(`\nRunning ${tests.length} tests...\n`); + +for (const { name, fn } of tests) { + try { + await fn(); + passed++; + console.log(` PASS ${name}`); + } catch (err) { + failed++; + console.log(` FAIL ${name}: ${err.message}`); + } +} + +console.log(`\n${passed} passed, ${failed} failed out of ${tests.length} tests`); +if (failed > 0) process.exit(1); diff --git a/examples/prompt-library/examples.mds b/examples/prompt-library/examples.mds new file mode 100644 index 00000000..888722c6 --- /dev/null +++ b/examples/prompt-library/examples.mds @@ -0,0 +1,16 @@ +--- +type: mds +--- + +@define few_shot(task, examples): +Here are some examples of the task: + +@for example in examples: +**Input**: {example.input} +**Output**: {example.output} + +@end +Now perform the same task. +@end + +@export few_shot diff --git a/examples/prompt-library/formatting.mds b/examples/prompt-library/formatting.mds new file mode 100644 index 00000000..ccdf93b4 --- /dev/null +++ b/examples/prompt-library/formatting.mds @@ -0,0 +1,44 @@ +--- +type: mds +--- + +@define bold(text): +**{text}** +@end + +@define italic(text): +*{text}* +@end + +@define h1(text): +# {text} +@end + +@define h2(text): +## {text} +@end + +@define h3(text): +### {text} +@end + +@define badge(label, value): +**{label}**: `{value}` +@end + +@define separator(): +--- +@end + +@define blockquote(text): +> {text} +@end + +@export bold +@export italic +@export h1 +@export h2 +@export h3 +@export badge +@export separator +@export blockquote diff --git a/examples/prompt-library/guardrails.mds b/examples/prompt-library/guardrails.mds new file mode 100644 index 00000000..f44ac236 --- /dev/null +++ b/examples/prompt-library/guardrails.mds @@ -0,0 +1,50 @@ +--- +type: mds +--- + +@define safety_rules(): +## Safety Guidelines + +- Never generate harmful, illegal, or unethical content +- Decline requests that could cause real-world harm +- Protect user privacy — never store or repeat personal information +- When uncertain, ask for clarification rather than guessing +@end + +@define output_json(): +## Output Format + +Respond with valid JSON only. No markdown, no explanation outside the JSON. +@end + +@define output_markdown(): +## Output Format + +Respond with well-structured Markdown using headers, lists, and code blocks. +@end + +@define output_plain(): +## Output Format + +Respond with plain text only. No formatting, no markdown. +@end + +@define tone_professional(): +Maintain a professional, clear, and concise tone throughout your response. +@end + +@define tone_casual(): +Keep it friendly and conversational. Use simple language. +@end + +@define tone_academic(): +Use precise academic language with proper citations and formal structure. +@end + +@export safety_rules +@export output_json +@export output_markdown +@export output_plain +@export tone_professional +@export tone_casual +@export tone_academic diff --git a/examples/prompt-library/personas.mds b/examples/prompt-library/personas.mds new file mode 100644 index 00000000..49933a87 --- /dev/null +++ b/examples/prompt-library/personas.mds @@ -0,0 +1,25 @@ +--- +type: mds +--- + +@define teacher(subject): +You are an expert {subject} teacher. Explain concepts clearly using analogies +and real-world examples. Adapt your explanation depth to the student's level. +@end + +@define code_reviewer(language): +You are a senior {language} developer performing code review. Focus on: +- Correctness and edge cases +- Performance implications +- Readability and maintainability +- Security concerns +@end + +@define creative_writer(genre): +You are a {genre} writer with a vivid imagination. Use rich sensory details, +compelling dialogue, and strong narrative structure. +@end + +@export teacher +@export code_reviewer +@export creative_writer diff --git a/examples/rollup-app/package.json b/examples/rollup-app/package.json new file mode 100644 index 00000000..3bac4f9c --- /dev/null +++ b/examples/rollup-app/package.json @@ -0,0 +1,19 @@ +{ + "name": "mds-rollup-test-app", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rollup -c" + }, + "dependencies": { + "@mdscript/mds": "file:../../packages/mds", + "@mdscript/rollup-plugin": "file:../../packages/rollup-plugin", + "@mdscript/bundler-utils": "file:../../packages/bundler-utils" + }, + "devDependencies": { + "rollup": "^4.0.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "typescript": "^5.4.0" + } +} diff --git a/examples/rollup-app/rollup.config.mjs b/examples/rollup-app/rollup.config.mjs new file mode 100644 index 00000000..552eced2 --- /dev/null +++ b/examples/rollup-app/rollup.config.mjs @@ -0,0 +1,14 @@ +import mdsPlugin from '@mdscript/rollup-plugin'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; + +export default { + input: 'src/main.ts', + output: { + file: 'dist/main.mjs', + format: 'es', + }, + plugins: [ + mdsPlugin({ vars: { debug: false, mode: 'rollup-build' } }), + nodeResolve({ extensions: ['.ts', '.js'] }), + ], +}; diff --git a/examples/rollup-app/src/main.ts b/examples/rollup-app/src/main.ts new file mode 100644 index 00000000..469ffa9d --- /dev/null +++ b/examples/rollup-app/src/main.ts @@ -0,0 +1,43 @@ +import claudeConfig, { metadata } from './prompts/claude-code.mds'; +import codePassthrough from '../../stress-test/edge/code-passthrough.mds'; +import escapedBraces from '../../stress-test/edge/escaped-braces.mds'; +import shadowingStress from '../../stress-test/edge/shadowing-stress.mds'; +import emptyCollections from '../../stress-test/edge/empty-collections.mds'; +import dataAnalyst from '../../stress-test/agents/data-analyst.mds'; +import codeReviewer from '../../stress-test/agents/code-reviewer.mds'; +import orchestrator from '../../stress-test/agents/orchestrator.mds'; +import deepNesting from '../../stress-test/edge/deep-nesting.mds'; +import falsyMatrix from '../../stress-test/edge/falsy-matrix.mds'; +import chainConsumer from '../../stress-test/shared/chain-consumer.mds'; +import main from '../../stress-test/main.mds'; + +console.log('=== Claude Code System Prompt ==='); +console.log(claudeConfig); +console.log('\n=== Build Metadata ==='); +console.log('Dependencies:', metadata.dependencies.length, 'files'); +console.log('Warnings:', metadata.warnings.length); + +console.log('\n=== Stress Test: Code Passthrough ==='); +console.log(codePassthrough); +console.log('\n=== Stress Test: Escaped Braces ==='); +console.log(escapedBraces); +console.log('\n=== Stress Test: Shadowing ==='); +console.log(shadowingStress); +console.log('\n=== Stress Test: Empty Collections ==='); +console.log(emptyCollections); +console.log('\n=== Stress Test: Data Analyst ==='); +console.log(dataAnalyst); +console.log('\n=== Stress Test: Code Reviewer ==='); +console.log(codeReviewer); +console.log('\n=== Stress Test: Orchestrator ==='); +console.log(orchestrator); +console.log('\n=== Stress Test: Deep Nesting ==='); +console.log(deepNesting); +console.log('\n=== Stress Test: Falsy Matrix ==='); +console.log(falsyMatrix); +console.log('\n=== Stress Test: Chain Consumer ==='); +console.log(chainConsumer); +console.log('\n=== Stress Test: Main ==='); +console.log(main); + +export { claudeConfig, metadata, codePassthrough, escapedBraces, shadowingStress, emptyCollections, dataAnalyst, codeReviewer, orchestrator, deepNesting, falsyMatrix, chainConsumer, main }; diff --git a/examples/rollup-app/src/prompts/claude-code.mds b/examples/rollup-app/src/prompts/claude-code.mds new file mode 100644 index 00000000..59db77b3 --- /dev/null +++ b/examples/rollup-app/src/prompts/claude-code.mds @@ -0,0 +1,40 @@ +--- +agent: Claude Code +version: "4.6" +capabilities: + - code generation + - debugging + - refactoring + - code review + - architecture design +rules: + - Always explain changes before making them + - Prefer editing existing files over creating new ones + - Run tests after changes + - Never hardcode secrets +max_file_edits: 10 +--- + +@import "./shared.mds" as shared + +# {agent} Configuration (v{version}) + +{shared.preamble()} + +## Capabilities + +@for cap in capabilities: +- {cap} +@end + +## Rules + +@for rule in rules: +1. {rule} +@end + +## Limits + +- Maximum file edits per turn: **{max_file_edits}** + +{shared.footer()} diff --git a/examples/rollup-app/src/prompts/shared.mds b/examples/rollup-app/src/prompts/shared.mds new file mode 100644 index 00000000..dc62f2b0 --- /dev/null +++ b/examples/rollup-app/src/prompts/shared.mds @@ -0,0 +1,17 @@ +--- +type: mds +--- + +@define preamble(): +You are an AI coding assistant. Follow the configuration below precisely. +Your behavior is governed by the rules and constraints defined in this prompt. +@end + +@define footer(): +--- + +*Configuration generated by MDS. Do not edit manually.* +@end + +@export preamble +@export footer diff --git a/examples/stress-test/agents/code-reviewer.mds b/examples/stress-test/agents/code-reviewer.mds new file mode 100644 index 00000000..00dcaa21 --- /dev/null +++ b/examples/stress-test/agents/code-reviewer.mds @@ -0,0 +1,43 @@ +--- +agent_name: CodeReview Bot +language: TypeScript +focus_areas: + - Type safety + - Error handling + - Performance + - Security + - Test coverage +severity_levels: + - critical + - warning + - info +--- + +@import { bold, badge, code } from "../lib/formatting.mds" +@import "../lib/guardrails.mds" as guard + +# {bold(agent_name)} + +{badge("Language", language)} + +@include guard + +## Focus Areas + +@for area in focus_areas: +- {bold(area)} +@end + +## Severity Levels + +@for level in severity_levels: +@if level == "critical": +- {code(level)} — Must fix before merge +@elseif level == "warning": +- {code(level)} — Should fix, may defer +@else: +- {code(level)} — Suggestion only +@end +@end + +When reviewing {language} code, apply {language}-specific best practices. diff --git a/examples/stress-test/agents/data-analyst.mds b/examples/stress-test/agents/data-analyst.mds new file mode 100644 index 00000000..1fd47b60 --- /dev/null +++ b/examples/stress-test/agents/data-analyst.mds @@ -0,0 +1,78 @@ +--- +agent_name: DataInsight Pro +version: "2.1" +model: claude-sonnet-4-6 +capabilities: + - SQL query generation + - Statistical analysis + - Data visualization + - Anomaly detection +constraints: + max_rows: 50000 + timeout: 30 + allowed_schemas: + - analytics + - reporting + - staging +tools: + - name: sql_runner + type: query + safe: true + - name: chart_builder + type: visualization + safe: true + - name: data_export + type: export + safe: false +debug: false +output_format: markdown +--- + +@import "../lib/index.mds" as lib +@import "../shared/output-formats.mds" + +# {lib.bold(agent_name)} Configuration + +{lib.badge("Version", version)} +{lib.badge("Model", model)} + +## Capabilities + +@for cap in capabilities: +- {lib.code(cap)} +@end + +## Tools + +@for tool in tools: +@if tool.safe: +- {lib.bold(tool.name)} ({tool.type}) — Safe +@else: +- {lib.bold(tool.name)} ({tool.type}) — Requires approval +@end +@end + +## Schema Access + +Allowed schemas: +@for schema in constraints.allowed_schemas: +- `{schema}` +@end + +Max rows: {constraints.max_rows} | Timeout: {constraints.timeout}s + +@if debug: +## Debug Mode + +Debug logging is enabled. All queries will be logged. +@end + +## Output + +@if output_format == "json": +{format_output("json", debug)} +@elseif output_format == "markdown": +{format_output("markdown", debug)} +@else: +{format_output("plain", debug)} +@end diff --git a/examples/stress-test/agents/orchestrator.mds b/examples/stress-test/agents/orchestrator.mds new file mode 100644 index 00000000..548996b3 --- /dev/null +++ b/examples/stress-test/agents/orchestrator.mds @@ -0,0 +1,62 @@ +--- +orchestrator_name: AgentOrch +max_concurrent: 3 +sub_agents: + - name: DataInsight Pro + role: analyst + active: true + - name: CodeReview Bot + role: reviewer + active: true + - name: Legacy Scanner + role: scanner + active: false + - name: Report Writer + role: writer + active: true +fallback_strategy: queue +debug: false +strict_output: true +--- + +@import "../lib/index.mds" as lib +@import "../shared/output-formats.mds" + +# {lib.bold(orchestrator_name)} + +{lib.badge("Max Concurrent", max_concurrent)} +{lib.badge("Fallback", fallback_strategy)} + +## Active Sub-Agents + +@for agent in sub_agents: +@if agent.active: +- {lib.bold(agent.name)} — Role: {lib.code(agent.role)} +@end +@end + +## Inactive Sub-Agents + +@for agent in sub_agents: +@if !agent.active: +- ~~{agent.name}~~ — Role: {lib.code(agent.role)} (disabled) +@end +@end + +## Orchestration Rules + +@if max_concurrent == 1: +Running in sequential mode — one agent at a time. +@else: +Running up to {max_concurrent} agents concurrently. +@end + +@if fallback_strategy == "queue": +When at capacity, additional tasks are queued. +@elseif fallback_strategy == "reject": +When at capacity, additional tasks are rejected. +@else: +Fallback strategy: {fallback_strategy} +@end + +{format_output("json", strict_output)} diff --git a/examples/stress-test/edge/code-passthrough.mds b/examples/stress-test/edge/code-passthrough.mds new file mode 100644 index 00000000..0a8b13c8 --- /dev/null +++ b/examples/stress-test/edge/code-passthrough.mds @@ -0,0 +1,36 @@ +--- +language: Python +framework: FastAPI +--- + +@import "../lib/formatting.mds" as fmt + +# {fmt.bold(language)} Code Examples + +Here's a {language} example using {framework}: + +```python +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/items/{item_id}") +async def read_item(item_id: int): + return {"item_id": item_id, "status": "active"} + +config = {"key": "value", "nested": {"deep": True}} +template = f"Hello {name}!" +``` + +After the code block, interpolation resumes: {language} with {framework}. + +```json +{ + "name": "test", + "config": { + "debug": true + } +} +``` + +Final interpolation: {fmt.badge("Stack", language)} diff --git a/examples/stress-test/edge/deep-nesting.mds b/examples/stress-test/edge/deep-nesting.mds new file mode 100644 index 00000000..c4596ca8 --- /dev/null +++ b/examples/stress-test/edge/deep-nesting.mds @@ -0,0 +1,50 @@ +--- +departments: + - name: Engineering + active: true + teams: + - name: Platform + lead: Alice + members: + - name: Bob + senior: true + - name: Carol + senior: false + - name: Frontend + lead: Dave + members: + - name: Eve + senior: true + - name: Marketing + active: false + teams: + - name: Growth + lead: Frank + members: + - name: Grace + senior: false +show_inactive: false +--- + +# Organization + +@for dept in departments: +@if dept.active: +## {dept.name} + +@for team in dept.teams: +### {team.name} (Lead: {team.lead}) + +@for member in team.members: +@if member.senior: +- **{member.name}** (Senior) +@else: +- {member.name} +@end +@end + +@end +@elseif show_inactive: +## {dept.name} (Inactive) +@end +@end diff --git a/examples/stress-test/edge/empty-collections.mds b/examples/stress-test/edge/empty-collections.mds new file mode 100644 index 00000000..49c164d9 --- /dev/null +++ b/examples/stress-test/edge/empty-collections.mds @@ -0,0 +1,37 @@ +--- +empty_arr: [] +empty_obj: {} +filled_arr: + - one +label: test +--- + +Before empty array loop. + +@for item in empty_arr: +FAIL: should not render {item} +@end + +After empty array loop. + +@for key, val in empty_obj: +FAIL: should not render {key} {val} +@end + +After empty object loop. + +@if empty_arr: +FAIL: empty array truthy +@end + +@if empty_obj: +FAIL: empty object truthy +@end + +Non-empty works: {label} + +@for item in filled_arr: +- {item} +@end + +Done. diff --git a/examples/stress-test/edge/escaped-braces.mds b/examples/stress-test/edge/escaped-braces.mds new file mode 100644 index 00000000..d16085d3 --- /dev/null +++ b/examples/stress-test/edge/escaped-braces.mds @@ -0,0 +1,14 @@ +--- +real_var: INTERPOLATED +label: Status +--- + +@import "../lib/formatting.mds" as fmt + +Escaped: \{not_a_var\} but real: {real_var} + +Mixed line: \{literal\} then {real_var} then \{another_literal\} + +{fmt.bold(real_var)} vs \{fmt.bold("nope")\} + +Template syntax guide: use \{var_name\} for literal braces and {label} for values. diff --git a/examples/stress-test/edge/falsy-matrix.mds b/examples/stress-test/edge/falsy-matrix.mds new file mode 100644 index 00000000..ced24985 --- /dev/null +++ b/examples/stress-test/edge/falsy-matrix.mds @@ -0,0 +1,102 @@ +--- +empty_string: "" +zero: 0 +false_val: false +empty_array: [] +empty_object: {} +non_empty_string: "hello" +one: 1 +true_val: true +non_empty_array: + - item +--- + +@if empty_string: +FAIL: empty string truthy +@else: +PASS: empty string falsy +@end + +@if zero: +FAIL: zero truthy +@else: +PASS: zero falsy +@end + +@if false_val: +FAIL: false truthy +@else: +PASS: false falsy +@end + +@if empty_array: +FAIL: empty array truthy +@else: +PASS: empty array falsy +@end + +@if empty_object: +FAIL: empty object truthy +@else: +PASS: empty object falsy +@end + +@if !empty_string: +PASS: negated empty string truthy +@else: +FAIL: negated empty string falsy +@end + +@if !false_val: +PASS: negated false truthy +@else: +FAIL: negated false falsy +@end + +@if non_empty_string: +PASS: non-empty string truthy +@else: +FAIL: non-empty string falsy +@end + +@if one: +PASS: one truthy +@else: +FAIL: one falsy +@end + +@if true_val: +PASS: true truthy +@else: +FAIL: true falsy +@end + +@if non_empty_array: +PASS: non-empty array truthy +@else: +FAIL: non-empty array falsy +@end + +@if zero == 0: +PASS: zero equals zero +@else: +FAIL: zero not equals zero +@end + +@if empty_string == "": +PASS: empty string equals empty string +@else: +FAIL: empty string not equals empty string +@end + +@if false_val == false: +PASS: false equals false +@else: +FAIL: false not equals false +@end + +@if false_val != true: +PASS: false not equals true +@else: +FAIL: false equals true +@end diff --git a/examples/stress-test/edge/shadowing-stress.mds b/examples/stress-test/edge/shadowing-stress.mds new file mode 100644 index 00000000..39bc590e --- /dev/null +++ b/examples/stress-test/edge/shadowing-stress.mds @@ -0,0 +1,40 @@ +--- +name: outer_value +items: + - first + - second +records: + - name: record_one + - name: record_two +--- + +@define greet(name): +Function sees: {name} +@end + +Before anything: {name} + +{greet("func_arg")} + +After function: {name} + +@for name in items: +Loop sees: {name} +@end + +After loop: {name} + +@for record in records: +Object loop sees record.name: {record.name} +Outer name still: {name} +@end + +After object loop: {name} + +@define transform(name): +Transform: {name} +@end + +{transform("transform_arg")} + +Final outer: {name} diff --git a/test_playground/err_arity.mds b/examples/stress-test/errors/bad-arity.mds similarity index 57% rename from test_playground/err_arity.mds rename to examples/stress-test/errors/bad-arity.mds index 71181987..a5f20c42 100644 --- a/test_playground/err_arity.mds +++ b/examples/stress-test/errors/bad-arity.mds @@ -2,4 +2,4 @@ Hello {name}! @end -{greet("Alice", "extra")} +{greet("Alice", "extra_arg")} diff --git a/examples/stress-test/errors/bad-circular-a.mds b/examples/stress-test/errors/bad-circular-a.mds new file mode 100644 index 00000000..7594a004 --- /dev/null +++ b/examples/stress-test/errors/bad-circular-a.mds @@ -0,0 +1,3 @@ +@import "./bad-circular-b.mds" as b + +Hello from A! diff --git a/examples/stress-test/errors/bad-circular-b.mds b/examples/stress-test/errors/bad-circular-b.mds new file mode 100644 index 00000000..a6f24660 --- /dev/null +++ b/examples/stress-test/errors/bad-circular-b.mds @@ -0,0 +1,3 @@ +@import "./bad-circular-a.mds" as a + +Hello from B! diff --git a/examples/stress-test/errors/bad-type.mds b/examples/stress-test/errors/bad-type.mds new file mode 100644 index 00000000..073b4e53 --- /dev/null +++ b/examples/stress-test/errors/bad-type.mds @@ -0,0 +1,7 @@ +--- +not_an_array: "just a string" +--- + +@for item in not_an_array: +- {item} +@end diff --git a/examples/stress-test/errors/bad-undefined.mds b/examples/stress-test/errors/bad-undefined.mds new file mode 100644 index 00000000..d7421ebd --- /dev/null +++ b/examples/stress-test/errors/bad-undefined.mds @@ -0,0 +1,5 @@ +--- +known_var: hello +--- + +{known_var} and {unknown_var} diff --git a/examples/stress-test/lib/examples.mds b/examples/stress-test/lib/examples.mds new file mode 100644 index 00000000..5d97a96b --- /dev/null +++ b/examples/stress-test/lib/examples.mds @@ -0,0 +1,16 @@ +--- +type: mds +--- + +@define few_shot(task, examples): +## Few-Shot Examples for: {task} + +@for ex in examples: +**Input**: {ex.input} +**Output**: {ex.output} + +@end +Now apply the same pattern to the user's request. +@end + +@export few_shot diff --git a/examples/stress-test/lib/formatting.mds b/examples/stress-test/lib/formatting.mds new file mode 100644 index 00000000..c24c60f1 --- /dev/null +++ b/examples/stress-test/lib/formatting.mds @@ -0,0 +1,42 @@ +--- +type: mds +--- + +@define bold(text): +**{text}** +@end + +@define italic(text): +*{text}* +@end + +@define code(text): +`{text}` +@end + +@define badge(label, value): +**{label}**: `{value}` +@end + +@define h2(text): +## {text} +@end + +@define h3(text): +### {text} +@end + +@define table_row(cells): +@for cell in cells: +| {cell} +@end +| +@end + +@export bold +@export italic +@export code +@export badge +@export h2 +@export h3 +@export table_row diff --git a/examples/stress-test/lib/guardrails.mds b/examples/stress-test/lib/guardrails.mds new file mode 100644 index 00000000..c9507622 --- /dev/null +++ b/examples/stress-test/lib/guardrails.mds @@ -0,0 +1,36 @@ +--- +type: mds +--- + +@define safety_rules(): +## Safety Guidelines + +- Never generate harmful, illegal, or unethical content +- Decline requests that could cause real-world harm +- Protect user privacy — never store or repeat personal information +- When uncertain, ask for clarification rather than guessing +- All outputs must be reviewed before deployment +@end + +@define output_json(): +Respond with valid JSON only. No markdown, no explanation outside the JSON structure. +@end + +@define output_markdown(): +Respond with well-structured Markdown using headers, lists, and code blocks where appropriate. +@end + +@define output_format(format_name): +@if format_name == "json": +{output_json()} +@elseif format_name == "markdown": +{output_markdown()} +@else: +Respond in plain text. No special formatting required. +@end +@end + +@export safety_rules +@export output_json +@export output_markdown +@export output_format diff --git a/examples/stress-test/lib/index.mds b/examples/stress-test/lib/index.mds new file mode 100644 index 00000000..9537bb51 --- /dev/null +++ b/examples/stress-test/lib/index.mds @@ -0,0 +1,4 @@ +@export * from "./formatting.mds" +@export * from "./guardrails.mds" +@export * from "./personas.mds" +@export * from "./examples.mds" diff --git a/examples/stress-test/lib/personas.mds b/examples/stress-test/lib/personas.mds new file mode 100644 index 00000000..5b905b0b --- /dev/null +++ b/examples/stress-test/lib/personas.mds @@ -0,0 +1,31 @@ +--- +type: mds +--- + +@define teacher(subject): +You are an expert {subject} teacher. Explain concepts clearly using analogies +and real-world examples. Adapt your explanation depth to the student's level. +Always provide practice exercises after explanations. +@end + +@define code_reviewer(language): +You are a senior {language} developer performing code review. Focus on: +- Correctness and edge cases +- Performance implications +- Readability and maintainability +- Security concerns +- Adherence to {language} idioms +@end + +@define data_analyst(domain, tools): +You are a data analyst specializing in {domain}. Your toolkit includes: +@for tool in tools: +- **{tool}** +@end + +Use these tools to provide actionable insights backed by data. +@end + +@export teacher +@export code_reviewer +@export data_analyst diff --git a/examples/stress-test/main.mds b/examples/stress-test/main.mds new file mode 100644 index 00000000..ac9a9f6f --- /dev/null +++ b/examples/stress-test/main.mds @@ -0,0 +1,74 @@ +--- +project_name: AI Agent Factory +version: "1.0" +environment: production +debug: false +enabled_agents: + - DataInsight Pro + - CodeReview Bot + - Orchestrator +metadata: + author: Stress Test Suite + generated: true +--- + +@import "./lib/index.mds" as lib +@import "./agents/data-analyst.mds" as analyst +@import "./agents/orchestrator.mds" as orch +@import "./shared/output-formats.mds" + +# {lib.bold(project_name)} v{version} + +{lib.badge("Environment", environment)} +{lib.badge("Debug", debug)} + +## Enabled Agents + +@for agent in enabled_agents: +- {lib.code(agent)} +@end + +## Safety + +{lib.safety_rules()} + +## Persona: Data Analyst + +{lib.data_analyst("Finance", enabled_agents)} + +## Output Configuration + +{format_output("markdown", debug)} + +## Code Block Passthrough + +```yaml +config: + name: "{project_name}" + nested: {key: value} + template: "@if condition:" +``` + +After code block: {project_name} still works. + +## Escaped Braces + +Literal braces: \{not_interpolated\} but this is: {version} + +## Included Modules + +@include analyst + +--- + +@include orch + +## Summary + +{lib.bold(project_name)} is configured with {lib.code(version)} in {environment} mode. + +@if debug: +Debug mode is ON — verbose logging enabled. +@else: +Running in production mode. +@end diff --git a/examples/stress-test/node-api-stress-test.mjs b/examples/stress-test/node-api-stress-test.mjs new file mode 100644 index 00000000..d4ff2cd0 --- /dev/null +++ b/examples/stress-test/node-api-stress-test.mjs @@ -0,0 +1,345 @@ +import { init, compile, compileFile, check, checkFile, getBackend, isMdsError } from '@mdscript/mds'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const r = (p) => resolve(__dirname, p); + +let passed = 0; +let failed = 0; +const failures = []; + +function assert(condition, name, detail) { + if (condition) { + passed++; + console.log(` PASS: ${name}`); + } else { + failed++; + const msg = detail ? `${name} — ${detail}` : name; + failures.push(msg); + console.log(` FAIL: ${msg}`); + } +} + +function assertContains(output, substring, name) { + assert(output.includes(substring), name, `expected output to contain "${substring}"`); +} + +function assertNotContains(output, substring, name) { + assert(!output.includes(substring), name, `expected output NOT to contain "${substring}"`); +} + +function bodyOf(output) { + const end = output.indexOf('\n---', 1); + return end === -1 ? output : output.slice(end + 4); +} + +async function run() { + console.log('Initializing MDS...'); + await init(); + const backend = getBackend(); + console.log(`Backend: ${backend}\n`); + + // ── Group 1: Library files (function-only, empty body) ── + console.log('── Library files ──'); + { + const result = await compileFile(r('lib/formatting.mds')); + assert(bodyOf(result.output).trim() === '', 'formatting.mds compiles to empty body'); + assert(result.warnings.length === 0, 'formatting.mds no warnings'); + } + { + const result = await compileFile(r('lib/guardrails.mds')); + assert(bodyOf(result.output).trim() === '', 'guardrails.mds compiles to empty body'); + } + { + const result = await compileFile(r('lib/personas.mds')); + assert(bodyOf(result.output).trim() === '', 'personas.mds compiles to empty body'); + } + { + const result = await compileFile(r('lib/examples.mds')); + assert(bodyOf(result.output).trim() === '', 'examples.mds compiles to empty body'); + } + + // ── Group 2: Shared utilities ── + console.log('\n── Shared utilities ──'); + { + const result = await compileFile(r('shared/tool-registry.mds')); + assertContains(result.output, '**code_exec** (Premium)', 'tool-registry premium conditional'); + assertContains(result.output, '**file_read**:', 'tool-registry basic tool'); + } + { + const result = await compileFile(r('shared/chain-consumer.mds')); + assertContains(result.output, '**Testing 3-level import chain**', 'chain-consumer bold'); + assertContains(result.output, 'Safety Guidelines', 'chain-consumer re-exported safety_rules'); + assertContains(result.output, 'Algebra teacher', 'chain-consumer re-exported teacher'); + assert(result.dependencies.length > 0, 'chain-consumer has dependencies'); + } + + // ── Group 3: Agents ── + console.log('\n── Agent files ──'); + { + const result = await compileFile(r('agents/data-analyst.mds')); + assertContains(result.output, '**DataInsight Pro**', 'data-analyst bold agent name'); + assertContains(result.output, '**Version**: `2.1`', 'data-analyst badge'); + assertContains(result.output, '`SQL query generation`', 'data-analyst capability loop'); + assertContains(result.output, '**sql_runner** (query) — Safe', 'data-analyst tool safe check'); + assertContains(result.output, '**data_export** (export) — Requires approval', 'data-analyst tool unsafe check'); + assertContains(result.output, '`analytics`', 'data-analyst nested dot-path schema'); + assertNotContains(result.output, 'Debug Mode', 'data-analyst debug=false hides debug section'); + assert(result.dependencies.length >= 2, `data-analyst has ${result.dependencies.length} deps`); + } + { + const result = await compileFile(r('agents/code-reviewer.mds')); + assertContains(result.output, '**CodeReview Bot**', 'code-reviewer bold name'); + assertContains(result.output, '`critical` — Must fix before merge', 'code-reviewer conditional severity'); + assertContains(result.output, '**Type safety**', 'code-reviewer focus area'); + assert(result.warnings.length > 0, 'code-reviewer warns about empty include'); + } + { + const result = await compileFile(r('agents/orchestrator.mds')); + assertContains(result.output, '**AgentOrch**', 'orchestrator bold name'); + assertContains(result.output, '**DataInsight Pro** — Role: `analyst`', 'orchestrator active agent'); + assertContains(result.output, '~~Legacy Scanner~~', 'orchestrator inactive agent'); + assertNotContains(result.output, '~~DataInsight Pro~~', 'orchestrator active not in inactive list'); + assertContains(result.output, 'Running up to 3 agents concurrently', 'orchestrator concurrent check'); + assertContains(result.output, 'tasks are queued', 'orchestrator fallback strategy'); + } + + // ── Group 4: Edge cases ── + console.log('\n── Edge cases ──'); + { + const result = await compileFile(r('edge/code-passthrough.mds')); + assertContains(result.output, 'return {"item_id": item_id', 'code-passthrough no interpolation in code'); + assertContains(result.output, 'After the code block, interpolation resumes: Python', 'code-passthrough resumes after code'); + assertContains(result.output, '"name": "test"', 'code-passthrough json block preserved'); + assertContains(result.output, '**Stack**: `Python`', 'code-passthrough final badge'); + } + { + const result = await compileFile(r('edge/escaped-braces.mds')); + assertContains(result.output, '{not_a_var}', 'escaped-braces literal braces'); + assertContains(result.output, 'INTERPOLATED', 'escaped-braces real var'); + assertContains(result.output, '{literal} then INTERPOLATED', 'escaped-braces mixed line'); + } + { + const result = await compileFile(r('edge/deep-nesting.mds')); + const body = bodyOf(result.output); + assertContains(body, '## Engineering', 'deep-nesting active dept'); + assertContains(body, '**Bob** (Senior)', 'deep-nesting 4-level deep senior check'); + assertContains(body, '- Carol', 'deep-nesting non-senior'); + assertNotContains(body, 'Marketing', 'deep-nesting inactive dept hidden'); + assertNotContains(body, 'Grace', 'deep-nesting inactive member hidden'); + } + { + const result = await compileFile(r('edge/falsy-matrix.mds')); + assertNotContains(result.output, 'FAIL', 'falsy-matrix all PASS, no FAIL'); + const passCount = (result.output.match(/PASS/g) || []).length; + assert(passCount === 15, `falsy-matrix has ${passCount}/15 PASS assertions`); + } + { + const result = await compileFile(r('edge/shadowing-stress.mds')); + assertContains(result.output, 'Before anything: outer_value', 'shadowing outer initial'); + assertContains(result.output, 'Function sees: func_arg', 'shadowing fn param'); + assertContains(result.output, 'After function: outer_value', 'shadowing restored after fn'); + assertContains(result.output, 'Loop sees: first', 'shadowing loop var'); + assertContains(result.output, 'After loop: outer_value', 'shadowing restored after loop'); + assertContains(result.output, 'Final outer: outer_value', 'shadowing final check'); + } + { + const result = await compileFile(r('edge/empty-collections.mds')); + assertNotContains(result.output, 'FAIL', 'empty-collections no FAIL'); + assertContains(result.output, 'Non-empty works: test', 'empty-collections label preserved'); + assertContains(result.output, '- one', 'empty-collections filled array works'); + } + + // ── Group 5: Main entry point ── + console.log('\n── Main entry point ──'); + { + const result = await compileFile(r('main.mds')); + assertContains(result.output, '**AI Agent Factory**', 'main bold project name'); + assertContains(result.output, 'Safety Guidelines', 'main safety_rules from barrel'); + assertContains(result.output, '`DataInsight Pro`', 'main enabled_agents loop'); + assertContains(result.output, '**DataInsight Pro** Configuration', 'main @include analyst'); + assertContains(result.output, '**AgentOrch**', 'main @include orchestrator'); + assertContains(result.output, '{not_interpolated}', 'main escaped braces'); + assertContains(result.output, '@if condition:', 'main code block passthrough'); + assertContains(result.output, 'Running in production mode', 'main debug=false conditional'); + assert(result.dependencies.length >= 7, `main has ${result.dependencies.length} transitive deps (expected 7+)`); + } + + // ── Group 6: Runtime variable overrides ── + console.log('\n── Runtime vars ──'); + { + const result = await compileFile(r('main.mds'), { + vars: { debug: true, environment: 'staging', version: '2.0-rc1' }, + }); + assertContains(result.output, 'v2.0-rc1', 'vars override version'); + assertContains(result.output, '`staging`', 'vars override environment'); + assertContains(result.output, 'Debug mode is ON', 'vars override debug to true'); + } + { + const result = await compileFile(r('main.mds'), { + vars: { debug: true }, + }); + assertContains(result.output, 'v1.0', 'minimal vars keep original version'); + assertContains(result.output, 'Debug mode is ON', 'minimal vars debug override'); + } + + // ── Group 7: compile() inline templates ── + console.log('\n── Inline compile() ──'); + { + const result = compile('Hello {name}!', { vars: { name: 'World' } }); + assert(result.output.trim() === 'Hello World!', 'inline simple interpolation'); + } + { + const src = [ + '---', + 'items:', + ' - alpha', + ' - beta', + '---', + '@for item in items:', + '- {item}', + '@end', + ].join('\n'); + const result = compile(src); + assertContains(result.output, '- alpha', 'inline loop alpha'); + assertContains(result.output, '- beta', 'inline loop beta'); + } + { + const src = [ + '---', + 'debug: true', + '---', + '@if debug:', + 'DEBUG ON', + '@else:', + 'DEBUG OFF', + '@end', + ].join('\n'); + const result = compile(src); + assertContains(result.output, 'DEBUG ON', 'inline truthy conditional'); + } + { + const src = [ + '@define greet(name):', + 'Hello {name}!', + '@end', + '{greet("World")}', + ].join('\n'); + const result = compile(src); + assertContains(result.output, 'Hello World!', 'inline function call'); + } + { + const src = 'Escaped: \\{literal\\} and plain text.'; + const result = compile(src); + assertContains(result.output, '{literal}', 'inline escaped braces'); + } + + // ── Group 8: check() and checkFile() ── + console.log('\n── Validation (check) ──'); + { + const checkResult = await checkFile(r('edge/shadowing-stress.mds')); + assert(Array.isArray(checkResult.warnings), 'checkFile shadowing returns warnings array'); + } + { + const result = check('Hello {name}!', { vars: { name: 'Test' } }); + assert(Array.isArray(result.warnings), 'check inline returns warnings array'); + } + { + try { + check('{undefined_thing}'); + assert(false, 'check rejects undefined var'); + } catch (err) { + assert(isMdsError(err), 'check undefined throws MdsError'); + } + } + + // ── Group 9: Error handling with isMdsError ── + console.log('\n── Error handling ──'); + { + try { + await compileFile(r('errors/bad-circular-a.mds')); + assert(false, 'circular import should throw'); + } catch (err) { + assert(isMdsError(err), 'circular import is MdsError'); + assert(err.code === 'mds::circular_import', `circular error code: ${err.code}`); + } + } + { + try { + await compileFile(r('errors/bad-arity.mds')); + assert(false, 'arity mismatch should throw'); + } catch (err) { + assert(isMdsError(err), 'arity error is MdsError'); + assert(err.code === 'mds::arity', `arity error code: ${err.code}`); + } + } + { + try { + await compileFile(r('errors/bad-undefined.mds')); + assert(false, 'undefined var should throw'); + } catch (err) { + assert(isMdsError(err), 'undefined var is MdsError'); + assert(err.code === 'mds::undefined_var', `undefined error code: ${err.code}`); + } + } + { + try { + await compileFile(r('errors/bad-type.mds')); + assert(false, 'type error should throw'); + } catch (err) { + assert(isMdsError(err), 'type error is MdsError'); + assert(err.code === 'mds::type_error', `type error code: ${err.code}`); + } + } + { + assert(!isMdsError(new Error('generic')), 'isMdsError rejects generic Error'); + assert(!isMdsError('string'), 'isMdsError rejects non-Error'); + assert(!isMdsError(null), 'isMdsError rejects null'); + } + + // ── Group 10: Dependencies tracking ── + console.log('\n── Dependencies ──'); + { + const result = await compileFile(r('shared/chain-consumer.mds')); + const deps = result.dependencies; + const hasReexport = deps.some(d => d.includes('reexport-chain.mds')); + assert(hasReexport, 'chain-consumer deps include reexport-chain.mds'); + const hasFormatting = deps.some(d => d.includes('formatting.mds')); + assert(hasFormatting, 'chain-consumer deps include formatting.mds (transitive through re-export)'); + } + { + const result = await compileFile(r('edge/code-passthrough.mds')); + const deps = result.dependencies; + assert(deps.length >= 1, `code-passthrough has ${deps.length} deps (formatting.mds)`); + } + { + const result = await compileFile(r('main.mds')); + const deps = result.dependencies; + assert(deps.length >= 7, `main.mds has ${deps.length} transitive deps (expected 7+)`); + const hasGuardrails = deps.some(d => d.includes('guardrails.mds')); + assert(hasGuardrails, 'main deps include guardrails.mds'); + const hasOrchestrator = deps.some(d => d.includes('orchestrator.mds')); + assert(hasOrchestrator, 'main deps include orchestrator.mds'); + const hasFormatting = deps.some(d => d.includes('formatting.mds')); + assert(hasFormatting, 'main deps include formatting.mds'); + } + + // ── Summary ── + console.log('\n══════════════════════════════════════'); + console.log(` Results: ${passed} passed, ${failed} failed`); + console.log('══════════════════════════════════════'); + if (failures.length > 0) { + console.log('\nFailures:'); + for (const f of failures) { + console.log(` - ${f}`); + } + } + + process.exit(failed > 0 ? 1 : 0); +} + +run().catch((err) => { + console.error('FATAL:', err); + process.exit(2); +}); diff --git a/examples/stress-test/shared/chain-consumer.mds b/examples/stress-test/shared/chain-consumer.mds new file mode 100644 index 00000000..d7e5e7d7 --- /dev/null +++ b/examples/stress-test/shared/chain-consumer.mds @@ -0,0 +1,13 @@ +--- +topic: Algebra +--- + +@import "./reexport-chain.mds" as chain + +# Module Chain Test + +{chain.bold("Testing 3-level import chain")} + +{chain.safety_rules()} + +{chain.teacher(topic)} diff --git a/examples/stress-test/shared/output-formats.mds b/examples/stress-test/shared/output-formats.mds new file mode 100644 index 00000000..0ecae45f --- /dev/null +++ b/examples/stress-test/shared/output-formats.mds @@ -0,0 +1,21 @@ +--- +type: mds +--- + +@define format_output(format, strict): +@if format == "json": +@if strict: +Output ONLY valid JSON. No comments, no trailing commas, no explanation. +@else: +Output JSON. Brief explanation is allowed before the JSON block. +@end +@elseif format == "markdown": +Output well-structured Markdown with headers and lists. +@elseif format == "csv": +Output CSV with headers in the first row. +@else: +Output plain text. +@end +@end + +@export format_output diff --git a/examples/stress-test/shared/reexport-chain.mds b/examples/stress-test/shared/reexport-chain.mds new file mode 100644 index 00000000..21221ea7 --- /dev/null +++ b/examples/stress-test/shared/reexport-chain.mds @@ -0,0 +1,3 @@ +@export bold from "../lib/formatting.mds" +@export safety_rules from "../lib/guardrails.mds" +@export teacher from "../lib/personas.mds" diff --git a/examples/stress-test/shared/tool-registry.mds b/examples/stress-test/shared/tool-registry.mds new file mode 100644 index 00000000..eeb88c8c --- /dev/null +++ b/examples/stress-test/shared/tool-registry.mds @@ -0,0 +1,34 @@ +--- +type: mds +tools: + web_search: + description: Search the web for real-time information + tier: basic + code_exec: + description: Execute code in a sandboxed environment + tier: premium + db_query: + description: Run read-only SQL queries against analytics DB + tier: premium + file_read: + description: Read files from the workspace + tier: basic +--- + +## Available Tools + +@for name, config in tools: +@if config.tier == "premium": +- **{name}** (Premium): {config.description} +@else: +- **{name}**: {config.description} +@end +@end + +@define tool_list(registry): +@for tool_name, tool_config in registry: +- {tool_name}: {tool_config.description} [{tool_config.tier}] +@end +@end + +@export tool_list diff --git a/examples/stress-test/vars-minimal.json b/examples/stress-test/vars-minimal.json new file mode 100644 index 00000000..35fbb261 --- /dev/null +++ b/examples/stress-test/vars-minimal.json @@ -0,0 +1,3 @@ +{ + "debug": true +} diff --git a/examples/stress-test/vars.json b/examples/stress-test/vars.json new file mode 100644 index 00000000..3c837a99 --- /dev/null +++ b/examples/stress-test/vars.json @@ -0,0 +1,6 @@ +{ + "debug": true, + "environment": "staging", + "version": "2.0-rc1", + "output_format": "json" +} diff --git a/examples/vite-app/package.json b/examples/vite-app/package.json new file mode 100644 index 00000000..1ca6181f --- /dev/null +++ b/examples/vite-app/package.json @@ -0,0 +1,19 @@ +{ + "name": "mds-vite-test-app", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "dependencies": { + "@mdscript/mds": "file:../../packages/mds", + "@mdscript/vite-plugin": "file:../../packages/vite-plugin", + "@mdscript/bundler-utils": "file:../../packages/bundler-utils" + }, + "devDependencies": { + "vite": "^6.0.0", + "typescript": "^5.4.0" + } +} diff --git a/examples/vite-app/src/main.ts b/examples/vite-app/src/main.ts new file mode 100644 index 00000000..fbad8da0 --- /dev/null +++ b/examples/vite-app/src/main.ts @@ -0,0 +1,19 @@ +import systemPrompt from './prompts/system.mds'; +import reviewerPrompt from './prompts/reviewer.mds'; +import { codePassthrough, escapedBraces, shadowingStress, emptyCollections } from './stress'; + +console.log('=== System Prompt ==='); +console.log(systemPrompt); +console.log('\n=== Reviewer Prompt ==='); +console.log(reviewerPrompt); + +console.log('\n=== Stress Test: Code Passthrough ==='); +console.log(codePassthrough); +console.log('\n=== Stress Test: Escaped Braces ==='); +console.log(escapedBraces); +console.log('\n=== Stress Test: Shadowing ==='); +console.log(shadowingStress); +console.log('\n=== Stress Test: Empty Collections ==='); +console.log(emptyCollections); + +export { systemPrompt, reviewerPrompt, codePassthrough, escapedBraces, shadowingStress, emptyCollections }; diff --git a/examples/vite-app/src/prompts/reviewer.mds b/examples/vite-app/src/prompts/reviewer.mds new file mode 100644 index 00000000..6bc7255b --- /dev/null +++ b/examples/vite-app/src/prompts/reviewer.mds @@ -0,0 +1,31 @@ +--- +name: CodeReviewer +focus_areas: + - security vulnerabilities + - performance bottlenecks + - error handling gaps + - naming conventions +strict_mode: true +--- + +@import "./rules.mds" as rules + +# {name} — Code Review Mode + +You are a meticulous code reviewer. Your job is to find issues before they +reach production. + +## Focus Areas + +@for area in focus_areas: +- {area} +@end + +@if strict_mode: +## Strict Mode + +In strict mode, you MUST flag every issue. Do not let anything slide. +Rate each finding as: CRITICAL, WARNING, or INFO. +@end + +{rules.response_format()} diff --git a/examples/vite-app/src/prompts/rules.mds b/examples/vite-app/src/prompts/rules.mds new file mode 100644 index 00000000..8ac77ea7 --- /dev/null +++ b/examples/vite-app/src/prompts/rules.mds @@ -0,0 +1,26 @@ +--- +type: mds +--- + +@define coding_standards(): +## Coding Standards + +- Write clean, readable code with meaningful names +- Always handle errors explicitly — no silent failures +- Write tests for all new functionality +- Keep functions focused — single responsibility +@end + +@define response_format(): +## Response Format + +When responding to code questions: + +1. **Understand** — restate the problem in your own words +2. **Plan** — outline your approach before coding +3. **Implement** — write the code with inline explanations +4. **Verify** — suggest tests or validation steps +@end + +@export coding_standards +@export response_format diff --git a/examples/vite-app/src/prompts/system.mds b/examples/vite-app/src/prompts/system.mds new file mode 100644 index 00000000..c0a444b7 --- /dev/null +++ b/examples/vite-app/src/prompts/system.mds @@ -0,0 +1,23 @@ +--- +agent_name: Kendra +role: senior software engineer +specialties: + - TypeScript + - Rust + - system design +tone: professional +--- + +@import "./rules.mds" as rules + +# {agent_name} + +You are **{agent_name}**, a {role} who specializes in: + +@for spec in specialties: +- {spec} +@end + +{rules.coding_standards()} + +{rules.response_format()} diff --git a/examples/vite-app/src/stress.ts b/examples/vite-app/src/stress.ts new file mode 100644 index 00000000..d5d155ca --- /dev/null +++ b/examples/vite-app/src/stress.ts @@ -0,0 +1,46 @@ +import codePassthrough from '../../stress-test/edge/code-passthrough.mds'; +import escapedBraces from '../../stress-test/edge/escaped-braces.mds'; +import shadowingStress from '../../stress-test/edge/shadowing-stress.mds'; +import emptyCollections from '../../stress-test/edge/empty-collections.mds'; +import dataAnalyst from '../../stress-test/agents/data-analyst.mds'; +import codeReviewer from '../../stress-test/agents/code-reviewer.mds'; +import orchestrator from '../../stress-test/agents/orchestrator.mds'; +import deepNesting from '../../stress-test/edge/deep-nesting.mds'; +import falsyMatrix from '../../stress-test/edge/falsy-matrix.mds'; +import chainConsumer from '../../stress-test/shared/chain-consumer.mds'; +import main from '../../stress-test/main.mds'; + +console.log('=== Stress Test: Code Passthrough ==='); +console.log(codePassthrough); + +console.log('\n=== Stress Test: Escaped Braces ==='); +console.log(escapedBraces); + +console.log('\n=== Stress Test: Shadowing Stress ==='); +console.log(shadowingStress); + +console.log('\n=== Stress Test: Empty Collections ==='); +console.log(emptyCollections); + +console.log('\n=== Stress Test: Data Analyst ==='); +console.log(dataAnalyst); + +console.log('\n=== Stress Test: Code Reviewer ==='); +console.log(codeReviewer); + +console.log('\n=== Stress Test: Orchestrator ==='); +console.log(orchestrator); + +console.log('\n=== Stress Test: Deep Nesting ==='); +console.log(deepNesting); + +console.log('\n=== Stress Test: Falsy Matrix ==='); +console.log(falsyMatrix); + +console.log('\n=== Stress Test: Chain Consumer ==='); +console.log(chainConsumer); + +console.log('\n=== Stress Test: Main ==='); +console.log(main); + +export { codePassthrough, escapedBraces, shadowingStress, emptyCollections, dataAnalyst, codeReviewer, orchestrator, deepNesting, falsyMatrix, chainConsumer, main }; diff --git a/examples/vite-app/src/test-metadata.ts b/examples/vite-app/src/test-metadata.ts new file mode 100644 index 00000000..a438e593 --- /dev/null +++ b/examples/vite-app/src/test-metadata.ts @@ -0,0 +1,10 @@ +import systemPrompt, { metadata } from './prompts/system.mds'; + +console.log('=== Prompt (first 100 chars) ==='); +console.log(systemPrompt.substring(0, 100) + '...'); +console.log('\n=== Metadata ==='); +console.log('Warnings:', metadata.warnings); +console.log('Dependencies:', metadata.dependencies); +console.log('Dependency count:', metadata.dependencies.length); + +export { systemPrompt, metadata }; diff --git a/examples/vite-app/tsconfig.json b/examples/vite-app/tsconfig.json new file mode 100644 index 00000000..6f0ae272 --- /dev/null +++ b/examples/vite-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "types": ["@mdscript/bundler-utils/mds"] + }, + "include": ["src"] +} diff --git a/examples/vite-app/vite.config.ts b/examples/vite-app/vite.config.ts new file mode 100644 index 00000000..27db22c9 --- /dev/null +++ b/examples/vite-app/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite'; +import mdsPlugin from '@mdscript/vite-plugin'; + +export default defineConfig({ + plugins: [ + mdsPlugin({ vars: { env: 'production', debug: false, mode: 'vite-build' } }), + ], + build: { + lib: { + entry: './src/main.ts', + formats: ['es'], + fileName: 'main', + }, + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/examples/webpack-app/package.json b/examples/webpack-app/package.json new file mode 100644 index 00000000..0543015e --- /dev/null +++ b/examples/webpack-app/package.json @@ -0,0 +1,18 @@ +{ + "name": "mds-webpack-test-app", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "webpack --config webpack.config.mjs" + }, + "dependencies": { + "@mdscript/mds": "file:../../packages/mds", + "@mdscript/webpack-loader": "file:../../packages/webpack-loader", + "@mdscript/bundler-utils": "file:../../packages/bundler-utils" + }, + "devDependencies": { + "webpack": "^5.0.0", + "webpack-cli": "^6.0.0" + } +} diff --git a/examples/webpack-app/src/index.js b/examples/webpack-app/src/index.js new file mode 100644 index 00000000..9dea84b2 --- /dev/null +++ b/examples/webpack-app/src/index.js @@ -0,0 +1,38 @@ +import prompt from './prompts/assistant.mds'; +import codePassthrough from '../../stress-test/edge/code-passthrough.mds'; +import escapedBraces from '../../stress-test/edge/escaped-braces.mds'; +import shadowingStress from '../../stress-test/edge/shadowing-stress.mds'; +import emptyCollections from '../../stress-test/edge/empty-collections.mds'; +import dataAnalyst from '../../stress-test/agents/data-analyst.mds'; +import codeReviewer from '../../stress-test/agents/code-reviewer.mds'; +import orchestrator from '../../stress-test/agents/orchestrator.mds'; +import deepNesting from '../../stress-test/edge/deep-nesting.mds'; +import falsyMatrix from '../../stress-test/edge/falsy-matrix.mds'; +import chainConsumer from '../../stress-test/shared/chain-consumer.mds'; +import main from '../../stress-test/main.mds'; + +console.log('=== Webpack-Bundled MDS Prompt ==='); +console.log(prompt); + +console.log('\n=== Stress Test: Code Passthrough ==='); +console.log(codePassthrough); +console.log('\n=== Stress Test: Escaped Braces ==='); +console.log(escapedBraces); +console.log('\n=== Stress Test: Shadowing ==='); +console.log(shadowingStress); +console.log('\n=== Stress Test: Empty Collections ==='); +console.log(emptyCollections); +console.log('\n=== Stress Test: Data Analyst ==='); +console.log(dataAnalyst); +console.log('\n=== Stress Test: Code Reviewer ==='); +console.log(codeReviewer); +console.log('\n=== Stress Test: Orchestrator ==='); +console.log(orchestrator); +console.log('\n=== Stress Test: Deep Nesting ==='); +console.log(deepNesting); +console.log('\n=== Stress Test: Falsy Matrix ==='); +console.log(falsyMatrix); +console.log('\n=== Stress Test: Chain Consumer ==='); +console.log(chainConsumer); +console.log('\n=== Stress Test: Main ==='); +console.log(main); diff --git a/examples/webpack-app/src/prompts/assistant.mds b/examples/webpack-app/src/prompts/assistant.mds new file mode 100644 index 00000000..ead7405e --- /dev/null +++ b/examples/webpack-app/src/prompts/assistant.mds @@ -0,0 +1,26 @@ +--- +name: WebpackBot +tasks: + - bundling + - tree shaking + - code splitting + - hot module replacement +verbose: true +--- + +# {name} + +You are **{name}**, an expert in JavaScript build tooling. + +## Core Tasks + +@for task in tasks: +- {task} +@end + +@if verbose: +## Verbose Mode + +When verbose mode is enabled, provide detailed explanations for every +optimization decision, including bundle size impact estimates. +@end diff --git a/examples/webpack-app/webpack.config.mjs b/examples/webpack-app/webpack.config.mjs new file mode 100644 index 00000000..c05ed6fb --- /dev/null +++ b/examples/webpack-app/webpack.config.mjs @@ -0,0 +1,30 @@ +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default { + mode: 'production', + entry: './src/index.js', + output: { + filename: 'main.js', + path: resolve(__dirname, 'dist'), + library: { type: 'module' }, + }, + experiments: { + outputModule: true, + }, + module: { + rules: [ + { + test: /\.mds$/, + use: { + loader: '@mdscript/webpack-loader', + options: { + vars: { debug: false, mode: 'webpack-build' }, + }, + }, + }, + ], + }, +}; diff --git a/package-lock.json b/package-lock.json index cb608c4e..99d0c1e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1427,23 +1427,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mds/bundler-utils": { + "node_modules/@mdscript/bundler-utils": { "resolved": "packages/bundler-utils", "link": true }, - "node_modules/@mds/mds": { + "node_modules/@mdscript/mds": { "resolved": "packages/mds", "link": true }, - "node_modules/@mds/rollup-plugin": { + "node_modules/@mdscript/rollup-plugin": { "resolved": "packages/rollup-plugin", "link": true }, - "node_modules/@mds/vite-plugin": { + "node_modules/@mdscript/vite-plugin": { "resolved": "packages/vite-plugin", "link": true }, - "node_modules/@mds/webpack-loader": { + "node_modules/@mdscript/webpack-loader": { "resolved": "packages/webpack-loader", "link": true }, @@ -2954,10 +2954,11 @@ } }, "packages/bundler-utils": { - "name": "@mds/bundler-utils", + "name": "@mdscript/bundler-utils", "version": "0.1.0", + "license": "MIT", "devDependencies": { - "@mds/mds": "file:../mds", + "@mdscript/mds": "file:../mds", "@types/node": "^22.0.0", "typescript": "^5.4.0" }, @@ -2965,7 +2966,7 @@ "node": ">=22.0.0" }, "peerDependencies": { - "@mds/mds": "^0.1.0" + "@mdscript/mds": "^0.1.0" } }, "packages/bundler-utils/node_modules/@types/node": { @@ -2986,7 +2987,7 @@ "license": "MIT" }, "packages/mds": { - "name": "@mds/mds", + "name": "@mdscript/mds", "version": "0.1.0", "devDependencies": { "@types/node": "^22.0.0", @@ -3017,13 +3018,14 @@ "license": "MIT" }, "packages/rollup-plugin": { - "name": "@mds/rollup-plugin", + "name": "@mdscript/rollup-plugin", "version": "0.1.0", + "license": "MIT", "dependencies": { - "@mds/bundler-utils": "^0.1.0" + "@mdscript/bundler-utils": "^0.1.0" }, "devDependencies": { - "@mds/mds": "file:../mds", + "@mdscript/mds": "file:../mds", "@types/node": "^22.0.0", "rollup": "^4.0.0", "typescript": "^5.4.0" @@ -3032,7 +3034,7 @@ "node": ">=22.0.0" }, "peerDependencies": { - "@mds/mds": "^0.1.0", + "@mdscript/mds": "^0.1.0", "rollup": "^3.0.0 || ^4.0.0" } }, @@ -3054,13 +3056,14 @@ "license": "MIT" }, "packages/vite-plugin": { - "name": "@mds/vite-plugin", + "name": "@mdscript/vite-plugin", "version": "0.1.0", + "license": "MIT", "dependencies": { - "@mds/bundler-utils": "^0.1.0" + "@mdscript/bundler-utils": "^0.1.0" }, "devDependencies": { - "@mds/mds": "file:../mds", + "@mdscript/mds": "file:../mds", "@types/node": "^22.0.0", "typescript": "^5.4.0", "vite": "^6.0.0" @@ -3069,7 +3072,7 @@ "node": ">=22.0.0" }, "peerDependencies": { - "@mds/mds": "^0.1.0", + "@mdscript/mds": "^0.1.0", "vite": "^5.0.0 || ^6.0.0" } }, @@ -3091,13 +3094,14 @@ "license": "MIT" }, "packages/webpack-loader": { - "name": "@mds/webpack-loader", + "name": "@mdscript/webpack-loader", "version": "0.1.0", + "license": "MIT", "dependencies": { - "@mds/bundler-utils": "^0.1.0" + "@mdscript/bundler-utils": "^0.1.0" }, "devDependencies": { - "@mds/mds": "file:../mds", + "@mdscript/mds": "file:../mds", "@types/node": "^22.0.0", "typescript": "^5.4.0", "webpack": "^5.0.0" @@ -3106,7 +3110,7 @@ "node": ">=22.0.0" }, "peerDependencies": { - "@mds/mds": "^0.1.0", + "@mdscript/mds": "^0.1.0", "webpack": "^5.0.0" } }, diff --git a/packages/bundler-utils/README.md b/packages/bundler-utils/README.md index 2e226d94..e2517f5f 100644 --- a/packages/bundler-utils/README.md +++ b/packages/bundler-utils/README.md @@ -1,4 +1,4 @@ -# @mds/bundler-utils +# @mdscript/bundler-utils Shared transform utilities for MDS bundler plugins (Vite, Rollup, Webpack). @@ -7,26 +7,26 @@ Shared transform utilities for MDS bundler plugins (Vite, Rollup, Webpack). ## Installation ```sh -npm install @mds/bundler-utils +npm install @mdscript/bundler-utils ``` ## Peer dependencies ```sh -npm install @mds/mds +npm install @mdscript/mds ``` ## Usage This package is primarily consumed by the bundler-specific plugin packages -(`@mds/vite-plugin`, `@mds/rollup-plugin`, `@mds/webpack-loader`). You only +(`@mdscript/vite-plugin`, `@mdscript/rollup-plugin`, `@mdscript/webpack-loader`). You only need to use it directly if you are writing a plugin for another bundler. ```ts -import { createMdsTransformer, formatMdsError, shouldTransform } from '@mds/bundler-utils'; +import { createMdsTransformer, formatMdsError, shouldTransform } from '@mdscript/bundler-utils'; -// Lazily initialize (call once per build, after loading @mds/mds) -const mds = await import('@mds/mds'); +// Lazily initialize (call once per build, after loading @mdscript/mds) +const mds = await import('@mdscript/mds'); const transformer = createMdsTransformer(mds, { vars: { env: 'production' } }); // Transform a .mds file to a JavaScript module @@ -45,7 +45,7 @@ To tell TypeScript about `.mds` imports, add the following to your `tsconfig.jso ```json { "compilerOptions": { - "types": ["@mds/bundler-utils/mds"] + "types": ["@mdscript/bundler-utils/mds"] } } ``` @@ -53,7 +53,7 @@ To tell TypeScript about `.mds` imports, add the following to your `tsconfig.jso Or add a triple-slash reference in any `.d.ts` file in your project: ```ts -/// +/// ``` This makes `import content from './prompt.mds'` type-safe: `content` is `string` @@ -66,11 +66,11 @@ and retry-on-rejection semantics. It is exported for bundler plugin authors who the same guarantee. ```ts -import { LazyInit } from '@mds/bundler-utils'; +import { LazyInit } from '@mdscript/bundler-utils'; // Factory is invoked at most once per successful resolution. const lazy = new LazyInit(async () => { - const mds = await import('@mds/mds'); + const mds = await import('@mdscript/mds'); return createMdsTransformer(mds, options); }); diff --git a/packages/bundler-utils/__test__/cjs-compat.spec.mjs b/packages/bundler-utils/__test__/cjs-compat.spec.mjs index 64111fe7..a91a87a8 100644 --- a/packages/bundler-utils/__test__/cjs-compat.spec.mjs +++ b/packages/bundler-utils/__test__/cjs-compat.spec.mjs @@ -1,5 +1,5 @@ /** - * CJS compatibility tests for @mds/bundler-utils. + * CJS compatibility tests for @mdscript/bundler-utils. * * Verifies that the CJS build (dist-cjs/) can be loaded via require() and * exports all expected symbols. This ensures the package is usable from diff --git a/packages/bundler-utils/__test__/integration.spec.mjs b/packages/bundler-utils/__test__/integration.spec.mjs index 6ef98fdb..bc3c4944 100644 --- a/packages/bundler-utils/__test__/integration.spec.mjs +++ b/packages/bundler-utils/__test__/integration.spec.mjs @@ -1,5 +1,5 @@ /** - * Integration tests for @mds/bundler-utils using real @mds/mds. + * Integration tests for @mdscript/bundler-utils using real @mdscript/mds. */ import { test, describe, before } from 'node:test'; import assert from 'node:assert/strict'; @@ -16,7 +16,7 @@ const CONSUMER_MDS = join(FIXTURES, 'import_consumer.mds'); const ENTRY_MDS = join(FIXTURES, 'imports/entry.mds'); // --------------------------------------------------------------------------- -// Load real @mds/mds +// Load real @mdscript/mds // --------------------------------------------------------------------------- const mds = await import('../../mds/dist/node.js'); await mds.init(); diff --git a/packages/bundler-utils/package.json b/packages/bundler-utils/package.json index 9298b25b..609ec4e8 100644 --- a/packages/bundler-utils/package.json +++ b/packages/bundler-utils/package.json @@ -1,8 +1,16 @@ { - "name": "@mds/bundler-utils", + "name": "@mdscript/bundler-utils", "version": "0.1.0", "description": "Shared utilities for MDS bundler plugins", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/dean0x/mds.git", + "directory": "packages/bundler-utils" + }, + "homepage": "https://github.com/dean0x/mds#readme", + "bugs": "https://github.com/dean0x/mds/issues", + "keywords": ["mds", "bundler", "vite", "rollup", "webpack", "markdown", "template"], "type": "module", "engines": { "node": ">=22.0.0" @@ -31,10 +39,10 @@ "test": "node --test __test__/*.spec.mjs" }, "peerDependencies": { - "@mds/mds": "^0.1.0" + "@mdscript/mds": "^0.1.0" }, "devDependencies": { - "@mds/mds": "file:../mds", + "@mdscript/mds": "file:../mds", "@types/node": "^22.0.0", "typescript": "^5.4.0" } diff --git a/packages/bundler-utils/src/transform.ts b/packages/bundler-utils/src/transform.ts index 8c8b2033..dcb1d528 100644 --- a/packages/bundler-utils/src/transform.ts +++ b/packages/bundler-utils/src/transform.ts @@ -54,7 +54,7 @@ function safeJsonForJs(value: unknown): string { * use and reuses the same instance across all subsequent transform calls. * * @param mds - The MDS compiler API (satisfies {@link MdsApi}). Pass the result - * of `import('@mds/mds')` or a compatible test double. + * of `import('@mdscript/mds')` or a compatible test double. * @param options - Optional plugin options. `options.vars` are forwarded to * every {@link MdsApi.compileFile} call as runtime template variables. * @returns An object with two methods: diff --git a/packages/bundler-utils/src/types.ts b/packages/bundler-utils/src/types.ts index 177fb49e..6d045429 100644 --- a/packages/bundler-utils/src/types.ts +++ b/packages/bundler-utils/src/types.ts @@ -2,15 +2,15 @@ * Minimal interface for the MDS compiler API required by bundler plugins. * Both the native Node.js backend and the WASM backend satisfy this interface. * - * Structural typing relationship: the real `@mds/mds` module namespace satisfies + * Structural typing relationship: the real `@mdscript/mds` module namespace satisfies * this interface by duck typing — no explicit `implements` declaration is needed * because TypeScript's structural type system enforces compatibility at the - * dynamic `import('@mds/mds')` call sites in transformer.ts. + * dynamic `import('@mdscript/mds')` call sites in transformer.ts. * * This interface intentionally omits `InitOptions` (the optional argument that * the real `init()` accepts) because bundler plugins always call `init()` with * no arguments. Widening the interface to include options the plugins never use - * would couple bundler-utils to the full @mds/mds API surface unnecessarily. + * would couple bundler-utils to the full @mdscript/mds API surface unnecessarily. */ export interface MdsApi { /** Compile a file at the given absolute path and return the compiled output. */ diff --git a/packages/mds/README.md b/packages/mds/README.md index 7300fc1b..3f770d43 100644 --- a/packages/mds/README.md +++ b/packages/mds/README.md @@ -1,11 +1,11 @@ -# @mds/mds +# @mdscript/mds JavaScript/TypeScript bindings for the [MDS](../../README.md) compiler. ## Installation ```sh -npm install @mds/mds +npm install @mdscript/mds ``` > **Note:** This package is pre-release and not yet published to npm. @@ -16,7 +16,7 @@ Node.js auto-selects the native addon and falls back to WASM if unavailable. No initialization required. ```ts -import { compile, check, compileFile, checkFile, getBackend, isMdsError } from '@mds/mds'; +import { compile, check, compileFile, checkFile, getBackend, isMdsError } from '@mdscript/mds'; // Compile MDS source to Markdown const result = compile('Hello {name}', { vars: { name: 'world' } }); @@ -41,7 +41,7 @@ The browser entry requires an explicit `init()` call before any compile/check operations. `init()` is idempotent — safe to call multiple times. ```ts -import { init, compile, check, isMdsError } from '@mds/mds'; +import { init, compile, check, isMdsError } from '@mdscript/mds'; await init(); // or with a custom WASM URL: @@ -71,7 +71,7 @@ MDS_BACKEND=wasm node my-script.js Use `isMdsError` to distinguish MDS compiler errors from other exceptions: ```ts -import { compile, isMdsError } from '@mds/mds'; +import { compile, isMdsError } from '@mdscript/mds'; try { compile(source); diff --git a/packages/mds/__test__/backend.spec.mjs b/packages/mds/__test__/backend.spec.mjs index 82a36e2f..6507b5c4 100644 --- a/packages/mds/__test__/backend.spec.mjs +++ b/packages/mds/__test__/backend.spec.mjs @@ -1,5 +1,5 @@ /** - * Backend selection tests for @mds/mds universal package. + * Backend selection tests for @mdscript/mds universal package. * Tests: U-B1 through U-B11 */ import { test, describe, before } from 'node:test'; diff --git a/packages/mds/__test__/browser.spec.mjs b/packages/mds/__test__/browser.spec.mjs index 7d412365..0ff54371 100644 --- a/packages/mds/__test__/browser.spec.mjs +++ b/packages/mds/__test__/browser.spec.mjs @@ -1,5 +1,5 @@ /** - * Browser entry point behavioral tests for @mds/mds. + * Browser entry point behavioral tests for @mdscript/mds. * Tests: U-BR1 through U-BR13 * * Imports dist/browser.js directly. Node.js ESM module state is shared within diff --git a/packages/mds/__test__/check.spec.mjs b/packages/mds/__test__/check.spec.mjs index cdd07e3e..a556afeb 100644 --- a/packages/mds/__test__/check.spec.mjs +++ b/packages/mds/__test__/check.spec.mjs @@ -1,5 +1,5 @@ /** - * check() and checkFile() tests for @mds/mds universal package. + * check() and checkFile() tests for @mdscript/mds universal package. * Tests: U-K1 through U-KF3 */ import { test, describe, before } from 'node:test'; diff --git a/packages/mds/__test__/compile.spec.mjs b/packages/mds/__test__/compile.spec.mjs index ff925e9d..d968bf7b 100644 --- a/packages/mds/__test__/compile.spec.mjs +++ b/packages/mds/__test__/compile.spec.mjs @@ -1,5 +1,5 @@ /** - * Core compile() tests for @mds/mds universal package. + * Core compile() tests for @mdscript/mds universal package. * Tests: U-C1 through U-C9 */ import { test, describe, before } from 'node:test'; diff --git a/packages/mds/__test__/compileFile.spec.mjs b/packages/mds/__test__/compileFile.spec.mjs index d8fe537b..97936fba 100644 --- a/packages/mds/__test__/compileFile.spec.mjs +++ b/packages/mds/__test__/compileFile.spec.mjs @@ -1,5 +1,5 @@ /** - * compileFile() tests for @mds/mds universal package. + * compileFile() tests for @mdscript/mds universal package. * Tests: U-CF1 through U-CF9 */ import { test, describe, before } from 'node:test'; diff --git a/packages/mds/__test__/error.spec.mjs b/packages/mds/__test__/error.spec.mjs index 81fed9fa..d7c1b15f 100644 --- a/packages/mds/__test__/error.spec.mjs +++ b/packages/mds/__test__/error.spec.mjs @@ -1,5 +1,5 @@ /** - * Error shape tests for @mds/mds universal package. + * Error shape tests for @mdscript/mds universal package. * Tests: U-E1 through U-E9 */ import { test, describe, before } from 'node:test'; diff --git a/packages/mds/__test__/fixtures/cross-dir/app/entry.mds b/packages/mds/__test__/fixtures/cross-dir/app/entry.mds new file mode 100644 index 00000000..dfdb6fe8 --- /dev/null +++ b/packages/mds/__test__/fixtures/cross-dir/app/entry.mds @@ -0,0 +1,7 @@ +--- +user: World +--- + +@import "../lib/helpers.mds" as helpers + +{helpers.greet(user)} diff --git a/packages/mds/__test__/fixtures/cross-dir/lib/helpers.mds b/packages/mds/__test__/fixtures/cross-dir/lib/helpers.mds new file mode 100644 index 00000000..c5c7b338 --- /dev/null +++ b/packages/mds/__test__/fixtures/cross-dir/lib/helpers.mds @@ -0,0 +1,9 @@ +--- +type: mds +--- + +@define greet(name): +Hello {name}! +@end + +@export greet diff --git a/packages/mds/__test__/helpers.mjs b/packages/mds/__test__/helpers.mjs index 2f689713..b3873fdb 100644 --- a/packages/mds/__test__/helpers.mjs +++ b/packages/mds/__test__/helpers.mjs @@ -1,5 +1,5 @@ /** - * Shared test helpers for @mds/mds tests. + * Shared test helpers for @mdscript/mds tests. */ import { fileURLToPath } from 'node:url'; import path from 'node:path'; diff --git a/packages/mds/__test__/native-backend.spec.mjs b/packages/mds/__test__/native-backend.spec.mjs index 80ad3d6b..e4218b41 100644 --- a/packages/mds/__test__/native-backend.spec.mjs +++ b/packages/mds/__test__/native-backend.spec.mjs @@ -1,5 +1,5 @@ /** - * Native backend tests for @mds/mds universal package. + * Native backend tests for @mdscript/mds universal package. * Tests: U-N1 through U-N6 * * Verifies that the native NAPI backend behaves correctly in isolation. diff --git a/packages/mds/__test__/perf.spec.mjs b/packages/mds/__test__/perf.spec.mjs index ca32f9cb..7b11abf7 100644 --- a/packages/mds/__test__/perf.spec.mjs +++ b/packages/mds/__test__/perf.spec.mjs @@ -1,5 +1,5 @@ /** - * Performance benchmarks for @mds/mds universal package. + * Performance benchmarks for @mdscript/mds universal package. * Tests: U-PF1 through U-PF5 * * These are lightweight benchmarks that verify the API can handle diff --git a/packages/mds/__test__/scanner.spec.mjs b/packages/mds/__test__/scanner.spec.mjs index 24b436db..55a853bd 100644 --- a/packages/mds/__test__/scanner.spec.mjs +++ b/packages/mds/__test__/scanner.spec.mjs @@ -1,5 +1,5 @@ /** - * Module scanner unit tests for @mds/mds universal package. + * Module scanner unit tests for @mdscript/mds universal package. * Tests: U-S1 through U-S10 * * Tests the normalizeVirtualKey and buildModulesMap utilities directly diff --git a/packages/mds/__test__/wasm-backend.spec.mjs b/packages/mds/__test__/wasm-backend.spec.mjs index 801be09f..9ba12db3 100644 --- a/packages/mds/__test__/wasm-backend.spec.mjs +++ b/packages/mds/__test__/wasm-backend.spec.mjs @@ -1,5 +1,5 @@ /** - * WASM backend unit tests for @mds/mds universal package. + * WASM backend unit tests for @mdscript/mds universal package. * Tests: U-WB1 through U-WB20 * * Imports dist/backend/wasm.js directly to exercise internal state diff --git a/packages/mds/__test__/wasm-compileFile.spec.mjs b/packages/mds/__test__/wasm-compileFile.spec.mjs index 4d4a61aa..ce3542ea 100644 --- a/packages/mds/__test__/wasm-compileFile.spec.mjs +++ b/packages/mds/__test__/wasm-compileFile.spec.mjs @@ -1,5 +1,5 @@ /** - * WASM backend compileFile/checkFile tests for @mds/mds universal package. + * WASM backend compileFile/checkFile tests for @mdscript/mds universal package. * Tests: U-WCF1 through U-WCF11 * * Uses subprocess isolation with MDS_BACKEND=wasm to force the WASM backend diff --git a/packages/mds/package.json b/packages/mds/package.json index e3ad7f4e..55924209 100644 --- a/packages/mds/package.json +++ b/packages/mds/package.json @@ -1,6 +1,16 @@ { - "name": "@mds/mds", + "name": "@mdscript/mds", "version": "0.1.0", + "description": "Universal JavaScript/TypeScript bindings for the MDS (Markdown Script) compiler — native addon with WASM fallback", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/dean0x/mds.git", + "directory": "packages/mds" + }, + "homepage": "https://github.com/dean0x/mds#readme", + "bugs": "https://github.com/dean0x/mds/issues", + "keywords": ["markdown", "template", "llm", "prompt", "compiler", "mds"], "type": "module", "engines": { "node": ">=22.0.0" diff --git a/packages/mds/src/backend/wasm.ts b/packages/mds/src/backend/wasm.ts index 8af24a76..211b9c2b 100644 --- a/packages/mds/src/backend/wasm.ts +++ b/packages/mds/src/backend/wasm.ts @@ -125,7 +125,7 @@ export function validateWasmShape(mod: unknown): asserts mod is WasmModule { for (const name of ['compile', 'check', 'scanImports'] as const) { if (typeof m[name] !== 'function') { throw new Error( - `@mds/mds: WASM module is missing required export "${name}". ` + + `@mdscript/mds: WASM module is missing required export "${name}". ` + `Ensure the module is built with: wasm-pack build crates/mds-wasm --target web --out-dir pkg`, ); } @@ -154,7 +154,7 @@ export async function initWasmNode(options?: InitOptions): Promise { } if (nodeFailures >= MAX_INIT_RETRIES) { throw new Error( - `@mds/mds: WASM backend failed to initialize after ${MAX_INIT_RETRIES} attempts. Check that the WASM module is built and accessible.`, + `@mdscript/mds: WASM backend failed to initialize after ${MAX_INIT_RETRIES} attempts. Check that the WASM module is built and accessible.`, ); } cachedNodePromise = _initNode(options).catch((err) => { @@ -204,7 +204,7 @@ async function _initNode(options?: InitOptions): Promise { const cause = lastError !== undefined ? ` Caused by: ${lastError.message}` : ''; throw new Error( - `@mds/mds: failed to load WASM module. Build it first with: wasm-pack build crates/mds-wasm --target nodejs --out-dir pkg${cause}`, + `@mdscript/mds: failed to load WASM module. Build it first with: wasm-pack build crates/mds-wasm --target nodejs --out-dir pkg${cause}`, ); } @@ -229,7 +229,7 @@ export async function initWasmBrowser(options?: InitOptions): Promise= MAX_BROWSER_RETRIES) { throw new Error( - `@mds/mds: WASM browser backend failed to initialize after ${MAX_BROWSER_RETRIES} attempts. ` + + `@mdscript/mds: WASM browser backend failed to initialize after ${MAX_BROWSER_RETRIES} attempts. ` + `Ensure 'mds-wasm' is bundled or provide a valid wasmUrl option.`, ); } @@ -260,7 +260,7 @@ async function _initBrowser(options?: InitOptions): Promise { imported = await import('mds-wasm'); } catch (err) { throw new Error( - `@mds/mds: failed to load WASM module in browser environment. ` + + `@mdscript/mds: failed to load WASM module in browser environment. ` + `Ensure 'mds-wasm' is bundled or provide a wasmUrl option. Caused by: ${String(err)}`, ); } @@ -271,7 +271,7 @@ async function _initBrowser(options?: InitOptions): Promise { if (typeof wasmMod.default !== 'function') { throw new Error( - '@mds/mds: WASM module missing default() initializer. ' + + '@mdscript/mds: WASM module missing default() initializer. ' + 'Build with: wasm-pack build crates/mds-wasm --target web --out-dir pkg', ); } @@ -288,7 +288,7 @@ async function _initBrowser(options?: InitOptions): Promise { msg.includes('fetch') ) { throw new Error( - `@mds/mds: WASM initialization blocked — check your Content Security Policy. ` + + `@mdscript/mds: WASM initialization blocked — check your Content Security Policy. ` + `Add 'wasm-unsafe-eval' to script-src. Original: ${msg}`, ); } diff --git a/packages/mds/src/browser.ts b/packages/mds/src/browser.ts index 9676be7f..66df7608 100644 --- a/packages/mds/src/browser.ts +++ b/packages/mds/src/browser.ts @@ -70,7 +70,7 @@ export function init(options?: InitOptions): Promise { function assertReady(): MdsBaseBackend { if (resolvedBackend === undefined) { - throw new Error('@mds/mds: call await init() before using compile/check in a browser environment'); + throw new Error('@mdscript/mds: call await init() before using compile/check in a browser environment'); } return resolvedBackend; } diff --git a/packages/mds/src/node.ts b/packages/mds/src/node.ts index 2a974eb9..1e337535 100644 --- a/packages/mds/src/node.ts +++ b/packages/mds/src/node.ts @@ -17,7 +17,7 @@ const rawBackend = process.env['MDS_BACKEND']; const forceBackend: BackendType | undefined = rawBackend === 'native' || rawBackend === 'wasm' ? rawBackend : undefined; if (rawBackend !== undefined && forceBackend === undefined) { - console.warn(`@mds/mds: ignoring unknown MDS_BACKEND value "${rawBackend}"; expected "native" or "wasm"`); + console.warn(`@mdscript/mds: ignoring unknown MDS_BACKEND value "${rawBackend}"; expected "native" or "wasm"`); } // --------------------------------------------------------------------------- @@ -156,12 +156,12 @@ async function ensureBackend(options?: InitOptions): Promise { return; } - console.warn('@mds/mds: native addon unavailable, falling back to WASM'); + console.warn('@mdscript/mds: native addon unavailable, falling back to WASM'); try { backend = await loadWasmNodeBackend(options); } catch (wasmErr) { throw new Error( - `@mds/mds: no backend available. Native: ${nativeResult.error.message}. WASM: ${String(wasmErr)}`, + `@mdscript/mds: no backend available. Native: ${nativeResult.error.message}. WASM: ${String(wasmErr)}`, ); } } @@ -190,7 +190,7 @@ export function init(options?: InitOptions): Promise { function assertReady(): MdsNodeBackend { if (backend === undefined) { throw new Error( - '@mds/mds: call await init() before using compile/check/compileFile/checkFile/getBackend', + '@mdscript/mds: call await init() before using compile/check/compileFile/checkFile/getBackend', ); } return backend; diff --git a/packages/rollup-plugin/README.md b/packages/rollup-plugin/README.md index bc4420cf..906edbbf 100644 --- a/packages/rollup-plugin/README.md +++ b/packages/rollup-plugin/README.md @@ -1,4 +1,4 @@ -# @mds/rollup-plugin +# @mdscript/rollup-plugin Rollup plugin for importing `.mds` templates as ES modules. @@ -7,13 +7,13 @@ Rollup plugin for importing `.mds` templates as ES modules. ## Installation ```sh -npm install @mds/rollup-plugin +npm install @mdscript/rollup-plugin ``` ## Peer dependencies ```sh -npm install @mds/mds rollup +npm install @mdscript/mds rollup ``` Supported: `rollup ^3.0.0 || ^4.0.0`. @@ -22,7 +22,7 @@ Supported: `rollup ^3.0.0 || ^4.0.0`. ```js // rollup.config.js -import mdsPlugin from '@mds/rollup-plugin'; +import mdsPlugin from '@mdscript/rollup-plugin'; export default { plugins: [ @@ -52,7 +52,7 @@ Add the module declaration to your `tsconfig.json` so TypeScript recognises ```json { "compilerOptions": { - "types": ["@mds/bundler-utils/mds"] + "types": ["@mdscript/bundler-utils/mds"] } } ``` diff --git a/packages/rollup-plugin/__test__/plugin.spec.mjs b/packages/rollup-plugin/__test__/plugin.spec.mjs index 2c0e9f3a..7ee159d2 100644 --- a/packages/rollup-plugin/__test__/plugin.spec.mjs +++ b/packages/rollup-plugin/__test__/plugin.spec.mjs @@ -1,5 +1,5 @@ /** - * Tests for @mds/rollup-plugin. + * Tests for @mdscript/rollup-plugin. */ import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; diff --git a/packages/rollup-plugin/package.json b/packages/rollup-plugin/package.json index c5a372ee..9dafd3e6 100644 --- a/packages/rollup-plugin/package.json +++ b/packages/rollup-plugin/package.json @@ -1,8 +1,16 @@ { - "name": "@mds/rollup-plugin", + "name": "@mdscript/rollup-plugin", "version": "0.1.0", "description": "Rollup plugin for importing MDS templates as ES modules", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/dean0x/mds.git", + "directory": "packages/rollup-plugin" + }, + "homepage": "https://github.com/dean0x/mds#readme", + "bugs": "https://github.com/dean0x/mds/issues", + "keywords": ["mds", "rollup", "rollup-plugin", "markdown", "template", "llm", "prompt"], "type": "module", "engines": { "node": ">=22.0.0" @@ -21,14 +29,14 @@ "test": "NODE_ENV=test node --test __test__/*.spec.mjs" }, "dependencies": { - "@mds/bundler-utils": "^0.1.0" + "@mdscript/bundler-utils": "^0.1.0" }, "peerDependencies": { - "@mds/mds": "^0.1.0", + "@mdscript/mds": "^0.1.0", "rollup": "^3.0.0 || ^4.0.0" }, "devDependencies": { - "@mds/mds": "file:../mds", + "@mdscript/mds": "file:../mds", "@types/node": "^22.0.0", "rollup": "^4.0.0", "typescript": "^5.4.0" diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index 8c6c85f5..b746f65a 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -1,5 +1,5 @@ -import type { MdsPluginOptions } from '@mds/bundler-utils'; -import { createMdsTransformer, formatMdsError, cleanId } from '@mds/bundler-utils'; +import type { MdsPluginOptions } from '@mdscript/bundler-utils'; +import { createMdsTransformer, formatMdsError, cleanId } from '@mdscript/bundler-utils'; // Structural subset of Rollup's PluginContext and Plugin. We intentionally keep // narrow interfaces rather than importing `Plugin` from 'rollup' because: @@ -28,7 +28,7 @@ type Transformer = ReturnType; /** * Inject a pre-built transformer for testing without going through the real - * @mds/mds import. Allows tests to provide a mock transformer that returns + * @mdscript/mds import. Allows tests to provide a mock transformer that returns * controlled warnings, dependencies, and output. * FOR TESTING ONLY — throws unless NODE_ENV=test. */ @@ -57,7 +57,7 @@ export default function mdsPlugin(options?: MdsPluginOptions): RollupPlugin { transformer = _testTransformer; return; } - const mds = await import('@mds/mds'); + const mds = await import('@mdscript/mds'); transformer = createMdsTransformer(mds, options); }, diff --git a/packages/vite-plugin/README.md b/packages/vite-plugin/README.md index ac8d70cb..dd640bd6 100644 --- a/packages/vite-plugin/README.md +++ b/packages/vite-plugin/README.md @@ -1,4 +1,4 @@ -# @mds/vite-plugin +# @mdscript/vite-plugin Vite plugin for importing `.mds` templates as ES modules with HMR support. @@ -7,13 +7,13 @@ Vite plugin for importing `.mds` templates as ES modules with HMR support. ## Installation ```sh -npm install @mds/vite-plugin +npm install @mdscript/vite-plugin ``` ## Peer dependencies ```sh -npm install @mds/mds vite +npm install @mdscript/mds vite ``` Supported: `vite ^5.0.0 || ^6.0.0`. @@ -23,7 +23,7 @@ Supported: `vite ^5.0.0 || ^6.0.0`. ```ts // vite.config.ts import { defineConfig } from 'vite'; -import mdsPlugin from '@mds/vite-plugin'; +import mdsPlugin from '@mdscript/vite-plugin'; export default defineConfig({ plugins: [ @@ -53,7 +53,7 @@ Add the module declaration to your `tsconfig.json` so TypeScript recognises ```json { "compilerOptions": { - "types": ["@mds/bundler-utils/mds"] + "types": ["@mdscript/bundler-utils/mds"] } } ``` diff --git a/packages/vite-plugin/__test__/plugin.spec.mjs b/packages/vite-plugin/__test__/plugin.spec.mjs index e3d24f1f..5fd1410e 100644 --- a/packages/vite-plugin/__test__/plugin.spec.mjs +++ b/packages/vite-plugin/__test__/plugin.spec.mjs @@ -1,5 +1,5 @@ /** - * Tests for @mds/vite-plugin. + * Tests for @mdscript/vite-plugin. */ import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index a3b899f8..5d5a9438 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -1,8 +1,16 @@ { - "name": "@mds/vite-plugin", + "name": "@mdscript/vite-plugin", "version": "0.1.0", "description": "Vite plugin for importing MDS templates as ES modules", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/dean0x/mds.git", + "directory": "packages/vite-plugin" + }, + "homepage": "https://github.com/dean0x/mds#readme", + "bugs": "https://github.com/dean0x/mds/issues", + "keywords": ["mds", "vite", "vite-plugin", "markdown", "template", "llm", "prompt"], "type": "module", "engines": { "node": ">=22.0.0" @@ -21,14 +29,14 @@ "test": "NODE_ENV=test node --test __test__/*.spec.mjs" }, "dependencies": { - "@mds/bundler-utils": "^0.1.0" + "@mdscript/bundler-utils": "^0.1.0" }, "peerDependencies": { - "@mds/mds": "^0.1.0", + "@mdscript/mds": "^0.1.0", "vite": "^5.0.0 || ^6.0.0" }, "devDependencies": { - "@mds/mds": "file:../mds", + "@mdscript/mds": "file:../mds", "@types/node": "^22.0.0", "typescript": "^5.4.0", "vite": "^6.0.0" diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 7fb6328e..09f2d32e 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,5 +1,5 @@ -import type { MdsPluginOptions } from '@mds/bundler-utils'; -import { createMdsTransformer, formatMdsError, cleanId, isMdsExtension } from '@mds/bundler-utils'; +import type { MdsPluginOptions } from '@mdscript/bundler-utils'; +import { createMdsTransformer, formatMdsError, cleanId, isMdsExtension } from '@mdscript/bundler-utils'; // Structural subset of Vite's PluginContext. We intentionally keep a narrow // interface rather than importing `Plugin` from 'vite' because: @@ -34,7 +34,7 @@ type Transformer = ReturnType; /** * Inject a pre-built transformer for testing without going through the real - * @mds/mds import. Allows tests to provide a mock transformer that returns + * @mdscript/mds import. Allows tests to provide a mock transformer that returns * controlled warnings, dependencies, and output. * FOR TESTING ONLY — throws unless NODE_ENV=test. */ @@ -64,7 +64,7 @@ export default function mdsPlugin(options?: MdsPluginOptions): VitePlugin { transformer = _testTransformer; return; } - const mds = await import('@mds/mds'); + const mds = await import('@mdscript/mds'); transformer = createMdsTransformer(mds, options); }, diff --git a/packages/webpack-loader/README.md b/packages/webpack-loader/README.md index d04da669..f72c74ab 100644 --- a/packages/webpack-loader/README.md +++ b/packages/webpack-loader/README.md @@ -1,4 +1,4 @@ -# @mds/webpack-loader +# @mdscript/webpack-loader Webpack 5 loader for importing `.mds` templates as ES modules. @@ -7,13 +7,13 @@ Webpack 5 loader for importing `.mds` templates as ES modules. ## Installation ```sh -npm install @mds/webpack-loader +npm install @mdscript/webpack-loader ``` ## Peer dependencies ```sh -npm install @mds/mds webpack +npm install @mdscript/mds webpack ``` Supported: `webpack ^5.0.0`. @@ -28,7 +28,7 @@ export default { { test: /\.mds$/, use: { - loader: '@mds/webpack-loader', + loader: '@mdscript/webpack-loader', options: { // optional vars: { env: 'production' }, @@ -59,7 +59,7 @@ Add the module declaration to your `tsconfig.json` so TypeScript recognises ```json { "compilerOptions": { - "types": ["@mds/bundler-utils/mds"] + "types": ["@mdscript/bundler-utils/mds"] } } ``` diff --git a/packages/webpack-loader/__test__/cjs-compat.spec.mjs b/packages/webpack-loader/__test__/cjs-compat.spec.mjs index 5bdce4d8..12533c12 100644 --- a/packages/webpack-loader/__test__/cjs-compat.spec.mjs +++ b/packages/webpack-loader/__test__/cjs-compat.spec.mjs @@ -1,5 +1,5 @@ /** - * CJS compatibility tests for @mds/webpack-loader. + * CJS compatibility tests for @mdscript/webpack-loader. * * Verifies that the CJS build (dist-cjs/) can be loaded via require() and * exports the default loader function. This is the primary condition for diff --git a/packages/webpack-loader/__test__/loader.spec.mjs b/packages/webpack-loader/__test__/loader.spec.mjs index ecd88ab3..caf3d11e 100644 --- a/packages/webpack-loader/__test__/loader.spec.mjs +++ b/packages/webpack-loader/__test__/loader.spec.mjs @@ -1,5 +1,5 @@ /** - * Tests for @mds/webpack-loader. + * Tests for @mdscript/webpack-loader. */ import { test, describe, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; diff --git a/packages/webpack-loader/package.json b/packages/webpack-loader/package.json index 23d7a22e..250f75ab 100644 --- a/packages/webpack-loader/package.json +++ b/packages/webpack-loader/package.json @@ -1,8 +1,16 @@ { - "name": "@mds/webpack-loader", + "name": "@mdscript/webpack-loader", "version": "0.1.0", "description": "Webpack loader for importing MDS templates as ES modules", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/dean0x/mds.git", + "directory": "packages/webpack-loader" + }, + "homepage": "https://github.com/dean0x/mds#readme", + "bugs": "https://github.com/dean0x/mds/issues", + "keywords": ["mds", "webpack", "webpack-loader", "markdown", "template", "llm", "prompt"], "type": "module", "engines": { "node": ">=22.0.0" @@ -27,14 +35,14 @@ "test": "NODE_ENV=test node --test __test__/*.spec.mjs" }, "dependencies": { - "@mds/bundler-utils": "^0.1.0" + "@mdscript/bundler-utils": "^0.1.0" }, "peerDependencies": { - "@mds/mds": "^0.1.0", + "@mdscript/mds": "^0.1.0", "webpack": "^5.0.0" }, "devDependencies": { - "@mds/mds": "file:../mds", + "@mdscript/mds": "file:../mds", "@types/node": "^22.0.0", "typescript": "^5.4.0", "webpack": "^5.0.0" diff --git a/packages/webpack-loader/src/index.ts b/packages/webpack-loader/src/index.ts index dd0e6a25..677b037c 100644 --- a/packages/webpack-loader/src/index.ts +++ b/packages/webpack-loader/src/index.ts @@ -1,13 +1,13 @@ -import type { MdsPluginOptions } from '@mds/bundler-utils'; -import { LazyInit, createMdsTransformer, formatMdsError } from '@mds/bundler-utils'; +import type { MdsPluginOptions } from '@mdscript/bundler-utils'; +import { LazyInit, createMdsTransformer, formatMdsError } from '@mdscript/bundler-utils'; // WORKAROUND: When compiled to CJS, TypeScript rewrites `import()` to -// `require()`, breaking ESM-only packages like `@mds/mds`. This wrapper +// `require()`, breaking ESM-only packages like `@mdscript/mds`. This wrapper // preserves native `import()` by creating a new Function at runtime — the // compiler cannot see through the string literal. // See: https://github.com/microsoft/TypeScript/issues/43329 // -// The wrapper is intentionally parameter-less (calls `import('@mds/mds')` +// The wrapper is intentionally parameter-less (calls `import('@mdscript/mds')` // directly) so that no arbitrary module ID can be passed through // new Function — eliminating the latent code-loading vector. // @@ -19,7 +19,7 @@ import { LazyInit, createMdsTransformer, formatMdsError } from '@mds/bundler-uti // or switch to an ESM-only build pipeline. // eslint-disable-next-line @typescript-eslint/no-implied-eval const esmImport: () => Promise = new Function( - 'return import("@mds/mds")', + 'return import("@mdscript/mds")', ) as () => Promise; // Hand-rolled rather than `import type { LoaderContext } from 'webpack'` because @@ -63,11 +63,11 @@ function getLazy(options: MdsPluginOptions, emitWarning: (err: Error) => void): 'esmImport() did not return a thenable. The new Function() wrapper is broken in this environment.', ); } - const mds = await importResult as typeof import('@mds/mds'); + const mds = await importResult as typeof import('@mdscript/mds'); const mdsAny = mds as Record; if (typeof mdsAny['compileFile'] !== 'function' || typeof mdsAny['init'] !== 'function') { throw new Error( - '@mds/mds module shape is unexpected: compileFile and init must both be functions. ' + + '@mdscript/mds module shape is unexpected: compileFile and init must both be functions. ' + 'Check that the installed version is compatible.', ); } @@ -113,7 +113,7 @@ export function _resetForTesting(): void { /** * Inject a pre-built transformer for testing without going through the real - * @mds/mds import. Allows tests to provide a mock transformer that returns + * @mdscript/mds import. Allows tests to provide a mock transformer that returns * controlled warnings, dependencies, and output. Pass null to tear down the * injected transformer (equivalent to calling _resetForTesting). */ diff --git a/packages/webpack-loader/tsconfig.cjs.json b/packages/webpack-loader/tsconfig.cjs.json index 89bb3c42..47ab0031 100644 --- a/packages/webpack-loader/tsconfig.cjs.json +++ b/packages/webpack-loader/tsconfig.cjs.json @@ -8,8 +8,8 @@ "declaration": false, "declarationMap": false, "paths": { - "@mds/bundler-utils": ["../bundler-utils/dist/index.d.ts"], - "@mds/mds": ["../mds/dist/node.d.ts"] + "@mdscript/bundler-utils": ["../bundler-utils/dist/index.d.ts"], + "@mdscript/mds": ["../mds/dist/node.d.ts"] } }, "include": ["src/**/*.ts"], diff --git a/spec.md b/spec.md index 05a332a8..9d528d34 100644 --- a/spec.md +++ b/spec.md @@ -692,7 +692,7 @@ A language server (Rust) providing diagnostics, completions, go-to-definition fo These are intentionally deferred to keep the language simple and the compiler focused: - Structured JSON output (chat message arrays) -- TypeScript/JS integration or runtime bindings (shipped post-v0.1: see `@mds/mds` npm package) +- TypeScript/JS *language* features — note that runtime bindings for calling the compiler from JS/TS *are* provided (see the `@mdscript/mds` npm package); this item refers to in-template scripting, which is out of scope - Built-in functions (upper, lower, join, etc.) - Recursion - Macros, async functions, streaming @@ -751,6 +751,4 @@ quoted_path := "\"" path_chars "\"" ## 12. Status -v0.1 — Initial release. The core compiler is feature-complete as described in this specification. - -Post-v0.1 additions (not yet released): negation in `@if` conditions (`!dot_path`), equality/inequality comparisons (`==`, `!=`), `@elseif` directive, NaN and Infinity rejection at parse time. These features are implemented and tested but will ship in the next release. +v0.1.0 — Initial public release. The core compiler is feature-complete as described in this specification, including negation in `@if` conditions (`!dot_path`), equality/inequality comparisons (`==`, `!=`), the `@elseif` directive, and `NaN`/`Infinity` rejection at parse time. diff --git a/test_playground/01_basic.mds b/test_playground/01_basic.mds deleted file mode 100644 index 9e2bd4b3..00000000 --- a/test_playground/01_basic.mds +++ /dev/null @@ -1,6 +0,0 @@ ---- -name: Claude -role: assistant ---- - -You are {name}, an AI {role}. diff --git a/test_playground/02_conditional.mds b/test_playground/02_conditional.mds deleted file mode 100644 index 0856a14c..00000000 --- a/test_playground/02_conditional.mds +++ /dev/null @@ -1,12 +0,0 @@ ---- -premium: true -name: Dean ---- - -Hello {name}! - -@if premium: -Thank you for being a premium member! You have access to all features. -@else: -Consider upgrading to premium for more features. -@end diff --git a/test_playground/03_loop.mds b/test_playground/03_loop.mds deleted file mode 100644 index 5c590347..00000000 --- a/test_playground/03_loop.mds +++ /dev/null @@ -1,10 +0,0 @@ ---- -languages: [Rust, TypeScript, Python, Go] ---- - -You are proficient in the following languages: -@for lang in languages: -- {lang} -@end - -Please respond using the most appropriate language for the task. diff --git a/test_playground/04_function.mds b/test_playground/04_function.mds deleted file mode 100644 index 7c8edd76..00000000 --- a/test_playground/04_function.mds +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Dean -project: MDS Compiler ---- - -@define section(title): -## {title} - -@end - -@define greeting(person, proj): -Welcome, {person}! You're working on {proj}. -@end - -{greeting(name, project)} - -{section("Guidelines")} -- Write clean code -- Test thoroughly -- Document changes diff --git a/test_playground/05_import_alias.mds b/test_playground/05_import_alias.mds deleted file mode 100644 index 35c75d14..00000000 --- a/test_playground/05_import_alias.mds +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: System Prompt ---- - -@import "./lib/format.mds" as fmt - -{fmt.heading(title)} - -This is a {fmt.bold("system prompt")} built with MDS. diff --git a/test_playground/06_import_merge.mds b/test_playground/06_import_merge.mds deleted file mode 100644 index f76154c4..00000000 --- a/test_playground/06_import_merge.mds +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Quick Start ---- - -@import "./lib/format.mds" - -{heading(title)} - -{bold("Step 1")}: Install the tool. -{bold("Step 2")}: Create a template. -{bold("Step 3")}: Build it. diff --git a/test_playground/07_import_selective.mds b/test_playground/07_import_selective.mds deleted file mode 100644 index d996ec12..00000000 --- a/test_playground/07_import_selective.mds +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: Dean ---- - -@import { bold, heading } from "./lib/format.mds" - -{heading("Welcome")} - -Hello {bold(name)}, only bold and heading were imported. diff --git a/test_playground/08_include.mds b/test_playground/08_include.mds deleted file mode 100644 index ffab0fef..00000000 --- a/test_playground/08_include.mds +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Dean ---- - -@import "./lib/footer.mds" as footer - -Hello {name}! - -This is the main content of the prompt. - -@include footer diff --git a/test_playground/09_complex_prompt.mds b/test_playground/09_complex_prompt.mds deleted file mode 100644 index 1c22f8f6..00000000 --- a/test_playground/09_complex_prompt.mds +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: Claude -role: coding assistant -expertise: [Rust, TypeScript, Python] -verbose: true -max_retries: 3 ---- - -@import "./lib/format.mds" as fmt -@import "./lib/rules.mds" - -{fmt.heading("System Prompt")} - -You are {fmt.bold(name)}, a {role}. - -{fmt.heading("Expertise")} - -You are an expert in: -@for lang in expertise: -{fmt.list_item(lang)} -@end - -{fmt.heading("Rules")} - -{safety_rules()} - -{tone_rules()} - -@if verbose: - -{fmt.heading("Debug Info")} - -This prompt was generated with verbose mode enabled. -Max retries: {max_retries}. -@end diff --git a/test_playground/10_nested.mds b/test_playground/10_nested.mds deleted file mode 100644 index 3601899a..00000000 --- a/test_playground/10_nested.mds +++ /dev/null @@ -1,19 +0,0 @@ ---- -sections: [auth, api, storage] -show_details: true ---- - -@define section_header(name): -## {name} -@end - -# Project Plan - -@for section in sections: -{section_header(section)} - -@if show_details: -This section requires implementation. -@end - -@end diff --git a/test_playground/11_escaped.mds b/test_playground/11_escaped.mds deleted file mode 100644 index e10776bc..00000000 --- a/test_playground/11_escaped.mds +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: Dean ---- - -Hello {name}! - -Use \{variable} syntax for interpolation in MDS. -Literal braces: \{not_a_var}. -But this IS a variable: {name}. diff --git a/test_playground/12_code_block.mds b/test_playground/12_code_block.mds deleted file mode 100644 index e1e3ecfe..00000000 --- a/test_playground/12_code_block.mds +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: Dean ---- - -Hello {name}! Here's some code: - -```python -def greet(name): - return f"Hello {name}!" -``` - -The code above should NOT have {name} interpolated inside the code block. -But this should: {name}. diff --git a/test_playground/13_reexport.mds b/test_playground/13_reexport.mds deleted file mode 100644 index 1145f40a..00000000 --- a/test_playground/13_reexport.mds +++ /dev/null @@ -1,2 +0,0 @@ -@export bold from "./lib/format.mds" -@export * from "./lib/rules.mds" diff --git a/test_playground/14_reexport_consumer.mds b/test_playground/14_reexport_consumer.mds deleted file mode 100644 index 94dfb507..00000000 --- a/test_playground/14_reexport_consumer.mds +++ /dev/null @@ -1,5 +0,0 @@ -@import "./13_reexport.mds" - -{bold("Re-exported!")} - -{safety_rules()} diff --git a/test_playground/15_runtime_vars.mds b/test_playground/15_runtime_vars.mds deleted file mode 100644 index 3d245a09..00000000 --- a/test_playground/15_runtime_vars.mds +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: default_name -role: default_role ---- - -Hello {name}! Your role is {role}. - -@for item in items: -- {item} -@end diff --git a/test_playground/err_circular_a.mds b/test_playground/err_circular_a.mds deleted file mode 100644 index ad5f574f..00000000 --- a/test_playground/err_circular_a.mds +++ /dev/null @@ -1 +0,0 @@ -@import "./err_circular_b.mds" diff --git a/test_playground/err_circular_b.mds b/test_playground/err_circular_b.mds deleted file mode 100644 index 5932dc4c..00000000 --- a/test_playground/err_circular_b.mds +++ /dev/null @@ -1 +0,0 @@ -@import "./err_circular_a.mds" diff --git a/test_playground/err_undefined.mds b/test_playground/err_undefined.mds deleted file mode 100644 index 68d1f456..00000000 --- a/test_playground/err_undefined.mds +++ /dev/null @@ -1 +0,0 @@ -Hello {undefined_var}! diff --git a/test_playground/hello.mds b/test_playground/hello.mds deleted file mode 100644 index 00ae816b..00000000 --- a/test_playground/hello.mds +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: World -items: [one, two, three] ---- - -Hello {name}! - -Your items: -@for item in items: -- {item} -@end diff --git a/test_playground/lib/footer.mds b/test_playground/lib/footer.mds deleted file mode 100644 index a21e6f33..00000000 --- a/test_playground/lib/footer.mds +++ /dev/null @@ -1,6 +0,0 @@ ---- -version: 1.0 ---- - ---- -*Generated by MDS v{version}. All rights reserved.* diff --git a/test_playground/lib/format.mds b/test_playground/lib/format.mds deleted file mode 100644 index 1af56aba..00000000 --- a/test_playground/lib/format.mds +++ /dev/null @@ -1,15 +0,0 @@ -@define bold(text): -**{text}** -@end - -@define heading(text): -# {text} -@end - -@define list_item(text): -- {text} -@end - -@export bold -@export heading -@export list_item diff --git a/test_playground/lib/rules.mds b/test_playground/lib/rules.mds deleted file mode 100644 index 7da1ac6e..00000000 --- a/test_playground/lib/rules.mds +++ /dev/null @@ -1,17 +0,0 @@ ---- -safety_level: strict ---- - -@define safety_rules(): -You must follow these safety rules: -1. Never generate harmful content -2. Always be truthful -3. Respect user privacy -@end - -@define tone_rules(): -Maintain a professional yet friendly tone at all times. -@end - -@export safety_rules -@export tone_rules diff --git a/test_playground/output.md b/test_playground/output.md deleted file mode 100644 index a9ea075b..00000000 --- a/test_playground/output.md +++ /dev/null @@ -1 +0,0 @@ -You are Claude, an AI assistant. diff --git a/test_playground/output/01_basic.md b/test_playground/output/01_basic.md deleted file mode 100644 index a9ea075b..00000000 --- a/test_playground/output/01_basic.md +++ /dev/null @@ -1 +0,0 @@ -You are Claude, an AI assistant. diff --git a/test_playground/output/02_conditional.md b/test_playground/output/02_conditional.md deleted file mode 100644 index 39d0b5f8..00000000 --- a/test_playground/output/02_conditional.md +++ /dev/null @@ -1,3 +0,0 @@ -Hello Dean! - -Thank you for being a premium member! You have access to all features. diff --git a/test_playground/output/03_loop.md b/test_playground/output/03_loop.md deleted file mode 100644 index 49957f4c..00000000 --- a/test_playground/output/03_loop.md +++ /dev/null @@ -1,7 +0,0 @@ -You are proficient in the following languages: -- Rust -- TypeScript -- Python -- Go - -Please respond using the most appropriate language for the task. diff --git a/test_playground/output/04_function.md b/test_playground/output/04_function.md deleted file mode 100644 index b22edd5d..00000000 --- a/test_playground/output/04_function.md +++ /dev/null @@ -1,7 +0,0 @@ -Welcome, Dean! You're working on MDS Compiler. - -## Guidelines - -- Write clean code -- Test thoroughly -- Document changes diff --git a/test_playground/output/05_import_alias.md b/test_playground/output/05_import_alias.md deleted file mode 100644 index 44c0b865..00000000 --- a/test_playground/output/05_import_alias.md +++ /dev/null @@ -1,3 +0,0 @@ -# System Prompt - -This is a **system prompt** built with MDS. diff --git a/test_playground/output/06_import_merge.md b/test_playground/output/06_import_merge.md deleted file mode 100644 index 9771d7ee..00000000 --- a/test_playground/output/06_import_merge.md +++ /dev/null @@ -1,5 +0,0 @@ -# Quick Start - -**Step 1**: Install the tool. -**Step 2**: Create a template. -**Step 3**: Build it. diff --git a/test_playground/output/07_import_selective.md b/test_playground/output/07_import_selective.md deleted file mode 100644 index da158b07..00000000 --- a/test_playground/output/07_import_selective.md +++ /dev/null @@ -1,3 +0,0 @@ -# Welcome - -Hello **Dean**, only bold and heading were imported. diff --git a/test_playground/output/08_include.md b/test_playground/output/08_include.md deleted file mode 100644 index 2f4fe67c..00000000 --- a/test_playground/output/08_include.md +++ /dev/null @@ -1,6 +0,0 @@ -Hello Dean! - -This is the main content of the prompt. - ---- -*Generated by MDS v1. All rights reserved.* diff --git a/test_playground/output/09_complex_prompt.md b/test_playground/output/09_complex_prompt.md deleted file mode 100644 index f918671c..00000000 --- a/test_playground/output/09_complex_prompt.md +++ /dev/null @@ -1,24 +0,0 @@ -# System Prompt - -You are **Claude**, a coding assistant. - -# Expertise - -You are an expert in: -- Rust -- TypeScript -- Python - -# Rules - -You must follow these safety rules: -1. Never generate harmful content -2. Always be truthful -3. Respect user privacy - -Maintain a professional yet friendly tone at all times. - -# Debug Info - -This prompt was generated with verbose mode enabled. -Max retries: 3. diff --git a/test_playground/output/10_nested.md b/test_playground/output/10_nested.md deleted file mode 100644 index c9defe38..00000000 --- a/test_playground/output/10_nested.md +++ /dev/null @@ -1,13 +0,0 @@ -# Project Plan - -## auth - -This section requires implementation. - -## api - -This section requires implementation. - -## storage - -This section requires implementation. diff --git a/test_playground/output/11_escaped.md b/test_playground/output/11_escaped.md deleted file mode 100644 index f776dc01..00000000 --- a/test_playground/output/11_escaped.md +++ /dev/null @@ -1,5 +0,0 @@ -Hello Dean! - -Use {variable} syntax for interpolation in MDS. -Literal braces: {not_a_var}. -But this IS a variable: Dean. diff --git a/test_playground/output/12_code_block.md b/test_playground/output/12_code_block.md deleted file mode 100644 index d77b779a..00000000 --- a/test_playground/output/12_code_block.md +++ /dev/null @@ -1,9 +0,0 @@ -Hello Dean! Here's some code: - -```python -def greet(name): - return f"Hello {name}!" -``` - -The code above should NOT have Dean interpolated inside the code block. -But this should: Dean. diff --git a/test_playground/output/14_reexport_consumer.md b/test_playground/output/14_reexport_consumer.md deleted file mode 100644 index d34ef0da..00000000 --- a/test_playground/output/14_reexport_consumer.md +++ /dev/null @@ -1,6 +0,0 @@ -**Re-exported!** - -You must follow these safety rules: -1. Never generate harmful content -2. Always be truthful -3. Respect user privacy diff --git a/test_playground/output/15_runtime_vars.md b/test_playground/output/15_runtime_vars.md deleted file mode 100644 index f1f7c1d9..00000000 --- a/test_playground/output/15_runtime_vars.md +++ /dev/null @@ -1,5 +0,0 @@ -Hello Override! Your role is developer. - -- alpha -- beta -- gamma diff --git a/test_playground/vars.json b/test_playground/vars.json deleted file mode 100644 index 55d5946d..00000000 --- a/test_playground/vars.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Dean", - "role": "developer", - "items": ["alpha", "beta", "gamma"] -} From c88f57b680f667cb7e149d700c6a6a4104b2a6a3 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 30 May 2026 00:14:31 +0300 Subject: [PATCH 02/12] =?UTF-8?q?feat(release):=20native=20+=20WASM=20npm?= =?UTF-8?q?=20packaging,=20release=20workflow,=20gates=20Phase=20A=20?= =?UTF-8?q?=E2=80=94=20native=20(napi)=20packaging:=20-=20Rename=20host=20?= =?UTF-8?q?package=20to=20@mdscript/mds-napi;=20loader=20strings=20->=20@m?= =?UTF-8?q?dscript/mds-napi-=20-=20napi=20v3=20config=20(binaryNam?= =?UTF-8?q?e/packageName/7=20targets)=20-=20node.ts=20requires=20the=20sco?= =?UTF-8?q?ped=20package;=20mds=20optionalDep=20->=20^0.1.0=20(kill=20file?= =?UTF-8?q?:)=20-=20A3=20name<->loader=20gate=20(scripts/verify-napi-names?= =?UTF-8?q?.mjs)=20-=20Ship=20index.js/index.d.ts/LICENSE/README=20via=20f?= =?UTF-8?q?iles;=20metadata=20Phase=20B=20=E2=80=94=20WASM=20as=20committe?= =?UTF-8?q?d=20workspace=20wrapper:=20-=20packages/mds-wasm=20(@mdscript/m?= =?UTF-8?q?ds-wasm)=20building=20node+web=20targets=20into=20dist/=20(stri?= =?UTF-8?q?p=20wasm-pack's=20.gitignore=20so=20npm=20packs=20dist)=20-=20w?= =?UTF-8?q?asm.ts=20loader=20+=20browser=20import=20+=20error=20strings=20?= =?UTF-8?q?->=20@mdscript/mds-wasm=20-=20@mdscript/mds=20depends=20on=20@m?= =?UTF-8?q?dscript/mds-wasm=20^0.1.0=20Release=20workflow=20(A6/A7/B4/D2/D?= =?UTF-8?q?3):=20-=207-target=20napi=20cross-compile=20matrix=20(--use-nap?= =?UTF-8?q?i-cross=20for=20linux=20musl/arm)=20-=20stage+verify=20job=20ru?= =?UTF-8?q?ns=20A3=20gate;=20workflow=5Fdispatch=20dry-run=20publishes=20n?= =?UTF-8?q?othing=20-=20npm=20publishes=20with=20provenance=20(OIDC=20id-t?= =?UTF-8?q?oken);=20crates=20->=20npm=20->=20GitHub=20release=20-=20D1=20v?= =?UTF-8?q?ersion-consistency=20gate=20(scripts/verify-versions.mjs)=20Per?= =?UTF-8?q?-package=20LICENSE=20copies;=20napi=20README.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 267 ++++++++++++++++++++++--------- .gitignore | 3 + crates/mds-napi/LICENSE | 21 +++ crates/mds-napi/README.md | 41 +++++ crates/mds-napi/index.js | 14 +- crates/mds-napi/package.json | 36 ++++- package-lock.json | 30 +++- packages/bundler-utils/LICENSE | 21 +++ packages/mds-wasm/LICENSE | 21 +++ packages/mds-wasm/README.md | 36 +++++ packages/mds-wasm/package.json | 41 +++++ packages/mds/LICENSE | 21 +++ packages/mds/package.json | 5 +- packages/mds/src/backend/wasm.ts | 28 ++-- packages/mds/src/node.ts | 2 +- packages/rollup-plugin/LICENSE | 21 +++ packages/vite-plugin/LICENSE | 21 +++ packages/webpack-loader/LICENSE | 21 +++ scripts/verify-napi-names.mjs | 127 +++++++++++++++ scripts/verify-versions.mjs | 78 +++++++++ 20 files changed, 742 insertions(+), 113 deletions(-) create mode 100644 crates/mds-napi/LICENSE create mode 100644 crates/mds-napi/README.md create mode 100644 packages/bundler-utils/LICENSE create mode 100644 packages/mds-wasm/LICENSE create mode 100644 packages/mds-wasm/README.md create mode 100644 packages/mds-wasm/package.json create mode 100644 packages/mds/LICENSE create mode 100644 packages/rollup-plugin/LICENSE create mode 100644 packages/vite-plugin/LICENSE create mode 100644 packages/webpack-loader/LICENSE create mode 100644 scripts/verify-napi-names.mjs create mode 100644 scripts/verify-versions.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e08087b..9ef3876b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,25 +1,143 @@ name: Release -# Triggered by pushing a version tag, e.g. `git tag v0.1.0 && git push --tags`. +# Two entry points: +# * push a version tag (v*) -> full coordinated release (crates + all npm pkgs) +# * workflow_dispatch -> DRY RUN: build the napi matrix, stage the +# platform packages, run the name + version gates, +# upload artifacts. Publishes NOTHING. +# +# The dry run lets the risky musl/arm cross-compile + the A3 name<->loader gate be +# validated before the real, deferred release. on: push: tags: - "v*" + workflow_dispatch: {} permissions: - contents: write # create the GitHub Release + contents: read env: CARGO_TERM_COLOR: always jobs: # --------------------------------------------------------------------------- - # crates.io — READY. Publishes the library then the CLI (CLI depends on the - # library, so order and index-propagation matter). - # Requires repo secret: CARGO_REGISTRY_TOKEN + # D1 — version-consistency gate. Cheap; fails fast before any build/publish. + # --------------------------------------------------------------------------- + version-gate: + name: Version gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 22 } + - name: Assert synchronized versions, no file: refs + run: node scripts/verify-versions.mjs + + # --------------------------------------------------------------------------- + # A6 — cross-compile the native addon for all 7 targets. + # Linux gnu/musl/arm use napi v3 `--use-napi-cross` (zig-based sysroots); + # bare `rustup target add` cannot link musl/arm. macOS x86_64 cross-links from + # the arm64 host via the installed target. `--no-js` preserves the hand-written + # index.js loader (index.d.ts still regenerates). + # --------------------------------------------------------------------------- + build-napi: + name: Build napi (${{ matrix.settings.target }}) + needs: [version-gate] + strategy: + fail-fast: false + matrix: + settings: + - host: macos-latest + target: aarch64-apple-darwin + build: napi build --platform --release --target aarch64-apple-darwin --no-js + - host: macos-latest + target: x86_64-apple-darwin + build: napi build --platform --release --target x86_64-apple-darwin --no-js + - host: ubuntu-latest + target: x86_64-unknown-linux-gnu + build: napi build --platform --release --target x86_64-unknown-linux-gnu --use-napi-cross --no-js + - host: ubuntu-latest + target: x86_64-unknown-linux-musl + build: napi build --platform --release --target x86_64-unknown-linux-musl --use-napi-cross --no-js + - host: ubuntu-latest + target: aarch64-unknown-linux-gnu + build: napi build --platform --release --target aarch64-unknown-linux-gnu --use-napi-cross --no-js + - host: ubuntu-latest + target: aarch64-unknown-linux-musl + build: napi build --platform --release --target aarch64-unknown-linux-musl --use-napi-cross --no-js + - host: windows-latest + target: x86_64-pc-windows-msvc + build: napi build --platform --release --target x86_64-pc-windows-msvc --no-js + runs-on: ${{ matrix.settings.host }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 22, cache: npm } + - uses: dtolnay/rust-toolchain@stable + with: { targets: ${{ matrix.settings.target }} } + - uses: Swatinem/rust-cache@v2 + - run: npm ci + - name: Build addon + working-directory: crates/mds-napi + run: npx ${{ matrix.settings.build }} + - uses: actions/upload-artifact@v4 + with: + name: bindings-${{ matrix.settings.target }} + path: crates/mds-napi/*.node + if-no-files-found: error + + # --------------------------------------------------------------------------- + # A3/A7/D2 — stage the per-platform npm packages from the built .node files, + # then run the CRITICAL name<->loader gate. Runs on BOTH tag push and the + # workflow_dispatch dry run; it never publishes. The publish job consumes the + # exact staged tree this job uploads. + # --------------------------------------------------------------------------- + stage-and-verify-napi: + name: Stage + verify platform packages + needs: [build-napi] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 22, cache: npm } + - run: npm ci + - name: Download all .node artifacts + uses: actions/download-artifact@v4 + with: + path: crates/mds-napi/artifacts + pattern: bindings-* + merge-multiple: true + - name: Stage per-platform packages + working-directory: crates/mds-napi + run: | + npx napi create-npm-dirs + npx napi artifacts --output-dir . + - name: Copy LICENSE into each platform package + run: for d in crates/mds-napi/npm/*/; do cp LICENSE "$d"; done + - name: A3 — name <-> loader verification gate + run: node scripts/verify-napi-names.mjs + - name: Upload staged napi tree + uses: actions/upload-artifact@v4 + with: + name: napi-staged + path: | + crates/mds-napi/npm/** + crates/mds-napi/*.node + + # =========================================================================== + # Everything below publishes — gated to tag pushes only. workflow_dispatch + # stops after stage-and-verify-napi above (the dry run). + # =========================================================================== + + # --------------------------------------------------------------------------- + # crates.io — library then CLI (CLI depends on the library; order + index + # propagation matter). Requires repo secret CARGO_REGISTRY_TOKEN. # --------------------------------------------------------------------------- publish-crates: name: Publish to crates.io + needs: [version-gate] + if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -41,88 +159,77 @@ jobs: run: cargo publish -p mds-cli --token "${{ secrets.CARGO_REGISTRY_TOKEN }}" # --------------------------------------------------------------------------- - # GitHub Release — READY. Cuts a release with notes once crates publish. + # A7/B4/D3 — publish all npm packages with provenance (OIDC). + # NPM_CONFIG_PROVENANCE=true makes every `npm publish` (including the ones napi + # prepublish runs for the platform packages) emit a provenance attestation. + # --------------------------------------------------------------------------- + publish-npm: + name: Publish to npm + needs: [stage-and-verify-napi, publish-crates] + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + id-token: write # OIDC for npm provenance + contents: read + env: + NPM_CONFIG_PROVENANCE: "true" + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + registry-url: "https://registry.npmjs.org" + - uses: dtolnay/rust-toolchain@stable + with: { targets: wasm32-unknown-unknown } + - uses: Swatinem/rust-cache@v2 + - uses: jetli/wasm-pack-action@v0.4.0 + - run: npm ci + - name: Restore staged napi tree (.node + npm/ platform dirs) + uses: actions/download-artifact@v4 + with: + name: napi-staged + path: crates/mds-napi + # A4 — regenerate index.d.ts on the publish host (hand-written index.js is + # preserved by --no-js); the stray host .node is not in the package `files`. + - name: Generate napi types + working-directory: crates/mds-napi + run: npx napi build --platform --release --no-js + - name: Re-run A3 gate against the restored tree + run: node scripts/verify-napi-names.mjs + - name: Publish platform packages (napi prepublish) + working-directory: crates/mds-napi + run: npx napi prepublish -t npm --skip-gh-release + - name: Publish host napi package + run: npm publish -w @mdscript/mds-napi --access public + - name: Build WASM + TS packages + run: | + npm run build -w @mdscript/mds-wasm + npm run build --workspaces --if-present + - name: Publish @mdscript/mds-wasm + run: npm publish -w @mdscript/mds-wasm --access public + - name: Publish @mdscript/mds + bundler packages + run: | + npm publish -w @mdscript/mds --access public + npm publish -w @mdscript/bundler-utils --access public + npm publish -w @mdscript/vite-plugin --access public + npm publish -w @mdscript/rollup-plugin --access public + npm publish -w @mdscript/webpack-loader --access public + + # --------------------------------------------------------------------------- + # GitHub Release — cut last, after crates + npm succeed. # --------------------------------------------------------------------------- github-release: name: GitHub Release - needs: [publish-crates] + needs: [publish-crates, publish-npm] + if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest + permissions: + contents: write # create the GitHub Release steps: - uses: actions/checkout@v4 - name: Create release run: gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --generate-notes env: GH_TOKEN: ${{ github.token }} - - # =========================================================================== - # npm publishing (native addon + universal + bundler packages) — NOT YET - # WIRED. Blocked on the "napi packaging" task: - # 1. Populate the `napi` config in crates/mds-napi/package.json with the - # target triples (currently `"triples": {}`) so the per-platform - # `@mdscript/mds-` packages can be generated and published. - # 2. Add the NPM_TOKEN repo secret and register the @mdscript org. - # 3. Validate the cross-compile matrix on a real CI run (musl/arm/windows - # cross toolchains are environment-sensitive). - # - # The job below is the canonical napi-rs build matrix, left commented until - # the packaging config above exists — publishing it half-configured would - # ship broken packages. See README/CHANGELOG for the intended package set. - # =========================================================================== - # - # build-napi: - # strategy: - # fail-fast: false - # matrix: - # settings: - # - { host: macos-latest, target: aarch64-apple-darwin } - # - { host: macos-latest, target: x86_64-apple-darwin } - # - { host: ubuntu-latest, target: x86_64-unknown-linux-gnu } - # - { host: ubuntu-latest, target: x86_64-unknown-linux-musl } - # - { host: ubuntu-latest, target: aarch64-unknown-linux-gnu } - # - { host: ubuntu-latest, target: aarch64-unknown-linux-musl } - # - { host: windows-latest, target: x86_64-pc-windows-msvc } - # runs-on: ${{ matrix.settings.host }} - # steps: - # - uses: actions/checkout@v4 - # - uses: actions/setup-node@v4 - # with: { node-version: 22, cache: npm } - # - uses: dtolnay/rust-toolchain@stable - # with: { targets: ${{ matrix.settings.target }} } - # - uses: Swatinem/rust-cache@v2 - # - run: npm ci - # - name: Build addon - # working-directory: crates/mds-napi - # run: npx napi build --platform --release --target ${{ matrix.settings.target }} --no-js - # - uses: actions/upload-artifact@v4 - # with: - # name: bindings-${{ matrix.settings.target }} - # path: crates/mds-napi/*.node - # - # publish-npm: - # needs: [build-napi] - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: actions/setup-node@v4 - # with: { node-version: 22, cache: npm, registry-url: "https://registry.npmjs.org" } - # - run: npm ci - # - uses: actions/download-artifact@v4 - # with: { path: crates/mds-napi/artifacts } - # - name: Stage platform packages & publish addon - # working-directory: crates/mds-napi - # run: | - # npx napi create-npm-dirs - # npx napi artifacts --output-dir . - # npx napi prepublish -t npm - # env: - # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - # - name: Build & publish universal + bundler packages - # run: | - # npm run build --workspaces --if-present - # npm publish -w @mdscript/mds --access public - # npm publish -w @mdscript/bundler-utils --access public - # npm publish -w @mdscript/vite-plugin --access public - # npm publish -w @mdscript/rollup-plugin --access public - # npm publish -w @mdscript/webpack-loader --access public - # env: - # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 9ef55e23..069af452 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ crates/mds-wasm/pkg-web/ node_modules/ crates/mds-napi/index.d.ts crates/mds-napi/package-lock.json +# napi-rs generated staging (created in CI by `napi create-npm-dirs`/`artifacts`) +crates/mds-napi/npm/ +crates/mds-napi/artifacts/ packages/*/dist/ packages/*/dist-cjs/ diff --git a/crates/mds-napi/LICENSE b/crates/mds-napi/LICENSE new file mode 100644 index 00000000..37193c4b --- /dev/null +++ b/crates/mds-napi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dean Sharon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/mds-napi/README.md b/crates/mds-napi/README.md new file mode 100644 index 00000000..1a39bc2d --- /dev/null +++ b/crates/mds-napi/README.md @@ -0,0 +1,41 @@ +# @mdscript/mds-napi + +Native Node.js bindings for the [MDS (Markdown Script)](https://github.com/dean0x/mds) +compiler, built with [napi-rs](https://napi.rs/). + +This is the high-performance backend used by [`@mdscript/mds`](https://www.npmjs.com/package/@mdscript/mds) +on Node.js. **Most users should depend on `@mdscript/mds`, not this package +directly** — `@mdscript/mds` loads this native addon automatically and falls back +to [`@mdscript/mds-wasm`](https://www.npmjs.com/package/@mdscript/mds-wasm) when a +prebuilt binary is unavailable. + +## How it loads + +This host package contains only the loader (`index.js`) and TypeScript types +(`index.d.ts`). The compiled `.node` binaries ship in per-platform packages +declared as `optionalDependencies`, filtered by `os`/`cpu`/`libc`: + +| Platform package | Target | +|------------------|--------| +| `@mdscript/mds-napi-darwin-arm64` | macOS Apple Silicon | +| `@mdscript/mds-napi-darwin-x64` | macOS Intel | +| `@mdscript/mds-napi-linux-x64-gnu` | Linux x64 (glibc) | +| `@mdscript/mds-napi-linux-x64-musl` | Linux x64 (musl) | +| `@mdscript/mds-napi-linux-arm64-gnu` | Linux arm64 (glibc) | +| `@mdscript/mds-napi-linux-arm64-musl` | Linux arm64 (musl) | +| `@mdscript/mds-napi-win32-x64-msvc` | Windows x64 | + +`index.js` selects the matching binary at runtime from `process.platform`, +`process.arch`, and (on Linux) the detected libc. + +## API + +```js +const { compile, check, compileFile, checkFile } = require('@mdscript/mds-napi'); +``` + +See `index.d.ts` for the full typed surface. + +## License + +MIT — see [LICENSE](./LICENSE). diff --git a/crates/mds-napi/index.js b/crates/mds-napi/index.js index d430d7ba..98d7c659 100644 --- a/crates/mds-napi/index.js +++ b/crates/mds-napi/index.js @@ -12,13 +12,13 @@ function isMusl() { function loadBinding() { const errors = []; const triples = { - 'darwin-arm64': ['mds-napi.darwin-arm64.node', '@mdscript/mds-darwin-arm64'], - 'darwin-x64': ['mds-napi.darwin-x64.node', '@mdscript/mds-darwin-x64'], - 'linux-x64-gnu': ['mds-napi.linux-x64-gnu.node', '@mdscript/mds-linux-x64-gnu'], - 'linux-x64-musl': ['mds-napi.linux-x64-musl.node', '@mdscript/mds-linux-x64-musl'], - 'linux-arm64-gnu': ['mds-napi.linux-arm64-gnu.node', '@mdscript/mds-linux-arm64-gnu'], - 'linux-arm64-musl': ['mds-napi.linux-arm64-musl.node', '@mdscript/mds-linux-arm64-musl'], - 'win32-x64': ['mds-napi.win32-x64-msvc.node', '@mdscript/mds-win32-x64-msvc'], + 'darwin-arm64': ['mds-napi.darwin-arm64.node', '@mdscript/mds-napi-darwin-arm64'], + 'darwin-x64': ['mds-napi.darwin-x64.node', '@mdscript/mds-napi-darwin-x64'], + 'linux-x64-gnu': ['mds-napi.linux-x64-gnu.node', '@mdscript/mds-napi-linux-x64-gnu'], + 'linux-x64-musl': ['mds-napi.linux-x64-musl.node', '@mdscript/mds-napi-linux-x64-musl'], + 'linux-arm64-gnu': ['mds-napi.linux-arm64-gnu.node', '@mdscript/mds-napi-linux-arm64-gnu'], + 'linux-arm64-musl': ['mds-napi.linux-arm64-musl.node', '@mdscript/mds-napi-linux-arm64-musl'], + 'win32-x64': ['mds-napi.win32-x64-msvc.node', '@mdscript/mds-napi-win32-x64-msvc'], }; let key; diff --git a/crates/mds-napi/package.json b/crates/mds-napi/package.json index 07a61f24..e77831ae 100644 --- a/crates/mds-napi/package.json +++ b/crates/mds-napi/package.json @@ -1,14 +1,40 @@ { - "name": "mds-napi", + "name": "@mdscript/mds-napi", "version": "0.1.0", - "description": "MDS compiler native Node.js bindings", + "description": "MDS compiler native Node.js bindings (napi-rs)", "main": "index.js", "types": "index.d.ts", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/dean0x/mds.git", + "directory": "crates/mds-napi" + }, + "homepage": "https://github.com/dean0x/mds#readme", + "bugs": "https://github.com/dean0x/mds/issues", + "keywords": ["markdown", "template", "llm", "prompt", "compiler", "mds", "napi"], + "engines": { + "node": ">=22.0.0" + }, "napi": { - "name": "mds-napi", - "triples": {} + "binaryName": "mds-napi", + "packageName": "@mdscript/mds-napi", + "targets": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "x86_64-pc-windows-msvc" + ] }, - "license": "MIT", + "files": [ + "index.js", + "index.d.ts", + "LICENSE", + "README.md" + ], "devDependencies": { "@napi-rs/cli": "^3.0.0" } diff --git a/package-lock.json b/package-lock.json index 99d0c1e2..2de7ee21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,14 @@ } }, "crates/mds-napi": { + "name": "@mdscript/mds-napi", "version": "0.1.0", "license": "MIT", "devDependencies": { "@napi-rs/cli": "^3.0.0" + }, + "engines": { + "node": ">=22.0.0" } }, "crates/mds-napi/node_modules/@emnapi/runtime": { @@ -1435,6 +1439,14 @@ "resolved": "packages/mds", "link": true }, + "node_modules/@mdscript/mds-napi": { + "resolved": "crates/mds-napi", + "link": true + }, + "node_modules/@mdscript/mds-wasm": { + "resolved": "packages/mds-wasm", + "link": true + }, "node_modules/@mdscript/rollup-plugin": { "resolved": "packages/rollup-plugin", "link": true @@ -2416,10 +2428,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/mds-napi": { - "resolved": "crates/mds-napi", - "link": true - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -2989,6 +2997,10 @@ "packages/mds": { "name": "@mdscript/mds", "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@mdscript/mds-wasm": "^0.1.0" + }, "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.4.0" @@ -2997,7 +3009,15 @@ "node": ">=22.0.0" }, "optionalDependencies": { - "mds-napi": "file:../../crates/mds-napi" + "@mdscript/mds-napi": "^0.1.0" + } + }, + "packages/mds-wasm": { + "name": "@mdscript/mds-wasm", + "version": "0.1.0", + "license": "MIT", + "engines": { + "node": ">=22.0.0" } }, "packages/mds/node_modules/@types/node": { diff --git a/packages/bundler-utils/LICENSE b/packages/bundler-utils/LICENSE new file mode 100644 index 00000000..37193c4b --- /dev/null +++ b/packages/bundler-utils/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dean Sharon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/mds-wasm/LICENSE b/packages/mds-wasm/LICENSE new file mode 100644 index 00000000..37193c4b --- /dev/null +++ b/packages/mds-wasm/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dean Sharon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/mds-wasm/README.md b/packages/mds-wasm/README.md new file mode 100644 index 00000000..23e30251 --- /dev/null +++ b/packages/mds-wasm/README.md @@ -0,0 +1,36 @@ +# @mdscript/mds-wasm + +WebAssembly build of the [MDS (Markdown Script)](https://github.com/dean0x/mds) +compiler. + +This package is the portable fallback used by [`@mdscript/mds`](https://www.npmjs.com/package/@mdscript/mds) +when the native addon (`@mdscript/mds-napi`) is unavailable, and it powers the +browser build. **Most users should depend on `@mdscript/mds`, not this package +directly** — `@mdscript/mds` selects the native addon on Node and this WASM build +on the web (or as a Node fallback) automatically. + +## What's inside + +Two builds, selected by package `exports` conditions: + +| Condition | Entry | Module type | Init | +|-----------|-------|-------------|------| +| `node` | `dist/node/mds_wasm.js` | CommonJS (`wasm-pack --target nodejs`) | none | +| `browser` / `default` | `dist/web/mds_wasm.js` | ESM (`wasm-pack --target web`) | call `default()` with the `.wasm` URL | + +Each build exposes `compile(source, options)`, `check(source, options)`, and +`scanImports(source)`. + +## Build + +```bash +npm run build -w @mdscript/mds-wasm +``` + +Requires [`wasm-pack`](https://rustwasm.github.io/wasm-pack/) and the +`wasm32-unknown-unknown` Rust target. Output is written to `dist/node` and +`dist/web`. + +## License + +MIT — see [LICENSE](./LICENSE). diff --git a/packages/mds-wasm/package.json b/packages/mds-wasm/package.json new file mode 100644 index 00000000..32b8bb1f --- /dev/null +++ b/packages/mds-wasm/package.json @@ -0,0 +1,41 @@ +{ + "name": "@mdscript/mds-wasm", + "version": "0.1.0", + "description": "WebAssembly build of the MDS (Markdown Script) compiler — used as the portable fallback by @mdscript/mds", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/dean0x/mds.git", + "directory": "packages/mds-wasm" + }, + "homepage": "https://github.com/dean0x/mds#readme", + "bugs": "https://github.com/dean0x/mds/issues", + "keywords": ["markdown", "template", "llm", "prompt", "compiler", "mds", "wasm", "webassembly"], + "engines": { + "node": ">=22.0.0" + }, + "exports": { + ".": { + "node": { + "types": "./dist/node/mds_wasm.d.ts", + "default": "./dist/node/mds_wasm.js" + }, + "browser": { + "types": "./dist/web/mds_wasm.d.ts", + "default": "./dist/web/mds_wasm.js" + }, + "default": { + "types": "./dist/web/mds_wasm.d.ts", + "default": "./dist/web/mds_wasm.js" + } + } + }, + "files": [ + "dist/", + "LICENSE", + "README.md" + ], + "scripts": { + "build": "wasm-pack build ../../crates/mds-wasm --target nodejs --out-dir ../../packages/mds-wasm/dist/node --out-name mds_wasm && wasm-pack build ../../crates/mds-wasm --target web --out-dir ../../packages/mds-wasm/dist/web --out-name mds_wasm && rm -f dist/node/.gitignore dist/web/.gitignore" + } +} diff --git a/packages/mds/LICENSE b/packages/mds/LICENSE new file mode 100644 index 00000000..37193c4b --- /dev/null +++ b/packages/mds/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dean Sharon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/mds/package.json b/packages/mds/package.json index 55924209..75a041bb 100644 --- a/packages/mds/package.json +++ b/packages/mds/package.json @@ -36,8 +36,11 @@ "test:native": "node --test __test__/native-backend.spec.mjs", "test:perf": "node --test __test__/perf.spec.mjs" }, + "dependencies": { + "@mdscript/mds-wasm": "^0.1.0" + }, "optionalDependencies": { - "mds-napi": "file:../../crates/mds-napi" + "@mdscript/mds-napi": "^0.1.0" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/packages/mds/src/backend/wasm.ts b/packages/mds/src/backend/wasm.ts index 211b9c2b..fa6bcf3e 100644 --- a/packages/mds/src/backend/wasm.ts +++ b/packages/mds/src/backend/wasm.ts @@ -179,13 +179,13 @@ async function _initNode(options?: InitOptions): Promise { const require = createRequire(import.meta.url); const candidates: readonly string[] = [ - // Workspace: pkg is built next to mds-wasm crate + // Workspace dev path: pkg is built next to the mds-wasm crate. Tried first so + // local development and CI use the freshly built artifact without a publish. new URL('../../../../crates/mds-wasm/pkg/mds_wasm.js', import.meta.url).pathname, - // Future npm package path: 'mds-wasm' is not yet published to npm and is - // not listed in package.json dependencies. This candidate is forward-looking - // — when the package is published, it will be resolvable here without code - // changes. Until then, it is skipped silently (MODULE_NOT_FOUND). - 'mds-wasm', + // Published package: resolved for installed consumers via @mdscript/mds's + // dependency on @mdscript/mds-wasm. Skipped silently (MODULE_NOT_FOUND) in dev + // when only the workspace path above is present. + '@mdscript/mds-wasm', ]; let lastError: Error | undefined; @@ -230,7 +230,7 @@ export async function initWasmBrowser(options?: InitOptions): Promise= MAX_BROWSER_RETRIES) { throw new Error( `@mdscript/mds: WASM browser backend failed to initialize after ${MAX_BROWSER_RETRIES} attempts. ` + - `Ensure 'mds-wasm' is bundled or provide a valid wasmUrl option.`, + `Ensure '@mdscript/mds-wasm' is bundled or provide a valid wasmUrl option.`, ); } cachedBrowserPromise = _initBrowser(options).catch((err) => { @@ -249,19 +249,19 @@ export async function initWasmBrowser(options?: InitOptions): Promise { - // Dynamic import — bundler resolves 'mds-wasm' or the caller provides the module. - // In browser environments, the bundler inlines the WASM module at build time. - // TypeScript cannot resolve 'mds-wasm' at compile time (it's a bundler alias). - // The shape is validated with validateWasmShape below. + // Dynamic import — bundler resolves '@mdscript/mds-wasm' or the caller provides + // the module. In browser environments, the bundler inlines the WASM module at + // build time. TypeScript cannot resolve the package's browser export at compile + // time, so the shape is validated with validateWasmShape below. let imported: unknown; try { // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore — 'mds-wasm' is a bundler-resolved module alias, not a npm dependency - imported = await import('mds-wasm'); + // @ts-ignore — '@mdscript/mds-wasm' is resolved by the bundler at build time + imported = await import('@mdscript/mds-wasm'); } catch (err) { throw new Error( `@mdscript/mds: failed to load WASM module in browser environment. ` + - `Ensure 'mds-wasm' is bundled or provide a wasmUrl option. Caused by: ${String(err)}`, + `Ensure '@mdscript/mds-wasm' is bundled or provide a wasmUrl option. Caused by: ${String(err)}`, ); } // validateWasmShape throws a descriptive error naming the missing member — diff --git a/packages/mds/src/node.ts b/packages/mds/src/node.ts index 1e337535..8c3d86fe 100644 --- a/packages/mds/src/node.ts +++ b/packages/mds/src/node.ts @@ -107,7 +107,7 @@ async function loadNativeBackend(): Promise<{ backend: MdsNodeBackend; error: nu try { const { createRequire } = await import('node:module'); const require = createRequire(import.meta.url); - const addon = require('mds-napi') as object; + const addon = require('@mdscript/mds-napi') as object; const { createNativeBackend } = await import('./backend/native.js'); const b = createNativeBackend(addon as Parameters[0]); return { backend: b, error: null }; diff --git a/packages/rollup-plugin/LICENSE b/packages/rollup-plugin/LICENSE new file mode 100644 index 00000000..37193c4b --- /dev/null +++ b/packages/rollup-plugin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dean Sharon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/vite-plugin/LICENSE b/packages/vite-plugin/LICENSE new file mode 100644 index 00000000..37193c4b --- /dev/null +++ b/packages/vite-plugin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dean Sharon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/webpack-loader/LICENSE b/packages/webpack-loader/LICENSE new file mode 100644 index 00000000..37193c4b --- /dev/null +++ b/packages/webpack-loader/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dean Sharon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/verify-napi-names.mjs b/scripts/verify-napi-names.mjs new file mode 100644 index 00000000..4c6af629 --- /dev/null +++ b/scripts/verify-napi-names.mjs @@ -0,0 +1,127 @@ +#!/usr/bin/env node +// A3 name<->loader verification gate (CRITICAL). +// +// The hand-maintained loader crates/mds-napi/index.js hardcodes, for each of the +// 7 supported platforms, the pair [.node filename, platform package name]. +// @napi-rs/cli generates the per-platform npm//package.json packages +// (name + the .node it ships) from the napi config. If those two ever drift, the +// published universal package will fail to load the binary at runtime on the +// affected platform — silently, only for users on that OS/arch. +// +// This gate asserts the generated packages EXACTLY match the loader's strings. +// Run it in CI after `napi create-npm-dirs` (+ `napi artifacts`), before publish. +// +// Usage: node scripts/verify-napi-names.mjs +// Exit 0 = match; exit 1 = mismatch (with a diff) or missing npm/ dir. +'use strict'; + +import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'; +import { join, dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const NAPI_DIR = join(ROOT, 'crates', 'mds-napi'); +const NPM_DIR = join(NAPI_DIR, 'npm'); + +const errors = []; +const fail = (msg) => errors.push(msg); + +// --- 1. Extract the loader's [binary, packageName] pairs from index.js -------- +const indexSrc = readFileSync(join(NAPI_DIR, 'index.js'), 'utf8'); +const pairRe = /\[\s*'([^']+\.node)'\s*,\s*'(@[^']+)'\s*\]/g; +const loaderPairs = new Map(); // packageName -> binaryFilename +for (const m of indexSrc.matchAll(pairRe)) { + loaderPairs.set(m[2], m[1]); +} +if (loaderPairs.size !== 7) { + fail(`index.js: expected 7 platform entries, found ${loaderPairs.size}`); +} + +// Expected os/cpu/libc derived from each package-name suffix. Strict on os/cpu; +// libc asserted only for musl (gnu's libc field varies across napi versions). +function expectedTraits(pkgName) { + const suffix = pkgName.replace(/^@mdscript\/mds-napi-/, ''); + const parts = suffix.split('-'); // e.g. linux-x64-musl, win32-x64-msvc, darwin-arm64 + const os = { darwin: 'darwin', linux: 'linux', win32: 'win32' }[parts[0]]; + const cpu = parts[1]; // x64 | arm64 + const libc = parts[2] === 'musl' ? 'musl' : null; + return { os, cpu, libc }; +} + +// --- 2. Read generated npm/*/package.json ------------------------------------- +if (!existsSync(NPM_DIR)) { + fail( + `crates/mds-napi/npm/ does not exist. Run 'npx napi create-npm-dirs' (and ` + + `'npx napi artifacts') in crates/mds-napi before this gate.`, + ); +} + +const generated = new Map(); // packageName -> { dir, main, files, os, cpu, libc } +if (existsSync(NPM_DIR)) { + for (const entry of readdirSync(NPM_DIR)) { + const dir = join(NPM_DIR, entry); + if (!statSync(dir).isDirectory()) continue; + const pkgPath = join(dir, 'package.json'); + if (!existsSync(pkgPath)) { + fail(`npm/${entry}: missing package.json`); + continue; + } + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); + generated.set(pkg.name, { + dir: entry, + main: pkg.main, + files: pkg.files ?? [], + os: pkg.os ?? [], + cpu: pkg.cpu ?? [], + libc: pkg.libc ?? null, + }); + } +} + +// --- 3. Cross-check: every loader entry has a matching generated package ------ +for (const [pkgName, binary] of loaderPairs) { + const gen = generated.get(pkgName); + if (!gen) { + fail(`loader references "${pkgName}" but no generated npm/* package has that name`); + continue; + } + // The .node filename the loader expects must be what the package actually ships. + if (gen.main !== binary) { + fail(`"${pkgName}": loader expects binary "${binary}" but package main is "${gen.main}"`); + } + if (!gen.files.includes(binary)) { + fail(`"${pkgName}": binary "${binary}" not listed in package files [${gen.files.join(', ')}]`); + } + const want = expectedTraits(pkgName); + if (want.os && !gen.os.includes(want.os)) { + fail(`"${pkgName}": os should include "${want.os}" but is [${gen.os.join(', ')}]`); + } + if (want.cpu && !gen.cpu.includes(want.cpu)) { + fail(`"${pkgName}": cpu should include "${want.cpu}" but is [${gen.cpu.join(', ')}]`); + } + if (want.libc) { + const libcArr = Array.isArray(gen.libc) ? gen.libc : []; + if (!libcArr.includes('musl')) { + fail(`"${pkgName}": libc should include "musl" but is ${JSON.stringify(gen.libc)}`); + } + } +} + +// --- 4. Cross-check: no generated package the loader doesn't know about ------- +for (const pkgName of generated.keys()) { + if (!loaderPairs.has(pkgName)) { + fail(`generated package "${pkgName}" has no matching entry in the index.js loader`); + } +} + +// --- Report ------------------------------------------------------------------- +if (errors.length > 0) { + console.error('✖ napi name<->loader verification FAILED:'); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); +} + +console.log(`✓ napi name<->loader gate: ${loaderPairs.size} platform packages match the loader`); +for (const [pkgName, binary] of loaderPairs) { + console.log(` ${pkgName} -> ${binary}`); +} diff --git a/scripts/verify-versions.mjs b/scripts/verify-versions.mjs new file mode 100644 index 00000000..99fd741d --- /dev/null +++ b/scripts/verify-versions.mjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node +// D1 synchronized-version gate. +// +// One coordinated release means every publishable artifact ships the SAME +// version. This gate asserts: +// 1. Every publishable package.json `version` == the workspace crate version. +// 2. No `file:` specifiers leak into any published dependency set. +// 3. Every internal `@mdscript/*` dependency is a caret range on that version +// (e.g. ^0.1.0), so installed consumers resolve the matching release. +// +// Run locally and in CI before publishing. Exit 0 = consistent; 1 = drift. +'use strict'; + +import { readFileSync, existsSync } from 'node:fs'; +import { join, dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); + +// Publishable npm packages (host napi package + universal + wasm + bundler set). +const PKG_PATHS = [ + 'crates/mds-napi/package.json', + 'packages/mds/package.json', + 'packages/mds-wasm/package.json', + 'packages/bundler-utils/package.json', + 'packages/vite-plugin/package.json', + 'packages/rollup-plugin/package.json', + 'packages/webpack-loader/package.json', +]; + +const DEP_SETS = ['dependencies', 'optionalDependencies', 'peerDependencies']; + +const errors = []; +const fail = (m) => errors.push(m); + +// --- Canonical version: the workspace crate version from root Cargo.toml ------ +const cargo = readFileSync(join(ROOT, 'Cargo.toml'), 'utf8'); +const wsVerMatch = cargo.match(/\[workspace\.package\][\s\S]*?\bversion\s*=\s*"([^"]+)"/); +if (!wsVerMatch) { + fail('Cargo.toml: could not find [workspace.package] version'); +} +const canonical = wsVerMatch ? wsVerMatch[1] : null; +const semverCaret = canonical ? `^${canonical}` : null; + +// --- Check every publishable package ------------------------------------------ +for (const rel of PKG_PATHS) { + const abs = join(ROOT, rel); + if (!existsSync(abs)) { + fail(`${rel}: missing (expected publishable package)`); + continue; + } + const pkg = JSON.parse(readFileSync(abs, 'utf8')); + + if (pkg.version !== canonical) { + fail(`${rel}: version "${pkg.version}" != workspace version "${canonical}"`); + } + + for (const set of DEP_SETS) { + const deps = pkg[set]; + if (!deps) continue; + for (const [name, spec] of Object.entries(deps)) { + if (typeof spec === 'string' && spec.startsWith('file:')) { + fail(`${rel}: ${set}["${name}"] uses a file: specifier ("${spec}")`); + } + if (name.startsWith('@mdscript/') && spec !== semverCaret) { + fail(`${rel}: ${set}["${name}"] is "${spec}", expected "${semverCaret}"`); + } + } + } +} + +if (errors.length > 0) { + console.error('✖ version-consistency gate FAILED:'); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); +} + +console.log(`✓ version gate: ${PKG_PATHS.length} packages + crates all at ${canonical}; no file: refs; internal deps pinned to ${semverCaret}`); From 68dea178df9e984482765e0b95d00250fa3010ce Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 30 May 2026 00:24:25 +0300 Subject: [PATCH 03/12] docs(community): SECURITY, CONTRIBUTING, issue/PR templates, dependabot, README badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase C community health (CODE_OF_CONDUCT deferred to #38 — see note): - SECURITY.md: GitHub private vuln reporting, supported-versions table, the verified resource-limit table (call/loop/output/import/nesting/value/dot caps), and the debug-panics 'never ship enabled' warning - CONTRIBUTING.md: real Rust/WASM/JS gates, MSRV 1.88, Node >=22, conventional commits - .github/ISSUE_TEMPLATE/{bug_report,feature_request,config}.yml (security routed to the advisory page, blank issues disabled) - .github/PULL_REQUEST_TEMPLATE.md (changelog/tests/conventional-commit checklist) - .github/dependabot.yml: cargo + npm + github-actions, weekly - README: CI/crates.io/npm/MIT badges + Contributing/Security sections Refs #38 --- .github/ISSUE_TEMPLATE/bug_report.yml | 60 ++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 +++ .github/ISSUE_TEMPLATE/feature_request.yml | 41 +++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 22 ++++++ .github/dependabot.yml | 24 +++++++ CONTRIBUTING.md | 84 ++++++++++++++++++++++ README.md | 18 +++++ SECURITY.md | 79 ++++++++++++++++++++ 8 files changed, 336 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..320dcb04 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,60 @@ +name: Bug report +description: Report incorrect behavior in the MDS compiler, CLI, bindings, or plugins +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a bug! Please **do not** use this form + for security vulnerabilities — see [SECURITY.md](../security/policy) and use + private vulnerability reporting instead. + - type: dropdown + id: component + attributes: + label: Component + description: Which part of MDS is affected? + options: + - CLI (`mds` binary) + - Library (`mds-core`) + - Native addon (`@mdscript/mds-napi`) + - WASM (`@mdscript/mds-wasm`) + - Universal bindings (`@mdscript/mds`) + - Vite plugin + - Rollup plugin + - Webpack loader + - Other / not sure + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Crate or npm package version (e.g. `mds-cli 0.1.0`, `@mdscript/mds 0.1.0`). + placeholder: "0.1.0" + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear description of the bug, including the actual vs. expected behavior. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Reproduction + description: > + A minimal `.mds` template (and any vars/imports) plus the exact command or + API call that triggers the bug. Minimal, self-contained repros get fixed fastest. + render: markdown + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: OS + arch, Node version (if applicable), Rust version (if building from source). + placeholder: "macOS 14 arm64, Node 22.3, rustc 1.88" + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..92126b0b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Report a security vulnerability + url: https://github.com/dean0x/mds/security/advisories/new + about: Please report security issues privately, not as public issues. See SECURITY.md. + - name: Question or discussion + url: https://github.com/dean0x/mds/discussions + about: For usage questions and open-ended discussion, please use Discussions. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..68e8cd2f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,41 @@ +name: Feature request +description: Suggest a new capability or improvement for MDS +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem / motivation + description: What are you trying to do that MDS doesn't support today? What's the use case? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: > + What should MDS do? If it's language syntax, show an example `.mds` snippet + and the Markdown you'd expect it to compile to. + render: markdown + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches you've thought about, and why they fall short. + validations: + required: false + - type: dropdown + id: component + attributes: + label: Component + options: + - Language / compiler + - CLI + - JS/TS bindings + - Bundler plugins + - Documentation + - Other + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..c604cb35 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ + + +## What does this PR do? + + + +## Related issues + + + +## Checklist + +- [ ] PR title follows [Conventional Commits](https://www.conventionalcommits.org/) +- [ ] Tests added/updated for the change (and assert behavior, not implementation) +- [ ] `CHANGELOG.md` updated under `## [Unreleased]` (for user-facing changes) +- [ ] Rust gates pass: `cargo test --workspace`, `cargo fmt --all --check`, + `cargo clippy --workspace --all-targets -- -D warnings` +- [ ] JS gates pass (if touched): `npm run build --workspaces && npm test --workspaces` +- [ ] No new compiler/linter warnings diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..1d8b2c6f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + commit-message: + prefix: "chore(deps)" + + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + commit-message: + prefix: "chore(deps)" + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + commit-message: + prefix: "chore(ci)" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..945cb0b3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,84 @@ +# Contributing to MDS + +Thanks for your interest in contributing! This document describes the local +workflow and the checks that must pass before a change can be merged. + +## Prerequisites + +- **Rust** — stable toolchain; the workspace MSRV is **1.88** (declared in the + root `Cargo.toml`). The published crates (`mds-core`, `mds-cli`) must compile on + 1.88. +- **Node.js** — **≥ 22** (see `engines` in the package manifests). +- **wasm-pack** + the `wasm32-unknown-unknown` target — for the WASM build/tests. +- **@napi-rs/cli** (installed via `npm ci`) — for the native addon. + +## Repository layout + +| Path | What it is | +|------|------------| +| `crates/mds-core` | The compiler library (published to crates.io as `mds-core`) | +| `crates/mds-cli` | The `mds` binary (published as `mds-cli`) | +| `crates/mds-wasm` | WASM bindings (`wasm-bindgen`) | +| `crates/mds-napi` | Native Node addon (`napi-rs`) — host package `@mdscript/mds-napi` | +| `packages/mds` | Universal JS/TS bindings (`@mdscript/mds`) | +| `packages/mds-wasm` | WASM workspace wrapper (`@mdscript/mds-wasm`) | +| `packages/{vite,rollup}-plugin`, `packages/webpack-loader`, `packages/bundler-utils` | Bundler integrations | +| `examples/` | Runnable templates and integration apps | + +## Quality gates + +All of the following must pass locally and in CI before merge. + +### Rust + +```bash +cargo test --workspace +cargo fmt --all --check +cargo clippy --workspace --all-targets -- -D warnings +cargo check -p mds-core -p mds-cli # on the 1.88 toolchain (MSRV) +``` + +Clippy warnings are treated as errors — keep the build warning-free. + +### WASM + +```bash +wasm-pack test --node crates/mds-wasm +``` + +### JavaScript / TypeScript + +```bash +npm ci +npm run build --workspaces --if-present +npm test --workspaces --if-present +``` + +Backend parity matters — when touching the JS bindings, run the `@mdscript/mds` +suite under both backends: + +```bash +MDS_BACKEND=native npm test -w @mdscript/mds +MDS_BACKEND=wasm npm test -w @mdscript/mds +``` + +## Pull requests + +- **Conventional Commits** — PR titles and commits follow + [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, + `refactor:`, `chore:`, `docs:`, …). +- **Update the CHANGELOG** — add user-facing changes under `## [Unreleased]` in + `CHANGELOG.md`. +- **Tests** — add or update tests for behavior changes; assert outcomes, not + implementation details. +- **No regressions** — every existing test must still pass. + +## Security + +Please report vulnerabilities privately — see [SECURITY.md](./SECURITY.md). Do not +open public issues for security problems. + +## Code of Conduct + +This project follows the [Contributor Covenant](./CODE_OF_CONDUCT.md). By +participating, you agree to uphold it. diff --git a/README.md b/README.md index 3ebc42a0..f6340236 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # MDS — Markdown Script +[![CI](https://github.com/dean0x/mds/actions/workflows/ci.yml/badge.svg)](https://github.com/dean0x/mds/actions/workflows/ci.yml) +[![crates.io](https://img.shields.io/crates/v/mds-cli.svg)](https://crates.io/crates/mds-cli) +[![npm](https://img.shields.io/npm/v/@mdscript/mds.svg)](https://www.npmjs.com/package/@mdscript/mds) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) + MDS is a template language for composable LLM prompt engineering. Write prompts with variables, loops, conditionals, functions, and imports — then compile them to clean Markdown. ## Quick Start @@ -124,6 +129,19 @@ live in [`examples/`](examples/). See [spec.md](spec.md) for the full MDS v0.1 language specification. +## Contributing + +Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for the local +workflow and quality gates, and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for +community expectations. + +## Security + +Please report vulnerabilities privately via GitHub's +[private vulnerability reporting](https://github.com/dean0x/mds/security/advisories/new), +not public issues. See [SECURITY.md](SECURITY.md) for the security model, built-in +resource limits, and supported versions. + ## License MIT — see [LICENSE](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..a7573f94 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,79 @@ +# Security Policy + +## Reporting a vulnerability + +**Please do not report security vulnerabilities through public GitHub issues, +discussions, or pull requests.** + +Report vulnerabilities privately through GitHub's +[private vulnerability reporting](https://github.com/dean0x/mds/security/advisories/new). +This routes the report to the maintainers privately and lets us collaborate on a +fix and coordinated disclosure. + +Please include, where possible: + +- A description of the issue and its impact +- The affected component (CLI, `mds-core`, WASM, native addon, a bundler plugin) +- Steps to reproduce, or a minimal `.mds` template / input that triggers it +- The version (crate or npm package) and your platform + +We aim to acknowledge reports within a few days and will keep you updated as we +investigate. + +## Supported versions + +MDS is pre-1.0. Security fixes are applied to the latest released minor series +only; please upgrade to the newest release before reporting. + +| Version | Supported | +|---------|-----------| +| 0.1.x | ✅ | +| < 0.1.0 | ❌ | + +## Security model & built-in controls + +MDS treats template sources, imported modules, and runtime variables as untrusted +input. The compiler enforces several defense-in-depth controls: + +### Filesystem boundary (`crates/mds-core/src/fs.rs`, `resolver.rs`) + +- **Path-traversal prevention** — import paths and the `output_dir` config value + are rejected if they escape the project root (`..` traversal). +- **Symlink rejection** — symlinked import paths are refused. Resolution is + written to be TOCTOU-safe (the resolved target is validated, not just the + pre-resolution path). +- **Null-byte rejection** — paths containing NUL bytes are rejected at the API + boundary rather than being passed to the OS. +- **Non-UTF-8 paths** are rejected at the public API boundary with an explicit + error instead of producing corrupted output. + +### Resource limits + +| Limit | Value | Location | +|-------|-------|----------| +| Max file size | 10 MB per source file | `resolver.rs` (`MAX_FILE_SIZE`) | +| Max `mds.json` size | 1 MB | `mds-cli/src/main.rs` (`MAX_CONFIG_SIZE`) | +| Max call depth | 128 | `evaluator.rs` (`MAX_CALL_DEPTH`) | +| Max iterations per loop | 100,000 | `evaluator.rs` (`MAX_LOOP_ITERATIONS`) | +| Max total iterations | 1,000,000 | `evaluator.rs` (`MAX_TOTAL_ITERATIONS`) | +| Max output size | 50 MB | `evaluator.rs` (`MAX_OUTPUT_SIZE`) | +| Max warnings | 1,000 | `evaluator.rs` (`MAX_WARNINGS`) | +| Max import depth | 64 | `resolver.rs` (`MAX_IMPORT_DEPTH`) | +| Max block nesting depth | 64 | `parser.rs` (`MAX_NESTING_DEPTH`) | +| Max value (YAML/JSON) nesting depth | 64 | `value.rs` (`MAX_VALUE_DEPTH`) | +| Max dot-path segments | 32 | `limits.rs` (`MAX_DOT_SEGMENTS`) | + +These guard against adversarial input causing stack overflow, unbounded memory +growth, or non-termination. + +## ⚠️ The `debug-panics` feature must never ship enabled + +`mds-core`, `mds-wasm`, and `mds-napi` expose an off-by-default `debug-panics` +Cargo feature. It surfaces the raw Rust panic payload (as `err.detail` on +`mds::internal` errors) to help diagnose unexpected panics during local +development. + +**Never enable `debug-panics` in a published or production build.** Panic +messages can contain absolute filesystem paths and other internal details that +should not be exposed to template authors or end users. All release builds and +published artifacts are built with the feature disabled. From d23f2dc9f104bde41599a229825a92b490801384 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 30 May 2026 00:26:08 +0300 Subject: [PATCH 04/12] docs(release): RELEASING.md runbook + CHANGELOG back to [Unreleased] Phase D release-engineering hardening (no tag/publish): - CHANGELOG: [0.1.0] -> [Unreleased] (drop hardcoded date; stamped at tag time) - RELEASING.md: ordered runbook (version-gate -> build-napi -> stage+verify A3 gate -> crates -> npm w/ provenance -> GitHub release), one-time maintainer prerequisites (npm org, OIDC/NPM_TOKEN, CARGO_REGISTRY_TOKEN, GitHub PVR, #38), local dry-run commands, and the A3 name-gate hard checkpoint - Note: mds-cli dry-run requires mds-core on the index first (expected) D1 version gate, D2 dry-run dispatch, D3 provenance/OIDC landed with the workflow. --- CHANGELOG.md | 7 +-- RELEASING.md | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 RELEASING.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ebde2745..f5b3a52e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.1.0] — 2026-05-29 +## [Unreleased] -Initial public release of the MDS (Markdown Script) compiler. +First public release of the MDS (Markdown Script) compiler. The release step +stamps the version number and date at tag time. ### Language features @@ -90,4 +91,4 @@ Initial public release of the MDS (Markdown Script) compiler. - 292 Rust tests (integration, unit, and doc-tests) plus the JavaScript package suites -[0.1.0]: https://github.com/dean0x/mds/releases/tag/v0.1.0 +[Unreleased]: https://github.com/dean0x/mds/commits/main diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..3ae51d66 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,124 @@ +# Releasing MDS + +MDS ships as a **single coordinated release**: both crates and all npm packages go +out together at the same version. This document is the ordered runbook. + +> The release is **deliberately a manual, triggered step.** Pushing a `v*` tag is +> what starts it. Until then, nothing publishes. + +## Versions that must match + +The [version-consistency gate](scripts/verify-versions.mjs) (run in CI and locally) +asserts these are all equal before anything publishes: + +- Workspace crate version — `Cargo.toml` `[workspace.package] version` (covers + `mds-core`, `mds-cli`, `mds-wasm`, `mds-napi`) +- Every publishable `package.json`: `@mdscript/mds-napi`, `@mdscript/mds`, + `@mdscript/mds-wasm`, `@mdscript/bundler-utils`, `@mdscript/vite-plugin`, + `@mdscript/rollup-plugin`, `@mdscript/webpack-loader` +- All internal `@mdscript/*` dependency ranges are `^` (no `file:`) + +## One-time prerequisites (maintainer / repo owner) + +These are **not** automated and must be done before the first release: + +1. **Register the `@mdscript` npm organization** (or scope) so the scoped packages + can be published. +2. **Configure npm publish auth** — either: + - npm **trusted publisher / OIDC** for this repo's `release.yml` (preferred; no + long-lived token), or + - add an `NPM_TOKEN` repo secret with publish rights to `@mdscript/*`. + Provenance requires the `id-token: write` permission (already set on the + publish job) plus publishing from GitHub Actions. +3. **Add the `CARGO_REGISTRY_TOKEN` repo secret** with publish rights to + `mds-core` and `mds-cli` on crates.io. +4. **Enable GitHub private vulnerability reporting** (Settings → Code security → + Private vulnerability reporting) so the SECURITY.md flow works. +5. Add **`CODE_OF_CONDUCT.md`** (tracked in #38) if not already present. + +## Pre-flight (before tagging) + +Run the local dry-runs and gates: + +```bash +# Rust +cargo test --workspace +cargo fmt --all --check +cargo clippy --workspace --all-targets -- -D warnings +cargo publish -p mds-core --dry-run +# NOTE: `cargo publish -p mds-cli --dry-run` fails locally with +# "no matching package named `mds-core` found" until mds-core is on crates.io — +# mds-cli has a path+version dep on it. This is expected; the release workflow +# publishes mds-core first (and waits for the index), then mds-cli. + +# JS +npm ci +npm run build -w @mdscript/mds-wasm +npm run build --workspaces --if-present +npm test --workspaces --if-present +node scripts/verify-versions.mjs + +# Packaging spot-check (inspect tarball contents) +npm pack -w @mdscript/mds --dry-run +npm pack -w @mdscript/mds-wasm --dry-run +npm pack -w @mdscript/mds-napi --dry-run +``` + +Then validate the **risky cross-compile + platform packaging** without publishing, +via the dry-run workflow: + +```bash +gh workflow run release.yml # workflow_dispatch — builds the 7-target + # napi matrix, stages platform packages, + # runs the A3 name<->loader gate, uploads + # artifacts. Publishes NOTHING. +``` + +Confirm the **A3 name-gate** step (`scripts/verify-napi-names.mjs`) passes in that +run. **This is a hard checkpoint** — if the generated platform package names or +their `.node` filenames drift from the hand-written `crates/mds-napi/index.js` +loader, the published universal package will fail to load the native binary at +runtime on the affected platform. Do not proceed past a failing gate. + +## Release (ordered) + +1. **Stamp the CHANGELOG.** Replace `## [Unreleased]` with `## [X.Y.Z] — ` + and update the link reference at the bottom. Commit on the release branch and + merge to `main`. +2. **Tag and push:** + ```bash + git checkout main && git pull + git tag vX.Y.Z + git push origin vX.Y.Z + ``` +3. The `release.yml` workflow then runs, in order: + 1. **version-gate** — synchronized-version check (fails fast). + 2. **build-napi** — cross-compiles the addon for all 7 targets. + 3. **stage-and-verify-napi** — `napi create-npm-dirs` + `artifacts`, copies + LICENSE into each platform dir, runs the **A3 name-gate**. + 4. **publish-crates** — `cargo publish` `mds-core`, wait for the index, then + `mds-cli`. + 5. **publish-npm** — regenerate `index.d.ts`, re-run the A3 gate, then publish + (with provenance): the **platform packages** (`napi prepublish`), the + **host** `@mdscript/mds-napi`, **`@mdscript/mds-wasm`**, the **universal** + `@mdscript/mds`, and the **bundler** packages. + 6. **github-release** — `gh release create` with generated notes. + +## Post-release + +- Verify each package on its registry (crates.io, npmjs.com) and that npm shows + the **provenance** attestation. +- Smoke test a clean install on a fresh machine/container: + `npm i @mdscript/mds` then `node -e "import('@mdscript/mds').then(m=>m.init())"`. +- Open a fresh `## [Unreleased]` section in `CHANGELOG.md`. + +## Notes + +- The 7 native targets: `aarch64-apple-darwin`, `x86_64-apple-darwin`, + `x86_64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`, + `aarch64-unknown-linux-gnu`, `aarch64-unknown-linux-musl`, + `x86_64-pc-windows-msvc`. Linux musl/arm builds use napi's `--use-napi-cross`. +- `wasm-opt` is currently disabled (`crates/mds-wasm/Cargo.toml`); re-enable once + CI provides Binaryen to recover ~10–20% wasm size. +- Platform packages are generated **in CI only** — they cannot be validated with a + local `npm pack`; use the dry-run workflow above instead. From 604e66acb753cde4815d5a9dbfc6fbdeeb4f35f6 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 30 May 2026 23:38:23 +0300 Subject: [PATCH 05/12] test(mds): load native addon via loader, not hardcoded bare .node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit native-backend.spec.mjs required crates/mds-napi/mds-napi.node directly. CI builds the addon with `napi build --platform`, which names it mds-napi..node, so the bare path was MODULE_NOT_FOUND on every CI OS (it only worked locally where a stale bare build happened to exist). Load through crates/mds-napi/index.js — the same loader production uses — which resolves bare-or-suffixed names and returns the identical raw addon. Reproduced the CI failure locally (suffixed-only) and verified the fix: 6/6 pass. --- packages/mds/__test__/native-backend.spec.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/mds/__test__/native-backend.spec.mjs b/packages/mds/__test__/native-backend.spec.mjs index e4218b41..926fbd2f 100644 --- a/packages/mds/__test__/native-backend.spec.mjs +++ b/packages/mds/__test__/native-backend.spec.mjs @@ -15,7 +15,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const FIXTURES = path.join(__dirname, 'fixtures'); -const napiAddon = require(path.join(__dirname, '../../..', 'crates/mds-napi/mds-napi.node')); +// Load the native addon through its loader (crates/mds-napi/index.js), which +// resolves the correct binary for the host. With `napi build --platform` (as CI +// builds it) the file is named mds-napi..node, so requiring a bare +// mds-napi.node is not portable across build flags. The loader returns the same +// raw addon exports either way. +const napiAddon = require(path.join(__dirname, '../../..', 'crates/mds-napi/index.js')); const nativeBackend = createNativeBackend(napiAddon); describe('native backend', () => { From aeea26ea26dfa0e79b7012559d5637a9e2c6567f Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 30 May 2026 23:39:29 +0300 Subject: [PATCH 06/12] docs(changelog): correct Rust test count (292 -> 590) Evaluator flagged the stale count; cargo test --workspace reports 590. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5b3a52e..d58ff223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,6 @@ stamps the version number and date at tag time. ### Tests -- 292 Rust tests (integration, unit, and doc-tests) plus the JavaScript package suites +- 590 Rust tests (integration, unit, and doc-tests across the workspace) plus the JavaScript package suites [Unreleased]: https://github.com/dean0x/mds/commits/main From b3c78c30dbd842e322a5727f6ad1cdfc9859d403 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 30 May 2026 23:46:54 +0300 Subject: [PATCH 07/12] fix(ci): wasm-pack manifest parse on macOS/windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'Build WASM (nodejs)' CI step failed on macOS/windows with 'invalid type: map, expected a string for key package.license' — those runners get an older wasm-pack (ubuntu has a newer one preinstalled) that doesn't resolve Cargo workspace inheritance for npm-metadata fields. Fix both ways: - crates/mds-wasm/Cargo.toml: make license/repository/keywords explicit literals (mds-wasm isn't published to crates.io, so inheritance buys nothing) - pin jetli/wasm-pack-action to version: latest on all 3 usages (ci x2, release) Verified: cargo check + wrapper build OK, pkg package.json license=MIT. --- .github/workflows/ci.yml | 4 ++++ .github/workflows/release.yml | 2 ++ crates/mds-wasm/Cargo.toml | 11 +++++++---- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76611698..ecc84a04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,8 @@ jobs: targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: latest - name: Build (nodejs + web targets) run: | wasm-pack build crates/mds-wasm --target nodejs --out-dir pkg @@ -87,6 +89,8 @@ jobs: run: npx napi build --platform --release --no-js # Build the WASM pkg so the WASM fallback (and wasm-backend tests) work. - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: latest - name: Build WASM (nodejs) run: wasm-pack build crates/mds-wasm --target nodejs --out-dir pkg - name: Build TS packages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ef3876b..08cf95ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -185,6 +185,8 @@ jobs: with: { targets: wasm32-unknown-unknown } - uses: Swatinem/rust-cache@v2 - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: latest - run: npm ci - name: Restore staged napi tree (.node + npm/ platform dirs) uses: actions/download-artifact@v4 diff --git a/crates/mds-wasm/Cargo.toml b/crates/mds-wasm/Cargo.toml index 1da5b4e3..8ff1cf5b 100644 --- a/crates/mds-wasm/Cargo.toml +++ b/crates/mds-wasm/Cargo.toml @@ -4,10 +4,13 @@ version.workspace = true edition.workspace = true rust-version.workspace = true description = "MDS compiler WebAssembly bindings" -license.workspace = true -readme.workspace = true -repository.workspace = true -keywords.workspace = true +# Explicit (not workspace-inherited) on purpose: older wasm-pack manifest parsers +# read these npm-metadata fields without resolving Cargo workspace inheritance and +# choke on `license.workspace = true` ("invalid type: map, expected a string"). +# mds-wasm is not published to crates.io, so DRY inheritance buys nothing here. +license = "MIT" +repository = "https://github.com/dean0x/mds" +keywords = ["markdown", "template", "llm", "prompt", "compiler"] categories = ["wasm", "template-engine"] [features] From 95d72dec998cc6bf5efcd2f821c02766382fa1ba Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 30 May 2026 23:51:00 +0300 Subject: [PATCH 08/12] =?UTF-8?q?fix(ci):=20cross-platform=20absolute-path?= =?UTF-8?q?=20check=20in=20bundler-utils=20test=20The=20'compiles=20file?= =?UTF-8?q?=20with=20imports'=20integration=20test=20asserted=20dep.starts?= =?UTF-8?q?With('/'),=20a=20POSIX-only=20absolute-path=20check=20that=20fa?= =?UTF-8?q?ils=20on=20Windows=20where=20absolute=20paths=20are=20'D:\?= =?UTF-8?q?=E2=80=A6'=20or=20'\\=3F\D:\=E2=80=A6'.=20Use=20node:path=20isA?= =?UTF-8?q?bsolute()=20instead.=20This=20was=20the=20sole=20remaining=20JS?= =?UTF-8?q?=20test=20failure=20on=20windows-latest=20(macOS=20was=20fixed?= =?UTF-8?q?=20by=20b3c78c3's=20wasm-pack=20manifest=20change;=20ubuntu=20w?= =?UTF-8?q?as=20already=20green).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/bundler-utils/__test__/integration.spec.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/bundler-utils/__test__/integration.spec.mjs b/packages/bundler-utils/__test__/integration.spec.mjs index bc3c4944..9fa9b8bd 100644 --- a/packages/bundler-utils/__test__/integration.spec.mjs +++ b/packages/bundler-utils/__test__/integration.spec.mjs @@ -3,7 +3,7 @@ */ import { test, describe, before } from 'node:test'; import assert from 'node:assert/strict'; -import { resolve, dirname, join } from 'node:path'; +import { resolve, dirname, join, isAbsolute } from 'node:path'; import { fileURLToPath } from 'node:url'; import { writeFileSync, unlinkSync, mkdirSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; @@ -41,9 +41,10 @@ describe('bundler-utils integration', () => { assert.ok(result.code.includes('export default'), 'should have default export'); assert.ok(result.dependencies.length >= 1, 'should have at least one dependency'); - // The dependency should be an absolute path + // The dependency should be an absolute path (cross-platform: POSIX `/…`, + // Windows `D:\…` or `\\?\D:\…`). for (const dep of result.dependencies) { - assert.ok(dep.startsWith('/'), `dependency should be absolute path: ${dep}`); + assert.ok(isAbsolute(dep), `dependency should be absolute path: ${dep}`); } }); From 6f1cf6b75cebca3404a097764a34f41eea620fd6 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sat, 30 May 2026 23:56:28 +0300 Subject: [PATCH 09/12] =?UTF-8?q?fix(ci):=20release.yml=20YAML=20parse=20e?= =?UTF-8?q?rrors=20blocking=20workflow=20startup=20The=20Release=20workflo?= =?UTF-8?q?w=20has=20never=20started=20successfully=20=E2=80=94=20GitHub?= =?UTF-8?q?=20displayed=20it=20by=20file=20path=20(not=20its=20'Release'?= =?UTF-8?q?=20name)=20and=20produced=20an=20instant=20startup=5Ffailure=20?= =?UTF-8?q?run=20on=20every=20push,=20which=20means=20a=20real=20'v0.1.0'?= =?UTF-8?q?=20tag=20push=20would=20fail=20to=20launch=20the=20release=20at?= =?UTF-8?q?=20all.=20Two=20YAML=20syntax=20errors:=20-=20step=20name=20'As?= =?UTF-8?q?sert=20synchronized=20versions,=20no=20file:=20refs'=20?= =?UTF-8?q?=E2=80=94=20the=20unquoted=20':=20'=20parses=20as=20a=20nested?= =?UTF-8?q?=20mapping.=20Quoted=20the=20value.=20-=20'with:=20{=20targets:?= =?UTF-8?q?=20${{=20matrix.settings.target=20}}=20}'=20=E2=80=94=20the=20e?= =?UTF-8?q?xpression's=20'}}'=20collides=20with=20the=20flow-mapping=20'}'?= =?UTF-8?q?.=20Converted=20to=20block=20style.=20Verified=20with=20actionl?= =?UTF-8?q?int=201.7.7:=20both=20workflows=20now=20lint=20clean=20(exit=20?= =?UTF-8?q?0).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08cf95ec..2899dfaa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 22 } - - name: Assert synchronized versions, no file: refs + - name: "Assert synchronized versions, no file: refs" run: node scripts/verify-versions.mjs # --------------------------------------------------------------------------- @@ -75,7 +75,8 @@ jobs: - uses: actions/setup-node@v4 with: { node-version: 22, cache: npm } - uses: dtolnay/rust-toolchain@stable - with: { targets: ${{ matrix.settings.target }} } + with: + targets: ${{ matrix.settings.target }} - uses: Swatinem/rust-cache@v2 - run: npm ci - name: Build addon From 1a5f0b8581d7712fe635b23811a7d61eb91baa3d Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 31 May 2026 00:03:50 +0300 Subject: [PATCH 10/12] fix(mds): cross-platform path containment in module scanner (Windows) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The module scanner rejected every compile on Windows with 'security: path escapes project root' because the containment guards used POSIX-only string prefixing — `absolutePath.startsWith(projectRoot + '/')` never matches a backslash-separated Windows path like 'D:\\a\\mds\\mds\\...'. - Add isWithinRoot() using path.relative (separator/drive/case aware); replace the two startsWith(projectRoot + '/') guards. - Filesystem-root guard: dirname(root) === root instead of root === '/'. - entryFilename: normalize separators to '/' so the entry virtual key matches the slash-based keys built by normalizeVirtualKey (mirrors Rust VirtualFs). - realpath symlink fallback: compare case-insensitively on win32 (realpath can return a different drive-letter case than resolve()). Verified on macOS: all 224 JS workspace tests pass (mds 122, bundler-utils 64, rollup 10, vite 14, webpack 14). --- packages/mds/src/util/module-scanner.ts | 40 ++++++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/mds/src/util/module-scanner.ts b/packages/mds/src/util/module-scanner.ts index d233ca40..0c5011fa 100644 --- a/packages/mds/src/util/module-scanner.ts +++ b/packages/mds/src/util/module-scanner.ts @@ -1,6 +1,6 @@ import { open, realpath } from 'node:fs/promises'; import { constants, existsSync } from 'node:fs'; -import { resolve, dirname, relative } from 'node:path'; +import { resolve, dirname, relative, isAbsolute, sep } from 'node:path'; // O_NOFOLLOW prevents the kernel from following a symlink at the final path // component. Using it closes the TOCTOU window between lstat and open. @@ -86,6 +86,21 @@ function _findProjectRootUncached(start: string): string { return start; } +/** + * Cross-platform check that `candidate` is the project root itself or nested + * within it. Uses `path.relative` rather than string prefix matching so it is + * correct on Windows (backslash separators, drive letters, case-insensitive + * filesystem) as well as POSIX: a candidate outside the root yields a relative + * path that is either absolute or begins with `..`. + */ +function isWithinRoot(root: string, candidate: string): boolean { + if (candidate === root) { + return true; + } + const rel = relative(root, candidate); + return rel.length > 0 && rel !== '..' && !rel.startsWith('..' + sep) && !isAbsolute(rel); +} + /** * Open a file descriptor with O_NOFOLLOW | O_RDONLY, translating the ELOOP / * ENOTDIR errors that the kernel emits when the path is a symlink into a clear @@ -210,11 +225,15 @@ export async function buildModulesMap( const absoluteEntry = resolve(entryPath); const projectRoot = findProjectRoot(dirname(absoluteEntry)); - const entryFilename = relative(projectRoot, absoluteEntry); + // Virtual keys are always slash-separated to mirror Rust's VirtualFs; on + // Windows `relative` yields backslashes, so normalize to '/'. + const entryFilename = relative(projectRoot, absoluteEntry).split(sep).join('/'); // Security: entry file must not be at filesystem root — that would disable the - // path traversal guard (projectRoot === '/' makes startsWith checks meaningless). - if (projectRoot === '/' || projectRoot === '') { + // path traversal guard (a root project dir makes containment checks meaningless). + // `dirname(root) === root` is true exactly at a filesystem root on every + // platform ('/', 'C:\\', '\\\\server\\share\\'). + if (projectRoot === '' || dirname(projectRoot) === projectRoot) { throw new Error('security: project root cannot be filesystem root'); } @@ -238,7 +257,7 @@ export async function buildModulesMap( const childAbsolute = resolve(absoluteDir, importPath); // Security: verify child is within project root. - if (!childAbsolute.startsWith(projectRoot + '/') && childAbsolute !== projectRoot) { + if (!isWithinRoot(projectRoot, childAbsolute)) { throw new Error( `security: import path escapes project root: ${childAbsolute} is outside ${projectRoot}`, ); @@ -265,7 +284,7 @@ export async function buildModulesMap( absolutePath: string, ): Promise<{ handle: Awaited>; size: number }> { // Security: verify path is within project root before opening. - if (!absolutePath.startsWith(projectRoot + '/') && absolutePath !== projectRoot) { + if (!isWithinRoot(projectRoot, absolutePath)) { throw new Error( `security: path escapes project root: ${absolutePath} is outside ${projectRoot}`, ); @@ -291,8 +310,13 @@ export async function buildModulesMap( // On platforms where O_NOFOLLOW=0 (e.g. Windows), the open() above did // not prevent symlink traversal. A post-open realpath comparison catches - // a symlink that was in place at open time. - if (resolved !== absolutePath) { + // a symlink that was in place at open time. Windows' filesystem is + // case-insensitive and `realpath` may return a different drive-letter + // case than `resolve` produced, so compare case-insensitively there. + const realpathMismatch = process.platform === 'win32' + ? resolved.toLowerCase() !== absolutePath.toLowerCase() + : resolved !== absolutePath; + if (realpathMismatch) { throw new Error( `security: path ${absolutePath} resolved to unexpected location ${resolved} — possible symlink`, ); From 945b0d5f874ef19b50c5b14855141d133d606967 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 31 May 2026 00:05:32 +0300 Subject: [PATCH 11/12] =?UTF-8?q?fix(mds):=20use=20fileURLToPath=20for=20s?= =?UTF-8?q?ubprocess=20cwd=20in=20perf=20test=20(Windows)=20U-PF0=20set=20?= =?UTF-8?q?the=20subprocess=20cwd=20to=20`new=20URL('.',=20import.meta.url?= =?UTF-8?q?).pathname`,=20which=20on=20Windows=20is=20'/D:/a/mds/...'=20?= =?UTF-8?q?=E2=80=94=20a=20non-existent=20path,=20so=20execFileSync=20fail?= =?UTF-8?q?ed=20with=20'spawnSync=20node.exe=20ENOENT'.=20Use=20=5F=5Fdirn?= =?UTF-8?q?ame=20(fileURLToPath),=20matching=20backend.spec.mjs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/mds/__test__/perf.spec.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/mds/__test__/perf.spec.mjs b/packages/mds/__test__/perf.spec.mjs index 7b11abf7..80a176bf 100644 --- a/packages/mds/__test__/perf.spec.mjs +++ b/packages/mds/__test__/perf.spec.mjs @@ -27,7 +27,9 @@ describe('performance', () => { const start = Date.now(); execFileSync(process.execPath, ['--input-type=module'], { input: `import '../dist/node.js';\nconsole.log('done');\n`, - cwd: new URL('.', import.meta.url).pathname, + // fileURLToPath, not URL.pathname: the latter yields '/D:/...' on Windows, + // which is not a valid cwd and makes spawnSync fail with ENOENT. + cwd: __dirname, encoding: 'utf8', }); const elapsed = Date.now() - start; From 88217d4428f37ce12e493bce7eb94e1cb907a9f1 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Sun, 31 May 2026 00:13:00 +0300 Subject: [PATCH 12/12] =?UTF-8?q?fix(ci):=20cross-platform=20test=20script?= =?UTF-8?q?s=20for=20bundler=20plugins=20(Windows)=20The=20rollup/vite/web?= =?UTF-8?q?pack-loader=20test=20scripts=20used=20a=20POSIX-only=20inline?= =?UTF-8?q?=20env=20assignment,=20'NODE=5FENV=3Dtest=20node=20--test=20...?= =?UTF-8?q?',=20which=20cmd.exe=20on=20Windows=20treats=20as=20a=20bogus?= =?UTF-8?q?=20command=20and=20fails=20before=20any=20test=20runs.=20rollup?= =?UTF-8?q?/vite=20gate=20=5FsetTransformerForTesting=20behind=20NODE=5FEN?= =?UTF-8?q?V=3Dtest,=20so=20set=20it=20inside=20those=20two=20specs=20inst?= =?UTF-8?q?ead=20and=20drop=20the=20prefix=20from=20all=20three=20scripts?= =?UTF-8?q?=20(webpack's=20setter=20is=20ungated).=20No=20new=20dependency?= =?UTF-8?q?=20=E2=80=94=20keeps=20the=20zero-dependency=20root=20intact.?= =?UTF-8?q?=20Verified=20locally:=20rollup=2010,=20vite=2014,=20webpack=20?= =?UTF-8?q?14=20all=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/rollup-plugin/__test__/plugin.spec.mjs | 5 +++++ packages/rollup-plugin/package.json | 2 +- packages/vite-plugin/__test__/plugin.spec.mjs | 5 +++++ packages/vite-plugin/package.json | 2 +- packages/webpack-loader/package.json | 2 +- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/rollup-plugin/__test__/plugin.spec.mjs b/packages/rollup-plugin/__test__/plugin.spec.mjs index 7ee159d2..0d85be62 100644 --- a/packages/rollup-plugin/__test__/plugin.spec.mjs +++ b/packages/rollup-plugin/__test__/plugin.spec.mjs @@ -7,6 +7,11 @@ import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import mdsPlugin, { _setTransformerForTesting } from '../dist/index.js'; +// _setTransformerForTesting is gated behind NODE_ENV=test. Set it here so the +// test script stays a plain cross-platform `node --test` (a `NODE_ENV=test` +// prefix is POSIX-only and fails under Windows cmd.exe). +process.env.NODE_ENV = 'test'; + const __dirname = dirname(fileURLToPath(import.meta.url)); const SIMPLE_MDS = resolve(__dirname, '../../mds/__test__/fixtures/simple.mds'); const CONSUMER_MDS = resolve(__dirname, '../../mds/__test__/fixtures/import_consumer.mds'); diff --git a/packages/rollup-plugin/package.json b/packages/rollup-plugin/package.json index 9dafd3e6..67e523e9 100644 --- a/packages/rollup-plugin/package.json +++ b/packages/rollup-plugin/package.json @@ -26,7 +26,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "test": "NODE_ENV=test node --test __test__/*.spec.mjs" + "test": "node --test __test__/*.spec.mjs" }, "dependencies": { "@mdscript/bundler-utils": "^0.1.0" diff --git a/packages/vite-plugin/__test__/plugin.spec.mjs b/packages/vite-plugin/__test__/plugin.spec.mjs index 5fd1410e..1b10121e 100644 --- a/packages/vite-plugin/__test__/plugin.spec.mjs +++ b/packages/vite-plugin/__test__/plugin.spec.mjs @@ -7,6 +7,11 @@ import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import mdsPlugin, { _setTransformerForTesting } from '../dist/index.js'; +// _setTransformerForTesting is gated behind NODE_ENV=test. Set it here so the +// test script stays a plain cross-platform `node --test` (a `NODE_ENV=test` +// prefix is POSIX-only and fails under Windows cmd.exe). +process.env.NODE_ENV = 'test'; + const __dirname = dirname(fileURLToPath(import.meta.url)); const SIMPLE_MDS = resolve(__dirname, '../../mds/__test__/fixtures/simple.mds'); diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 5d5a9438..5f2ef087 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -26,7 +26,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "test": "NODE_ENV=test node --test __test__/*.spec.mjs" + "test": "node --test __test__/*.spec.mjs" }, "dependencies": { "@mdscript/bundler-utils": "^0.1.0" diff --git a/packages/webpack-loader/package.json b/packages/webpack-loader/package.json index 250f75ab..e3ecf943 100644 --- a/packages/webpack-loader/package.json +++ b/packages/webpack-loader/package.json @@ -32,7 +32,7 @@ "clean:cjs": "node -e \"require('fs').rmSync('dist-cjs', { recursive: true, force: true })\"", "prebuild": "npm run clean:cjs", "build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node ../../scripts/write-cjs-package.cjs dist-cjs", - "test": "NODE_ENV=test node --test __test__/*.spec.mjs" + "test": "node --test __test__/*.spec.mjs" }, "dependencies": { "@mdscript/bundler-utils": "^0.1.0"