Upgrade to unified 11 / remark 11 and publish as ESM - #213
Merged
Conversation
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>
This was referenced Aug 21, 2026
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>
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-clican require an ESM-only@docfy/core, and a CommonJS.docfy-config.jscan require ESM-only plugins. No dual builds, noawait import()plumbing, and no forcing users onto.mjs.What changed
@docfy/coreand@docfy/plugin-with-proseare now ES modules on unified 11 / remark 11 / rehype 11. Going ESM also deletes theexport 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 thetocplugin readsnode.data.idwhile the tree is still markdown. Generated ids are byte-identical.mdast-util-tocremoved — declared but never imported..d.tsshims deleted; every one of those packages ships its own types now.@docfy/ember-clinow loads.docfy-config.js,.mjsand.cjs, all synchronously, and raises an explicit error for the one case it can't handle (top-levelawait).The one non-obvious problem
Highlighting has to move from remark to rehype, and that exposed an ordering bug.
remark-hbsescapes{{in code blocks on the mdast tree; rehype highlighters inject<span>s afterwards, reintroducing bare{{, so Ember's template compiler chokes:Escaping now happens at the hast stage via a new
escape-curlies-in-codeplugin —runWithHastruns after all rehype plugins. With that in place highlight.js 11 works, sohighlightjs-glimmerworks (#116). Verifiedhljs-template-variableintest-app-classic's built output.Jest → vitest
Jest 29 cannot
require()ESM regardless of Node's support — it throws ontrough/index.js.@docfy/coreand@docfy/plugin-with-prosemove to vitest, matching@docfy/ember-vite. No test bodies changed beyond__dirname→import.meta.dirname, named imports, and.jsextensions.Vitest keys snapshots differently, so the
.snapfiles were rewritten. I diffed old against new key-by-key before accepting:plugin-with-prose— identical HTML, only jest'sArray [serialization differsintegration-remark-plugins— differs only from the plugin swaps:rehype-autolink-headingsorders attributes differently,remark-math6 renders bare math as<code class="language-math">, katex dropped a wrapper spanVerification
@docfy/core@docfy/plugin-with-prose@docfy/ember-vitepnpm -r compiletest-app-classicember buildsucceeds, with a CommonJS config requiring ESM-only pluginstest-app-vitevite buildsucceedssite/The 5 core failures are
repo-info(4) andgenerating-edit-url(1). They fail onmaintoo: they were run from a git worktree, sogit-repo-inforeports 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:^20.19.0 || >=22.12.0. This is what makesrequire(esm)work.require('@docfy/core')returns the module namespace, so it needs.default. ESM and TypeScriptesModuleInteropconsumers are unaffected — frontile and both test apps needed no code change.@docfy/core/lib/plugin.js.remark-highlight.jsand@mapbox/rehype-prismno longer work →rehype-highlightorrehype-prism-plus.remarkHbsOptions.escapeCurliesCode/escapeCurliesInlineCode; remove them from your config.highlight.jsdependency.rehype-highlightbrings its own vialowlight. Doing both at once broke language registration in frontile's SSR bundle.Deliberately left out
escape-curlies-in-codeis duplicated acrossember-cliandember-vite, matching how those two already duplicate their other Docfy plugins. Deduplicating is its own change.@types/unistv2 pin stays, with the comment rewritten to explain why. The Ember integrations' plugins are written against v2's looseNode(index signature, sonode.value/node.depthtype-check on a bare node) and intentionally build synthetic nodes and reassignnode.type— which strict mdastRoot/RootContentcannot express. It's types-only; runtime is unaffected. I prototyped the strict version and it needs those plugins redesigned, not retyped.remark-hbsstill works but is from 2021 onunist-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-infostays at v3; v4+ replacedbrowsetemplatewith.edit(), an unrelatedrepo-info.tsrefactor.Pre-existing lint failures untouched by this PR:
packages/emberlint:hbs(nested splattributes),test-app-classiclint:types(@glint/coreisn't a dependency),test-app-vitelint:types.Not verified either way:
test-app-vite's testem run fails withReferenceError: define is not definedin@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