Files
vercel__next.js/test/e2e/app-dir/node-worker-threads/node-worker-threads.test.ts
Public Theta cb96548418 Fix Turbopack worker_threads URL resolution (#93432)
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>
2026-05-12 21:43:06 +00:00

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')
})
})