fix: register MDX plugin at top level to fix transform ordering#160
fix: register MDX plugin at top level to fix transform ordering#160NathanDrake2406 wants to merge 1 commit intocloudflare:mainfrom
Conversation
Plugins returned from the config() hook run after vite:import-analysis, so @mdx-js/rollup transforms never fire. Move MDX to a top-level proxy plugin with enforce:"pre" that delegates to the lazily-created MDX instance once config() populates it. Fixes cloudflare#77
aed8241 to
62c1026
Compare
|
/bigbonk review |
commit: |
There was a problem hiding this comment.
Good fix for the root cause — plugins returned from config() hooks do run too late in the pipeline. The proxy-delegate pattern is a reasonable way to solve this. A few issues to address:
-
Missing
confighook proxy —@mdx-js/rolluphas aconfig()hook that initializes its processors with the Vitemode(development vs production). The proxy doesn't forward it, so the delegate'sconfig()is never called by Vite. Currently the MDX plugin has a fallback intransform()that creates processors without the mode, but this means MDX compilation always runs in production mode even during dev (no JSX dev runtime, etc.). This is a latent correctness issue. -
Unnecessary
resolveIdproxy —@mdx-js/rollupdoesn't have aresolveIdhook (confirmed by reading its source). The proxy'sresolveIddelegation is dead code. -
Comment merge issue — The MDX proxy plugin was inserted between the
vinext:react-canarycomment and its plugin object, merging two unrelated comments together (lines 2191-2196).
| resolveId(id, importer, options) { | ||
| if (!mdxDelegate?.resolveId) return; | ||
| const hook = mdxDelegate.resolveId; | ||
| const fn = typeof hook === "function" ? hook : hook.handler; | ||
| return fn.call(this, id, importer, options); |
There was a problem hiding this comment.
@mdx-js/rollup does not have a resolveId hook — its source only exposes name, config, and transform. This entire block is dead code. Consider removing it.
| resolveId(id, importer, options) { | |
| if (!mdxDelegate?.resolveId) return; | |
| const hook = mdxDelegate.resolveId; | |
| const fn = typeof hook === "function" ? hook : hook.handler; | |
| return fn.call(this, id, importer, options); | |
| resolveId(...args: Parameters<NonNullable<Plugin['resolveId']>>) { | |
| if (!mdxDelegate?.resolveId) return; | |
| const hook = mdxDelegate.resolveId; | |
| const fn = typeof hook === "function" ? hook : hook.handler; | |
| return fn.call(this, ...args); | |
| }, |
Actually, since @mdx-js/rollup has no resolveId, this should just be removed entirely to avoid confusion.
| { | ||
| name: "vinext:mdx", | ||
| enforce: "pre", | ||
| resolveId(id, importer, options) { | ||
| if (!mdxDelegate?.resolveId) return; | ||
| const hook = mdxDelegate.resolveId; | ||
| const fn = typeof hook === "function" ? hook : hook.handler; | ||
| return fn.call(this, id, importer, options); | ||
| }, | ||
| transform(code, id, options) { | ||
| if (!mdxDelegate?.transform) return; | ||
| const hook = mdxDelegate.transform; | ||
| const fn = typeof hook === "function" ? hook : hook.handler; | ||
| return fn.call(this, code, id, options); | ||
| }, |
There was a problem hiding this comment.
The @mdx-js/rollup plugin also has a config() hook that initializes its format-aware processors using env.mode (development vs production). Since the proxy doesn't forward config(), Vite never calls it on the delegate. The MDX plugin has a fallback inside transform() that creates processors without the mode, but this means MDX will always compile in production mode — losing JSX dev runtime support during development.
Please add a config proxy:
| { | |
| name: "vinext:mdx", | |
| enforce: "pre", | |
| resolveId(id, importer, options) { | |
| if (!mdxDelegate?.resolveId) return; | |
| const hook = mdxDelegate.resolveId; | |
| const fn = typeof hook === "function" ? hook : hook.handler; | |
| return fn.call(this, id, importer, options); | |
| }, | |
| transform(code, id, options) { | |
| if (!mdxDelegate?.transform) return; | |
| const hook = mdxDelegate.transform; | |
| const fn = typeof hook === "function" ? hook : hook.handler; | |
| return fn.call(this, code, id, options); | |
| }, | |
| { | |
| name: "vinext:mdx", | |
| enforce: "pre", | |
| config(config, env) { | |
| if (!mdxDelegate?.config) return; | |
| const hook = mdxDelegate.config; | |
| const fn = typeof hook === "function" ? hook : hook.handler; | |
| return fn.call(this, config, env); | |
| }, | |
| transform(code, id, options) { | |
| if (!mdxDelegate?.transform) return; | |
| const hook = mdxDelegate.transform; | |
| const fn = typeof hook === "function" ? hook : hook.handler; | |
| return fn.call(this, code, id, options); | |
| }, | |
| }, |
| // Shim React canary/experimental APIs (ViewTransition, addTransitionType) | ||
| // that exist in Next.js's bundled React canary but not in stable React 19. | ||
| // Proxy plugin for @mdx-js/rollup. The real MDX plugin is created lazily |
There was a problem hiding this comment.
The insertion merged two unrelated comments. Lines 2191-2192 are the original comment for vinext:react-canary ("Shim React canary/experimental APIs..."), and lines 2193-2196 are the new MDX proxy comment. They read as one continuous block. Add a blank line between them:
| // Shim React canary/experimental APIs (ViewTransition, addTransitionType) | |
| // that exist in Next.js's bundled React canary but not in stable React 19. | |
| // Proxy plugin for @mdx-js/rollup. The real MDX plugin is created lazily | |
| // Shim React canary/experimental APIs (ViewTransition, addTransitionType) | |
| // that exist in Next.js's bundled React canary but not in stable React 19. | |
| // Proxy plugin for @mdx-js/rollup. The real MDX plugin is created lazily |
| expect(mdxProxy).toBeDefined(); | ||
| expect(mdxProxy.enforce).toBe("pre"); | ||
| // Proxy should be inert when no MDX files are detected (mdxDelegate is null) | ||
| expect(mdxProxy.resolveId("./foo.ts", undefined, {})).toBeUndefined(); |
There was a problem hiding this comment.
The test verifies the proxy is inert when no delegate is set, which is good. Two suggestions:
-
It would be more valuable to also test that when
mdxDelegateis populated, the proxy actually delegates. Currently this is only verified implicitly by the (unchecked) E2E test item in the PR description. -
Consider also asserting the proxy has a
confighook (once added per the comment above), since that's part of the contract.
|
Review submitted. Here's a summary of the feedback I posted: Overall: The fix correctly identifies the root cause — plugins returned from Three issues flagged:
|
Summary
@mdx-js/rolluprunning too late in the Vite plugin pipeline, causing MDX files to fail to compilevinext:mdxproxy plugin in the top-level plugin array withenforce: 'pre'instead of returning the MDX plugin from theconfig()hookresolveIdandtransformto the real@mdx-js/rollupinstance, which is created duringconfig()and stored in a shared variableFixes #77
Details
Root cause: The MDX plugin was returned from
vinext:config'sconfig()hook viaviteConfig.plugins. Plugins merged fromconfig()hooks land late in the pipeline — aftervite:import-analysishas already tried (and failed) to process.mdximports.Fix: Instead of returning the plugin from
config(), avinext:mdxproxy plugin is registered directly in the top-level plugin array returned byvinext(). It hasenforce: 'pre', guaranteeing itsresolveIdandtransformhooks run beforevite:import-analysis. The proxy handles both function-style and object-style Rollup hooks (i.e.{ handler, order }form).Test plan
pnpm run build)pnpm run lint— 0 warnings, 0 errors)pnpm run typecheck)pnpm test— 50 files, 2051 tests passed, 3 skipped)vinext:mdxplugin exists withenforce: 'pre'and is inert when no MDX delegate is set.mdxfiles inapp/