Skip to content

Commit cd696bb

Browse files
committed
fix: skip MDX files during RSC scan-build to prevent parse errors (issue #659)
Adds a vinext:rsc-scan-mdx-guard plugin that runs with enforce:"pre" to ensure MDX files are skipped before rsc:scan-strip processes them. This prevents es-module-lexer parse errors when MDX files contain frontmatter (---) or JSX that isn't valid JavaScript syntax. Also adds a regression test that uses MDX files with YAML frontmatter loaded via import.meta.glob to verify the build succeeds.
1 parent 99ae33f commit cd696bb

2 files changed

Lines changed: 181 additions & 0 deletions

File tree

packages/vinext/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2601,6 +2601,22 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] {
26012601
return fn.call(this, code, id, options);
26022602
},
26032603
},
2604+
// Guard plugin: during RSC scan-build, skip MDX files to prevent
2605+
// rsc:scan-strip from trying to parse MDX frontmatter/JSX as JavaScript.
2606+
// rsc:scan-strip runs on all files during scan-build, and if it encounters
2607+
// raw MDX (frontmatter ---, JSX <span>), es-module-lexer's parse() fails.
2608+
// This plugin runs with enforce:"pre" before rsc:scan-strip (which is
2609+
// enforce:"post") to ensure MDX files are handled first. Even though
2610+
// vinext:mdx also has enforce:"pre", it may not be invoked on every MDX
2611+
// file during scan-build, so this guard provides an extra layer.
2612+
{
2613+
name: "vinext:rsc-scan-mdx-guard",
2614+
enforce: "pre",
2615+
transform(code, id) {
2616+
if (!id.endsWith(".mdx")) return null;
2617+
return { code, map: null };
2618+
},
2619+
},
26042620
// Shim React canary/experimental APIs (ViewTransition, addTransitionType)
26052621
// that exist in Next.js's bundled React canary but not in stable React 19.
26062622
// Provides graceful no-op fallbacks so projects using these APIs degrade

tests/tsconfig-path-alias-build.test.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,169 @@ export default handler;
235235
expect(buildOutput).not.toContain('import.meta.glob("@/content/posts/**/*.mdx"');
236236
expect(buildOutput).not.toContain("@/content/posts/");
237237
}, 60_000);
238+
239+
it("import.meta.glob with MDX files containing frontmatter does not cause parse errors (issue #659)", async () => {
240+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-mdx-frontmatter-build-"));
241+
tmpDirs.push(root);
242+
const workerEntryPath = path
243+
.resolve(import.meta.dirname, "../packages/vinext/src/server/app-router-entry.ts")
244+
.replace(/\\/g, "/");
245+
const cfPluginPath = path.resolve(
246+
import.meta.dirname,
247+
"./fixtures/cf-app-basic/node_modules/@cloudflare/vite-plugin/dist/index.mjs",
248+
);
249+
const { cloudflare } = (await import(pathToFileURL(cfPluginPath).href)) as {
250+
cloudflare: (opts?: {
251+
viteEnvironment?: { name: string; childEnvironments?: string[] };
252+
}) => import("vite").Plugin;
253+
};
254+
255+
fs.symlinkSync(
256+
path.resolve(import.meta.dirname, "../node_modules"),
257+
path.join(root, "node_modules"),
258+
"junction",
259+
);
260+
261+
writeFixtureFile(
262+
root,
263+
"package.json",
264+
JSON.stringify(
265+
{
266+
name: "vinext-mdx-frontmatter-test",
267+
private: true,
268+
type: "module",
269+
},
270+
null,
271+
2,
272+
),
273+
);
274+
writeFixtureFile(
275+
root,
276+
"wrangler.jsonc",
277+
`{
278+
"name": "vinext-mdx-frontmatter-test",
279+
"compatibility_date": "2026-02-12",
280+
"compatibility_flags": ["nodejs_compat"],
281+
"main": "./worker/index.ts",
282+
"assets": {
283+
"not_found_handling": "none",
284+
"binding": "ASSETS"
285+
}
286+
}
287+
`,
288+
);
289+
writeFixtureFile(
290+
root,
291+
"tsconfig.json",
292+
JSON.stringify(
293+
{
294+
compilerOptions: {
295+
target: "ES2022",
296+
module: "ESNext",
297+
moduleResolution: "bundler",
298+
jsx: "react-jsx",
299+
strict: true,
300+
skipLibCheck: true,
301+
types: ["vite/client", "@vitejs/plugin-rsc/types"],
302+
paths: {
303+
"@/*": ["./*"],
304+
},
305+
},
306+
include: ["app", "lib", "content", "*.ts", "*.tsx"],
307+
},
308+
null,
309+
2,
310+
),
311+
);
312+
writeFixtureFile(
313+
root,
314+
"app/layout.tsx",
315+
`export default function RootLayout({
316+
children,
317+
}: {
318+
children: React.ReactNode;
319+
}) {
320+
return (
321+
<html lang="en">
322+
<body>{children}</body>
323+
</html>
324+
);
325+
}
326+
`,
327+
);
328+
writeFixtureFile(
329+
root,
330+
"app/page.tsx",
331+
`import { getGlobPostCount } from "../lib/mdx-loader";
332+
333+
export default function HomePage() {
334+
return <main>home {getGlobPostCount()}</main>;
335+
}
336+
`,
337+
);
338+
writeFixtureFile(
339+
root,
340+
"app/mdx-probe/page.mdx",
341+
`# Probe
342+
343+
This file exists only to trigger vinext's MDX auto-detection.
344+
`,
345+
);
346+
writeFixtureFile(
347+
root,
348+
"lib/mdx-loader.ts",
349+
`type MdxModule = {
350+
default: React.ComponentType;
351+
frontmatter?: {
352+
title: string;
353+
date: string;
354+
};
355+
};
356+
357+
export const mdxModules = import.meta.glob("@/content/posts/**/*.mdx", {
358+
eager: true,
359+
}) as Record<string, MdxModule>;
360+
361+
export function getGlobPostCount(): number {
362+
return Object.keys(mdxModules).length;
363+
}
364+
`,
365+
);
366+
writeFixtureFile(
367+
root,
368+
"content/posts/2025/08/20/second-post/index.mdx",
369+
`---
370+
title: "Second Post"
371+
date: "2025-08-20"
372+
---
373+
374+
<span className="text-red-500">This is a post with frontmatter and JSX.</span>
375+
`,
376+
);
377+
writeFixtureFile(
378+
root,
379+
"worker/index.ts",
380+
`import handler from ${JSON.stringify(workerEntryPath)};
381+
382+
export default handler;
383+
`,
384+
);
385+
386+
const builder = await createBuilder({
387+
root,
388+
configFile: false,
389+
plugins: [
390+
vinext({ appDir: root }),
391+
cloudflare({ viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] } }),
392+
],
393+
logLevel: "silent",
394+
});
395+
await builder.buildApp();
396+
397+
const buildOutput = readTextFilesRecursive(path.join(root, "dist"));
398+
expect(buildOutput).not.toContain('import.meta.glob("@/content/posts/**/*.mdx"');
399+
expect(buildOutput).not.toContain("@/content/posts/");
400+
expect(buildOutput).not.toContain("---");
401+
expect(buildOutput).not.toContain("text-red-500");
402+
}, 60_000);
238403
});

0 commit comments

Comments
 (0)