Files
Sebastian "Sebbie" Silbermann 55e7e1903a [test] Cover the prerender worker-thread backend with an addon we control (#97543)
`experimental.workerThreads` decides whether static generation runs in
real worker threads or forked child processes, and a native addon
declared with `NODE_MODULE` can only be loaded once per process. That is
why the flag defaults to false (#9199) and why the static export worker
was fixed to respect it rather than hardcoding threads on (#25063).
Nothing tested it: the only suite that tried, `firebase-grpc`, had its
assertion skipped since 2019, and modern `firebase` ships no native
module at all, so its remaining test asserted only that a build
succeeds.

This adds a `single-context-addon` fixture, deliberately declared with
`NODE_MODULE`, and a production suite asserting both directions: the
build succeeds by default and fails with "Module did not self-register"
once worker threads are enabled. The fixture loads the addon from
`next.config.js` as well as from the page, because that failure only
happens on a second `dlopen` within one process, so an addon loaded only
inside the worker would register there and the build would pass.

A third case documents a bug rather than intended behaviour. `next
build` runs Turbopack in a worker thread and that worker re-evaluates
`next.config.js`, so requiring a non-context-aware addon from the config
breaks the build even with `experimental.workerThreads` off. Webpack and
rspack are unaffected, since their build workers are forked child
processes. Isolated with an unguarded `require` and default flags,
Turbopack exits 1 with "Module did not self-register" where webpack
exits 0. It is the same class of failure #9199 and #25063 fixed, in a
worker those PRs did not touch. The assertion is branched on the bundler
and carries a note to drop the branch once Turbopack stops evaluating
the config on a worker thread. The `isMainThread` guard in the fixture's
config keeps the first two cases pointed at the static generation worker
instead.

A development suite covers the other direction, asserting that
evaluating a route does not put such an addon on one of the threads
`next dev` uses regardless of the flag, so it would catch a change that
moved route evaluation onto the dev validation pool. The expectation is
the same with and without Cache Components; both were checked by logging
the thread id from the page's module scope, and the route is evaluated
on the main thread either way.

`firebase-grpc` is removed, since it covered the same flag with a
skipped assertion and a vacuous one.
2026-08-20 11:57:04 +02:00

63 lines
2.0 KiB
JavaScript

// A minimal stand-in for the `bindings` npm package, reproducing only the part
// both bundlers model: locate the calling package's root by walking up for a
// `package.json`, then load the compiled binary from `build/Release`.
//
// Neither bundler evaluates this file. `@vercel/nft` maps the `bindings`
// specifier to its own bundled copy, and Turbopack maps it to
// `WellKnownFunctionKind::NodeBindings`; both then resolve the binary's path
// themselves. `build/Release/<name>` is node-gyp's default output location and
// is on both of their candidate lists, so this agrees with what they trace.
const fs = require('fs')
const path = require('path')
function getCallerFile() {
const { prepareStackTrace } = Error
try {
Error.prepareStackTrace = (_error, stack) => stack
for (const frame of new Error().stack) {
const fileName = frame.getFileName()
if (typeof fileName === 'string' && fileName !== __filename) {
return fileName
}
}
} finally {
Error.prepareStackTrace = prepareStackTrace
}
throw new Error('bindings: could not determine the calling file')
}
function getPackageRoot(file) {
let dir = path.dirname(file)
while (!fs.existsSync(path.join(dir, 'package.json'))) {
const parent = path.dirname(dir)
if (parent === dir) {
throw new Error(`bindings: found no package.json above ${file}`)
}
dir = parent
}
return dir
}
module.exports = function bindings(name) {
const binary = path.join(
getPackageRoot(getCallerFile()),
'build',
'Release',
name
)
if (!fs.existsSync(binary)) {
throw new Error(
`bindings: ${binary} does not exist. The fixture addon is compiled by ` +
`node-gyp during install, which requires the package to be listed in ` +
`the test's pnpm.onlyBuiltDependencies.`
)
}
// Deliberately not wrapped. A non-context-aware addon fails on this line with
// Node's own `ERR_DLOPEN_FAILED` / "Module did not self-register", and the
// worker-thread tests assert on that message.
return require(binary)
}