Skip to content

Conversation

@pi0
Copy link
Member

@pi0 pi0 commented Nov 17, 2025

This PR changes how external dependencies are resolved in nitro v3.

In development mode a much faster implementation is used that leverages native builder resolution and filter hooks.

In production mode, external dependencies are now bundled by default unless added to traceDeps.

Opt-in tracing requires nf3 as dependency (an auto prompt will ask it) and is much faster with less resolution and better subpath guessing.

@vercel
Copy link

vercel bot commented Nov 17, 2025

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

Project Deployment Preview Comments Updated (UTC)
nitro.build Ready Ready Preview Comment Dec 3, 2025 9:42pm

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 17, 2025

Open in StackBlitz

npm i https://pkg.pr.new/nitrojs/nitro@3799

commit: 11174c2

@coderabbitai
Copy link

coderabbitai bot commented Nov 24, 2025

📝 Walkthrough

Walkthrough

Replaces previous Rollup-based node externals tracing with a new custom Rollup externals plugin, introduces path-aware regex helpers, expands NitroOptions with flexible noExternals/traceDeps and wasm defaults, removes several example/preset externals inlines, and adds dynamic dependency import/install utilities and related test updates.

Changes

Cohort / File(s) Summary
New externals plugin
src/build/plugins/externals.ts
Adds a new Rollup plugin externals(opts) and ExternalsOptions type that externalizes modules by include/exclude, resolves with export conditions, optionally traces via nf3, and normalizes Windows paths.
Build integration & refactor
src/build/config.ts, src/build/plugins.ts
Introduces internal getNoExternals(nitro: Nitro): RegExp[], computes noExternal patterns (including wasm handling and user patterns), and replaces rollupNodeFileTrace integration with the new externals plugin usage.
Path/regex utilities & virtual filter
src/utils/regex.ts, src/build/plugins/virtual.ts
Adds pathRegExp() and toPathRegExp() (Windows-aware path regex helpers) and switches virtual module filter to use path-aware regex matching.
Config types & defaults
src/types/config.ts, src/config/defaults.ts
Changes NitroOptions: removes previous externals shape, adds `noExternals?: boolean
Examples & presets cleanup
examples/shiki/nitro.config.ts, examples/vite-ssr-solid/vite.config.mjs, examples/vite-ssr-vue-router/vite.config.mjs, src/presets/cloudflare/dev.ts, src/presets/deno/preset.ts
Removes various externals/noExternal/inline runtime transpilation settings from example and preset configs.
Environment & runtime tweaks
src/build/vite/env.ts, src/runtime/internal/vite/dev-worker.mjs, src/runtime/meta.ts
Injects dev-time nitro-specific noExternal rules, adjusts createServiceEnvironment behavior for dev/prod, changes dev-worker debug env var to NITRO_DEBUG, and reorders pkgDir export position.
Dependency + package manifest
package.json
Moves nf3 to devDependencies and peerDependencies (optional), and bumps unwasm from ^0.5.1 to ^0.5.2; reorders a test script line.
Dynamic dep import utility
src/utils/dep.ts
Adds importDep<T>(opts, _retry?) to dynamically import or install missing deps (with CI/test/interactive behavior), using exsolve and nypm and logging.
Tests & fixtures updates
test/examples.test.ts, test/fixture/nitro.config.ts, test/fixture/server/routes/modules.ts, test/presets/node.test.ts, test/presets/vercel.test.ts, vitest.config.ts, test/...
Removes one example prod exclusion, adds traceDeps: ["@fixture"] to fixture config, removes a fixture alias import and usage, removes Windows-specific test gating, moves vercel.json setup to beforeAll, and sets NODE_ENV=production in vitest config.
Minor export/type reorder
src/runtime/meta.ts, src/types/config.ts
Reordered export lines and adjusted imports (removed NodeExternalsOptions import), reflecting the new options shape.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing extra attention:
    • src/build/plugins/externals.ts — resolution logic, export-conditions handling, nf3 tracing integration, Windows path/file URL handling.
    • src/build/config.ts & src/build/plugins.ts — correctness of getNoExternals() and its interactions with wasm/defaults and dev vs prod modes.
    • src/types/config.ts & src/config/defaults.ts — public API/type shape changes and downstream impact.
    • src/utils/dep.ts — runtime install logic and side-effect behavior in CI/test environments.
    • Test changes affecting cross-platform behavior (removed Windows skips).

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor!: overhaul externals support' follows conventional commits format with a breaking change indicator and clearly describes the main change in the PR.
Description check ✅ Passed The description is directly related to the changeset, explaining the overhaul of externals support with specific details about dev/production modes and the nf3 dependency integration.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bundle-by-default

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

@pi0 pi0 marked this pull request as ready for review December 3, 2025 18:49
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
test/presets/node.test.ts (1)

3-3: Remove commented-out import instead of leaving dead code.

The isWindows import is no longer used after removing the Windows-specific skip condition. Consider removing it entirely rather than commenting it out.

-// import { isWindows } from "std-env";
src/build/plugins/externals.ts (1)

64-70: Minor: Redundant prefix check.

The id.includes(":") check handles existing prefixed builtins, but builtinModules from Node.js only contains unprefixed names (e.g., "fs", not "node:fs"). The ternary will always take the false branch (adding node:). This is harmless but the conditional could be simplified.

         if (builtinModules.includes(id)) {
           return {
             resolvedBy: PLUGIN_NAME,
             external: true,
-            id: id.includes(":") ? id : `node:${id}`,
+            id: `node:${id}`,
           };
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dbc1cd2 and 0834380.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • examples/shiki/nitro.config.ts (0 hunks)
  • examples/vite-ssr-solid/vite.config.mjs (0 hunks)
  • examples/vite-ssr-vue-router/vite.config.mjs (0 hunks)
  • package.json (1 hunks)
  • src/build/config.ts (3 hunks)
  • src/build/plugins.ts (2 hunks)
  • src/build/plugins/externals.ts (1 hunks)
  • src/build/plugins/virtual.ts (2 hunks)
  • src/build/vite/env.ts (2 hunks)
  • src/config/defaults.ts (2 hunks)
  • src/presets/cloudflare/dev.ts (0 hunks)
  • src/presets/deno/preset.ts (0 hunks)
  • src/runtime/internal/vite/dev-worker.mjs (1 hunks)
  • src/runtime/meta.ts (1 hunks)
  • src/types/config.ts (2 hunks)
  • src/utils/regex.ts (1 hunks)
  • test/examples.test.ts (1 hunks)
  • test/fixture/nitro.config.ts (1 hunks)
  • test/fixture/server/routes/modules.ts (0 hunks)
  • test/presets/node.test.ts (2 hunks)
  • test/presets/vercel.test.ts (2 hunks)
  • vitest.config.ts (1 hunks)
💤 Files with no reviewable changes (6)
  • src/presets/cloudflare/dev.ts
  • examples/shiki/nitro.config.ts
  • examples/vite-ssr-solid/vite.config.mjs
  • src/presets/deno/preset.ts
  • test/fixture/server/routes/modules.ts
  • examples/vite-ssr-vue-router/vite.config.mjs
🧰 Additional context used
🧬 Code graph analysis (5)
src/build/vite/env.ts (1)
src/runtime/meta.ts (1)
  • runtimeDependencies (13-31)
src/build/plugins/virtual.ts (1)
src/utils/regex.ts (1)
  • pathRegExp (7-16)
src/build/plugins/externals.ts (2)
src/utils/regex.ts (2)
  • toPathRegExp (18-26)
  • escapeRegExp (3-5)
src/runtime/meta.ts (1)
  • pkgDir (11-11)
src/build/config.ts (2)
src/utils/regex.ts (2)
  • pathRegExp (7-16)
  • toPathRegExp (18-26)
src/runtime/meta.ts (1)
  • pkgDir (11-11)
src/build/plugins.ts (1)
src/build/plugins/externals.ts (1)
  • externals (26-154)
🪛 ast-grep (0.40.0)
src/build/vite/env.ts

[warning] 42-42: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^(${runtimeDependencies.join("|")})$)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

src/build/plugins/virtual.ts

[warning] 30-32: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
^(${[...modules.keys()].map((id) => pathRegExp(id)).join("|")})$
)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (23)
src/runtime/internal/vite/dev-worker.mjs (1)

41-41: LGTM!

The change from DEBUG to NITRO_DEBUG provides better namespace isolation for Nitro-specific debugging, avoiding conflicts with other tools that use the generic DEBUG environment variable.

test/presets/vercel.test.ts (1)

530-533: LGTM!

Moving the vercel.json file creation into a beforeAll hook is a good practice for test setup, ensuring proper sequencing of test initialization.

src/runtime/meta.ts (1)

11-11: LGTM!

Reordering the pkgDir export is a non-functional change that doesn't impact the public API.

src/build/plugins/virtual.ts (1)

2-2: LGTM! Windows path compatibility improvement.

Replacing escapeRegExp with pathRegExp adds Windows path separator handling (converting backslashes to a pattern that matches both / and \), which is essential for cross-platform compatibility.

Regarding the static analysis warning about ReDoS: while constructing regex from variables can be risky, in this case:

  • The module IDs are controlled by the build system, not user input
  • pathRegExp already applies escapeRegExp internally, neutralizing special regex characters
  • The risk of malicious input is minimal in this build-time context

Also applies to: 32-32

src/utils/regex.ts (2)

7-16: LGTM! Windows-aware path escaping is correctly implemented.

The pathRegExp function properly handles cross-platform path matching by:

  1. Normalizing Windows backslashes to forward slashes before escaping
  2. After escaping special regex characters, converting slashes to [/\\] on Windows to match both separators

This ensures path patterns work correctly regardless of the platform or path separator format.


18-26: LGTM! Good input validation with informative error handling.

The toPathRegExp function provides a clean API that:

  • Preserves existing RegExp objects unchanged
  • Converts strings to path-aware regex patterns
  • Includes proper type checking with a descriptive TypeError that includes the invalid input as the cause

The error handling approach using { cause: input } is a modern best practice for debugging.

test/examples.test.ts (1)

21-21: Ensure the vite-ssr-tss-react example is properly configured for production builds.

The vite-ssr-tss-react example directory exists with appropriate Vite and package configuration. However, verify that the example's production build and tests pass by running the test suite, as configuration inspection alone cannot confirm test execution status.

vitest.config.ts (1)

3-4: No action needed. Setting NODE_ENV to "production" in vitest.config.ts does not break tests. Analysis shows no test files depend on NODE_ENV being unset or set to "test" or "development". The few tests that reference process.env only interact with custom environment variables (e.g., NITRO_A, NITRO_BUILDER, MAIL_HOST), not NODE_ENV. The production mode configuration is intentional for this server framework.

package.json (1)

159-159: No action required for unwasm package version.

Version 0.5.2 of the unwasm package exists and is the latest release on npm with no known security vulnerabilities reported.

src/config/defaults.ts (1)

36-36: LGTM!

The new wasm and traceDeps defaults are correctly initialized as empty objects/arrays, aligning with the updated NitroOptions type surface.

Also applies to: 75-75

test/presets/node.test.ts (1)

36-41: LGTM!

The simplified test correctly validates externals tracing by checking for the traced dependency file. The removal of platform-specific gating aligns with the unified externals handling introduced in this PR.

test/fixture/nitro.config.ts (1)

53-53: LGTM!

The traceDeps: ["@fixture"] configuration correctly enables dependency tracing for the fixture package, which is verified by the updated test in test/presets/node.test.ts.

src/build/vite/env.ts (2)

42-44: Static analysis false positive: ReDoS warning is not applicable here.

The runtimeDependencies array imported from nitro/meta contains hardcoded, known-safe package names (e.g., "crossws", "h3", "defu"). Since these are controlled literals without user input or special regex characters, there is no ReDoS risk. The pattern ^(name1|name2|...)$ is safe and efficient.


77-79: LGTM!

The service environment correctly delegates to the base noExternal configuration in dev mode while using standalone bundling for production, consistent with the nitro environment pattern.

src/build/plugins.ts (1)

59-74: LGTM!

The externals plugin integration is well-structured:

  • Correctly gated on node mode with noExternals !== true
  • Dev/prerender mode appropriately disables tracing for faster iteration
  • Production builds trace dependencies to the server output directory
  • The exclude/include patterns properly map to base config and traceDeps
src/build/config.ts (1)

59-86: LGTM!

The getNoExternals helper consolidates noExternal pattern computation cleanly:

  • Correctly bundles TypeScript files, virtual modules, and the Nitro package directory
  • Conditionally includes .wasm files when wasm is enabled
  • Properly transforms user-provided noExternals patterns via toPathRegExp
  • Sorting by regex source length is a reasonable optimization for pattern matching order
src/types/config.ts (1)

244-245: LGTM!

The updated noExternals and new traceDeps type definitions provide a flexible API:

  • noExternals accepts either a boolean to disable externalization entirely or an array of patterns to exclude
  • traceDeps accepts patterns to specify which dependencies to trace

Both being optional maintains backward compatibility.

src/build/plugins/externals.ts (6)

11-22: LGTM!

The ExternalsOptions type is well-defined with appropriate field types. The trace option correctly omits fields that are computed internally (rootDir, exportConditions, traceOptions).


31-46: LGTM!

The filter logic correctly:

  • Excludes virtual modules (\0), internal references (#~.), protocol imports, and query-string imports
  • Requires matching at least one include pattern when specified
  • Rejects any match against exclude patterns

133-152: LGTM!

The buildEnd handler correctly:

  • Short-circuits when tracing is disabled or no paths were traced
  • Dynamically imports nf3 to avoid bundling it when unused
  • Configures tracing with proper export conditions and root directory

206-218: LGTM with note on cache pattern.

The getPkgJSON caching implementation works correctly. The cache-on-function pattern ((getPkgJSON as any)._cache) is functional but unconventional. A module-level Map or WeakMap would be more idiomatic, though this is a minor stylistic preference.


220-237: LGTM!

The flattenExports utility correctly handles nested exports structures, distinguishing between subpaths (starting with .) and conditions. The recursive processing properly builds the flattened array of subpath/fsPath/condition triples.


163-173: The toImport function requires both name and subpath to trace absolute paths, which may miss bare package root imports in edge cases.

If resolvedPath points to a package directory without a file extension (e.g., /path/node_modules/lodash), both toImport (line 167) and the guessSubpath fallback (line 177) will return undefined because they require subpath to exist. In such cases, the module won't be traced. While standard module resolvers typically resolve to actual files (e.g., index.js), a custom resolver or edge case could trigger this. Clarify whether this limitation is intentional, or consider handling bare package roots by returning join(name) when subpath is missing.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0834380 and 11174c2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • package.json (3 hunks)
  • src/build/plugins/externals.ts (1 hunks)
  • src/utils/dep.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/build/plugins/externals.ts
  • package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: tests-rolldown (windows-latest)
  • GitHub Check: tests-rollup (windows-latest)
  • GitHub Check: tests-rollup (ubuntu-latest)
🔇 Additional comments (6)
src/utils/dep.ts (6)

1-3: LGTM!

The imports are appropriate for the dynamic dependency resolution and installation functionality.


5-12: LGTM!

The function signature is well-structured with clear parameter names and appropriate generic typing.


38-42: LGTM!

The error handling provides a clear, actionable message when installation is declined or unavailable.


44-50: LGTM!

The installation process is well-implemented with good UX:

  • Dynamic import of nypm avoids unnecessary loading
  • Clear logging with timing information
  • Uses addDevDependency appropriately

52-52: LGTM!

The recursive retry with _retry=true ensures the module is properly resolved after installation, with cache disabled for a fresh resolution attempt.


23-36: Remove the concern about the cancel parameter on line 34—it is correct.

The syntax cancel: "undefined" is a valid strategy for the consola.prompt API. When the prompt is canceled (e.g., via Ctrl+C), it resolves with undefined. This matches the consola documentation and is not a bug.

Consider clarifying the comment on line 25 to mention both conditions.

The comment states "Do not install dependencies in test mode," but the condition also checks _retry. For clarity:

  if (_retry || isTest) {
-   shouldInstall = false; // Do not install dependencies in test mode
+   shouldInstall = false; // Do not install dependencies in test mode or on retry

Likely an incorrect or invalid review comment.

@pi0 pi0 merged commit 3a2d266 into main Dec 3, 2025
12 checks passed
@pi0 pi0 deleted the bundle-by-default branch December 3, 2025 23:12
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.

2 participants