Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
91cf21d
feat(mds)!: basePath propagation (#180), browser lint exports (#215),…
dean0x Aug 16, 2026
0330597
refactor(mds): simplify option builders and remove throwFileBasePathE…
dean0x Aug 16, 2026
ceb0fca
fix: address self-review issues
dean0x Aug 16, 2026
5cb4734
test(mds): add cross-surface browser vs node lint parity test (AC-P3-12)
dean0x Aug 16, 2026
d1f4a3c
refactor(mds): drop redundant type assertion in compileOpts
dean0x Aug 16, 2026
52f085d
refactor(mds): add NapiFileCheckOpts named type to NapiAddon interface
dean0x Aug 16, 2026
9795723
docs(changelog): correct BREAKING section for basePath rejection on f…
dean0x Aug 16, 2026
7ef8369
docs(mds): fix stale comments referencing deleted helper functions
dean0x Aug 16, 2026
ef6e7b0
docs(mds): fix README browser-section and option-rejection accuracy
dean0x Aug 16, 2026
50b37c9
docs(mds): fix consumer-browser.ts docblock for AC-P3-20
dean0x Aug 16, 2026
891db04
docs(changelog): fix three factual inaccuracies in [Unreleased] section
dean0x Aug 16, 2026
3a9e2be
refactor(mds): add basePath?: never to file-surface types and unify o…
dean0x Aug 16, 2026
225d5a6
fix(mds): remove unused forwardOpts import and update stale comment i…
dean0x Aug 16, 2026
cfd84d5
docs(mds): update stale compileSrcOpt/fileCompileOpt references in U-…
dean0x Aug 16, 2026
2c239f7
fix(mds): restore forwardOpts import erroneously removed in prior commit
dean0x Aug 16, 2026
2d9207f
docs(mds): restore truncated LINT_RULE_NAMES comment in node.ts
dean0x Aug 16, 2026
07e0a64
docs(mds): replace stale per-surface builder names in AC-P3-05 test c…
dean0x Aug 16, 2026
c98815c
refactor(mds): route remaining option-building through forwardOpts fo…
dean0x Aug 16, 2026
8900c5c
fix(mds): derive checkOpts forwarding from METHOD_KEYS.check not chec…
dean0x Aug 16, 2026
f8629e9
docs(changelog): correct false source-compatibility and sync-throw cl…
dean0x Aug 16, 2026
931ab79
docs(mds): fix stale U-C7 comment in compile.spec.mjs referencing del…
dean0x Aug 16, 2026
a25bfdc
fix(mds): address PR3 review findings — defense-in-depth, spread orde…
dean0x Aug 16, 2026
1444b58
test(mds): fix U-OV-27 WASM leg, U-OV-24 oracle, U-OV-34 shape check
dean0x Aug 16, 2026
fe87d31
docs(mds): strip internal tracker IDs from shipped source docblocks
dean0x Aug 16, 2026
843d503
docs(mds): fix broken JSDoc sentence and reduce duplicate checkOpts c…
dean0x Aug 16, 2026
02daefc
fix: address self-review issues
dean0x Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,128 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **`basePath` option is now honored on `compile()`, `check()`, and `lint()` (#180).**
Previously `basePath` was accepted by the unknown-option validator (so no error was
thrown) but was silently discarded before reaching the backend: the forwarding builders
(`compileOpt`/`varsOpt`) never included it in what they passed through. Templates
containing `@import` or `@extends` directives compiled with a string-source call and a
`basePath` option would either fail to resolve their imports (native backend) or fail
silently (WASM backend). The fix adds `basePath` to both `CompileOptions` and
`CheckOptions` and propagates it to the backend for the string-source methods
(`compile`, `check`). `compileFile` and `checkFile` deliberately exclude
`basePath` — the base directory for file operations is derived from the file
path itself (see the BREAKING subsection below).

The WASM backend has no filesystem access and cannot resolve file-relative imports; it
now **rejects** a non-null `basePath` immediately with `mds::invalid_options` instead
of silently ignoring it, so misconfigured callers receive an actionable error rather
than a silent wrong answer. `{basePath: undefined}` is treated as absent on both
backends (`!= null` check; value-is-intent). To use `basePath` with import resolution,
set `MDS_BACKEND=native`.

### Added

- **`lint` and `lintVirtual` are now exported from the browser entry point (#215).**
Previously only `compile` and `check` were available from the WASM/browser surface;
the underlying WASM module already supported linting but the exports were missing from
`browser.ts`. Both functions are available after `init()` and follow the same
unknown-option guard used by `compile`/`check`.

All eight lint types (`LintDiagnostic`, `LintFileOptions`, `LintFileReport`,
`LintOptions`, `LintResult`, `LintRuleName`, `LintSpan`, `RuleSeverity`) and the
`LINT_RULE_NAMES` constant are now exported from the browser entry point as well as
the Node.js entry point.

- **`SourceMapV3` is now exported from the Node.js and browser entry points.**
`MarkdownResult.sourceMap` has always been typed as `SourceMapV3`, but the type was
only re-exported from `index.ts`, which the package `exports` map does not resolve —
so consumers could receive the value but not name its type. Purely additive.

### **BREAKING** — File-method `basePath` rejection, TypeScript option types, and WASM `basePath` rejection (#180, #213)

#### `compileFile` and `checkFile` now reject `basePath` (#180)

`compileFile(path, options?)` and `checkFile(path, options?)` previously accepted a
`basePath` option and silently discarded it — the option passed the unknown-key
validator but was dropped before the backend was reached, so file resolution always
used the directory containing the path. Both functions now **throw synchronously**
(`Error { code: 'mds::invalid_options' }`) when `basePath` is non-null. The base
directory for file-based operations is always derived from the file path itself.

**Migration:** remove `basePath` from any options object passed to `compileFile` or
`checkFile`. This throw is **synchronous** — `.catch()` on the returned promise does
not receive it; wrap the call in `try/catch`. This is also a **compile-time break**
when a variable typed as `CompileOptions` or `CheckOptions` is passed to these
functions — see the compatibility notes below. Audit all call sites.

#### `FileOptions` no longer extends `CompileOptions` (#213)

`FileOptions` (used by `compileFile`) was previously declared as
`interface FileOptions extends CompileOptions`. This inheritance was an error:
`CompileOptions` now carries `basePath`, which is not valid for file-based
operations. `FileOptions` is now a standalone interface with its own `vars`,
`sourceMap`, and `sourcesContent` fields.

**Compatibility:** this change is a **compile-time break** for code that passes a
`CompileOptions`-typed variable to `compileFile`. After this PR, `CompileOptions`
carries `basePath?: string` while `FileOptions` declares `basePath?: never`;
TypeScript reports `"Types of property 'basePath' are incompatible"` at any such
assignment or call. Code that never reuses a string-surface options variable for
file operations compiles without changes.

**Migration for shared variables:** retype the variable as `FileOptions`, or
destructure only the accepted fields:
```ts
const { vars, sourceMap, sourcesContent } = compileOpts;
compileFile(path, { vars, sourceMap, sourcesContent });
```

#### `checkFile` parameter type changed from `CheckOptions` to `CheckFileOptions` (#213)

`checkFile(path, options?)` previously accepted `CheckOptions`. After this PR,
`CheckOptions` carries a `basePath` field that is not valid for file-based operations;
the parameter is now typed as `CheckFileOptions` — a new interface with only
`vars?: Record<string, unknown>`.

**Compatibility:** this type narrowing is a **compile-time break** for code that
passes a `CheckOptions`-typed variable to `checkFile`. `CheckOptions` carries
`basePath?: string` while `CheckFileOptions` declares `basePath?: never`; TypeScript
reports `"Types of property 'basePath' are incompatible"` at any such call. Code
that never reuses a string-surface variable for `checkFile` compiles without changes.

**Migration for shared variables:** retype the variable as `CheckFileOptions`, or
restrict it to `{ vars?: Record<string, unknown> }` at the call site.

#### `LintFileOptions` gained `basePath?: never` (#213)

`LintFileOptions` (used by `lintFile` and `lintVirtual`) previously had the shape
`{ vars?, rules? }`. It now declares `basePath?: never`.

**Compatibility:** this is a **compile-time break** for code that assigns a variable
whose inferred type includes a `basePath` field to a `LintFileOptions`-typed slot.
For example, passing a `LintOptions`-typed variable directly to `lintFile` or
`lintVirtual` now fails with `TS2322` — `LintOptions.basePath` is `string | undefined`
which is not assignable to `never`. Code that passes a fresh object literal without
`basePath`, or a variable that was already typed as `{ vars?, rules? }`, continues to
compile unchanged.

**Migration:** at each `lintFile` / `lintVirtual` call site that passes a
`LintOptions`-typed variable, either extract a narrowed copy
(`const { basePath: _unused, ...fileOpts } = opts`) or redeclare the variable as
`LintFileOptions` when `basePath` was never meaningful there.

#### WASM backend rejects `basePath` on string-surface methods (#180)

`compile(source, { basePath: '/dir' })` and `check(source, { basePath: '/dir' })`
previously silently ignored `basePath` on the WASM backend. They now throw
`Error { code: 'mds::invalid_options' }`. `lint(source, { basePath: '/dir' })` was
already documented as WASM-unsupported; it now enforces this at runtime too.

**Migration:** switch to the native backend (`MDS_BACKEND=native`) when you need
import resolution with a `basePath` in WASM environments.

### **BREAKING** — Interpolation syntax: `{x}` → `{{x}}`

Interpolation now uses **double braces**: `{{variable}}`, `{{obj.field}}`,
Expand Down
76 changes: 62 additions & 14 deletions packages/mds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,22 @@ console.log(getBackend()); // 'native' | 'wasm'

## Browser usage

The browser entry requires an explicit `init()` call before any compile/check
The browser entry requires an explicit `init()` call before any compile/check/lint
operations. `init()` is idempotent and safe to call multiple times.

```ts
import { init, compile, check, isMdsError } from '@mdscript/mds';
import { init, compile, check, lint, lintVirtual, isMdsError } from '@mdscript/mds';

await init();
// or with a custom WASM URL:
await init({ wasmUrl: '/assets/mds_bg.wasm' });

const result = compile('# {{title}}', { vars: { title: 'Hello' } });
const lintResult = lint('# {{title}}', { vars: { title: 'Hello' } });
const virtualResult = lintVirtual({ 'entry.mds': '# {{title}}' }, 'entry.mds');
```

> `compileFile` and `checkFile` are not available in browser environments.
> `compileFile`, `checkFile`, and `lintFile` are not available in browser environments.

## Backend selection (`MDS_BACKEND`)

Expand Down Expand Up @@ -102,35 +104,78 @@ try {

### Options

#### Option matrix

Each cell names the accepted option type. `basePath` is a string-surface option only;
file-path methods derive the base directory from the file argument.

| | String source | File path |
|-----------|------------------------------|--------------------|
| `compile` | `CompileOptions` | `FileOptions` |
| `check` | `CheckOptions` | `CheckFileOptions` |
| `lint` | `LintOptions` | `LintFileOptions` |

#### Option types

```ts
// CompileOptions — accepted by compile() and compileFile()
interface CompileOptions {
// CheckOptions — accepted by check() (string source)
// basePath: required when the source contains @import or @extends.
// WASM backend: basePath throws mds::invalid_options (no filesystem access);
// set MDS_BACKEND=native to use the native backend with import resolution.
// {basePath: undefined} is treated as absent on both backends.
interface CheckOptions {
vars?: Record<string, unknown>;
basePath?: string;
}

// CompileOptions — accepted by compile() (string source)
// Extends CheckOptions: inherits vars and basePath.
// basePath behaves the same as for CheckOptions (see above).
interface CompileOptions extends CheckOptions {
sourceMap?: boolean; // generate Source Map v3; result gains a `sourceMap` field
sourcesContent?: boolean; // embed source text in map (requires sourceMap: true)
// ⚠ Privacy: embeds the full template source
}

// CheckOptions — accepted by check() and checkFile() only
// Source-map options are NOT accepted; passing them throws mds::invalid_options
interface CheckOptions {
// FileOptions — accepted by compileFile()
// basePath is NOT accepted: the base directory is derived from the file path.
// basePath?: never blocks assigning a CompileOptions variable to this type (TS2322).
interface FileOptions {
vars?: Record<string, unknown>;
sourceMap?: boolean;
sourcesContent?: boolean;
basePath?: never; // not accepted; present to produce a compile-time error when a string-surface variable is passed
}

// CheckFileOptions — accepted by checkFile()
// basePath is NOT accepted (base directory from file path).
// basePath?: never blocks assigning a CheckOptions variable to this type (TS2322).
// Source-map options are NOT accepted; passing them throws mds::invalid_options.
interface CheckFileOptions {
vars?: Record<string, unknown>;
basePath?: never; // not accepted; present to produce a compile-time error when a string-surface variable is passed
}

// LintOptions — accepted by lint() (string-source)
// basePath: required when the source contains @import or @extends.
// WASM backend: basePath throws mds::invalid_options — rejects instead of
// silently ignoring so misconfigured callers see an actionable error.
// Set MDS_BACKEND=native to use the native backend, or use lintVirtual with
// pre-resolved modules.
interface LintOptions {
vars?: Record<string, unknown>;
rules?: Record<string, 'off' | 'info' | 'warn' | 'error'>;
basePath?: string; // base directory for @import resolution; required when the source
// contains @import or @extends. Ignored by the WASM backend.
basePath?: string;
}

// LintFileOptions — accepted by lintFile() and lintVirtual()
// basePath is NOT accepted: lintFile derives the base directory from the file path;
// lintVirtual resolves imports against the caller-supplied module map, not the filesystem.
// basePath?: never blocks assigning a LintOptions variable to this type (TS2322).
interface LintFileOptions {
vars?: Record<string, unknown>;
rules?: Record<string, 'off' | 'info' | 'warn' | 'error'>;
basePath?: never; // not accepted; present to produce a compile-time error when a string-surface variable is passed
}

// InitOptions
Expand All @@ -139,10 +184,13 @@ interface InitOptions {
}
```

**Strict unknown-option rejection:** passing any key not listed above throws
`Error { code: 'mds::invalid_options' }` immediately, before calling the backend.
This applies to `compile`, `compileFile`, `check`, `checkFile`, `lint`, `lintFile`,
and `lintVirtual`.
**Unknown-option rejection:** passing an unrecognised key to a public method throws
`Error { code: 'mds::invalid_options' }` before calling the backend; the error names
the offending key(s) and lists the accepted keys. Exception: passing `basePath` to a
file-path method (`compileFile` or `checkFile`) **throws synchronously** with a
purpose-built message (same channel as unknown-key rejection); the message does not
include an accepted-keys list. `.catch()` on the returned promise does not receive
this error — use `try/catch` around the call.

**Source maps:** for string-source compiles (`compile`) `sources[0]` in the generated
map is `"input.mds"`. For stdin builds via the CLI it is `"<stdin>"`.
Expand Down
Loading
Loading