Skip to content

fix(externals): force-trace named traceDeps to fix pnpm nested deps#4391

Open
pi0x wants to merge 16 commits into
mainfrom
fix/trace-include
Open

fix(externals): force-trace named traceDeps to fix pnpm nested deps#4391
pi0x wants to merge 16 commits into
mainfrom
fix/trace-include

Conversation

@pi0x

@pi0x pi0x commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Closes #4372

Problem

Under pnpm, transitive dependencies are not hoisted to the top-level node_modules — a native, non-bundleable dependency (e.g. sharp / bcrypt) that is only an indirect dependency lives exclusively under node_modules/.pnpm/.... nft can't statically detect such packages when they're loaded dynamically (native bindings), so they're dropped from the trace and the build fails / ships incomplete output. The known workaround was pnpm's publicHoistPattern.

Fix

resolveTraceDeps() now also returns traceInclude — the resolved named deps (builtins + user traceDeps, negations removed, RegExp entries excluded since they can't be resolved by name). The buildEnd handler forwards them to nf3's traceNodeModules via its new traceInclude option.

nf3 resolves each name from rootDir and from the roots of traced packages that declare it as a dependency — so a pnpm nested dep resolves from its dependent's real .pnpm location and gets force-traced (native binaries included).

Depends on

Draft until that nf3 change is released and the nf3 dependency here is bumped to a version that includes it (otherwise CI typecheck fails against the published types).

Tests

test/unit/trace-deps.test.ts — added coverage for the traceInclude output (builtins + user, RegExp excluded, negation handling). All unit tests pass.

Pass the resolved named `traceDeps` (builtins + user, RegExp excluded) to
nf3's `traceNodeModules` via the new `traceInclude` option. nft cannot
statically detect dynamically-loaded packages (e.g. native bindings), and
under pnpm such a nested dependency is not hoisted — it only resolves from
the dependent package's real `.pnpm` location, which nf3 now handles.

Requires nf3 with `traceInclude` support (unjs/nf3#50).

Closes #4372
@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nitro.build Ready Ready Preview, Comment Jul 2, 2026 5:49pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR updates trace-dependency collection, removes deno-specific serveStatic support across runtime and docs, adds OpenAPI fixture coverage, changes a Vercel function copy step, and refreshes documentation and package versions.

Changes

Trace dependency forwarding

Layer / File(s) Summary
traceInclude computation and forwarding
src/build/plugins/externals.ts
resolveTraceDeps derives traceInclude from non-RegExp deps and buildEnd forwards it to traceNodeModules.
traceInclude unit tests
test/unit/trace-deps.test.ts
Three tests verify named deps are included, RegExp and negated selectors are excluded, and traceInclude is omitted when all selectors are negated.

ServeStatic deno removal

Layer / File(s) Summary
serveStatic contract and preset
src/types/config.ts, docs/3.config/0.index.md, src/presets/deno/preset.ts
serveStatic no longer accepts "deno" in the type, docs, or Deno preset.
public assets handlers
src/build/virtual/public-assets.ts
The virtual public-assets handler map and generated Deno template are removed, leaving the remaining handlers intact.
Vite environment conditions
src/build/vite/env.ts
Vite environment resolution now uses shared helpers for export conditions and dev runner selection.

Vercel function directory copy

Layer / File(s) Summary
Custom config copy filtering
src/presets/vercel/utils.ts
The .func directory copy now uses clone mode and excludes .vc-config.json.

OpenAPI fixture and regression coverage

Layer / File(s) Summary
Fixture route metadata and config
test/vite/openapi-fixture/api/meta/test.ts, test/vite/openapi-fixture/vite.config.ts
The fixture adds route metadata for OpenAPI output and enables Nitro OpenAPI generation in Vite.
OpenAPI dev-server assertions
test/vite/openapi.test.ts
The Vitest suite checks OpenAPI JSON, the route response, and Swagger/Scalar UI HTML output.

Documentation and dependency refresh

Layer / File(s) Summary
Migration and deployment docs
docs/1.docs/99.migration.md, docs/2.deploy/20.providers/zerops.md
The migration guide adds the plugin rename note, and the Zerops guide updates setup and deploy steps.
Package manifest refresh
package.json
Runtime and dev dependency versions are updated, and several dev-only packages are removed.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • nitrojs/nitro#4175: Modifies the same resolveTraceDeps() and externals() tracing flow in src/build/plugins/externals.ts.
  • nitrojs/nitro#4398: Touches the same serveStatic/public-assets path by removing deno-specific handling.
  • nitrojs/nitro#4316: Shares the route-meta.ts precedence change and OpenAPI fixture coverage pattern.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes unrelated docs, Deno/public-assets, Vite/OpenAPI, route-meta, env, and vercel code beyond #4372. Split the unrelated doc, config, and feature changes into separate PRs so this branch stays focused on traceDeps tracing.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Uses conventional-commit format and clearly describes the external tracing fix for pnpm nested dependencies.
Description check ✅ Passed The description directly matches the pnpm nested-dependency traceDeps fix and its tests.
Linked Issues check ✅ Passed The code adds traceInclude forwarding and tests, aligning with #4372’s nested .pnpm tracing requirement.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/trace-include

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pi0 pi0 marked this pull request as ready for review June 29, 2026 19:10
@pi0 pi0 self-requested a review as a code owner June 29, 2026 19:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/unit/trace-deps.test.ts`:
- Around line 43-50: The trace deps test is too loose because it only checks
that traceInclude entries are strings, which would still pass if the RegExp
selector were incorrectly stringified. Tighten the assertion in
trace-deps.test.ts around resolveTraceDeps to verify the exact traceInclude
membership and count for the ["my-pkg", /my-.*-pkg/] case, using the existing
defaults fixture and the resolveTraceDeps result to ensure only the expected
builtins plus "my-pkg" are present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d67ddb6c-1c92-46e6-a3e8-a797f95e462a

📥 Commits

Reviewing files that changed from the base of the PR and between 28fab16 and 848ff04.

📒 Files selected for processing (2)
  • src/build/plugins/externals.ts
  • test/unit/trace-deps.test.ts

Comment on lines +43 to +50
it("returns named deps as traceInclude (builtins + user, RegExp excluded)", () => {
const result = resolveTraceDeps(["my-pkg", /my-.*-pkg/], defaults);
expect(result.traceInclude).toContain("sharp");
expect(result.traceInclude).toContain("canvas");
expect(result.traceInclude).toContain("my-pkg");
// RegExp entries cannot be resolved by name and must be excluded
expect(result.traceInclude!.every((d) => typeof d === "string")).toBe(true);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the exact traceInclude membership.

This test doesn't really prove the RegExp selector is excluded. An implementation that accidentally stringifies the regex would still pass typeof d === "string". Please assert the final members/count instead.

Suggested test tightening
   it("returns named deps as traceInclude (builtins + user, RegExp excluded)", () => {
     const result = resolveTraceDeps(["my-pkg", /my-.*-pkg/], defaults);
-    expect(result.traceInclude).toContain("sharp");
-    expect(result.traceInclude).toContain("canvas");
-    expect(result.traceInclude).toContain("my-pkg");
-    // RegExp entries cannot be resolved by name and must be excluded
-    expect(result.traceInclude!.every((d) => typeof d === "string")).toBe(true);
+    expect(result.traceInclude).toEqual(
+      expect.arrayContaining(["sharp", "canvas", "my-pkg"])
+    );
+    expect(result.traceInclude).toHaveLength(3);
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("returns named deps as traceInclude (builtins + user, RegExp excluded)", () => {
const result = resolveTraceDeps(["my-pkg", /my-.*-pkg/], defaults);
expect(result.traceInclude).toContain("sharp");
expect(result.traceInclude).toContain("canvas");
expect(result.traceInclude).toContain("my-pkg");
// RegExp entries cannot be resolved by name and must be excluded
expect(result.traceInclude!.every((d) => typeof d === "string")).toBe(true);
});
it("returns named deps as traceInclude (builtins + user, RegExp excluded)", () => {
const result = resolveTraceDeps(["my-pkg", /my-.*-pkg/], defaults);
expect(result.traceInclude).toEqual(
expect.arrayContaining(["sharp", "canvas", "my-pkg"])
);
expect(result.traceInclude).toHaveLength(3);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/trace-deps.test.ts` around lines 43 - 50, The trace deps test is
too loose because it only checks that traceInclude entries are strings, which
would still pass if the RegExp selector were incorrectly stringified. Tighten
the assertion in trace-deps.test.ts around resolveTraceDeps to verify the exact
traceInclude membership and count for the ["my-pkg", /my-.*-pkg/] case, using
the existing defaults fixture and the resolveTraceDeps result to ensure only the
expected builtins plus "my-pkg" are present.

@pkg-pr-new

pkg-pr-new Bot commented Jun 29, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/nitro@4391

commit: 8ed9f73

@katywings

Copy link
Copy Markdown

@pi0 It might be related to the pkg.pr preview 🤔, but I am getting the following error when building Nitropage with this preview:

error during build:
Build failed with 1 error:

[plugin nitro:externals]
SyntaxError: The requested module '@vercel/nft' does not provide an export named 'nodeFileTrace'
file:///path/to/project/node_modules/.pnpm/nf3@0.3.18/node_modules/nf3/dist/_chunks/trace.mjs:6
import { nodeFileTrace } from "@vercel/nft";

@pi0

pi0 commented Jul 2, 2026

Copy link
Copy Markdown
Member

Very strange @katywings bundled @vercel/nft is not even ESM, it is CJS, it seems something is trying to bundle nitro > nf3 > vercel/nft itself inside output!! Do you have a branch i can test?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/2.deploy/20.providers/zerops.md (1)

87-97: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale access-token/login instructions left alongside new push command.

Lines 87-91 (get access token, zcli login <token>) remain even though the intent of this change is to replace the manual login-then-deploy flow with a direct @zerops/zcli push (Line 95). If zcli push no longer requires a prior manual login, these leftover steps are confusing/redundant; if it still requires login, the flow should say so explicitly.

📝 Suggested cleanup
 ## Build and deploy

-Open [Settings > Access Token Management](https://app.zerops.io/settings/token-management) in the Zerops app and generate a new access token.
-
-Log in using your access token with the following command:
-
-:pm-x{command="`@zerops/zcli` login <token>"}
-
 Navigate to the root of your app (where `zerops.yaml` is located) and run the following command to trigger the deploy:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/2.deploy/20.providers/zerops.md` around lines 87 - 97, The Zerops
deployment section still includes the old access-token and `@zerops/zcli login
<token>` steps even though the updated flow centers on `@zerops/zcli push`.
Update the instructions in the Zerops provider doc so the flow is explicit and
non-redundant: either remove the manual login/token steps if `push` works
directly, or clearly state that `login` is required before `push`. Use the `zcli
login` and `zcli push` steps as the reference points when editing the deployment
instructions.
src/build/virtual/public-assets.ts (1)

96-101: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Silent fallback when serveStatic value is unrecognized (e.g. stale "deno").

readAssetHandler[...] || "null" silently degrades to a no-op asset reader if serveStatic doesn't match a known key — which now includes any leftover "deno" value from configs written before this change (untyped JS configs, extended presets, or dynamic values bypass the type-level removal). This means all public asset reads would silently resolve to null with no error or warning, fully breaking static asset serving without any diagnostic. As per coding guidelines, src/**/*.{ts,js} should "Prefer explicit errors over silent failures; use warnings for recoverable situations; throw for invalid states" and "Include actionable context in error messages."

🛡️ Proposed fix to warn on unrecognized `serveStatic` values
         // prettier-ignore
         type _serveStaticAsKey = Exclude<typeof nitro.options.serveStatic, boolean> | "true" | "false";
         // prettier-ignore
-        const handlerName = readAssetHandler[nitro.options.serveStatic as _serveStaticAsKey] || "null";
+        const resolvedHandlerName = readAssetHandler[nitro.options.serveStatic as _serveStaticAsKey];
+        if (!resolvedHandlerName && nitro.options.serveStatic !== false) {
+          nitro.logger.warn(
+            `Unknown \`serveStatic\` value "${nitro.options.serveStatic}", falling back to no static asset serving.`
+          );
+        }
+        const handlerName = resolvedHandlerName || "null";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/build/virtual/public-assets.ts` around lines 96 - 101, In the virtual
public-assets builder, `readAssetHandler[nitro.options.serveStatic as
_serveStaticAsKey] || "null"` silently hides invalid `serveStatic` values and
falls back to a no-op reader; update `handlerName` resolution in
`src/build/virtual/public-assets.ts` to explicitly detect unrecognized values
(including stale `"deno"` or other dynamic inputs), then emit a clear warning or
throw with actionable context instead of defaulting to `"null"`. Keep the
existing `readAssetHandler` mapping and `readAssetImport` flow, but guard the
lookup so `nitro.options.serveStatic` is validated before constructing the
virtual import string.

Source: Coding guidelines

🧹 Nitpick comments (1)
package.json (1)

90-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid "latest" for a pinned-style manifest.

@cloudflare/workers-types is set to "latest" while every other dependency uses a caret-pinned range. This makes installs non-reproducible and can silently pull in breaking type changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 90, The dependency declaration for
`@cloudflare/workers-types` is using a moving "latest" tag instead of a pinned
semver range, which makes installs non-reproducible. Update the package.json
dependency entry to match the other caret-pinned versions so the manifest stays
stable and avoids unexpected type changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docs/2.deploy/20.providers/zerops.md`:
- Around line 87-97: The Zerops deployment section still includes the old
access-token and `@zerops/zcli login <token>` steps even though the updated flow
centers on `@zerops/zcli push`. Update the instructions in the Zerops provider
doc so the flow is explicit and non-redundant: either remove the manual
login/token steps if `push` works directly, or clearly state that `login` is
required before `push`. Use the `zcli login` and `zcli push` steps as the
reference points when editing the deployment instructions.

In `@src/build/virtual/public-assets.ts`:
- Around line 96-101: In the virtual public-assets builder,
`readAssetHandler[nitro.options.serveStatic as _serveStaticAsKey] || "null"`
silently hides invalid `serveStatic` values and falls back to a no-op reader;
update `handlerName` resolution in `src/build/virtual/public-assets.ts` to
explicitly detect unrecognized values (including stale `"deno"` or other dynamic
inputs), then emit a clear warning or throw with actionable context instead of
defaulting to `"null"`. Keep the existing `readAssetHandler` mapping and
`readAssetImport` flow, but guard the lookup so `nitro.options.serveStatic` is
validated before constructing the virtual import string.

---

Nitpick comments:
In `@package.json`:
- Line 90: The dependency declaration for `@cloudflare/workers-types` is using a
moving "latest" tag instead of a pinned semver range, which makes installs
non-reproducible. Update the package.json dependency entry to match the other
caret-pinned versions so the manifest stays stable and avoids unexpected type
changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e52d7b0c-b87f-4c43-b200-68a3cb25f6cb

📥 Commits

Reviewing files that changed from the base of the PR and between 848ff04 and c94e3a1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • docs/1.docs/99.migration.md
  • docs/2.deploy/20.providers/zerops.md
  • docs/3.config/0.index.md
  • package.json
  • src/build/plugins/route-meta.ts
  • src/build/virtual/public-assets.ts
  • src/build/vite/env.ts
  • src/presets/deno/preset.ts
  • src/presets/vercel/utils.ts
  • src/types/config.ts
  • test/unit/public-assets.test.ts
  • test/vite/openapi-fixture/api/meta/test.ts
  • test/vite/openapi-fixture/vite.config.ts
  • test/vite/openapi.test.ts
✅ Files skipped from review due to trivial changes (3)
  • test/vite/openapi-fixture/vite.config.ts
  • src/build/plugins/route-meta.ts
  • docs/1.docs/99.migration.md

@pi0 pi0 changed the base branch from main to fix/static-wrangler July 2, 2026 17:48
@pi0 pi0 changed the base branch from fix/static-wrangler to main July 2, 2026 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

traceDeps fails to trace nested dependencies in .pnpm folder

7 participants