mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
fef54db9b4
* 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. * fix(mdx): return stub module for undetected MDX files during scan-build The guard plugin was a no-op that passed raw MDX through unchanged. When rsc:scan-strip received this raw MDX, es-module-lexer.parse() failed on frontmatter (---) and JSX syntax. The real fix is in vinext:mdx: when mdxDelegate is null (no MDX files in app/ or pages/), return a valid JS stub module instead of undefined. This handles MDX files that enter the build graph via import.meta.glob outside app/pages. - Remove vinext:rsc-scan-mdx-guard plugin (was a no-op) - Modify vinext:mdx to return stub module when mdxDelegate is null - Remove app/mdx-probe/page.mdx from test (was hiding the bug) - Replace fragile '---' assertion with specific frontmatter checks Fixes issue #659 * fix(mdx): lazily compile globbed mdx imports Compile plain .mdx files on demand when they enter the graph via import.meta.glob outside app/pages so scan-build stops crashing without discarding runtime content. Restore the existing happy-path MDX fixture, add focused lazy-compilation coverage, and extract shared Cloudflare fixture setup to keep the regression tests honest. * fix review issues * fix: use absolute path for root tsconfig alias to fix import.meta.glob resolution toViteAliasReplacement was returning "/" (Vite root-relative convention) when the tsconfig path alias pointed to the project root (e.g. "@/*": ["./*"]). This worked for regular imports but broke import.meta.glob because Vite's glob plugin resolves patterns via this.resolve(), which couldn't properly resolve the "/" replacement to matching filesystem files. Changed to return the absolute path instead, which works correctly for both regular imports and glob pattern resolution. Signed-off-by: Divanshu Chauhan <divkix@divkix.me> Made-with: Cursor * refactor: address review feedback on MDX lazy initialization - Add clarifying comments for mdxDelegatePromise caching semantics - Document subtle control flow when hasUserMdxPlugin is true - Suppress duplicate warning on on-demand MDX init (only warn during detection) - Add case-insensitive extension check in transform hook - Add test for query parameter edge cases (?raw, ?v=123, etc.) * refactor: address review feedback on MDX handling - Merge duplicate query-param MDX skip tests in pages-router.test.ts - Tighten hasUserMdxPlugin control flow comment for clarity - Make scanDirForMdx case-insensitive to align with transform check Refs: PR #668 review feedback * docs: clarify mdx delegate fallback comment * chore: trigger ci --------- Signed-off-by: Divanshu Chauhan <divkix@divkix.me>
247 lines
7.4 KiB
TypeScript
247 lines
7.4 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import type { Plugin, PluginOption } from "vite-plus";
|
|
import vinext from "../packages/vinext/src/index.js";
|
|
|
|
const originalCwd = process.cwd();
|
|
|
|
function setupProject(vitePackageJson: Record<string, unknown>): string {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-vite-major-"));
|
|
fs.mkdirSync(path.join(root, "pages"), { recursive: true });
|
|
fs.mkdirSync(path.join(root, "node_modules", "vite"), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(root, "package.json"),
|
|
JSON.stringify({ name: "test-project", version: "1.0.0" }, null, 2),
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(root, "node_modules", "vite", "package.json"),
|
|
JSON.stringify(vitePackageJson, null, 2),
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(root, "pages", "index.tsx"),
|
|
"export default function Page() { return <div>hello</div>; }\n",
|
|
);
|
|
return root;
|
|
}
|
|
|
|
function isPlugin(plugin: PluginOption): plugin is Plugin {
|
|
return !!plugin && !Array.isArray(plugin) && typeof plugin === "object" && "name" in plugin;
|
|
}
|
|
|
|
function findNamedPlugin(plugins: ReturnType<typeof vinext>, name: string) {
|
|
return plugins.find((plugin): plugin is Plugin => isPlugin(plugin) && plugin.name === name);
|
|
}
|
|
|
|
afterEach(() => {
|
|
process.chdir(originalCwd);
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("Vite tsconfig paths support", () => {
|
|
it("keeps vite-tsconfig-paths on Vite 7", async () => {
|
|
const root = setupProject({ name: "vite", version: "7.3.1" });
|
|
process.chdir(root);
|
|
|
|
const plugins = vinext({ appDir: root });
|
|
|
|
expect(findNamedPlugin(plugins, "vite-tsconfig-paths")).toBeDefined();
|
|
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it("uses resolve.tsconfigPaths on Vite 8 instead of vite-tsconfig-paths", async () => {
|
|
const root = setupProject({ name: "vite", version: "8.0.0" });
|
|
process.chdir(root);
|
|
|
|
const plugins = vinext({ appDir: root });
|
|
|
|
expect(findNamedPlugin(plugins, "vite-tsconfig-paths")).toBeUndefined();
|
|
|
|
const configPlugin = findNamedPlugin(plugins, "vinext:config") as {
|
|
config?: (
|
|
config: { root: string },
|
|
env: { command: "serve"; mode: string },
|
|
) => Promise<{
|
|
resolve?: Record<string, unknown>;
|
|
}>;
|
|
};
|
|
const resolvedConfig = await configPlugin.config?.(
|
|
{ root },
|
|
{ command: "serve", mode: "development" },
|
|
);
|
|
|
|
expect(resolvedConfig?.resolve?.tsconfigPaths).toBe(true);
|
|
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it("materializes simple tsconfig path aliases into resolve.alias on Vite 8", async () => {
|
|
const root = setupProject({ name: "vite", version: "8.0.0" });
|
|
process.chdir(root);
|
|
fs.writeFileSync(
|
|
path.join(root, "tsconfig.json"),
|
|
JSON.stringify(
|
|
{
|
|
compilerOptions: {
|
|
baseUrl: ".",
|
|
paths: {
|
|
"@/*": ["./*"],
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
|
|
const plugins = vinext({ appDir: root });
|
|
const configPlugin = findNamedPlugin(plugins, "vinext:config") as {
|
|
config?: (
|
|
config: { root: string },
|
|
env: { command: "serve"; mode: string },
|
|
) => Promise<{
|
|
resolve?: Record<string, unknown>;
|
|
}>;
|
|
};
|
|
const resolvedConfig = await configPlugin.config?.(
|
|
{ root },
|
|
{ command: "serve", mode: "development" },
|
|
);
|
|
|
|
const alias = resolvedConfig?.resolve?.alias as Record<string, string>;
|
|
expect(alias).toBeDefined();
|
|
expect(alias["@"]).toBeDefined();
|
|
expect(path.isAbsolute(alias["@"])).toBe(true);
|
|
expect(alias["@"].replace(/\\/g, "/")).toContain(root.replace(/\\/g, "/"));
|
|
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it("materializes path aliases inherited via tsconfig extends on Vite 8", async () => {
|
|
const root = setupProject({ name: "vite", version: "8.0.0" });
|
|
process.chdir(root);
|
|
fs.mkdirSync(path.join(root, "src"), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(root, "tsconfig.base.json"),
|
|
JSON.stringify(
|
|
{
|
|
compilerOptions: {
|
|
baseUrl: ".",
|
|
paths: {
|
|
"@/*": ["src/*"],
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(root, "tsconfig.json"),
|
|
JSON.stringify(
|
|
{
|
|
extends: "./tsconfig.base.json",
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
|
|
const plugins = vinext({ appDir: root });
|
|
const configPlugin = findNamedPlugin(plugins, "vinext:config") as {
|
|
config?: (
|
|
config: { root: string },
|
|
env: { command: "serve"; mode: string },
|
|
) => Promise<{
|
|
resolve?: Record<string, unknown>;
|
|
}>;
|
|
};
|
|
const resolvedConfig = await configPlugin.config?.(
|
|
{ root },
|
|
{ command: "serve", mode: "development" },
|
|
);
|
|
|
|
expect(resolvedConfig?.resolve?.alias).toEqual(
|
|
expect.objectContaining({
|
|
"@": "/src",
|
|
}),
|
|
);
|
|
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it("does not override user-defined resolve.tsconfigPaths on Vite 8", async () => {
|
|
const root = setupProject({ name: "vite", version: "8.0.0" });
|
|
process.chdir(root);
|
|
|
|
const plugins = vinext({ appDir: root });
|
|
const configPlugin = findNamedPlugin(plugins, "vinext:config") as {
|
|
config?: (
|
|
config: { root: string; resolve?: Record<string, unknown> },
|
|
env: { command: "serve"; mode: string },
|
|
) => Promise<{
|
|
resolve?: Record<string, unknown>;
|
|
}>;
|
|
};
|
|
const resolvedConfig = await configPlugin.config?.(
|
|
{ root, resolve: { tsconfigPaths: false } },
|
|
{ command: "serve", mode: "development" },
|
|
);
|
|
|
|
expect(resolvedConfig?.resolve?.tsconfigPaths).toBeUndefined();
|
|
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it("uses bundled Vite version from npm alias packages", async () => {
|
|
const root = setupProject({
|
|
name: "@voidzero-dev/vite-plus-core",
|
|
version: "0.1.11",
|
|
bundledVersions: { vite: "8.0.0" },
|
|
});
|
|
process.chdir(root);
|
|
|
|
const plugins = vinext({ appDir: root });
|
|
|
|
expect(findNamedPlugin(plugins, "vite-tsconfig-paths")).toBeUndefined();
|
|
|
|
const configPlugin = findNamedPlugin(plugins, "vinext:config") as {
|
|
config?: (
|
|
config: { root: string },
|
|
env: { command: "serve"; mode: string },
|
|
) => Promise<{
|
|
resolve?: Record<string, unknown>;
|
|
}>;
|
|
};
|
|
const resolvedConfig = await configPlugin.config?.(
|
|
{ root },
|
|
{ command: "serve", mode: "development" },
|
|
);
|
|
|
|
expect(resolvedConfig?.resolve?.tsconfigPaths).toBe(true);
|
|
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it("falls back to Vite 7 for npm alias packages without bundled versions", async () => {
|
|
const root = setupProject({
|
|
name: "@voidzero-dev/vite-plus-core",
|
|
version: "0.1.11",
|
|
});
|
|
process.chdir(root);
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
|
|
const plugins = vinext({ appDir: root });
|
|
|
|
expect(findNamedPlugin(plugins, "vite-tsconfig-paths")).toBeDefined();
|
|
expect(warn).toHaveBeenCalledOnce();
|
|
expect(warn).toHaveBeenCalledWith(
|
|
"[vinext] Could not determine Vite major version from @voidzero-dev/vite-plus-core; assuming Vite 7",
|
|
);
|
|
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
});
|