Files
vercel__next.js/test/e2e
Tobias Koppers 43d81b9d55 feat(turbopack): resolve /-rooted imports from the project directory (#97799)
### What?

A request starting with `/` — `import '/content/where'`,
`require('/foo.js')` — now resolves from
the **project directory** (the one holding `next.config`), and cannot
reach outside it. Previously
it resolved from the root of the filesystem while reporting *"server
relative imports are not
implemented yet"*.

This is the base of a stack: the follow-up on top of it moves
`import.meta.glob`'s `/`-rooted
patterns onto the same root, so a pattern and a plain import agree on
what `/` means.

### Why?

The feature was half-present and mislabelled. The
`Request::ServerRelative` arm rewrote `/foo` to
`./foo` and resolved it against the filesystem root, then emitted a "not
implemented yet" issue
unless an import-map alias had already matched. Users got a diagnostic
telling them to give up on
something that in fact resolved.

Worse, resolving from the *filesystem* root means `/` escapes the
project. In a Turbopack execution
test — where the filesystem root is the repository root —
`require('/package.json')` resolved this
repository's own `package.json`. In a monorepo, where Turbopack's root
is the workspace root and the
app lives in `apps/web`, `/content/x.js` would reach a workspace-level
file rather than the app's.

The intended semantics are Vite's, and they were confirmed by
measurement rather than from memory.
In a workspace where `content/*.js` exists *both* at the workspace root
and under the Vite root, so
the two are distinguishable, Vite 5.4.21:

- resolves `/content/…` from the configured `root` — changing `root`
changes which file wins;
- fails outright for a file that exists only above `root`, rather than
walking up;
- behaves identically for a plain `import` and for `import.meta.glob`.

### How?

`ResolveOptions` gains `server_relative_root`, the directory a
`/`-rooted request resolves from, and
`ResolveOptionsContext` exposes it so embedders can set it. Next.js sets
it to the project directory
in the client, server and edge resolve contexts — all of which already
had it to hand, so nothing new
is threaded through.

Three deliberate choices:

- **The option is optional and defaults to the previous behaviour.**
`turbopack-core` is used well
beyond Next.js; an embedder that doesn't set it keeps resolving `/` from
the filesystem root. This
is also why the change is provably contained — the whole
`turbopack-tests` suite passes with no
  snapshot churn.
- **It is named and documented in filesystem terms**, tied to the
existing `Request::ServerRelative`
vocabulary, rather than introducing a "project root" concept into
`turbopack-core`, which must stay
  free of Next.js concepts.
- **There is no fallback.** Resolution happens in the configured root
and stops; a request that isn't
there is a normal module-not-found. A fallback to the filesystem root
would reintroduce exactly the
  escape this fixes.

The "not implemented yet" issue is removed. The equivalent issue for
Windows-style absolute requests
is untouched, as is the separate tsconfig `extends` handling, which
treats a rooted path as absolute
per TypeScript's own rule.

### Testing

The Turbopack execution harness already has two distinct roots — the
repository root as the
filesystem root, and each test's own directory — so pointing the new
option at the test directory
makes "project directory" and "filesystem root" genuinely different, and
the distinction testable
without a Next.js app. Two fixtures: one where a `/`-rooted import and
require resolve from the
project directory, and one asserting that `/package.json`, which exists
only at the filesystem root,
does *not* resolve. The former emits no issues at all, which is what
pins the removal of the
diagnostic.

The end-to-end case uses the existing monorepo fixture, where the
project directory really is not the
filesystem root. A file of the same name exists in both
`apps/web/content` and the workspace root, so
the rendered value alone identifies which root was used — a test that
merely asserted "it renders"
would have passed under the old behaviour too.

### Known gap

TypeScript resolves a leading `/` as an absolute path on disk, so a
`/`-rooted import in a `.ts` file
still fails type checking (`TS2307`) even though it bundles and runs.
The e2e page is therefore
JavaScript. A tsconfig `paths` mapping works around it for users, but it
also makes the request match
the import map instead, so the two resolvers overlap; that deserves its
own decision and is tracked
separately rather than papered over here.

<!-- NEXT_JS_LLM -->

<!-- fleet 3acc7e63-52a8-4472-89cd-b1bbb8ad59fa -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
2026-08-25 21:46:47 +02:00
..
…