Skip to content

Type frontmatter from a schema - #532

Open
hipstersmoothie wants to merge 5 commits into
mdx-js:mainfrom
hipstersmoothie:frontmatter-typing
Open

Type frontmatter from a schema#532
hipstersmoothie wants to merge 5 commits into
mdx-js:mainfrom
hipstersmoothie:frontmatter-typing

Conversation

@hipstersmoothie

@hipstersmoothie hipstersmoothie commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Lets users type their frontmatter from a schema.

Today the built-in remark-mdx-frontmatter plugin types the frontmatter export as any. This PR adds an optional type option:

// tsconfig.json
{
  "mdx": {
    "checkMdx": true,
    "plugins": [
      ["remark-frontmatter", ["toml", "yaml"]],
      ["remark-mdx-frontmatter", { "type": "import('./frontmatter.js').Frontmatter" }]
    ]
  }
}

With that config, in every MDX file:

---
title: Hello
---

# {frontmatter.title}   // ← checked against Frontmatter

The type value is a TypeScript type expression (JSDoc-style), so it can reference a type from another module or be written inline.

What you get

  1. Typed exportfrontmatter is typed as the configured type instead of any, so body usages (frontmatter.title) are checked. When no type is configured, behavior is unchanged (any when frontmatter is present, undefined otherwise).

  2. Value checking (when checkMdx is enabled) — the YAML frontmatter values are checked against the type, with diagnostics mapped back onto the frontmatter source:

    • missing required properties, and wrong value types → reported at the block;
    • unknown keys → reported on the offending key.
    ---
    title: 123      # Type 'number' is not assignable to type 'string'
    unknown: x      # Object literal may only specify known properties
    ---             # (removing a required key reports it as missing)
    
  3. Per-key editor features — each YAML key gets hover, go to definition (jumps to the schema field), and completion from the schema, including completion on blank lines and while a key is still being typed. Only cleanly-parsed maps are value-checked (a half-typed block won't emit false "missing field" errors); the navigation features are recovered even from a partial parse. TOML value-checking is out of scope for now; the export is still typed.

Plugin API change

To let a plugin map generated code back onto the source, VirtualCodePlugin.finalize is extended (backward compatibly):

  • it now receives the MDX source: finalize(mdx);
  • it may return either a string (unchanged) or { value, mappings }, where mappings are Volar CodeMappings whose generatedOffsets are relative to the returned value and whose sourceOffsets are absolute offsets into the MDX file.

getEmbeddedCodes rebases those mappings onto the embedded JavaScript file. Existing plugins that return a string are unaffected.

Changes

  • lib/plugins/plugin.js — extend the VirtualCodePlugin contract (VirtualCodeResult typedef; finalize(mdx)).
  • lib/plugins/remark-mdx-frontmatter.js — add the type option; emit the value-check + navigation code with mappings when a type + YAML are present.
  • lib/plugins/frontmatter-schema.js — new; builds the checked code and per-key navigation from parsed YAML (uses yaml).
  • lib/virtual-code.js — thread the MDX source into finalize; collect, pad, and emit plugin mappings.
  • package.json — add yaml dependency.
  • README + language-service README — document the type option, value checking, and key navigation.
  • fixtures/frontmatter — a schema + example documents, wired into the extension test workspace so the feature can be tried via "Run Extension".
  • Tests + a changeset.

There's also a separate commit, "Dedupe language server LSP dependencies for lockfile-free installs", that pins two drifting transitive deps so a fresh (lockfile-free) npm install produces a working tree again — CI was red on main independent of this feature. Happy to split that into its own PR if preferred.

Testing

  • npm test: type build clean, 96/96 API tests pass, 0 cancelled.
  • Verified end-to-end through @mdx-js/language-server against the new fixture: TS2322 / TS2353 / TS2741 diagnostics on the frontmatter, go-to-definition from a YAML key into the schema, and completion offering the schema fields.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d6e8343

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@mdx-js/language-server Patch
@mdx-js/language-service Minor
vscode-mdx Minor
@mdx-js/typescript-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the 👋 phase/new Post is being triaged automatically label Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Hi! It seems you removed the template which we require. Here are our templates (pick the one you want to use and click *raw* to see its source):

I won’t send you any further notifications about this, but I’ll keep on updating this comment, and hide it when done!

Thanks,
— bb

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.42342% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.51%. Comparing base (660067a) to head (d6e8343).

Files with missing lines Patch % Lines
...language-service/lib/plugins/frontmatter-schema.js 98.58% 5 Missing ⚠️
...uage-service/lib/plugins/remark-mdx-frontmatter.js 96.42% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #532      +/-   ##
==========================================
+ Coverage   92.43%   93.51%   +1.08%     
==========================================
  Files          13       14       +1     
  Lines        1982     2421     +439     
==========================================
+ Hits         1832     2264     +432     
- Misses        150      157       +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

hipstersmoothie and others added 2 commits July 7, 2026 01:00
Add a `type` option to the built-in `remark-mdx-frontmatter` plugin. When
set, the `frontmatter` export is typed with the given type instead of `any`,
so usages in the MDX body are checked.

When `checkMdx` is enabled, the YAML frontmatter values are also checked
against the type: missing properties, wrong value types, and unknown keys are
reported on the offending line within the frontmatter block.

To support this, `VirtualCodePlugin.finalize` now receives the MDX source and
may return source mappings alongside its generated code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The repo ships no lockfile (`.npmrc` sets `package-lock = false`), so a fresh
`npm install` resolves floating ranges to whatever is newest. Recently that
became a broken set:

- `@volar/language-service` (protocol `^3.17.5` -> 3.18.x) and
  `vscode-languageserver@9` (protocol pinned 3.17.5) install two copies of the
  protocol types, so `tsc` sees two identities of `WorkspaceEdit` and every
  re-exported protocol symbol (TS2308 / TS2345).
- `vscode-markdown-languageservice@0.5.0-alpha.13` compiles
  `import uri from 'vscode-uri'`, but `vscode-uri`'s ESM has no default export,
  so the server throws on start under Node's `require(esm)` and every
  integration test hangs.

Both are transitive, and every `^3.17.5` range is satisfied by 3.17.5, so
declaring `vscode-languageserver-protocol@3.17.5` as a direct dependency dedupes
the protocol/types to one version. Pinning `vscode-markdown-languageservice` to
alpha.12 (the last release before the breaking default import) avoids the crash.
Restores a green `npm test` (0 type errors, 88/88 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hipstersmoothie and others added 3 commits July 7, 2026 07:27
Unit-test `buildFrontmatterValidation` directly for the value branches that the
snapshot tests didn't reach: numbers, booleans, null, sequences, nested maps,
alias values, and non-scalar keys, plus the non-map / empty inputs that yield no
validation. Also exercise the plugin's typed-export-only path for non-map
frontmatter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds `fixtures/frontmatter` (a schema, a valid document, and one with
intentional errors) and registers it in the fixtures workspace, so the feature
can be exercised end to end via the "Run Extension" launch configuration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the frontmatter value checking with a second, navigation-only mapping:
property accesses on a schema-typed binding (`__mdxFrontmatterFields`) so each
YAML key resolves to its schema field for hover, go to definition, and
completion. Blank lines map to a zero-length anchor so completion offers every
field with an empty prefix, and keys are recovered from a partially-parsed block
so completion works while a key is being typed.

`buildFrontmatterValidation` now returns an array of mappings; `finalize`
rebases them all.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hipstersmoothie
hipstersmoothie marked this pull request as ready for review July 7, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

👋 phase/new Post is being triaged automatically

Development

Successfully merging this pull request may close these issues.

1 participant