This adds tree-shaking support for this syntax:
```javascript
import * as ns from './lib';
ns["foo"]
```
Instead of expanding the usage to `All` for this one, we can restrict
ourself to just "foo".
See attached fixture.
For modules that we were not using, that we're asynchronously loading,
we would still generate the loader but the file would not exist. This
meant building a Next.js app with:
```javascript
// pure.js — side-effect free: nothing observable happens on evaluation
export const version = '1.0.0'
// app/page.tsx
export default async function Page() {
const {} = await import('../pure') // ← nothing destructured out
return <p>hello world</p>
}
```
Would error with:
```
▲ Next.js 16.3.0-canary.87 (Turbopack)
Creating an optimized production build ...
FATAL: An unexpected Turbopack error occurred.
> Build error occurred
Error [TurbopackInternalError]: Failed to write app endpoint /page
Caused by:
- ModuleId not found for ident: [project]/pure.js [app-rsc] (ecmascript, async loader)
Debug info:
...
- Execution of PatternMapping::resolve_request failed
- ModuleId not found for ident: [project]/pure.js [app-rsc] (ecmascript, async loader)
```
Now, we re-write those loaders to empty promises:
```diff
async function evaluationOnly() {
- const {} = await __turbopack_context__.A("…/pure.js [test] (ecmascript, async loader)");
+ const {} = await Promise.resolve({});
}
async function named() {
const { used } = await __turbopack_context__.A("…/named.js [test] (ecmascript, async loader)");
console.log(used);
}
async function pattern(locale) {
const {} = await __turbopack_context__.f({
"./locales/effectful.js": {
id: ()=>"…/locales/effectful.js [test] (ecmascript, async loader)",
module: ()=>__turbopack_context__.A("…/locales/effectful.js …")
},
"./locales/pure.js": {
- id: ()=>"…/locales/pure.js [test] (ecmascript, async loader)",
- module: ()=>__turbopack_context__.A("…/locales/pure.js …")
+ id: ()=>undefined,
+ module: ()=>Promise.resolve({})
}
}).import(`./locales/${locale}.js`);
}
```
It turns `UnusedReferences` into a map between references and target
that can be dropped, where the target IDs are the unused targets. It
then passes the reference through to `pattern_mapping.rs` so it can
determine the `dropped_targets`. It needs to be target-specific as you
can have a situation (like above) where some targets are pure and some
aren't.
Watching too many files can hurt performance, and impacts how much work
Turbopack is doing. See for example the conversation around
https://github.com/vercel/next.js/pull/94597.
This PR handles the following four cases:
- Git worktrees: we can include a guard to prevent exiting a worktree
(by looking at the `.git` file).
- Git repository boundaries: we can include a guard to prevent us from
exiting a checked-out repository and watching the repository’s parent
directory.
- We will log a warning if this happens.
- Empty / bogus `package.lock` files & `package-lock.json` files (and
equivalents using other package managers): if we spot a
`package-lock.json` file but it has no packages or a `package-lock.json`
w/o an associated `package.json` file. We can ignore them. This handles
the case of accidentally running `npm install` in any directory.
- Home directory detection: we can detect if we’re going to start
watching your home dir and rollback up if this is the case.
- We will log a warning if this happens.
- If your Next config is in your home dir, we will still run your app
there as Next config is the furthest we can rollback
- NPM, Yarn, Lerna & Bun workspaces: based on the `workspaces` field in
the `package.json` or `lerna.json`
I would recommend using split-view to review. I also cleaned up the
`warnDuplicatedLockFiles()` function because it was a bit unnecessary to
repeat the full message.
Closes https://github.com/vercel/next.js/issues/92978
Used Claude + this tool (https://github.com/vercel/next.js/pull/96158)
to look at Turbopack's runtime. With a large fixture with a lot of
imports, this part was bailing out:
```
{
"kind": "deopt",
"bailoutType": "deopt-eager",
"reason": "wrong map",
"functionName": "getChunkRelativeUrl",
"original": {
"source": "turbopack:///[turbopack]/browser/runtime/base/runtime-base.ts",
"line": 486,
"column": 11
},
"severity": "high"
}
```
After the fix, this went away.
---------
Co-authored-by: Luke Sandberg <[email protected]>
Imports the tests that Webpack uses for CJS tree-shaking. One test suite
is skipped `deep-reexports`, that's because we don't support that deep
of analysis at the moment and we just bail. WIP because after landing
this stack I will work on improving this.
I did have to make some edits to the tests, a summary is below (courtesy
of Claude):
```
1. Stripped ?query request suffixes (all cases).
Webpack uses require("./x?1") / ?2 to force distinct module instances. Turbopack shares one instance across require sites and aggregates usage, so the queries were removed (require("./x")). The behavioral assertions hold on a shared instance.
2. Distinct copies where per-instance semantics are genuinely required.
A few cases actually depend on separate instances, so we replaced the query with real duplicate files:
- mutate → module-1.js / module-2.js (so mutating one import doesn't leak into the other).
- reexports → add-to-counter-1/2/3.js (the counter must increment 3×).
- reexports-define-property → add-to-counter-1/2.js.
3. Removed __resourceQuery (webpack module-query magic — undefined in turbopack).
In reexports and reexports-define-property: require("./m" + __resourceQuery) → require("./m"), and the dynamic dispatch module reexport.js (require("./" + __resourceQuery.slice(1))) was split into static reexport-direct.js / reexport-indirect.js.
4. Commented out __webpack_exports_info__ / usedExports assertions.
Webpack exposes per-export usage introspection (x.usedExports, __webpack_exports_info__.x.used) that turbopack doesn't populate for CJS. Those specific assertions were commented (the real behavioral checks kept) in: require-member-access, require-member-access-deferred, require-destructuring, this-in-exported-function(-modern), reexport-require-binding, mutate, cjs-to-esm.
5. Case-specific rewrites.
- cjs-to-esm — dropped the require.cache[require.resolve(...)].exports comparison (webpack runtime introspection).
- namespace — changed expect(await import(x)).toBe(require(x)) (object identity) to assert the observable exports (ns.abc, ns.default); turbopack's import() yields an exotic Module namespace, never the same/deep-equal object as require().
6. mjs removed entirely — built on __resourceQuery + per-query instances + .js/.mjs interop divergence; can't even evaluate in turbopack and tests behavior turbopack doesn't share.
```
All tests should be passing.
This PR adds support for `turbopackRemoveUnusedImports` in CJS modules.
If modules are side-effect free and their imports are unused we do the
following re-writes:
```
require('./pure') → 0;
console.log('x') console.log('x')
```
```
const x = require('./pure') → const x = 0
```
```
const dead = require('./pure'), keep = 'v' → const dead = 0, keep = 'v'
```
```
const {} = require('./pure') → const {} = 0
```
```
if (cond) require('./pure') → if (cond) 0;
```
SWC should then drop the 0.
This PR introduces a new method `analyze_require_usage` that analyzes
how something imported with `require()` is used. It has two passes: one
catches simple things like `const { a } = require(...)` and
`require(...).foo`, the next one then handles if you import using `const
x = require(...)`.
This then gets populated into `export_usage_info`, this will expand the
set of exports we can drop through
https://github.com/vercel/next.js/pull/95716.
Noticed this bug where we were assuming chunks were plain strings, which
may not always be the case given #95261. When putting together
https://github.com/react/react/pull/37095, I noticed this was also a
spot we were missing so this fixes that.
Almost all of this diff is a regression test.
This has been causing a lot of confusion when contributors have been
debugging tree shaking, eg.
https://github.com/vercel/next.js/issues/95698 and the linked issues.
Turbopack's standard tree-shaking features are controlled through the
`turbopackRemoveUnusedExports` and `turbopackRemoveUnusedImports`
options which default to true.
This PR introduces two new things: a pass of the analyzer that
recognises exports from CJS modules (in the form of `exports.foo`, see
`analyze_cjs_exports`) and then a CodeGen that reads `export_usage_info`
to remove CJS exports that are not used.
This PR does not add anything additionally to `export_usage_info`, so
this means that we only trim from modules that are imported with `import
{ foo } from "bar"`. The CodeGen depends on the output of
`analyze_cjs_exports` for the locations of exports, whether it is safe
to drop exports (its pretty conservative), and also whether `__esModule`
is present.
This is a small performance thing Claude pointed out that I've added:
```
// Only the pages-router `route-loader` reads `__TURBOPACK_PAGE_BOOTSTRAP`. App routes
// navigate via flight and never use it, so skip app entries to keep `_buildManifest.js`
// (loaded on every page) small.
```
This PR builds on top of the other PRs in this stack, it is the PR that
switches to inlining the bootstrap code and dropping the runtime file
that each route had.
We used to generate the runtime file for each `ChunkGroupResult` in
`evaluated_chunk_group()`, I've added `chunk_group_bootstrap_params`
which are the parameters that were unique to each chunk group. These are
inlined into each page's HTML in
https://github.com/vercel/next.js/pull/94666. They look like:
```javascript
{"otherChunks":["output/0_9x_turbopack-tests_tests_snapshot_basic-tree-shake_export-namespace_input_1mqgz2-._.js"],
"runtimeModuleIds":["[project]/turbopack/crates/turbopack-tests/tests/snapshot/basic-tree-shake/export-namespace/input/index.js [test] (ecmascript)"]}
```
Whether or not the inline-able code is generated is based on the
`inline_chunk_group_bootstrap` which is still false in all cases at the
moment but will be turned to true in
https://github.com/vercel/next.js/pull/94666.
This method generates the params for this code (random example for
reference):
```javascript
(globalThis["TURBOPACK"] || (globalThis["TURBOPACK"] = [])).push([
"output/1ece_tests_snapshot_basic-tree-shake_export-namespace_input_index_0tbr8qy.js",
{"otherChunks":["output/0_9x_turbopack-tests_tests_snapshot_basic-tree-shake_export-namespace_input_1mqgz2-._.js"],"runtimeModuleIds":["[project]/turbopack/crates/turbopack-tests/tests/snapshot/basic-tree-shake/export-namespace/input/index.js [test] (ecmascript)"]}
]);
```
This is a pretty small change on its own (no functionality change), it
makes it a separate method so that it can be called in future PRs. I
will flag that the shape for inline-d bootstrap code will be slightly
different (further up the stack). Because it isn't a chunk itself it
does not have the first item in the array
(`output/1ece_tests_snapshot_basic-tree-shake_export-namespace_input_index_0tbr8qy.js`):
```javascript
(globalThis["TURBOPACK"] || (globalThis["TURBOPACK"] = [])).push({"otherChunks":["static/chunks/node_modules_react-dom_82bf80._.js","static/chunks/_4f9bce._.js","static/chunks/pages_index_tsx_5a9e._.js"],"runtimeModuleIds":[96168]});
```
This item only has `otherChunks` and `runtimeModuleIds`. New code is
added in https://github.com/vercel/next.js/pull/94664 to handle this
different shape.
The goal of this PR is to extract all the generic runtime code into one
asset that is shared by each route. This makes the per-route runtime
tiny, which will be removed in later PRs in this stack.
The diff is unfortunately massive, however, these are almost all because
of the snapshots.
---------
Co-authored-by: Benjamin Woodruff <[email protected]>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Common piece of feedback I've gotten on my PRs. Would be nice to have it
in the `AGENTS.md` as I often use Claude to one shot the plumbing of a
flag.
though - maybe we should have a turbopack-specific AGENTS.md?
This PR simplifies the way that we're preserving
`registration.updateViaCache` after the code-gen that rewrites the
options in the service worker registration
This is a follow-up to https://github.com/vercel/next.js/pull/95431; I
think we can directly mention this in this guide to make people's lives
easier (the PWA guide is quite long for this small chunk of code)
2026-07-09 23:05:00 -07:00
Sam PoderGitHubvercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
We recently rolled out this new syntax for bundling service workers with
Turbopack:
```
await navigator.serviceWorker.register(new URL('../lib/service-workers/1.js', import.meta.url), ...)
```
This PR updates the documentation that previously mentioned service
workers.
---------
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Picked up this from "Turbopack Work Ideas 2026":
> 4. (S) Turbopack Warnings are before SSG Errors
> - Solution: print Turbopack warnings after SSG
I think this is a pretty bare-bones fix for this - let me know if the
idea was to do something different!
See an example output:
```
✓ Running next.config.js took 6ms
▲ Next.js 16.3.0-canary.75 (Turbopack)
Creating an optimized production build ...
✓ Compiled successfully in 889ms
Running TypeScript ...
Finished TypeScript in 38ms ...
Collecting page data using 5 workers ...
Generating static pages using 5 workers (0/4) ...
Error occurred prerendering page "/boom". Read more: https://nextjs.org/docs/messages/prerender-error
Error: boom during prerender
at <unknown> (app/boom/page.js:2:9)
1 | export default function Page() {
> 2 | throw new Error("boom during prerender")
| ^
3 | }
4 | {
digest: '1942497547'
}
Export encountered an error on /boom/page: /boom, exiting the build.
⨯ Next.js build worker exited with code: 1 and signal: null
Turbopack build encountered 1 warning:
./app/join-cwd.js:4:10
Warning: Dynamic filesystem access causes tracing of the whole project
[90m2 |[0m
[90m3 |[0m [36mexport[0m [36mdefault[0m [36mfunction[0m (f) {
[33m[1m>[0m [90m4 |[0m [36mreturn[0m path.join(process.cwd(), f)
[90m |[0m [33m[1m^^^^^^^^^^^^^^^^^^^^^^^^^^^[0m
[90m5 |[0m }
[90m6 |[0m
Static analysis determined that this filesystem access causes the whole project to be traced and included in the output.
This is usually unintentional and leads to all source files (including the public folder) to be deployed as part of the server code.
This can slow down deployments or lead to failures when size limits are exceeded.
To resolve this, you can
- make sure they are statically scoped to some subfolder: path.join(process.cwd(), 'data', bar), or
- only use them in development, or
- add ignore comments: path.join(/*turbopackIgnore: true*/ process.cwd(), bar), or
- remove them.
Import trace:
Server Component:
./app/join-cwd.js
./app/page.js
```
I'm introducing a new endpoint that doesn't compile SSR when doing a
page navigation, I'd like to call it a `rscEndpoint` but currently this
name is taken. `rscEndpoint` appears to be used for detecting changes
for HMR so I've renamed it to `rscHmrEndpoint` so I can use that name
for the new `rscEndpoint`.
This PR adds E2E test for the new service worker syntax introduced in
https://github.com/vercel/next.js/pull/94923. The tests do the
following:
`service-worker-multiple` tests that you can not have multiple service
workers with the same scope. this is because their file names would
clash; additionally, this is generally a bad practise.
`service-worker-scopes` tests that you register multiple service workers
with different scopes and they will be different files
`service-worker` tests that service workers work when registered using
the new syntax.
This PR is the PR that "turns on" the service worker feature, this is
because in the analyzer we are now looking for code like:
```
await navigator.serviceWorker.register(new URL('../lib/service-workers/1.js', import.meta.url), ...)
```
And transforms it into registering the URL that Next.js will now serve
based on scope.
URL scheme is:
```
scope: "/" -> `sw.js`
scope: "/offline/mode" -> `sw-offline-mode.js`
```
etc.
It also inserts the `ServiceWorkerEntryModule`s so they can be
discovered by `next-api` (see
https://github.com/vercel/next.js/pull/94922).
This is the big change in the `next-` part of the repo to support this.
The code goes through the module graph to find any service workers that
need compiling and then compiles them. It also has the code changes to
`packages/next/src/server/lib/router-utils/filesystem.ts` that serve
these files on their respective routes.
Sorting-by-ID was flagged as bad for g-zipping:
> This sorts by chunk item id.
> Tobias has been saying that this is bad, because it gzips worse
> We should be sort by path, so that similar modules stay together. E.g.
if you have many similar icon modules
> Sorting by chunk item results in random order
Switched to sorting by path!
# What
When building a chunk, lay out chunk_items in sorted order.
# Why
There are a number of ways turbopack can create chunk groups with
identical sets of chunk items, but different chunk parents or different
availability info. This means we cannot trivially deduplicate these
chunks. Heuristics inside of `make_production_chunks` means we also do
not always observe 'trivial merge opportunities' there either.
By canonically sorting these chunks we can ensure that we produce the
same content and thus select the same filenames.
See https://vercel.slack.com/archives/C09R44U5HQW/p1781814781879439, on
https://vercel.com/academy when switching to Turbopack, we started
shipping duplicate chunks that only differed by order. Now, module
ordering is fixed in chunks so that the file name hashes are consistent
and we don't ship duplicates.
For vercel-academy,
before:
| | Client JS | Client CSS | Client Source Maps | Server Bundled JS |
Server Unbundled | Server Source Maps |
| --------- | ------------------ | ------------------- |
------------------ | -------------------- | --------------------- |
-------------------- |
| **Total** | 41 files / 7.24 MB | 5 files / 580.57 KB | - | 286 files /
18.30 MB | 1700 files / 48.26 MB | 286 files / 78.63 MB |[4:54
PM]to:[4:54 PM]## Totals
after:
| | Client JS | Client CSS | Client Source Maps | Server Bundled JS |
Server Unbundled | Server Source Maps |
| --------- | ------------------ | ------------------- |
------------------ | -------------------- | --------------------- |
-------------------- |
| **Total** | 36 files / 4.43 MB | 5 files / 580.57 KB | - | 286 files /
18.30 MB | 1700 files / 48.26 MB | 286 files / 78.63 MB |
Follow-up to conversation in the Turbopack office hours this week. I've
gone through and update that maths to match what the comment seemed to
be intending. The biggest change is the change from request = 3 to
request = 2 which trickles down to a lot of the equations.
`results` became `segments` and `results_final` became `locked_prefix`,
the `segments` name I'm not too attached to, I'm just trying to
communicate that that vector stores the "other" parts of the path that
aren't fixed and can change. Meanwhile, the `locked_prefix` has to be at
the start - it contains variables we don't know the value of (and
anything that came before it) or also the starting "." or "..". I think
this is what "final" was originally trying to communicate.
I also had Claude generate tests for this function based on the
pre-existing tests in the `node` repo. There was some divergence related
to absolute paths if anyone is curious:
| Input | turbopack | node |
|---|---|---|
| `["", "foo"]` | `/foo` | `foo` |
| `["", "", "foo"]` | `/foo` | `foo` |
| `["", "..", "..", "/foo"]` | `/../../foo` | `../../foo` |
| `["/"]` | `` (empty) | `/` |
| `["/", "."]` | `` (empty) | `/` |
| `["/foo", "../../../bar"]` | `/../../bar` | `/bar` |
| `["/", ".."]` | `/..` | `/` |
| `["/", "..", ".."]` | `/../..` | `/` |
| `["", "."]` | `` (empty) | `.` |
| `[""]` | `` (empty) | `.` |
| `["", ""]` | `` (empty) | `.` |
I can follow-up and fix these as well if that'd be worthwhile, though I
think it was mentioned that we don't support absolute paths
Talked IRL, we found that calling this in `visit_export_default_expr()`
cut down the `module-cost` benchmark on ESM execution by about 10ms
(approx. 35%).
Doing this unlocks a codegen optimization to avoid arrow functions, this
special case was needed to support `export default ...` which is a kind
of declaration all on its own.
2026-06-10 15:01:02 -07:00
Sam PoderGitHubvercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
There's a lot of code in the runtime that enables top-level async
support. We may not always need to ship this. It makes up approximately
10% of the runtime. This PR separates this code into a separate module:
`async-module.ts`. This is included in the runtime based on this
condition:
```rust
!self.module_graph.async_module_info().await?.is_empty()
```
---------
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
This is another thing flagged by @bgw's Claude:
```
3. Dead duplicate match arm in is_truthy for StrictEqual (lines 2469–2494)
( … StrictEqual, Constant(a), Constant(b)) if a.is_value_type() => Some(a ==
b),
( … StrictEqual, Constant(a), Constant(b)) if a.is_value_type() => { /*
same-type check */ }
Identical pattern + guard. The second arm — the only one that handles
cross-type equality conservatively — is unreachable. Looks like a half-applied
fix.
```
I've removed the second arm with a double type check. Tests appear to
still pass and I can't think of a case where we'd hit the second arm.