mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
cb96548418
Fixes #93427 ## What? This fixes Turbopack's handling of Node.js `worker_threads` entries created with a `URL` object: ```ts new Worker(new URL('./worker.ts', import.meta.url)) ``` Turbopack was resolving this form with the same project-root context used for string/path worker entries. That makes relative URL worker entries fail when the relative URL should be resolved from the module that constructs it. This PR keeps the existing behavior for string/path Node.js workers, but resolves relative URL worker entries from the module that creates the URL. ## Why? The issue reports a regression where this pattern worked in `next@16.1.7`, but fails in `next@16.2.4` and `next@16.3.0-canary.8`: ```ts new Worker(new URL('../../worker-entry.mjs', import.meta.url)) new Worker(new URL('../../worker-entry.mts', import.meta.url)) ``` The failure happens before the app can build because Turbopack tries to resolve the relative worker entry from the wrong context: ```text Module not found: Can't resolve '../../worker-entry.mjs' Module not found: Can't resolve '../../worker-entry.mts' ``` Node.js documents `Worker`'s `filename` argument as accepting either a string path or a WHATWG `URL` object. Relative string paths are resolved relative to the current working directory. By contrast, `new URL(relative, import.meta.url)` constructs a URL by resolving the relative specifier against the importing module's URL. webpack also documents `new Worker(new URL("./worker.js", import.meta.url))` as the supported Node.js `worker_threads` syntax. Matching that documented pattern avoids treating URL-object workers as if they were string-path workers. ## How? In `turbopack-ecmascript` worker reference analysis: - Detect relative `JsValue::Url` entries passed to the Node.js `Worker` constructor. - Use the importing module's parent directory as the resolution context for those URL entries. - Keep the existing traced project directory context for string/path worker entries and other Node.js worker forms. This keeps the fix scoped to the URL-object case from the issue while preserving the existing behavior for string-path workers. ## Tests Added an e2e regression test to the existing Turbopack-specific `node-worker-threads` suite: ```text test/e2e/app-dir/node-worker-threads ``` The new test covers a Route Handler that creates a worker with: ```ts new Worker(new URL('../../worker-dir/url-worker.ts', import.meta.url)) ``` Verified locally: ```text cargo fmt --check --package turbopack-ecmascript cargo check --package turbopack-ecmascript pnpm build-all pnpm test-dev-turbo test/e2e/app-dir/node-worker-threads/node-worker-threads.test.ts pnpm test-start-turbo test/e2e/app-dir/node-worker-threads/node-worker-threads.test.ts ``` <!-- NEXT_JS_LLM_PR --> Co-authored-by: Luke Sandberg <lukesandberg@users.noreply.github.com>
112 lines
4.1 KiB
TypeScript
112 lines
4.1 KiB
TypeScript
import { isNextDev, nextTestSetup } from 'e2e-utils'
|
|
|
|
describe('node-worker-threads', () => {
|
|
const { next, skipped, isTurbopack } = nextTestSetup({
|
|
files: __dirname,
|
|
skipDeployment: true,
|
|
dependencies: {
|
|
pino: '9.6.0',
|
|
jspdf: '4.2.1',
|
|
},
|
|
})
|
|
|
|
if (skipped) {
|
|
return
|
|
}
|
|
|
|
// These tests are Turbopack-specific since they rely on Turbopack's worker bundling
|
|
if (!isTurbopack) {
|
|
it.skip('webpack doesnt support bundling worker-threads', () => {})
|
|
return
|
|
}
|
|
|
|
it('should handle simple worker with relative path', async () => {
|
|
const res = await next.fetch('/api/simple-worker-test')
|
|
const data = await res.json()
|
|
expect(res.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(data.message).toBe('pong from simple worker')
|
|
})
|
|
|
|
it('should handle worker with new URL(..., import.meta.url)', async () => {
|
|
const res = await next.fetch('/api/url-worker-test')
|
|
const data = await res.json()
|
|
expect(res.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(data.message).toBe('pong from url worker')
|
|
})
|
|
|
|
it('should handle self-referencing worker with __filename', async () => {
|
|
const res = await next.fetch('/api/worker-test')
|
|
const data = await res.json()
|
|
expect(res.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(data.message).toBe('pong')
|
|
})
|
|
|
|
it('should handle pino logger with transport (thread-stream)', async () => {
|
|
// Pino with transports uses thread-stream internally, which creates worker_threads
|
|
// with a broad pattern like join(__dirname, 'lib', 'worker.js') that can match
|
|
// non-evaluatable files like package.json. This tests that we properly downgrade
|
|
// those errors to warnings via loose_errors.
|
|
const res = await next.fetch('/api/pino-test')
|
|
const data = await res.json()
|
|
expect(res.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
})
|
|
|
|
it('should not expose __turbopack internal workerData to user code', async () => {
|
|
// Verify that internal __turbopack_globals__ data used to forward globals
|
|
// is not visible to user code accessing workerData
|
|
const res = await next.fetch('/api/workerdata-check')
|
|
const data = await res.json()
|
|
expect(res.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
// The __turbopack_globals__ key should NOT be visible to user code
|
|
expect(data.hasTurbopackKeys).toBe(false)
|
|
expect(data.turbopackKeys).toEqual([])
|
|
})
|
|
|
|
it('should handle jsPDF which uses Worker with eval: true (issue #91642)', async () => {
|
|
// jsPDF internally creates Worker threads with { eval: true }, passing
|
|
// inline JS code instead of a file path. Turbopack should not try to
|
|
// resolve the first argument as a module reference in this case.
|
|
const res = await next.fetch('/api/jspdf-test')
|
|
const data = await res.json()
|
|
expect(res.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(data.size).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should handle PNG file import in worker', async () => {
|
|
// Test that static assets (like PNG images) can be imported and used in workers
|
|
// The server worker returns the PNG URL, then we fetch it from the client
|
|
// to verify the URL is correctly formed and accessible
|
|
const res = await next.fetch('/api/png-worker-test')
|
|
const data = await res.json()
|
|
expect(res.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(data.pngInfo).toBeDefined()
|
|
expect(data.pngInfo.width).toBe(1)
|
|
expect(data.pngInfo.height).toBe(1)
|
|
|
|
const url = new URL(data.pngInfo.url, 'http://localhost')
|
|
expect(url.pathname).toMatch(
|
|
/\/_next\/static.*\/test-image\.[0-9a-z_-]+\.png/
|
|
)
|
|
if (!isNextDev) {
|
|
if (next.assetToken) {
|
|
expect(url.searchParams.get('dpl')).toBe(next.assetToken)
|
|
} else {
|
|
expect(url.searchParams.get('dpl')).toBeNull()
|
|
}
|
|
}
|
|
|
|
// Now fetch the PNG URL from the client to verify it's accessible
|
|
// This tests that the URL generated by the server worker is correct
|
|
const pngRes = await next.fetch(data.pngInfo.url)
|
|
expect(pngRes.status).toBe(200)
|
|
expect(pngRes.headers.get('content-type')).toBe('image/png')
|
|
})
|
|
})
|