mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
### What? `import.meta.url` evaluated in a Turbopack-compiled module on Windows produced an invalid file URI: the path portion contained backslashes (`\`) and was not URL-encoded, e.g. ``` file://C:\Users\me\project\apps\web\app\page.tsx ``` This affects any code that hands the value to `URL`, `fileURLToPath`, source maps, dev tools, or anything else that expects a [valid file URI](https://en.wikipedia.org/wiki/File_URI_scheme). This PR makes `import.meta.url` return a correct, percent-encoded file URI on all platforms, e.g. `file:///C:/Users/me/project/apps/web/app/page.tsx`. ### Why? The existing codegen in `references/esm/meta.rs` emitted ```js `file://${__turbopack_context__.P("<rel/path>")}` ``` where `__turbopack_context__.P` is the runtime helper `resolveAbsolutePath`. On the Node-side runtime (`shared-node/node-externals-utils.ts`) that helper returns `path.join(ABSOLUTE_ROOT, modulePath)`, which is OS-native — so on Windows it returns a backslash-separated path with no percent-encoding. The result was then concatenated into a `file://...` template literal, producing an invalid URI. The initial hint that `turbopack-core/src/source_map/utils.rs` was the culprit turned out to be a false lead: that code path uses `FileSystemPath::try_join`, which already normalizes `\` to `/` on Windows. The bug is in the codegen + runtime helper for `import.meta.url`, not in source-map handling. ### How? - Introduce a new runtime function `__turbopack_resolve_file_url__` (shortcut `F`) whose contract is to return a complete `file://` URI for a given relative module path. - Node implementation (`shared-node/node-externals-utils.ts`): uses `url.pathToFileURL(resolveAbsolutePath(modulePath)).href`. `pathToFileURL` handles drive letters (`file:///C:/...`), normalizes slashes, and percent-encodes path segments. - Browser implementation (`browser/runtime/base/runtime-base.ts`): returns `` `file:///ROOT/${modulePath ?? ''}` `` — the browser runtime intentionally does not expose the real filesystem path, so it returns the same stable placeholder as before, just with a valid `file:///` prefix instead of having callers concatenate one. - Change the `import.meta.url` codegen in `references/esm/meta.rs` to call `__turbopack_resolve_file_url__($formatted)` directly instead of concatenating a `file://` prefix to the absolute path. The pre-existing `encode_path` is kept only to keep the embedded JS string literal safe (the runtime helper is responsible for the URI-level encoding). - Update `TurbopackBaseContext` and add a `ResolveFileUrl` type alias in `runtime-types.d.ts` so the new shortcut is typed. - Register the new shortcut in `TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS` so user code can also reference `__turbopack_resolve_file_url__` (mirroring the existing `__turbopack_resolve_absolute_path__`). `P` is left in place and unchanged because it's still used elsewhere. - Regenerate the affected Turbopack snapshot fixtures (`import-meta/*`, `comptime/typeof`, `runtime/default_*_runtime`, `workers/*`, `debug-ids/*`, etc.). The functional change in the generated code is uniform: `` `file://${__turbopack_context__.P(<rel>)}` `` → `__turbopack_context__.F(<rel>)`. - Drop the `// TODO: These file URIs are wrong on turbopack+windows` comment and the `.replaceAll('\\', '/')` workarounds from `test/e2e/app-dir/non-root-project-monorepo/non-root-project-monorepo.test.ts`, so the assertion now verifies that the URI is correctly slashed and encoded on all platforms. ### Verification - `cargo clippy --workspace --all-targets` — clean - `cargo fmt --check` — clean - `cargo test -p turbopack-tests --test snapshot` — 87/87 pass - `cargo test -p turbopack-tests --test execution` — 219/219 pass (1 ignored) - `pnpm test-dev-turbo test/e2e/app-dir/non-root-project-monorepo/non-root-project-monorepo.test.ts` — 9/9 pass - `pnpm test-start-turbo test/e2e/app-dir/non-root-project-monorepo/non-root-project-monorepo.test.ts` — 6/6 pass - `pnpm test-dev-webpack test/e2e/app-dir/non-root-project-monorepo/non-root-project-monorepo.test.ts` — 9/9 pass (verifying webpack path still works) Windows itself can't be exercised in the sandbox, but the new code path no longer depends on `path.sep` or any OS-native string manipulation for the URI: `url.pathToFileURL` is the canonical Node API for producing a file URI from any OS-native path, and the test assertion has been tightened so that a regression on Windows would fail in CI rather than be papered over. <!-- NEXT_JS_LLM_PR --> --------- Co-authored-by: v-work-app[bot] <262237222+v-work-app[bot]@users.noreply.github.com> Co-authored-by: Claude <[email protected]> Co-authored-by: Tobias Koppers <[email protected]>
310 lines
11 KiB
TypeScript
310 lines
11 KiB
TypeScript
import { nextTestSetup, FileRef } from 'e2e-utils'
|
|
import {
|
|
waitForRedbox,
|
|
waitForNoRedbox,
|
|
getRedboxCallStack,
|
|
getRedboxSource,
|
|
} from 'next-test-utils'
|
|
import * as path from 'path'
|
|
|
|
describe('non-root-project-monorepo', () => {
|
|
const { next, skipped, isTurbopack, isNextDev, isRspack } = nextTestSetup({
|
|
files: {
|
|
apps: new FileRef(path.resolve(__dirname, 'apps')),
|
|
packages: new FileRef(path.resolve(__dirname, 'packages')),
|
|
'pnpm-workspace.yaml': `packages:
|
|
- 'apps/*'
|
|
- 'packages/*'
|
|
`,
|
|
},
|
|
packageJson: require('./package.json'),
|
|
buildCommand: 'pnpm build',
|
|
startCommand: (global as any).isNextDev ? 'pnpm dev' : 'pnpm start',
|
|
installCommand: 'pnpm i',
|
|
skipDeployment: true,
|
|
})
|
|
|
|
if (skipped) {
|
|
return
|
|
}
|
|
|
|
describe('monorepo-package', () => {
|
|
it('should work during RSC', async () => {
|
|
const $ = await next.render$('/monorepo-package-rsc')
|
|
expect($('p').text()).toBe('Hello Typescript')
|
|
})
|
|
|
|
it('should work during SSR', async () => {
|
|
const $ = await next.render$('/monorepo-package-ssr')
|
|
expect($('p').text()).toBe('Hello Typescript')
|
|
})
|
|
|
|
it('should work on client-side', async () => {
|
|
const browser = await next.browser('/monorepo-package-ssr')
|
|
expect(await browser.elementByCss('p').text()).toBe('Hello Typescript')
|
|
await waitForNoRedbox(browser)
|
|
expect(await browser.elementByCss('p').text()).toBe('Hello Typescript')
|
|
await browser.close()
|
|
})
|
|
})
|
|
|
|
describe('import.meta.url', () => {
|
|
it('should work during RSC', async () => {
|
|
const $ = await next.render$('/import-meta-url-rsc')
|
|
expect($('p').text()).toMatch(
|
|
/^file:\/\/.*\/next-install-[^/]+\/apps\/web\/app\/import-meta-url-rsc\/page.tsx$/
|
|
)
|
|
})
|
|
|
|
it('should work during SSR', async () => {
|
|
const $ = await next.render$('/import-meta-url-ssr')
|
|
expect($('p').text()).toMatch(
|
|
/^file:\/\/.*\/next-install-[^/]+\/apps\/web\/app\/import-meta-url-ssr\/page.tsx$/
|
|
)
|
|
})
|
|
|
|
it('should work on client-side', async () => {
|
|
const browser = await next.browser('/import-meta-url-ssr')
|
|
await waitForNoRedbox(browser)
|
|
if (isTurbopack) {
|
|
// Turbopack intentionally doesn't expose the full path to the browser bundles
|
|
expect(await browser.elementByCss('p').text()).toBe(
|
|
'file:///ROOT/apps/web/app/import-meta-url-ssr/page.tsx'
|
|
)
|
|
} else {
|
|
expect(await browser.elementByCss('p').text()).toMatch(
|
|
/^file:\/\/.*\/next-install-[^/]+\/apps\/web\/app\/import-meta-url-ssr\/page.tsx$/
|
|
)
|
|
}
|
|
await browser.close()
|
|
})
|
|
|
|
// Verifies that non-URL-safe characters in file paths (here: a literal
|
|
// space) are correctly percent-encoded in the resulting `file://` URI.
|
|
describe('non-url-safe characters', () => {
|
|
it('should encode special chars during RSC', async () => {
|
|
const $ = await next.render$('/import-meta-url-encoded-rsc')
|
|
expect($('p').text()).toMatch(
|
|
/^file:\/\/.*\/next-install-[^/]+\/apps\/web\/app\/import-meta-url-encoded-rsc\/with%20space.ts$/
|
|
)
|
|
})
|
|
|
|
it('should encode special chars during SSR', async () => {
|
|
const $ = await next.render$('/import-meta-url-encoded-ssr')
|
|
expect($('p').text()).toMatch(
|
|
/^file:\/\/.*\/next-install-[^/]+\/apps\/web\/app\/import-meta-url-encoded-ssr\/with%20space.ts$/
|
|
)
|
|
})
|
|
|
|
it('should encode special chars on client-side', async () => {
|
|
const browser = await next.browser('/import-meta-url-encoded-ssr')
|
|
await waitForNoRedbox(browser)
|
|
if (isTurbopack) {
|
|
// Turbopack intentionally doesn't expose the full path to the browser bundles
|
|
expect(await browser.elementByCss('p').text()).toBe(
|
|
'file:///ROOT/apps/web/app/import-meta-url-encoded-ssr/with%20space.ts'
|
|
)
|
|
} else {
|
|
expect(await browser.elementByCss('p').text()).toMatch(
|
|
/^file:\/\/.*\/next-install-[^/]+\/apps\/web\/app\/import-meta-url-encoded-ssr\/with%20space.ts$/
|
|
)
|
|
}
|
|
await browser.close()
|
|
})
|
|
})
|
|
})
|
|
|
|
if (isNextDev) {
|
|
describe('source-maps', () => {
|
|
it('should work on RSC', async () => {
|
|
const browser = await next.browser('/source-maps-rsc')
|
|
await waitForRedbox(browser)
|
|
|
|
if (isTurbopack) {
|
|
// TODO the function name should be hidden
|
|
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
|
|
"app/separate-file.ts (1:7) @ module evaluation
|
|
|
|
> 1 | throw new Error('Expected error')
|
|
| ^
|
|
2 |"
|
|
`)
|
|
expect(await getRedboxCallStack(browser)).toMatchInlineSnapshot(`
|
|
[
|
|
"module evaluation app/separate-file.ts (1:7)",
|
|
"innerArrowFunction app/source-maps-rsc/page.tsx (13:28)",
|
|
"innerFunction app/source-maps-rsc/page.tsx (10:3)",
|
|
"Page app/source-maps-rsc/page.tsx (4:5)",
|
|
]
|
|
`)
|
|
} else if (isRspack) {
|
|
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
|
|
"app/separate-file.ts (1:11) @ eval
|
|
|
|
> 1 | throw new Error('Expected error')
|
|
| ^
|
|
2 |"
|
|
`)
|
|
expect(await getRedboxCallStack(browser)).toMatchInlineSnapshot(`
|
|
[
|
|
"eval app/separate-file.ts (1:11)",
|
|
"<FIXME-file-protocol>",
|
|
"<FIXME-file-protocol>",
|
|
"innerArrowFunction app/source-maps-rsc/page.tsx (14:3)",
|
|
"innerFunction app/source-maps-rsc/page.tsx (10:3)",
|
|
"Page app/source-maps-rsc/page.tsx (4:5)",
|
|
]
|
|
`)
|
|
} else {
|
|
// TODO the function name is incorrect
|
|
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
|
|
"app/separate-file.ts (1:11) @ eval
|
|
|
|
> 1 | throw new Error('Expected error')
|
|
| ^
|
|
2 |"
|
|
`)
|
|
// TODO(veil): webpack runtime code shouldn't be included in stack trace (see https://linear.app/vercel/issue/NDX-509)
|
|
// TODO(veil): https://linear.app/vercel/issue/NDX-677
|
|
expect(await getRedboxCallStack(browser)).toMatchInlineSnapshot(`
|
|
[
|
|
"eval app/separate-file.ts (1:11)",
|
|
"<FIXME-file-protocol>",
|
|
"innerArrowFunction app/source-maps-rsc/page.tsx (14:3)",
|
|
"innerFunction app/source-maps-rsc/page.tsx (10:3)",
|
|
"Page app/source-maps-rsc/page.tsx (4:5)",
|
|
]
|
|
`)
|
|
}
|
|
await browser.close()
|
|
})
|
|
|
|
it('should work on SSR', async () => {
|
|
const browser = await next.browser('/source-maps-ssr')
|
|
await waitForRedbox(browser)
|
|
|
|
if (isTurbopack) {
|
|
// TODO the function name should be hidden
|
|
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
|
|
"app/separate-file.ts (1:7) @ module evaluation
|
|
|
|
> 1 | throw new Error('Expected error')
|
|
| ^
|
|
2 |"
|
|
`)
|
|
expect(await getRedboxCallStack(browser)).toMatchInlineSnapshot(`
|
|
[
|
|
"module evaluation app/separate-file.ts (1:7)",
|
|
"innerArrowFunction app/source-maps-ssr/page.tsx (15:28)",
|
|
"innerFunction app/source-maps-ssr/page.tsx (12:3)",
|
|
"Page app/source-maps-ssr/page.tsx (6:5)",
|
|
]
|
|
`)
|
|
} else if (isRspack) {
|
|
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
|
|
"app/separate-file.ts (1:7) @ eval
|
|
|
|
> 1 | throw new Error('Expected error')
|
|
| ^
|
|
2 |"
|
|
`)
|
|
expect(await getRedboxCallStack(browser)).toMatchInlineSnapshot(`
|
|
[
|
|
"eval app/separate-file.ts (1:7)",
|
|
"<FIXME-next-dist-dir>",
|
|
"<FIXME-next-dist-dir>",
|
|
"<FIXME-next-dist-dir>",
|
|
"innerArrowFunction app/source-maps-ssr/page.tsx (16:3)",
|
|
"innerFunction app/source-maps-ssr/page.tsx (12:3)",
|
|
"Page app/source-maps-ssr/page.tsx (6:5)",
|
|
]
|
|
`)
|
|
} else {
|
|
// TODO the function name should be hidden
|
|
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
|
|
"app/separate-file.ts (1:7) @ eval
|
|
|
|
> 1 | throw new Error('Expected error')
|
|
| ^
|
|
2 |"
|
|
`)
|
|
// TODO(veil): webpack runtime code shouldn't be included in stack trace (see https://linear.app/vercel/issue/NDX-509)
|
|
expect(await getRedboxCallStack(browser)).toMatchInlineSnapshot(`
|
|
[
|
|
"eval app/separate-file.ts (1:7)",
|
|
"<FIXME-next-dist-dir>",
|
|
"innerArrowFunction app/source-maps-ssr/page.tsx (16:3)",
|
|
"innerFunction app/source-maps-ssr/page.tsx (12:3)",
|
|
"Page app/source-maps-ssr/page.tsx (6:5)",
|
|
]
|
|
`)
|
|
}
|
|
await browser.close()
|
|
})
|
|
|
|
it('should work on client-side', async () => {
|
|
const browser = await next.browser('/source-maps-client')
|
|
await waitForRedbox(browser)
|
|
|
|
if (isTurbopack) {
|
|
// TODO the function name should be hidden
|
|
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
|
|
"app/separate-file.ts (1:7) @ module evaluation
|
|
|
|
> 1 | throw new Error('Expected error')
|
|
| ^
|
|
2 |"
|
|
`)
|
|
expect(await getRedboxCallStack(browser)).toMatchInlineSnapshot(`
|
|
[
|
|
"module evaluation app/separate-file.ts (1:7)",
|
|
"innerArrowFunction app/source-maps-client/page.tsx (16:28)",
|
|
"innerFunction app/source-maps-client/page.tsx (13:3)",
|
|
"effectCallback app/source-maps-client/page.tsx (7:5)",
|
|
]
|
|
`)
|
|
} else if (isRspack) {
|
|
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
|
|
"app/separate-file.ts (1:7) @ eval
|
|
|
|
> 1 | throw new Error('Expected error')
|
|
| ^
|
|
2 |"
|
|
`)
|
|
expect(await getRedboxCallStack(browser)).toMatchInlineSnapshot(`
|
|
[
|
|
"eval app/separate-file.ts (1:7)",
|
|
"<FIXME-next-dist-dir>",
|
|
"<FIXME-next-dist-dir>",
|
|
"<FIXME-next-dist-dir>",
|
|
"innerArrowFunction app/source-maps-client/page.tsx (17:3)",
|
|
"innerFunction app/source-maps-client/page.tsx (13:3)",
|
|
"effectCallback app/source-maps-client/page.tsx (7:5)",
|
|
]
|
|
`)
|
|
} else {
|
|
// TODO the function name should be hidden
|
|
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
|
|
"app/separate-file.ts (1:7) @ eval
|
|
|
|
> 1 | throw new Error('Expected error')
|
|
| ^
|
|
2 |"
|
|
`)
|
|
// TODO(veil): webpack runtime code shouldn't be included in stack trace (see https://linear.app/vercel/issue/NDX-509)
|
|
expect(await getRedboxCallStack(browser)).toMatchInlineSnapshot(`
|
|
[
|
|
"eval app/separate-file.ts (1:7)",
|
|
"<FIXME-next-dist-dir>",
|
|
"innerArrowFunction app/source-maps-client/page.tsx (17:3)",
|
|
"innerFunction app/source-maps-client/page.tsx (13:3)",
|
|
"effectCallback app/source-maps-client/page.tsx (7:5)",
|
|
]
|
|
`)
|
|
}
|
|
await browser.close()
|
|
})
|
|
})
|
|
}
|
|
})
|