You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Turbopack 16.3: postcss and webpack-loaders Node.js pools race to overwrite one [turbopack]_runtime.js, breaking cold builds (follow-up to #96591) #96619
A Turbopack next build project with both a postcss.config.js and any .scss file. The linked repro is six source files: app/layout.js, app/page.js, one CSS module, one SCSS module, an empty postcss.config.js, package.json.
Build with a cold .next.
In a large enough app, cold builds fail intermittently:
Turbopack build failed with 7 errors:
./src/styles/<font>.module.css
Error: Error evaluating Node.js code
TypeError: __turbopack_context__.a is not a function
at mod (postcss.config.js_.loader.mjs:11:31) [.next/build/chunks/[root-of-the-server]__<hash>._.js:56:30]
Every module that goes through postcss fails; which modules, and how many, differs run to run.
#96591 reports the same failure and identified the trigger, PR #94376, along with the .cjs workaround. It was closed automatically because its reproduction link was not live yet
at filing time. Opening this as a fresh issue per the bot's instructions, with the repro public
up front and with an explanation of the intermittency that report does not cover.
Current vs. Expected behavior
Current. A build with a postcss config and any SCSS spawns two Turbopack Node.js transform
pools, postcss and webpack-loaders, and both write their runtime to the same path, .next/build/chunks/[turbopack]_runtime.js. Since PR #94376 ("Smaller runtime size" in the 16.3 blog post) ships top-level-async support only
when async_module_info is non-empty, the two variants are no longer identical: the webpack-loaders variant omits contextPrototype.a = asyncModule.
The generated postcss.config.js_.loader.mjs uses top-level await, so its chunk calls __turbopack_context__.a(...). Whichever pool writes the shared file last decides whether the
helper is there - so the same source tree builds or fails nondeterministically.
ab.sh in the repro shows the two variants deterministically, by running one pool at a time:
build
pool
runtime size
asyncModule occurrences
SCSS import removed
postcss only
36110 B
2
postcss.config.js removed
webpack-loaders only
31680 B
0
default
both, same output path
last writer wins
n/a
The same two variants appear in the real app where we hit this (36116 B / 31686 B - the 6-byte
delta is longer paths). Failing builds there have the smaller, helper-less runtime.
loader.sh shows the other half, also deterministically - renaming the identical config file .js -> .cjs removes the async loader entirely:
config
chunks referencing an async config loader
loader emitted
asyncModule in runtime
postcss.config.js
4
postcss.config.js_.loader.mjs
2
postcss.config.cjs
0
none
0
Expected. The shared runtime contains every helper the chunks written against it reference -
or each pool writes its own runtime file.
A warm .next masks the bug - a leftover runtime may already contain the helper. We hit it
after making CI key its .next cache by Next.js version, which made every CI build cold.
Caveat on the repro: ab.sh and loader.sh prove both halves of the mechanism
deterministically, but the minimal project does not lose the race - the postcss pool
consistently writes last there, so repro.sh (N cold builds) stays green. We could not shrink
the race itself: 40+ mixed CSS/SCSS routes, App Router vs Pages Router, SCSS-first vs CSS-first
module graphs, a lone dynamically imported SCSS, and an npm-workspaces monorepo layout all
stayed green. In our real app (Pages Router, pnpm monorepo, Panda CSS + SCSS) it fails on
roughly 2 of 9 cold builds on 16.3.0, and 3 of 4 on 16.3.1-canary.0 - the rate seems sensitive
to machine load.
Present in 16.3.0 and 16.3.1-canary.0. 16.2.12 is unaffected.
Suggested fix
As in #96591: for the Node.js evaluate runtimes, either always include top-level-async support,
or ensure the virtual _.loader.mjs lands in async_module_info before runtime assembly, or
avoid top-level await in that loader. Additionally, having the two pools write to distinct paths
would remove the nondeterminism regardless.
Link to the code that reproduces this issue
https://github.com/finnan444/nextjs-turbopack-runtime-pool-race
To Reproduce
next buildproject with both apostcss.config.jsand any.scssfile. The linked repro is six source files:app/layout.js,app/page.js, one CSS module, one SCSS module, an emptypostcss.config.js,package.json..next.Every module that goes through postcss fails; which modules, and how many, differs run to run.
#96591 reports the same failure and identified the trigger, PR #94376, along with the
.cjsworkaround. It was closed automatically because its reproduction link was not live yetat filing time. Opening this as a fresh issue per the bot's instructions, with the repro public
up front and with an explanation of the intermittency that report does not cover.
Current vs. Expected behavior
Current. A build with a postcss config and any SCSS spawns two Turbopack Node.js transform
pools,
postcssandwebpack-loaders, and both write their runtime to the same path,.next/build/chunks/[turbopack]_runtime.js. Since PR #94376 ("Smaller runtime size" in the16.3 blog post) ships top-level-async support only
when
async_module_infois non-empty, the two variants are no longer identical: thewebpack-loadersvariant omitscontextPrototype.a = asyncModule.The generated
postcss.config.js_.loader.mjsuses top-levelawait, so its chunk calls__turbopack_context__.a(...). Whichever pool writes the shared file last decides whether thehelper is there - so the same source tree builds or fails nondeterministically.
ab.shin the repro shows the two variants deterministically, by running one pool at a time:asyncModuleoccurrencespostcss.config.jsremovedThe same two variants appear in the real app where we hit this (36116 B / 31686 B - the 6-byte
delta is longer paths). Failing builds there have the smaller, helper-less runtime.
loader.shshows the other half, also deterministically - renaming the identical config file.js->.cjsremoves the async loader entirely:asyncModulein runtimepostcss.config.jspostcss.config.js_.loader.mjspostcss.config.cjsExpected. The shared runtime contains every helper the chunks written against it reference -
or each pool writes its own runtime file.
Notes
postcss.config.js->.cjsor.mjs.Turbopack only emits the async
_.loader.mjsfor configs whose path ends in.js..nextmasks the bug - a leftover runtime may already contain the helper. We hit itafter making CI key its
.nextcache by Next.js version, which made every CI build cold.ab.shandloader.shprove both halves of the mechanismdeterministically, but the minimal project does not lose the race - the postcss pool
consistently writes last there, so
repro.sh(N cold builds) stays green. We could not shrinkthe race itself: 40+ mixed CSS/SCSS routes, App Router vs Pages Router, SCSS-first vs CSS-first
module graphs, a lone dynamically imported SCSS, and an npm-workspaces monorepo layout all
stayed green. In our real app (Pages Router, pnpm monorepo, Panda CSS + SCSS) it fails on
roughly 2 of 9 cold builds on 16.3.0, and 3 of 4 on 16.3.1-canary.0 - the rate seems sensitive
to machine load.
16.3.0and16.3.1-canary.0.16.2.12is unaffected.Suggested fix
As in #96591: for the Node.js evaluate runtimes, either always include top-level-async support,
or ensure the virtual
_.loader.mjslands inasync_module_infobefore runtime assembly, oravoid top-level await in that loader. Additionally, having the two pools write to distinct paths
would remove the nondeterminism regardless.
Provide environment information
Which area(s) are affected?
Turbopack
Which stage(s) are affected?
next build (local), next build (Vercel/CI)