Skip to content

Upgrade to unified 11 / remark 11 and publish as ESM - #213

Merged
josemarluedke merged 3 commits into
mainfrom
claude/remark-upgrade-research-644867
Aug 21, 2026
Merged

Upgrade to unified 11 / remark 11 and publish as ESM#213
josemarluedke merged 3 commits into
mainfrom
claude/remark-upgrade-research-644867

Conversation

@josemarluedke

Copy link
Copy Markdown
Owner

Closes the long-standing blocker in #93, unblocks #116, and answers #150.

Why this is possible now

Docfy has been stuck on unified 9 because the remark/rehype ecosystem moved to ESM and a CommonJS broccoli addon couldn't load ESM-only packages. That constraint is gone: Node supports require() of ES modules, unflagged since 20.19 / 22.12. So @docfy/ember-cli can require an ESM-only @docfy/core, and a CommonJS .docfy-config.js can require ESM-only plugins. No dual builds, no await import() plumbing, and no forcing users onto .mjs.

What changed

@docfy/core and @docfy/plugin-with-prose are now ES modules on unified 11 / remark 11 / rehype 11. Going ESM also deletes the export default X; module.exports = X; interop hack that no ESM loader can execute.

  • remark-slug (deprecated) replaced by a ~20-line internal transform. It stays at the mdast stage because the toc plugin reads node.data.id while the tree is still markdown. Generated ids are byte-identical.
  • mdast-util-toc removed — declared but never imported.
  • Nine hand-written .d.ts shims deleted; every one of those packages ships its own types now.
  • @docfy/ember-cli now loads .docfy-config.js, .mjs and .cjs, all synchronously, and raises an explicit error for the one case it can't handle (top-level await).

The one non-obvious problem

Highlighting has to move from remark to rehype, and that exposed an ordering bug. remark-hbs escapes {{ in code blocks on the mdast tree; rehype highlighters inject <span>s afterwards, reintroducing bare {{, so Ember's template compiler chokes:

Parse error on line 23:
...tuation mustache">{{<span class="hljs-cl
-----------------------^

Escaping now happens at the hast stage via a new escape-curlies-in-code plugin — runWithHast runs after all rehype plugins. With that in place highlight.js 11 works, so highlightjs-glimmer works (#116). Verified hljs-template-variable in test-app-classic's built output.

Jest → vitest

Jest 29 cannot require() ESM regardless of Node's support — it throws on trough/index.js. @docfy/core and @docfy/plugin-with-prose move to vitest, matching @docfy/ember-vite. No test bodies changed beyond __dirnameimport.meta.dirname, named imports, and .js extensions.

Vitest keys snapshots differently, so the .snap files were rewritten. I diffed old against new key-by-key before accepting:

  • heading extraction, URL generation (auto + manual), internal-link rewriting — byte-identical
  • plugin-with-prose — identical HTML, only jest's Array [ serialization differs
  • integration-remark-plugins — differs only from the plugin swaps: rehype-autolink-headings orders attributes differently, remark-math 6 renders bare math as <code class="language-math">, katex dropped a wrapper span

Verification

@docfy/core 52/57 pass — the 5 failures are pre-existing
@docfy/plugin-with-prose 2/2
@docfy/ember-vite 60/60
pnpm -r compile clean
test-app-classic ember build succeeds, with a CommonJS config requiring ESM-only plugins
test-app-vite vite build succeeds
frontile site/ client + ssr + prerender build, 21/21 node tests — config-only changes, no source changes

The 5 core failures are repo-info (4) and generating-edit-url (1). They fail on main too: they were run from a git worktree, so git-repo-info reports a root containing .claude/worktrees/.... The snapshot diff shows that path prefix is the only delta.

Breaking changes

Documented in docs/ember/upgrade-guide.md:

  • Node ^20.19.0 || >=22.12.0. This is what makes require(esm) work.
  • Plain require('@docfy/core') returns the module namespace, so it needs .default. ESM and TypeScript esModuleInterop consumers are unaffected — frontile and both test apps needed no code change.
  • Deep imports from ESM need an extension: @docfy/core/lib/plugin.js.
  • remark-highlight.js and @mapbox/rehype-prism no longer work → rehype-highlight or rehype-prism-plus.
  • Docfy now owns remarkHbsOptions.escapeCurliesCode / escapeCurliesInlineCode; remove them from your config.
  • Don't bump a direct highlight.js dependency. rehype-highlight brings its own via lowlight. Doing both at once broke language registration in frontile's SSR bundle.

Deliberately left out

  • escape-curlies-in-code is duplicated across ember-cli and ember-vite, matching how those two already duplicate their other Docfy plugins. Deduplicating is its own change.
  • The @types/unist v2 pin stays, with the comment rewritten to explain why. The Ember integrations' plugins are written against v2's loose Node (index signature, so node.value / node.depth type-check on a bare node) and intentionally build synthetic nodes and reassign node.type — which strict mdast Root/RootContent cannot express. It's types-only; runtime is unaffected. I prototyped the strict version and it needs those plugins redesigned, not retyped.
  • remark-hbs still works but is from 2021 on unist-util-visit@2. A 0.5 on current deps would drop the last stale link. Its inline-component limitation (the The easy way to support Unified and Remark ESM (latest) #150 caveat) is unchanged — I confirmed identical behaviour on unified 9 and 11, so not a regression.
  • hosted-git-info stays at v3; v4+ replaced browsetemplate with .edit(), an unrelated repo-info.ts refactor.

Pre-existing lint failures untouched by this PR: packages/ember lint:hbs (nested splattributes), test-app-classic lint:types (@glint/core isn't a dependency), test-app-vite lint:types.

Not verified either way: test-app-vite's testem run fails with ReferenceError: define is not defined in @embroider/virtual/test-support.js. It isn't in CI and I couldn't get a clean baseline install to compare against.

🤖 Generated with Claude Code

josemarluedke and others added 3 commits August 21, 2026 11:11
Docfy has been pinned to unified 9 because the remark/rehype ecosystem moved to
ESM and a CommonJS broccoli addon could not load ESM-only packages. Node has
since shipped `require()` of ES modules (unflagged in 20.19 / 22.12), which
removes that blocker entirely: `@docfy/ember-cli` can require an ESM-only
`@docfy/core`, and a CommonJS `.docfy-config.js` can require ESM-only plugins.
No dual builds, no `await import()` plumbing, no forced `.mjs` config.

`@docfy/core` and `@docfy/plugin-with-prose` are now ES modules. Going ESM also
removes the `export default X; module.exports = X;` interop hack, which no ESM
loader can execute.

Highlighting had to move from remark to rehype, which surfaced a real ordering
bug: remark-hbs escapes `{{` in code blocks on the mdast tree, but rehype
highlighters inject spans afterwards and reintroduce bare `{{`, so Ember's
template compiler chokes. Escaping now runs at the hast stage via a new
escape-curlies-in-code plugin (`runWithHast` executes after all rehype plugins).
With that in place highlight.js 11 works, so highlightjs-glimmer works.

Also drops nine hand-written .d.ts shims for packages that now ship their own
types, and one dead dependency (mdast-util-toc was declared but never imported).

Jest cannot require ESM regardless of Node's support, so @docfy/core and
@docfy/plugin-with-prose move to vitest, matching @docfy/ember-vite. Snapshots
were diffed key-by-key against the jest output before being accepted: heading
ids, URL generation and internal-link rewriting are byte-identical, and the only
content changes come from the plugin swaps themselves.

Verified: both test apps build, the frontile site builds (client + ssr +
prerender) with config-only changes.

BREAKING CHANGE: requires Node ^20.19.0 || >=22.12.0. Plain
`require('@docfy/core')` now returns the module namespace, so it needs
`.default`; ESM and TypeScript esModuleInterop consumers are unaffected. Deep
imports from ESM need a file extension. remark-highlight.js and
@mapbox/rehype-prism no longer work — use rehype-highlight or rehype-prism-plus.
Docfy now owns remarkHbsOptions.escapeCurliesCode / escapeCurliesInlineCode.
See docs/ember/upgrade-guide.md.

Refs #93, #116, #150

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vitest keys snapshots as `describe > test` where jest used `describe test`. This
file kept the jest-style key, so vitest found no match and CI (which refuses to
write new snapshots) failed with the snapshot reported obsolete.

Key rename only; the recorded content is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The deploy preview rendered a blank page with "There is no route named docs".
Two separate bugs, one of which hid the other.

1. remark-code-import v1 refuses to read files outside `rootDir` (default:
   cwd), and test-app-vite sources ../docs. Bumping it from 0.2 to 1.2 without
   passing `rootDir` made every page fail to process.

2. @docfy/ember-vite swallowed processAll() errors with a debug() call, so that
   failure produced a *successful* build whose virtual output module fell back
   to an empty `{name: '/', pages: [], children: []}`. No Docfy routes, blank
   site, green CI. Production builds now fail loudly with the underlying error;
   the dev server still warns and keeps serving so HMR can recover.

Also fixes syntax highlighting, which the regenerated templates exposed:
rehype-highlight resolves `options.languages || common`, so passing a custom
`languages` map *replaces* the default language set rather than extending it.
Registering glimmer had silently turned off highlighting for every other
language. Both test apps now spread lowlight's `common` back in, and the docs
call the trap out explicitly.

Verified by loading the built site, not just building it: routes resolve, the
sidebar and TOC render, highlighting works across languages plus glimmer, and
`{{` round-trips through code blocks with no leftover backslashes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@josemarluedke
josemarluedke merged commit 5dda3fa into main Aug 21, 2026
9 checks passed
@josemarluedke
josemarluedke deleted the claude/remark-upgrade-research-644867 branch August 21, 2026 21:03
josemarluedke added a commit that referenced this pull request Aug 21, 2026
The new templates guard caught a real bug, and it is one I introduced in #213.

Four of the committed .gjs templates have `.claude/worktrees/<branch-name>/`
baked into their `@editUrl` values, because I generated and committed them from
inside a git worktree. Every "Edit this page" link on those four demo pages
currently 404s.

The underlying cause is a Docfy bug worth fixing separately: `getRepoEditUrl`
computes `path.relative(getRepoInfo(root).root, root)`, and for a linked
worktree `git-repo-info` reports the *main* repository root while `root` lives
under the worktree directory, so the relative path picks up the worktree
prefix. That is also the real reason the repo-info and generating-edit-url
tests fail in a worktree — I had been calling those purely environmental, which
undersold it.

Verified the correction is exactly the inverse of the pollution: rebuilding in
a worktree reproduces the previously committed bytes exactly, so these files
equal a build output minus the worktree segment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant