From 7c8ee52fef821c9837bfb9364714206b29665d88 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Tue, 26 May 2026 13:42:47 +1000 Subject: [PATCH 1/8] fix(pages): preserve import.meta.url source paths Pages Router production bundles previously left direct import.meta.url expressions for the bundler to evaluate from generated output. Server renders could expose dist/server/entry.js, and prerendered Pages output could miss the client entry needed for hydration to replace the server value with the Turbopack-style browser source URL. The fix rewrites direct import.meta.url reads in user modules to source-file URLs at transform time, keeps new URL asset bases intact, and loads the Pages client entry from the build manifest for Node production and prerender servers. Regression coverage ports the Next.js import-meta deploy test, including the prerender and hydrated browser path. --- packages/vinext/src/index.ts | 4 + .../vinext/src/plugins/import-meta-url.ts | 191 ++++++++++++++++++ packages/vinext/src/server/prod-server.ts | 87 ++++++-- tests/import-meta-url.test.ts | 64 ++++++ tests/pages-router.test.ts | 127 ++++++++++++ 5 files changed, 458 insertions(+), 15 deletions(-) create mode 100644 packages/vinext/src/plugins/import-meta-url.ts create mode 100644 tests/import-meta-url.test.ts diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 9b75c9c41..192ce1ec9 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -137,6 +137,7 @@ import { } from "./build/ssr-manifest.js"; import { stripServerExports } from "./plugins/strip-server-exports.js"; import { removeConsoleCalls } from "./plugins/remove-console.js"; +import { createImportMetaUrlPlugin } from "./plugins/import-meta-url.js"; import { hasMdxFiles } from "./utils/mdx-scan.js"; import { scanPublicFileRoutes } from "./utils/public-routes.js"; import tsconfigPaths from "vite-tsconfig-paths"; @@ -3812,6 +3813,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { }, }, }, + createImportMetaUrlPlugin({ + getRoot: () => root, + }), // Inline binary assets fetched via `fetch(new URL("./asset", import.meta.url))` — // see src/plugins/og-assets.ts createOgInlineFetchAssetsPlugin(), diff --git a/packages/vinext/src/plugins/import-meta-url.ts b/packages/vinext/src/plugins/import-meta-url.ts new file mode 100644 index 000000000..31f2e034e --- /dev/null +++ b/packages/vinext/src/plugins/import-meta-url.ts @@ -0,0 +1,191 @@ +import { normalizePath, parseAst, type Plugin } from "vite"; +import MagicString from "magic-string"; +import fs from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +type NodeLike = { + type?: string; + start?: number; + end?: number; + [key: string]: unknown; +}; + +type ImportMetaUrlEnvironment = "client" | "server"; + +type RewriteResult = { + code: string; + map: ReturnType; +}; + +export function createImportMetaUrlPlugin(options: { getRoot: () => string | undefined }): Plugin { + return { + name: "vinext:import-meta-url", + enforce: "post", + transform(code, id) { + const root = options.getRoot(); + if (!root) return null; + const cleanId = cleanModuleId(id); + if (!isTransformableUserModule(cleanId, root)) return null; + + const environment: ImportMetaUrlEnvironment = + this.environment?.name === "client" ? "client" : "server"; + const rewritten = rewriteImportMetaUrl(code, cleanId, root, environment); + if (!rewritten) return null; + return { + code: rewritten.code, + map: rewritten.map, + }; + }, + }; +} + +export function rewriteImportMetaUrl( + code: string, + id: string, + root: string, + environment: ImportMetaUrlEnvironment, +): RewriteResult | null { + if (!code.includes("import.meta.url")) return null; + + let ast: unknown; + try { + ast = parseAst(code); + } catch { + return null; + } + + const ranges = collectImportMetaUrlRanges(ast); + if (ranges.length === 0) return null; + + const replacement = JSON.stringify(importMetaUrlValue(id, root, environment)); + const output = new MagicString(code); + for (const range of ranges) { + output.overwrite(range.start, range.end, replacement); + } + + return { + code: output.toString(), + map: output.generateMap({ hires: "boundary" }), + }; +} + +function cleanModuleId(id: string): string { + return id.split("?", 1)[0]; +} + +function isTransformableUserModule(id: string, root: string): boolean { + if (!id || id.startsWith("\0")) return false; + if (!path.isAbsolute(id)) return false; + if (id.includes("/node_modules/") || id.includes("\\node_modules\\")) return false; + if (!/\.(tsx?|jsx?|mjs)$/.test(id)) return false; + + const canonicalId = canonicalizePath(id); + const canonicalRoot = canonicalizePath(root); + const normalizedId = normalizePath(canonicalId); + const normalizedRoot = normalizePath(canonicalRoot); + if (!isPathWithin(normalizedId, normalizedRoot)) return false; + + const relativePath = normalizePath(path.relative(canonicalRoot, canonicalId)); + const firstSegment = relativePath.split("/")[0]; + return ( + firstSegment !== ".next" && firstSegment !== ".vinext-local-package" && firstSegment !== "dist" + ); +} + +function isPathWithin(candidate: string, root: string): boolean { + return candidate === root || candidate.startsWith(root.endsWith("/") ? root : `${root}/`); +} + +function importMetaUrlValue( + id: string, + root: string, + environment: ImportMetaUrlEnvironment, +): string { + const canonicalId = canonicalizePath(id); + if (environment === "client") { + const relativePath = normalizePath(path.relative(canonicalizePath(root), canonicalId)); + return `file:///ROOT/${relativePath}`; + } + + return pathToFileURL(canonicalId).href; +} + +function canonicalizePath(value: string): string { + try { + return fs.realpathSync.native(value); + } catch { + return path.resolve(value); + } +} + +function collectImportMetaUrlRanges(ast: unknown): Array<{ start: number; end: number }> { + const ranges: Array<{ start: number; end: number }> = []; + + function visit(value: unknown): void { + if (!isNodeLike(value)) return; + + if (isImportMetaUrlNode(value)) { + ranges.push({ start: value.start, end: value.end }); + return; + } + + if (isNewUrlExpression(value)) { + const args = nodeArray(value.arguments); + for (let index = 0; index < args.length; index += 1) { + if (index === 1 && isImportMetaUrlNode(args[index])) continue; + visit(args[index]); + } + visit(value.callee); + return; + } + + for (const [key, child] of Object.entries(value)) { + if (key === "type" || key === "start" || key === "end" || key === "loc") continue; + if (Array.isArray(child)) { + for (const item of child) visit(item); + } else { + visit(child); + } + } + } + + visit(ast); + return ranges; +} + +function isNodeLike(value: unknown): value is NodeLike { + return !!value && typeof value === "object"; +} + +function isIdentifierNamed(value: unknown, name: string): boolean { + return isNodeLike(value) && value.type === "Identifier" && value.name === name; +} + +function isImportMetaNode(value: unknown): boolean { + return ( + isNodeLike(value) && + value.type === "MetaProperty" && + isIdentifierNamed(value.meta, "import") && + isIdentifierNamed(value.property, "meta") + ); +} + +function isImportMetaUrlNode(value: unknown): value is NodeLike & { start: number; end: number } { + return ( + isNodeLike(value) && + value.type === "MemberExpression" && + typeof value.start === "number" && + typeof value.end === "number" && + isImportMetaNode(value.object) && + isIdentifierNamed(value.property, "url") + ); +} + +function isNewUrlExpression(value: NodeLike): boolean { + return value.type === "NewExpression" && isIdentifierNamed(value.callee, "URL"); +} + +function nodeArray(value: unknown): unknown[] { + return Array.isArray(value) ? value : []; +} diff --git a/packages/vinext/src/server/prod-server.ts b/packages/vinext/src/server/prod-server.ts index 9b183aa94..6e5050673 100644 --- a/packages/vinext/src/server/prod-server.ts +++ b/packages/vinext/src/server/prod-server.ts @@ -87,6 +87,7 @@ import { resolveRequestProtocol, resolveRequestHost as resolveHost, } from "./proxy-trust.js"; +import { isUnknownRecord } from "../utils/record.js"; /** Convert a Node.js IncomingMessage into a ReadableStream for Web Request body. */ function readNodeStream(req: IncomingMessage): ReadableStream { @@ -1320,6 +1321,9 @@ type PagesServerEntryPageRoute = { }; }; +type ClientBuildManifest = Parameters[0]; +type ClientBuildManifestChunk = ClientBuildManifest[string]; + function isPagesServerEntryPageRoute(value: unknown): value is PagesServerEntryPageRoute { if (!value || typeof value !== "object" || !("pattern" in value)) return false; if (typeof value.pattern !== "string") return false; @@ -1335,6 +1339,59 @@ function readPagesServerEntryPageRoutes(value: unknown): PagesServerEntryPageRou return Array.isArray(value) && value.every(isPagesServerEntryPageRoute) ? value : undefined; } +function readStringArray(value: unknown): string[] | undefined { + return Array.isArray(value) && value.every((item): item is string => typeof item === "string") + ? value + : undefined; +} + +function readClientBuildManifest(manifestPath: string): ClientBuildManifest | undefined { + if (!fs.existsSync(manifestPath)) return undefined; + + try { + const value: unknown = JSON.parse(fs.readFileSync(manifestPath, "utf-8")); + if (!isUnknownRecord(value)) return undefined; + + const manifest: ClientBuildManifest = {}; + for (const [key, entry] of Object.entries(value)) { + if (!isUnknownRecord(entry) || typeof entry.file !== "string") continue; + + const imports = readStringArray(entry.imports); + const dynamicImports = readStringArray(entry.dynamicImports); + const css = readStringArray(entry.css); + const assets = readStringArray(entry.assets); + const chunk: ClientBuildManifestChunk = { + file: entry.file, + ...(entry.isEntry === true ? { isEntry: true } : {}), + ...(entry.isDynamicEntry === true ? { isDynamicEntry: true } : {}), + ...(imports ? { imports } : {}), + ...(dynamicImports ? { dynamicImports } : {}), + ...(css ? { css } : {}), + ...(assets ? { assets } : {}), + }; + + manifest[key] = chunk; + } + + return manifest; + } catch { + return undefined; + } +} + +function findClientEntryFile( + buildManifest: ClientBuildManifest, + assetBase: string, +): string | undefined { + for (const entry of Object.values(buildManifest)) { + if (entry.isEntry && entry.file) { + return manifestFileWithBase(entry.file, assetBase); + } + } + + return undefined; +} + /** * Start the Pages Router production server. * @@ -1406,22 +1463,22 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { ssrManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")); } - // Load the build manifest to compute lazy chunks — chunks only reachable via - // dynamic imports (React.lazy, next/dynamic). These should not be - // modulepreloaded since they are fetched on demand. + // Load the build manifest to expose the Pages Router client entry and compute + // lazy chunks. Prerendered HTML is rendered through this Node server too, so + // it needs the same client-entry global that Cloudflare builds inject into + // the Worker entry at build time. const buildManifestPath = path.join(clientDir, ".vite", "manifest.json"); - if (fs.existsSync(buildManifestPath)) { - try { - const buildManifest = JSON.parse(fs.readFileSync(buildManifestPath, "utf-8")); - const lazyChunks = computeLazyChunks(buildManifest).map((file: string) => - manifestFileWithBase(file, assetBase), - ); - if (lazyChunks.length > 0) { - globalThis.__VINEXT_LAZY_CHUNKS__ = lazyChunks; - } - } catch { - /* ignore parse errors */ - } + const buildManifest = readClientBuildManifest(buildManifestPath); + if (buildManifest) { + globalThis.__VINEXT_CLIENT_ENTRY__ = findClientEntryFile(buildManifest, assetBase); + + const lazyChunks = computeLazyChunks(buildManifest).map((file) => + manifestFileWithBase(file, assetBase), + ); + globalThis.__VINEXT_LAZY_CHUNKS__ = lazyChunks.length > 0 ? lazyChunks : undefined; + } else { + globalThis.__VINEXT_CLIENT_ENTRY__ = undefined; + globalThis.__VINEXT_LAZY_CHUNKS__ = undefined; } // Build the static file metadata cache at startup (same as App Router). diff --git a/tests/import-meta-url.test.ts b/tests/import-meta-url.test.ts new file mode 100644 index 000000000..420937b95 --- /dev/null +++ b/tests/import-meta-url.test.ts @@ -0,0 +1,64 @@ +import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test"; +import fsp from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { rewriteImportMetaUrl } from "../packages/vinext/src/plugins/import-meta-url.js"; + +describe("vinext:import-meta-url plugin", () => { + let tmpDir: string; + let realRoot: string; + let linkedRoot: string; + let pagePath: string; + + beforeAll(async () => { + tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-import-meta-url-")); + realRoot = path.join(tmpDir, "real-app"); + linkedRoot = path.join(tmpDir, "linked-app"); + pagePath = path.join(realRoot, "pages", "index.tsx"); + + await fsp.mkdir(path.dirname(pagePath), { recursive: true }); + await fsp.writeFile(pagePath, `export const url = import.meta.url;\n`); + await fsp.symlink(realRoot, linkedRoot, "junction"); + }); + + afterAll(async () => { + if (tmpDir) { + await fsp.rm(tmpDir, { recursive: true, force: true }); + } + }); + + it("normalizes client import.meta.url to a Turbopack-style /ROOT source URL", () => { + const result = rewriteImportMetaUrl( + `export const url = import.meta.url;\n`, + pagePath, + linkedRoot, + "client", + ); + + expect(result?.code).toContain(`"file:///ROOT/pages/index.tsx"`); + }); + + it("preserves the real server source file URL", () => { + const result = rewriteImportMetaUrl( + `export const url = import.meta.url;\n`, + pagePath, + linkedRoot, + "server", + ); + + expect(result?.code).toMatch(/"file:\/\/\/.*\/pages\/index\.tsx"/); + expect(result?.code).not.toContain("linked-app"); + }); + + it("does not rewrite the import.meta.url base argument in new URL asset expressions", () => { + const result = rewriteImportMetaUrl( + `const asset = new URL("./font.ttf", import.meta.url);\nconst url = import.meta.url;\n`, + pagePath, + linkedRoot, + "client", + ); + + expect(result?.code).toContain(`new URL("./font.ttf", import.meta.url)`); + expect(result?.code).toContain(`const url = "file:///ROOT/pages/index.tsx"`); + }); +}); diff --git a/tests/pages-router.test.ts b/tests/pages-router.test.ts index c899f066d..48deed70c 100644 --- a/tests/pages-router.test.ts +++ b/tests/pages-router.test.ts @@ -5012,6 +5012,12 @@ export default class MyDocument extends Document { ); await buildPagesFixtureToOutDir(tmpRoot, outDir); + const { runPrerender } = await import("../packages/vinext/src/build/run-prerender.js"); + await runPrerender({ + root: tmpRoot, + pagesBundlePath: path.join(outDir, "server", "entry.js"), + concurrency: 1, + }); const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js"); prodServer = unwrapStartedProdServer( @@ -5070,6 +5076,127 @@ export default class MyDocument extends Document { }); }); +// Ported from Next.js: test/e2e/import-meta/import-meta.test.ts +// https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/import-meta/import-meta.test.ts +describe("Pages Router import.meta.url in production", () => { + let tmpRoot: string; + let outDir: string; + let prodServer: import("node:http").Server; + let prodUrl: string; + + function decodeHtmlText(text: string): string { + return text.replace(/"/g, '"').replace(/&/g, "&"); + } + + function collectJavaScriptFiles(dir: string): string[] { + const files: string[] = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...collectJavaScriptFiles(fullPath)); + } else if (entry.isFile() && entry.name.endsWith(".js")) { + files.push(fullPath); + } + } + return files; + } + + beforeAll(async () => { + tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-import-meta-url-")); + outDir = path.join(tmpRoot, "dist"); + + await fsp.symlink( + path.resolve(import.meta.dirname, "../node_modules"), + path.join(tmpRoot, "node_modules"), + "junction", + ); + await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" })); + await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true }); + await fsp.writeFile( + path.join(tmpRoot, "pages", "_app.tsx"), + `export default function MyApp({ Component, pageProps }: any) { + return ; +} +`, + ); + await fsp.writeFile( + path.join(tmpRoot, "pages", "index.tsx"), + `export default function Page() { + const data = { url: import.meta.url }; + return
{JSON.stringify(data)}
; +} +`, + ); + + await buildPagesFixtureToOutDir(tmpRoot, outDir); + const { runPrerender } = await import("../packages/vinext/src/build/run-prerender.js"); + await runPrerender({ + root: tmpRoot, + pagesBundlePath: path.join(outDir, "server", "entry.js"), + concurrency: 1, + }); + + const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js"); + prodServer = unwrapStartedProdServer( + await startProdServer({ + port: 0, + host: "127.0.0.1", + outDir, + }), + ); + const addr = prodServer.address() as { port: number }; + prodUrl = `http://127.0.0.1:${addr.port}`; + }, 120000); + + afterAll(async () => { + if (prodServer) { + await new Promise((resolve) => prodServer.close(() => resolve())); + } + if (tmpRoot) { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + it("preserves the page module file URL during server rendering", async () => { + const res = await fetch(`${prodUrl}/`); + expect(res.status).toBe(200); + const html = await res.text(); + const match = html.match(/
([^<]*)<\/div>/); + expect(match).not.toBeNull(); + const data = JSON.parse(decodeHtmlText(match![1])) as { url: string }; + + expect(data.url).toMatch(/^file:\/\/\//); + expect(data.url).toMatch(/\/pages\/index\.tsx$/); + expect(data.url).not.toContain("/dist/server/entry.js"); + }); + + it("normalizes the page module file URL in the client page chunk", () => { + const jsFiles = collectJavaScriptFiles(path.join(outDir, "client")); + const clientCode = jsFiles.map((file) => fs.readFileSync(file, "utf8")).join("\n"); + + expect(clientCode).toContain("file:///ROOT/pages/index.tsx"); + expect(clientCode).not.toContain("/dist/server/entry.js"); + }); + + it("renders the client module file URL in the browser after hydration", async () => { + const { chromium } = await import("@playwright/test"); + const browser = await chromium.launch({ headless: true }); + try { + const page = await browser.newPage(); + await page.goto(prodUrl, { waitUntil: "networkidle" }); + await page.waitForFunction( + () => document.getElementById("test-data")?.textContent?.includes("file:///ROOT/"), + undefined, + { timeout: 5000 }, + ); + const testData = await page.locator("#test-data").textContent(); + expect(testData).toBe(JSON.stringify({ url: "file:///ROOT/pages/index.tsx" })); + } finally { + await browser.close(); + } + }, 30000); +}); + describe("router __NEXT_DATA__ correctness (Pages Router)", () => { let routerServer: ViteDevServer; let routerBaseUrl: string; From 62f950361085108ee2442a162f770762378851b2 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Tue, 26 May 2026 14:06:26 +1000 Subject: [PATCH 2/8] fix(tests): run import.meta browser coverage in e2e The import-meta regression test launched Playwright from a Vitest integration shard. That shard does not install Playwright browser binaries in CI, so it failed before exercising the runtime behavior. Move the browser assertion into the existing pages-router-prod Playwright project, keep the lower-boundary server and client chunk assertions in Vitest, and cache the import-meta plugin root paths after an early source-text bailout to address review feedback. --- .../vinext/src/plugins/import-meta-url.ts | 62 +++++++++++++++---- .../e2e/pages-router-prod/production.spec.ts | 52 +++++++++++----- .../pages-basic/pages/import-meta.tsx | 7 +++ tests/pages-router.test.ts | 18 ------ 4 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 tests/fixtures/pages-basic/pages/import-meta.tsx diff --git a/packages/vinext/src/plugins/import-meta-url.ts b/packages/vinext/src/plugins/import-meta-url.ts index 31f2e034e..3ad637f78 100644 --- a/packages/vinext/src/plugins/import-meta-url.ts +++ b/packages/vinext/src/plugins/import-meta-url.ts @@ -18,19 +18,40 @@ type RewriteResult = { map: ReturnType; }; +type RootPaths = { + root: string; + canonicalRoot: string; + normalizedRoot: string; +}; + export function createImportMetaUrlPlugin(options: { getRoot: () => string | undefined }): Plugin { + let rootPaths: RootPaths | undefined; + + function getRootPaths(): RootPaths | undefined { + const root = options.getRoot(); + if (!root) return rootPaths; + if (!rootPaths || rootPaths.root !== root) { + rootPaths = createRootPaths(root); + } + return rootPaths; + } + return { name: "vinext:import-meta-url", enforce: "post", + configResolved(config) { + rootPaths = createRootPaths(options.getRoot() ?? config.root); + }, transform(code, id) { - const root = options.getRoot(); - if (!root) return null; + if (!code.includes("import.meta.url")) return null; + const paths = getRootPaths(); + if (!paths) return null; const cleanId = cleanModuleId(id); - if (!isTransformableUserModule(cleanId, root)) return null; + if (!isTransformableUserModule(cleanId, paths)) return null; const environment: ImportMetaUrlEnvironment = this.environment?.name === "client" ? "client" : "server"; - const rewritten = rewriteImportMetaUrl(code, cleanId, root, environment); + const rewritten = rewriteImportMetaUrlWithRootPaths(code, cleanId, paths, environment); if (!rewritten) return null; return { code: rewritten.code, @@ -47,6 +68,16 @@ export function rewriteImportMetaUrl( environment: ImportMetaUrlEnvironment, ): RewriteResult | null { if (!code.includes("import.meta.url")) return null; + return rewriteImportMetaUrlWithRootPaths(code, id, createRootPaths(root), environment); +} + +function rewriteImportMetaUrlWithRootPaths( + code: string, + id: string, + rootPaths: RootPaths, + environment: ImportMetaUrlEnvironment, +): RewriteResult | null { + if (!code.includes("import.meta.url")) return null; let ast: unknown; try { @@ -58,7 +89,7 @@ export function rewriteImportMetaUrl( const ranges = collectImportMetaUrlRanges(ast); if (ranges.length === 0) return null; - const replacement = JSON.stringify(importMetaUrlValue(id, root, environment)); + const replacement = JSON.stringify(importMetaUrlValue(id, rootPaths, environment)); const output = new MagicString(code); for (const range of ranges) { output.overwrite(range.start, range.end, replacement); @@ -74,19 +105,26 @@ function cleanModuleId(id: string): string { return id.split("?", 1)[0]; } -function isTransformableUserModule(id: string, root: string): boolean { +function createRootPaths(root: string): RootPaths { + const canonicalRoot = canonicalizePath(root); + return { + root, + canonicalRoot, + normalizedRoot: normalizePath(canonicalRoot), + }; +} + +function isTransformableUserModule(id: string, rootPaths: RootPaths): boolean { if (!id || id.startsWith("\0")) return false; if (!path.isAbsolute(id)) return false; if (id.includes("/node_modules/") || id.includes("\\node_modules\\")) return false; if (!/\.(tsx?|jsx?|mjs)$/.test(id)) return false; const canonicalId = canonicalizePath(id); - const canonicalRoot = canonicalizePath(root); const normalizedId = normalizePath(canonicalId); - const normalizedRoot = normalizePath(canonicalRoot); - if (!isPathWithin(normalizedId, normalizedRoot)) return false; + if (!isPathWithin(normalizedId, rootPaths.normalizedRoot)) return false; - const relativePath = normalizePath(path.relative(canonicalRoot, canonicalId)); + const relativePath = normalizePath(path.relative(rootPaths.canonicalRoot, canonicalId)); const firstSegment = relativePath.split("/")[0]; return ( firstSegment !== ".next" && firstSegment !== ".vinext-local-package" && firstSegment !== "dist" @@ -99,12 +137,12 @@ function isPathWithin(candidate: string, root: string): boolean { function importMetaUrlValue( id: string, - root: string, + rootPaths: RootPaths, environment: ImportMetaUrlEnvironment, ): string { const canonicalId = canonicalizePath(id); if (environment === "client") { - const relativePath = normalizePath(path.relative(canonicalizePath(root), canonicalId)); + const relativePath = normalizePath(path.relative(rootPaths.canonicalRoot, canonicalId)); return `file:///ROOT/${relativePath}`; } diff --git a/tests/e2e/pages-router-prod/production.spec.ts b/tests/e2e/pages-router-prod/production.spec.ts index f8ad2db23..c7b2b260b 100644 --- a/tests/e2e/pages-router-prod/production.spec.ts +++ b/tests/e2e/pages-router-prod/production.spec.ts @@ -60,27 +60,47 @@ test.describe("Pages Router Production Build", () => { expect(content).toContain("hello-world"); }); - // NOTE: Pages Router production build doesn't inject client