From 58b2132355552c942916b5a1051d43ecfbb68901 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 17 Nov 2025 14:04:26 +0100 Subject: [PATCH 01/30] refactor!: disable externals tracing by default --- package.json | 2 +- pnpm-lock.yaml | 10 ++++----- src/build/plugins.ts | 36 ++++++++++++++----------------- src/presets/_nitro/base-worker.ts | 1 - src/presets/deno/preset.ts | 1 - src/types/config.ts | 2 -- 6 files changed, 22 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 2291758144..a760cd1302 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", - "nf3": "^0.1.10", + "nf3": "^0.2.0", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.97.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3957063349..f62aab07d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,8 +28,8 @@ importers: specifier: ^2.6.1 version: 2.6.1 nf3: - specifier: ^0.1.10 - version: 0.1.10 + specifier: ^0.2.0 + version: 0.2.0 ofetch: specifier: ^2.0.0-alpha.3 version: 2.0.0-alpha.3 @@ -5617,8 +5617,8 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - nf3@0.1.10: - resolution: {integrity: sha512-bT6FITvXLd8Z9Qbt0NsMz90diyLNK8H4Sp2vZ9IGLrKxsF5djM+F2vQmR6GyvtlP2y47XMZjjVFpPClgMB8USQ==} + nf3@0.2.0: + resolution: {integrity: sha512-TtkYFah0uejbqjkDHIf75pasYzbnXNPoZ4eRZHIBV/GwMfLG8woxXIawEo82R/Kaanw0UDxTjO47k2sI+fhcHg==} node-abi@3.85.0: resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} @@ -12615,7 +12615,7 @@ snapshots: negotiator@1.0.0: {} - nf3@0.1.10: {} + nf3@0.2.0: {} node-abi@3.85.0: dependencies: diff --git a/src/build/plugins.ts b/src/build/plugins.ts index 07dedf2078..1daa512b64 100644 --- a/src/build/plugins.ts +++ b/src/build/plugins.ts @@ -125,27 +125,23 @@ export function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { ); // Externals Plugin - if (!nitro.options.noExternals) { + // WIP: Opt-in tracing for prod + // WIP: Simpler externals for dev + if (nitro.options.dev) { plugins.push( - rollupNodeFileTrace( - defu(nitro.options.externals, { - outDir: nitro.options.output.serverDir, - moduleDirectories: nitro.options.nodeModulesDirs, - external: nitro.options.nodeModulesDirs, - inline: [...base.noExternal], - traceOptions: { - base: "/", - processCwd: nitro.options.rootDir, - exportsOnly: true, - }, - traceAlias: { - "h3-nightly": "h3", - ...nitro.options.externals?.traceAlias, - }, - exportConditions: nitro.options.exportConditions as string[], - writePackageJson: true, - } satisfies NodeExternalsOptions) - ) + rollupNodeFileTrace({ + outDir: nitro.options.output.serverDir, + moduleDirectories: nitro.options.nodeModulesDirs, + external: nitro.options.nodeModulesDirs, + inline: [...base.noExternal], + traceOptions: { + base: "/", + exportsOnly: true, + processCwd: nitro.options.rootDir, + }, + exportConditions: nitro.options.exportConditions as string[], + writePackageJson: true, + }) ); } diff --git a/src/presets/_nitro/base-worker.ts b/src/presets/_nitro/base-worker.ts index e3f2c64037..d877d1a9c5 100644 --- a/src/presets/_nitro/base-worker.ts +++ b/src/presets/_nitro/base-worker.ts @@ -5,7 +5,6 @@ const baseWorker = defineNitroPreset( entry: null as any, // Abstract node: false, minify: true, - noExternals: true, rollupConfig: { output: { format: "iife", diff --git a/src/presets/deno/preset.ts b/src/presets/deno/preset.ts index 867cdff391..956cd29669 100644 --- a/src/presets/deno/preset.ts +++ b/src/presets/deno/preset.ts @@ -14,7 +14,6 @@ const denoDeploy = defineNitroPreset( }, exportConditions: ["deno"], node: false, - noExternals: true, serveStatic: "deno", commands: { preview: "", diff --git a/src/types/config.ts b/src/types/config.ts index 4098e7b7da..00ce82c43c 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -238,8 +238,6 @@ export interface NitroOptions extends PresetOptions { node: boolean; moduleSideEffects: string[]; oxc?: OXCOptions; - noExternals: boolean; - externals: NodeExternalsOptions; replace: Record string)>; commonJS?: RollupCommonJSOptions; exportConditions?: string[]; From da1bc0a32e42ef81d1ba4bcf6c55bd049012aa63 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 17 Nov 2025 14:11:38 +0100 Subject: [PATCH 02/30] update --- examples/shiki/nitro.config.ts | 3 --- src/build/plugins.ts | 3 +-- src/presets/cloudflare/dev.ts | 7 ------- src/types/config.ts | 2 +- 4 files changed, 2 insertions(+), 13 deletions(-) diff --git a/examples/shiki/nitro.config.ts b/examples/shiki/nitro.config.ts index e6c865a6bd..e87bfb1938 100644 --- a/examples/shiki/nitro.config.ts +++ b/examples/shiki/nitro.config.ts @@ -2,7 +2,4 @@ import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", - externals: { - inline: ["shiki/core"], - }, }); diff --git a/src/build/plugins.ts b/src/build/plugins.ts index 1daa512b64..fc4b700673 100644 --- a/src/build/plugins.ts +++ b/src/build/plugins.ts @@ -1,8 +1,7 @@ -import type { Nitro, NodeExternalsOptions } from "nitro/types"; +import type { Nitro } from "nitro/types"; import type { Plugin } from "rollup"; import type { BaseBuildConfig } from "./config.ts"; import { hash } from "ohash"; -import { defu } from "defu"; import unimportPlugin from "unimport/unplugin"; import { unwasm } from "unwasm/plugin"; import replace from "@rollup/plugin-replace"; diff --git a/src/presets/cloudflare/dev.ts b/src/presets/cloudflare/dev.ts index 9eb8105b6e..68efb4fa4d 100644 --- a/src/presets/cloudflare/dev.ts +++ b/src/presets/cloudflare/dev.ts @@ -1,6 +1,5 @@ import { resolve } from "node:path"; import { promises as fs } from "node:fs"; -import { fileURLToPath } from "mlly"; import type { Nitro } from "nitro/types"; import { findFile } from "pkg-types"; import { resolveModulePath } from "exsolve"; @@ -82,12 +81,6 @@ export async function cloudflareDevModule(nitro: Nitro) { environment: config.environment, }; - // Make sure runtime is transpiled - nitro.options.externals.inline = nitro.options.externals.inline || []; - nitro.options.externals.inline.push( - fileURLToPath(new URL("runtime/", import.meta.url)) - ); - // Add plugin to inject bindings to dev server nitro.options.plugins = nitro.options.plugins || []; nitro.options.plugins.unshift( diff --git a/src/types/config.ts b/src/types/config.ts index 00ce82c43c..a01b70b23b 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -36,7 +36,7 @@ import type { NitroFrameworkInfo } from "./nitro.ts"; import type { NitroOpenAPIConfig } from "./openapi.ts"; export type { NitroOpenAPIConfig } from "./openapi.ts"; import type { NitroPreset } from "./preset.ts"; -import type { NodeExternalsOptions, OXCOptions } from "./rollup.ts"; +import type { OXCOptions } from "./rollup.ts"; import type { RollupConfig } from "./rollup.ts"; import type { NitroRouteConfig, NitroRouteRules } from "./route-rules.ts"; From 5c39d67e41d9f520ed96aa3c63968567fb098041 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 17 Nov 2025 14:41:39 +0100 Subject: [PATCH 03/30] tmp fixes --- src/build/plugins.ts | 1 + test/presets/node.test.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/build/plugins.ts b/src/build/plugins.ts index fc4b700673..850f6159d7 100644 --- a/src/build/plugins.ts +++ b/src/build/plugins.ts @@ -132,6 +132,7 @@ export function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { outDir: nitro.options.output.serverDir, moduleDirectories: nitro.options.nodeModulesDirs, external: nitro.options.nodeModulesDirs, + noTrace: nitro.options.dev, inline: [...base.noExternal], traceOptions: { base: "/", diff --git a/test/presets/node.test.ts b/test/presets/node.test.ts index 1b793120b3..76d4e27150 100644 --- a/test/presets/node.test.ts +++ b/test/presets/node.test.ts @@ -33,7 +33,8 @@ describe("nitro:preset:node-middleware", async () => { expect(noncached2.headers.get("etag")).toBeNull(); }); - it.skipIf(isWindows)("should not bundle externals", () => { + // TODO: Enable when explicit tracedDeps config added + it.todo("should not bundle externals", () => { const serverNodeModules = resolve(ctx.outDir, "server/node_modules"); expect( existsSync(resolve(serverNodeModules, "@fixture/nitro-utils/extra.mjs")) From 93bf265f4b135ba36ae2f687e8eeeecbe5b08167 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 17 Nov 2025 14:45:26 +0100 Subject: [PATCH 04/30] lint --- test/presets/node.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/presets/node.test.ts b/test/presets/node.test.ts index 76d4e27150..9545f1cc35 100644 --- a/test/presets/node.test.ts +++ b/test/presets/node.test.ts @@ -1,6 +1,6 @@ import { existsSync } from "node:fs"; import { resolve } from "pathe"; -import { isWindows } from "std-env"; +// import { isWindows } from "std-env"; import { describe, expect, it } from "vitest"; import { setupTest, startServer, testNitro } from "../tests.ts"; From 0a41856c9addc71fce27beada1701dbbea8ef5ad Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 18 Nov 2025 18:10:38 +0100 Subject: [PATCH 05/30] dev externals --- src/build/config.ts | 13 +---- src/build/plugins.ts | 37 ++++++------ src/build/plugins/externals.ts | 71 ++++++++++++++++++++++++ src/build/vite/env.ts | 2 +- src/runtime/internal/vite/dev-worker.mjs | 2 +- 5 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 src/build/plugins/externals.ts diff --git a/src/build/config.ts b/src/build/config.ts index 112f39caac..30dd71a894 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -33,21 +33,14 @@ export function baseBuildConfig(nitro: Nitro) { ...nitro.options.replace, }; - const noExternal: (string | RegExp | ((id: string) => boolean))[] = [ - "#", - "~", - "@/", - "~~", - "@@/", - "virtual:", + const noExternal: (string | RegExp)[] = [ + /^(?:[\0#~.]|virtual:)/, "nitro", pkgDir, nitro.options.serverDir, nitro.options.buildDir, dirname(nitro.options.entry), - ...(nitro.options.wasm === false - ? [] - : [(id: string) => id.endsWith(".wasm")]), + ...(nitro.options.wasm === false ? [] : [/\.wasm$/]), ...nitro.options.handlers .map((m) => m.handler) .filter((i) => typeof i === "string"), diff --git a/src/build/plugins.ts b/src/build/plugins.ts index 850f6159d7..828aa418a7 100644 --- a/src/build/plugins.ts +++ b/src/build/plugins.ts @@ -14,13 +14,14 @@ import { serverAssets } from "./plugins/server-assets.ts"; import { storage } from "./plugins/storage.ts"; import { virtual } from "./plugins/virtual.ts"; import { errorHandler } from "./plugins/error-handler.ts"; -import { rollupNodeFileTrace } from "nf3"; +// import { rollupNodeFileTrace } from "nf3"; import { rendererTemplate } from "./plugins/renderer-template.ts"; import { featureFlags } from "./plugins/feature-flags.ts"; import { nitroResolveIds } from "./plugins/resolve.ts"; import { sourcemapMinify } from "./plugins/sourcemap-min.ts"; import { raw } from "./plugins/raw.ts"; import { runtimeConfig } from "./plugins/runtime-config.ts"; +import { externals } from "./plugins/externals.ts"; export function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { const plugins: Plugin[] = []; @@ -127,24 +128,26 @@ export function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { // WIP: Opt-in tracing for prod // WIP: Simpler externals for dev if (nitro.options.dev) { - plugins.push( - rollupNodeFileTrace({ - outDir: nitro.options.output.serverDir, - moduleDirectories: nitro.options.nodeModulesDirs, - external: nitro.options.nodeModulesDirs, - noTrace: nitro.options.dev, - inline: [...base.noExternal], - traceOptions: { - base: "/", - exportsOnly: true, - processCwd: nitro.options.rootDir, - }, - exportConditions: nitro.options.exportConditions as string[], - writePackageJson: true, - }) - ); + plugins.push(externals({ exclude: base.noExternal })); } + // plugins.push( + // rollupNodeFileTrace({ + // outDir: nitro.options.output.serverDir, + // moduleDirectories: nitro.options.nodeModulesDirs, + // external: nitro.options.nodeModulesDirs, + // noTrace: nitro.options.dev, + // inline: [...base.noExternal], + // traceOptions: { + // base: "/", + // exportsOnly: true, + // processCwd: nitro.options.rootDir, + // }, + // exportConditions: nitro.options.exportConditions as string[], + // writePackageJson: true, + // }) + // ); + // Minify if ( nitro.options.sourcemap && diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts new file mode 100644 index 0000000000..c701338fea --- /dev/null +++ b/src/build/plugins/externals.ts @@ -0,0 +1,71 @@ +import type { Plugin } from "rollup"; +import { pathToFileURL } from "node:url"; + +export type ExternalsOptions = { + include?: FilterMatcher[]; + exclude?: FilterMatcher[]; +}; + +export function externals(opts: ExternalsOptions): Plugin { + const filter = createFilter(opts); + return { + name: "nitro:externals", + resolveId: { + order: "pre", + filter: { id: { exclude: /^(?:[\0#~.]|[a-z0-9]{2,}:)|\?/ } }, + async handler(id, importer, rOpts) { + if (!filter(id)) { + return null; + } + const resolved = await this.resolve(id, importer, rOpts); + if (!resolved) { + return null; + } + return { + id: pathToFileURL(resolved.id).href, + external: true, + }; + }, + }, + }; +} + +// --- helper functions for filter matching --- + +type FilterFunction = (id: string) => boolean; +type FilterMatcher = string | RegExp | FilterFunction; + +function createFilter(opts: { + include?: FilterMatcher[]; + exclude?: FilterMatcher[]; +}) { + const includeMatchers = (opts?.include || []) + .map((m) => toMatcher(m)) + .filter(Boolean) as FilterFunction[]; + const excludeMatchers = (opts?.exclude || []) + .map((m) => toMatcher(m)) + .filter(Boolean) as FilterFunction[]; + + // Should match one of the includes and none of the excludes + return (id: string) => { + if (includeMatchers.length > 0 && !includeMatchers.some((fn) => fn(id))) { + return false; + } + if (excludeMatchers.some((fn) => fn(id))) { + return false; + } + return true; + }; +} + +function toMatcher( + filter: string | RegExp | ((id: string) => boolean) +): FilterFunction | undefined { + if (typeof filter === "string") { + return (id: string) => id.startsWith(filter); + } else if (filter instanceof RegExp) { + return (id: string) => filter.test(id); + } else if (typeof filter === "function") { + return filter; + } +} diff --git a/src/build/vite/env.ts b/src/build/vite/env.ts index 558accec40..b0d19eaee5 100644 --- a/src/build/vite/env.ts +++ b/src/build/vite/env.ts @@ -44,7 +44,7 @@ export function createNitroEnvironment( ), ...runtimeDependencies, ] - : true, // in production, NF3 tracks externals + : true, // production build is standalone conditions: ctx.nitro!.options.exportConditions, externalConditions: ctx.nitro!.options.exportConditions, }, diff --git a/src/runtime/internal/vite/dev-worker.mjs b/src/runtime/internal/vite/dev-worker.mjs index b1f60722a7..c2935d24eb 100644 --- a/src/runtime/internal/vite/dev-worker.mjs +++ b/src/runtime/internal/vite/dev-worker.mjs @@ -38,7 +38,7 @@ class EnvRunner { }, }, new ESModulesEvaluator(), - process.env.DEBUG ? console.debug : undefined + process.env.NITRO_DEBUG ? console.debug : undefined ); this.reload(); From 7820804900954cd77d0130dfbc3f1c32b90ef74e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 20 Nov 2025 00:38:29 +0100 Subject: [PATCH 06/30] wip --- src/build/config.ts | 10 ++++--- src/build/plugins/externals.ts | 52 ++++++++++++++++------------------ 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/build/config.ts b/src/build/config.ts index 30dd71a894..302530a04d 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -1,7 +1,7 @@ import type { Nitro, NitroImportMeta } from "nitro/types"; import { dirname } from "pathe"; import { defineEnv } from "unenv"; -import { pkgDir, runtimeDependencies, presetsDir } from "nitro/meta"; +import { runtimeDependencies, presetsDir, runtimeDir } from "nitro/meta"; export type BaseBuildConfig = ReturnType; @@ -35,10 +35,12 @@ export function baseBuildConfig(nitro: Nitro) { const noExternal: (string | RegExp)[] = [ /^(?:[\0#~.]|virtual:)/, - "nitro", - pkgDir, - nitro.options.serverDir, + /^nitro$/, + /nitro\/(dist|app|~internal)/, + runtimeDir, + presetsDir, nitro.options.buildDir, + ...nitro.options.scanDirs, dirname(nitro.options.entry), ...(nitro.options.wasm === false ? [] : [/\.wasm$/]), ...nitro.options.handlers diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index c701338fea..f8d7d7a8b5 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -1,29 +1,41 @@ import type { Plugin } from "rollup"; import { pathToFileURL } from "node:url"; +import { isAbsolute } from "pathe"; export type ExternalsOptions = { include?: FilterMatcher[]; exclude?: FilterMatcher[]; }; +const NO_EXTERNAL_RE = /^(?:[\0#~.]|[a-z0-9]{2,}:)|\?/; + export function externals(opts: ExternalsOptions): Plugin { - const filter = createFilter(opts); + const exclude = createFilter(opts?.exclude || []); return { name: "nitro:externals", resolveId: { order: "pre", - filter: { id: { exclude: /^(?:[\0#~.]|[a-z0-9]{2,}:)|\?/ } }, + filter: { id: { exclude: NO_EXTERNAL_RE } }, async handler(id, importer, rOpts) { - if (!filter(id)) { + if (exclude(id)) { return null; } const resolved = await this.resolve(id, importer, rOpts); - if (!resolved) { - return null; + if ( + !resolved?.id || + NO_EXTERNAL_RE.test(resolved.id) || + !filter(resolved.id) + ) { + return resolved; + } + if (id.includes("test")) { + console.log(">", id); } return { - id: pathToFileURL(resolved.id).href, external: true, + id: isAbsolute(resolved.id) + ? pathToFileURL(resolved.id).href + : resolved.id, }; }, }, @@ -35,36 +47,20 @@ export function externals(opts: ExternalsOptions): Plugin { type FilterFunction = (id: string) => boolean; type FilterMatcher = string | RegExp | FilterFunction; -function createFilter(opts: { - include?: FilterMatcher[]; - exclude?: FilterMatcher[]; -}) { - const includeMatchers = (opts?.include || []) - .map((m) => toMatcher(m)) - .filter(Boolean) as FilterFunction[]; - const excludeMatchers = (opts?.exclude || []) +function createFilter(matchers: FilterMatcher[] = []) { + const matcherFns = matchers .map((m) => toMatcher(m)) - .filter(Boolean) as FilterFunction[]; - - // Should match one of the includes and none of the excludes - return (id: string) => { - if (includeMatchers.length > 0 && !includeMatchers.some((fn) => fn(id))) { - return false; - } - if (excludeMatchers.some((fn) => fn(id))) { - return false; - } - return true; - }; + .filter((m): m is FilterFunction => typeof m === "function"); + return (id: string) => matcherFns.some((fn) => fn(id)); } function toMatcher( filter: string | RegExp | ((id: string) => boolean) ): FilterFunction | undefined { if (typeof filter === "string") { - return (id: string) => id.startsWith(filter); + return Object.assign((id: string) => id.startsWith(filter), { filter }); } else if (filter instanceof RegExp) { - return (id: string) => filter.test(id); + return Object.assign((id: string) => filter.test(id), { filter }); } else if (typeof filter === "function") { return filter; } From 0df8543034c5f2a3ff9a7ad913bcf9d93e63ca3d Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 24 Nov 2025 19:41:10 +0100 Subject: [PATCH 07/30] update --- src/build/config.ts | 4 ++- src/build/plugins.ts | 2 +- src/build/plugins/externals.ts | 54 ++++++++++------------------------ src/utils/regex.ts | 3 ++ 4 files changed, 23 insertions(+), 40 deletions(-) create mode 100644 src/utils/regex.ts diff --git a/src/build/config.ts b/src/build/config.ts index 302530a04d..d6c43e49b2 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -2,6 +2,7 @@ import type { Nitro, NitroImportMeta } from "nitro/types"; import { dirname } from "pathe"; import { defineEnv } from "unenv"; import { runtimeDependencies, presetsDir, runtimeDir } from "nitro/meta"; +import { escapeRegExp } from "../utils/regex.ts"; export type BaseBuildConfig = ReturnType; @@ -36,9 +37,10 @@ export function baseBuildConfig(nitro: Nitro) { const noExternal: (string | RegExp)[] = [ /^(?:[\0#~.]|virtual:)/, /^nitro$/, - /nitro\/(dist|app|~internal)/, + /nitro\/(dist|app|cache|storage|context|database|task|runtime-config|~internal)/, runtimeDir, presetsDir, + new RegExp(escapeRegExp(nitro.options.rootDir) + "(?!.*node_modules)"), nitro.options.buildDir, ...nitro.options.scanDirs, dirname(nitro.options.entry), diff --git a/src/build/plugins.ts b/src/build/plugins.ts index 828aa418a7..93747ac75a 100644 --- a/src/build/plugins.ts +++ b/src/build/plugins.ts @@ -128,7 +128,7 @@ export function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { // WIP: Opt-in tracing for prod // WIP: Simpler externals for dev if (nitro.options.dev) { - plugins.push(externals({ exclude: base.noExternal })); + plugins.push(externals({ noExternal: base.noExternal })); } // plugins.push( diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index f8d7d7a8b5..c9eae0e385 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -1,36 +1,28 @@ import type { Plugin } from "rollup"; import { pathToFileURL } from "node:url"; import { isAbsolute } from "pathe"; +import { escapeRegExp } from "../../utils/regex.ts"; export type ExternalsOptions = { - include?: FilterMatcher[]; - exclude?: FilterMatcher[]; + noExternal?: (string | RegExp)[]; }; -const NO_EXTERNAL_RE = /^(?:[\0#~.]|[a-z0-9]{2,}:)|\?/; - export function externals(opts: ExternalsOptions): Plugin { - const exclude = createFilter(opts?.exclude || []); + const noExternal: RegExp[] = [ + /^(?:[\0#~.]|[a-z0-9]{2,}:)|\?/, + ...(opts?.noExternal || []).map((i) => toRegexFilter(i)), + ]; + return { name: "nitro:externals", resolveId: { order: "pre", - filter: { id: { exclude: NO_EXTERNAL_RE } }, + filter: { id: { exclude: noExternal } }, async handler(id, importer, rOpts) { - if (exclude(id)) { - return null; - } const resolved = await this.resolve(id, importer, rOpts); - if ( - !resolved?.id || - NO_EXTERNAL_RE.test(resolved.id) || - !filter(resolved.id) - ) { + if (!resolved?.id || noExternal.some((p) => p.test(resolved.id))) { return resolved; } - if (id.includes("test")) { - console.log(">", id); - } return { external: true, id: isAbsolute(resolved.id) @@ -42,26 +34,12 @@ export function externals(opts: ExternalsOptions): Plugin { }; } -// --- helper functions for filter matching --- - -type FilterFunction = (id: string) => boolean; -type FilterMatcher = string | RegExp | FilterFunction; - -function createFilter(matchers: FilterMatcher[] = []) { - const matcherFns = matchers - .map((m) => toMatcher(m)) - .filter((m): m is FilterFunction => typeof m === "function"); - return (id: string) => matcherFns.some((fn) => fn(id)); -} - -function toMatcher( - filter: string | RegExp | ((id: string) => boolean) -): FilterFunction | undefined { - if (typeof filter === "string") { - return Object.assign((id: string) => id.startsWith(filter), { filter }); - } else if (filter instanceof RegExp) { - return Object.assign((id: string) => filter.test(id), { filter }); - } else if (typeof filter === "function") { - return filter; +function toRegexFilter(input: string | RegExp): RegExp { + if (input instanceof RegExp) { + return input; + } + if (typeof input === "string") { + return new RegExp("^" + escapeRegExp(input)); } + throw new TypeError("Expected a string or RegExp", { cause: input }); } diff --git a/src/utils/regex.ts b/src/utils/regex.ts new file mode 100644 index 0000000000..8dc58e051d --- /dev/null +++ b/src/utils/regex.ts @@ -0,0 +1,3 @@ +export function escapeRegExp(string: string): string { + return string.replace(/[-\\^$*+?.()|[\]{}]/g, String.raw`\$&`); +} From 2591e1db012622bf595ea2a767b97e1a635682d5 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 25 Nov 2025 13:56:44 +0100 Subject: [PATCH 08/30] workaround a --- src/build/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/build/config.ts b/src/build/config.ts index d6c43e49b2..3d6c3b7ced 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -38,6 +38,7 @@ export function baseBuildConfig(nitro: Nitro) { /^(?:[\0#~.]|virtual:)/, /^nitro$/, /nitro\/(dist|app|cache|storage|context|database|task|runtime-config|~internal)/, + /node_modules\/(source-map)/, runtimeDir, presetsDir, new RegExp(escapeRegExp(nitro.options.rootDir) + "(?!.*node_modules)"), From 98bcc79550b65592b48e4d0963becf01cf7476fd Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 25 Nov 2025 14:17:06 +0100 Subject: [PATCH 09/30] improve plugin --- src/build/plugins.ts | 7 +++++- src/build/plugins/externals.ts | 42 ++++++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/build/plugins.ts b/src/build/plugins.ts index 93747ac75a..dface4002d 100644 --- a/src/build/plugins.ts +++ b/src/build/plugins.ts @@ -128,7 +128,12 @@ export function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { // WIP: Opt-in tracing for prod // WIP: Simpler externals for dev if (nitro.options.dev) { - plugins.push(externals({ noExternal: base.noExternal })); + plugins.push( + externals({ + rootDir: nitro.options.rootDir, + noExternal: base.noExternal, + }) + ); } // plugins.push( diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index c9eae0e385..17d10d70b3 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -1,12 +1,17 @@ import type { Plugin } from "rollup"; import { pathToFileURL } from "node:url"; import { isAbsolute } from "pathe"; +import { resolveModulePath } from "exsolve"; +import { builtinModules } from "node:module"; import { escapeRegExp } from "../../utils/regex.ts"; export type ExternalsOptions = { + rootDir: string; noExternal?: (string | RegExp)[]; }; +const PLUGIN_NAME = "nitro:externals"; + export function externals(opts: ExternalsOptions): Plugin { const noExternal: RegExp[] = [ /^(?:[\0#~.]|[a-z0-9]{2,}:)|\?/, @@ -14,20 +19,49 @@ export function externals(opts: ExternalsOptions): Plugin { ]; return { - name: "nitro:externals", + name: PLUGIN_NAME, resolveId: { order: "pre", filter: { id: { exclude: noExternal } }, async handler(id, importer, rOpts) { + // Externalize built-in modules with normalized prefix + if (builtinModules.includes(id)) { + return { + resolvedBy: PLUGIN_NAME, + external: true, + id: id.includes(":") ? id : `node:${id}`, + }; + } + + // Resolve const resolved = await this.resolve(id, importer, rOpts); + + // Check if not resolved or explicitly marked as no external if (!resolved?.id || noExternal.some((p) => p.test(resolved.id))) { return resolved; } + + // Resolve to absolute path (rollup quirk) + let externalId = resolved.id; + if (!isAbsolute(externalId)) { + externalId = + resolveModulePath(externalId, { + try: true, + from: importer || opts.rootDir, + }) || externalId; + } + + // Convert to file URL if absolute path for windows compatibility + if (isAbsolute(externalId)) { + externalId = pathToFileURL(externalId).href; + } + + // Mark as external return { + ...resolved, + resolvedBy: PLUGIN_NAME, external: true, - id: isAbsolute(resolved.id) - ? pathToFileURL(resolved.id).href - : resolved.id, + id: externalId, }; }, }, From 073b0bd1482a4023bdd6c52bc24259902c984c44 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 25 Nov 2025 14:45:10 +0100 Subject: [PATCH 10/30] rolldown fixes --- src/build/rolldown/config.ts | 1 + src/build/rolldown/dev.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/build/rolldown/config.ts b/src/build/rolldown/config.ts index 72a4ad5e2d..919d4e8a30 100644 --- a/src/build/rolldown/config.ts +++ b/src/build/rolldown/config.ts @@ -13,6 +13,7 @@ export const getRolldownConfig = (nitro: Nitro): RolldownOptions => { const tsc = nitro.options.typescript.tsConfig?.compilerOptions; let config = { + platform: nitro.options.node ? "node" : "neutral", cwd: nitro.options.rootDir, input: nitro.options.entry, external: [ diff --git a/src/build/rolldown/dev.ts b/src/build/rolldown/dev.ts index 24dd1a19a9..984f34021e 100644 --- a/src/build/rolldown/dev.ts +++ b/src/build/rolldown/dev.ts @@ -90,6 +90,7 @@ export async function watchDev(nitro: Nitro, config: RolldownOptions) { break; } case "ERROR": { + nitro.logger.error(event.error); nitro.hooks.callHook("dev:error", event.error); } } From cddda42829ca877493b88e174f9bd5d3316cac20 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 25 Nov 2025 22:20:14 +0100 Subject: [PATCH 11/30] improve handling cjs externals --- src/build/config.ts | 1 - src/build/plugins/externals.ts | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/build/config.ts b/src/build/config.ts index 3d6c3b7ced..d6c43e49b2 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -38,7 +38,6 @@ export function baseBuildConfig(nitro: Nitro) { /^(?:[\0#~.]|virtual:)/, /^nitro$/, /nitro\/(dist|app|cache|storage|context|database|task|runtime-config|~internal)/, - /node_modules\/(source-map)/, runtimeDir, presetsDir, new RegExp(escapeRegExp(nitro.options.rootDir) + "(?!.*node_modules)"), diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index 17d10d70b3..b97442cae0 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -34,10 +34,21 @@ export function externals(opts: ExternalsOptions): Plugin { } // Resolve - const resolved = await this.resolve(id, importer, rOpts); + let resolved = await this.resolve(id, importer, rOpts); + + // Keep CommonJS external imports as ESM externals (rollup quirk) unless explicitly marked as no external to bundle + const cjsResolved = resolved?.meta?.commonjs + ?.resolved as typeof resolved; + if (cjsResolved) { + if (noExternal.some((p) => p.test(cjsResolved.id))) { + // return null; + return resolved; + } + resolved = cjsResolved as typeof resolved; + } // Check if not resolved or explicitly marked as no external - if (!resolved?.id || noExternal.some((p) => p.test(resolved.id))) { + if (!resolved?.id || noExternal.some((p) => p.test(resolved!.id))) { return resolved; } From 833ac46211d03f0b7917130f9e2bd83982a55f24 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 25 Nov 2025 22:23:10 +0100 Subject: [PATCH 12/30] remove buildDir from default noExternal --- src/build/config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/build/config.ts b/src/build/config.ts index d6c43e49b2..4a6fe4cf11 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -41,7 +41,6 @@ export function baseBuildConfig(nitro: Nitro) { runtimeDir, presetsDir, new RegExp(escapeRegExp(nitro.options.rootDir) + "(?!.*node_modules)"), - nitro.options.buildDir, ...nitro.options.scanDirs, dirname(nitro.options.entry), ...(nitro.options.wasm === false ? [] : [/\.wasm$/]), From c7c99687d5a203dafa368a4cdd9ef925af21e83d Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 27 Nov 2025 01:14:43 +0100 Subject: [PATCH 13/30] update --- examples/vite-ssr-solid/vite.config.mjs | 1 - examples/vite-ssr-vue-router/vite.config.mjs | 3 - src/build/config.ts | 1 + src/build/plugins.ts | 34 +-- src/build/plugins/externals.ts | 213 ++++++++++++++++--- src/build/vite/env.ts | 12 +- src/presets/_nitro/base-worker.ts | 1 + src/types/config.ts | 1 + test/examples.test.ts | 4 +- test/presets/vercel.test.ts | 9 +- vitest.config.ts | 2 + 11 files changed, 211 insertions(+), 70 deletions(-) diff --git a/examples/vite-ssr-solid/vite.config.mjs b/examples/vite-ssr-solid/vite.config.mjs index b9b708b68f..23fdc5dbd4 100644 --- a/examples/vite-ssr-solid/vite.config.mjs +++ b/examples/vite-ssr-solid/vite.config.mjs @@ -7,7 +7,6 @@ export default defineConfig({ esbuild: { jsx: "preserve", jsxImportSource: "solid-js" }, environments: { ssr: { - resolve: { noExternal: true /* fixes tests */ }, build: { rollupOptions: { input: "./src/entry-server.tsx" } }, }, client: { diff --git a/examples/vite-ssr-vue-router/vite.config.mjs b/examples/vite-ssr-vue-router/vite.config.mjs index 8813347f06..577118e51f 100644 --- a/examples/vite-ssr-vue-router/vite.config.mjs +++ b/examples/vite-ssr-vue-router/vite.config.mjs @@ -5,9 +5,6 @@ import { nitro } from "nitro/vite"; export default defineConfig((_env) => ({ plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()], - nitro: { - noExternals: true, - }, environments: { client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } }, ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } }, diff --git a/src/build/config.ts b/src/build/config.ts index 4a6fe4cf11..cca74a68b6 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -40,6 +40,7 @@ export function baseBuildConfig(nitro: Nitro) { /nitro\/(dist|app|cache|storage|context|database|task|runtime-config|~internal)/, runtimeDir, presetsDir, + !nitro.options.dev && nitro.options.buildDir, new RegExp(escapeRegExp(nitro.options.rootDir) + "(?!.*node_modules)"), ...nitro.options.scanDirs, dirname(nitro.options.entry), diff --git a/src/build/plugins.ts b/src/build/plugins.ts index dface4002d..baeea9933f 100644 --- a/src/build/plugins.ts +++ b/src/build/plugins.ts @@ -108,7 +108,7 @@ export function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { ) ); - // User virtuals + // User virtual templates plugins.push(virtual(nitro.options.virtual, nitro.vfs)); // Renderer template @@ -116,7 +116,7 @@ export function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { plugins.push(rendererTemplate(nitro)); } - // Replace Plugin + // Replace plugins.push( (replace as unknown as typeof replace.default)({ preventAssignment: true, @@ -124,35 +124,21 @@ export function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { }) ); - // Externals Plugin - // WIP: Opt-in tracing for prod - // WIP: Simpler externals for dev - if (nitro.options.dev) { + // Externals (require Node.js compatible resolution) + if (nitro.options.node && nitro.options.noExternals !== true) { plugins.push( externals({ rootDir: nitro.options.rootDir, - noExternal: base.noExternal, + conditions: nitro.options.exportConditions || ["default"], + exclude: [...base.noExternal], + include: nitro.options.dev ? undefined : [], + trace: nitro.options.dev + ? false + : { outDir: nitro.options.output.serverDir }, }) ); } - // plugins.push( - // rollupNodeFileTrace({ - // outDir: nitro.options.output.serverDir, - // moduleDirectories: nitro.options.nodeModulesDirs, - // external: nitro.options.nodeModulesDirs, - // noTrace: nitro.options.dev, - // inline: [...base.noExternal], - // traceOptions: { - // base: "/", - // exportsOnly: true, - // processCwd: nitro.options.rootDir, - // }, - // exportConditions: nitro.options.exportConditions as string[], - // writePackageJson: true, - // }) - // ); - // Minify if ( nitro.options.sourcemap && diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index b97442cae0..2470ded96c 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -1,28 +1,63 @@ import type { Plugin } from "rollup"; +import type { ExternalsTraceOptions } from "nf3"; import { pathToFileURL } from "node:url"; -import { isAbsolute } from "pathe"; +import { isAbsolute, join } from "pathe"; import { resolveModulePath } from "exsolve"; -import { builtinModules } from "node:module"; import { escapeRegExp } from "../../utils/regex.ts"; +import { builtinModules, createRequire } from "node:module"; +import type { PackageJson } from "pkg-types"; export type ExternalsOptions = { rootDir: string; - noExternal?: (string | RegExp)[]; + conditions: string[]; + exclude?: (string | RegExp)[]; + include?: (string | RegExp)[]; + trace?: + | false + | Omit< + ExternalsTraceOptions, + "rootDir" | "exportConditions" | "traceOptions" + >; }; const PLUGIN_NAME = "nitro:externals"; export function externals(opts: ExternalsOptions): Plugin { - const noExternal: RegExp[] = [ + const exclude: RegExp[] = [ /^(?:[\0#~.]|[a-z0-9]{2,}:)|\?/, - ...(opts?.noExternal || []).map((i) => toRegexFilter(i)), + ...(opts?.exclude || []).map((i) => toRegexFilter(i)), ]; + const include: RegExp[] | undefined = opts?.include + ? (opts?.include || []).map((i) => toRegexFilter(i)) + : undefined; + + const filter = (id: string) => { + // Most match at least one include (if specified) + if (include && !include.some((r) => r.test(id))) { + return false; + } + // Most not match any exclude + if (exclude.some((r) => r.test(id))) { + return false; + } + return true; + }; + + const tryResolve = (id: string, from: string | undefined) => + resolveModulePath(id, { + try: true, + from: from && isAbsolute(from) ? from : opts.rootDir, + conditions: opts.conditions, + }); + + const tracedPaths = new Set(); + return { name: PLUGIN_NAME, resolveId: { order: "pre", - filter: { id: { exclude: noExternal } }, + filter: { id: { exclude, include } }, async handler(id, importer, rOpts) { // Externalize built-in modules with normalized prefix if (builtinModules.includes(id)) { @@ -33,52 +68,91 @@ export function externals(opts: ExternalsOptions): Plugin { }; } - // Resolve + // Resolve by other resolvers let resolved = await this.resolve(id, importer, rOpts); - // Keep CommonJS external imports as ESM externals (rollup quirk) unless explicitly marked as no external to bundle - const cjsResolved = resolved?.meta?.commonjs - ?.resolved as typeof resolved; + // Skip rolldown-plugin-commonjs resolver for externals + const cjsResolved = resolved?.meta?.commonjs?.resolved; if (cjsResolved) { - if (noExternal.some((p) => p.test(cjsResolved.id))) { - // return null; - return resolved; + if (!filter(cjsResolved.id)) { + return resolved; // Bundled and wrapped by CJS plugin } - resolved = cjsResolved as typeof resolved; + resolved = cjsResolved /* non-wrapped */; } - // Check if not resolved or explicitly marked as no external - if (!resolved?.id || noExternal.some((p) => p.test(resolved!.id))) { + // Check if not resolved or explicitly marked as excluded + if (!resolved?.id || !filter(resolved!.id)) { return resolved; } - // Resolve to absolute path (rollup quirk) - let externalId = resolved.id; - if (!isAbsolute(externalId)) { - externalId = - resolveModulePath(externalId, { - try: true, - from: importer || opts.rootDir, - }) || externalId; + // Normalize to absolute path + let resolvedPath = resolved.id; + if (!isAbsolute(resolvedPath)) { + resolvedPath = tryResolve(resolvedPath, importer) || resolvedPath; } - // Convert to file URL if absolute path for windows compatibility - if (isAbsolute(externalId)) { - externalId = pathToFileURL(externalId).href; + // Tracing mode + if (opts.trace) { + let importId = toImport(id) || toImport(resolvedPath); + if (!importId) { + return resolved; + } + if (!tryResolve(importId, importer)) { + const guessed = await guessSubpath(resolvedPath, opts.conditions); + if (!guessed) { + console.log( + `[${PLUGIN_NAME}] Unable to trace external: ${id} (resolved to ${resolvedPath})` + ); + return resolved; + } + importId = guessed; + } + tracedPaths.add(resolvedPath); + return { + ...resolved, + resolvedBy: PLUGIN_NAME, + external: true, + id: importId, + }; } - // Mark as external + // Resolve as absolute path external return { ...resolved, resolvedBy: PLUGIN_NAME, external: true, - id: externalId, + id: isAbsolute(resolvedPath) + ? pathToFileURL(resolvedPath).href // windows compat + : resolvedPath, }; }, }, + buildEnd: { + order: "post", + async handler() { + if (!opts.trace || tracedPaths.size === 0) { + return; + } + console.log(tracedPaths); + const { traceNodeModules } = await import("nf3"); + await traceNodeModules([...tracedPaths], { + ...opts.trace, + exportConditions: opts.conditions, + rootDir: opts.rootDir, + writePackageJson: true, // deno compat + traceOptions: { + base: "/", + exportsOnly: true, + processCwd: opts.rootDir, + }, + }); + }, + }, }; } +// ---- Internal utils ---- + function toRegexFilter(input: string | RegExp): RegExp { if (input instanceof RegExp) { return input; @@ -88,3 +162,84 @@ function toRegexFilter(input: string | RegExp): RegExp { } throw new TypeError("Expected a string or RegExp", { cause: input }); } + +const NODE_MODULES_RE = + /^(?.+\/node_modules\/)(?[^/@]+|@[^/]+\/[^/]+)(?:\/(?.+))?$/; + +const IMPORT_RE = /^(?!\.)(?[^/@]+|@[^/]+\/[^/]+)(?:\/(?.+))?$/; + +function toImport(id: string): string | undefined { + if (isAbsolute(id)) { + const { name, subpath } = + NODE_MODULES_RE.exec(id)?.groups || ({} as Record); + if (name && subpath) { + return join(name, subpath); + } + } else if (IMPORT_RE.test(id)) { + return id; + } +} + +function guessSubpath(path: string, conditions: string[]): string | undefined { + const { dir, name, subpath } = NODE_MODULES_RE.exec(path)?.groups || {}; + if (!dir || !name || !subpath) { + return; + } + const pkgDir = join(dir, name) + "/"; + const exports = getPkgJSON(pkgDir)?.exports; + if (!exports || typeof exports !== "object") { + return; + } + for (const e of flattenExports(exports)) { + if (!conditions.includes(e.condition || "default")) { + continue; + } + if (e.fsPath === subpath) { + return join(name, e.subpath); + } + if (e.fsPath.includes("*")) { + const fsPathRe = new RegExp( + "^" + escapeRegExp(e.fsPath).replace(String.raw`\*`, "(.+?)") + "$" + ); + if (fsPathRe.test(subpath)) { + const matched = fsPathRe.exec(subpath)?.[1]; + if (matched) { + return join(name, e.subpath.replace("*", matched)); + } + } + } + } +} + +function getPkgJSON(dir: string): PackageJson | undefined { + const cache = ((getPkgJSON as any)._cache ||= new Map()); + if (cache.has(dir)) { + return cache.get(dir); + } + try { + const pkg = createRequire(dir)("./package.json"); + cache.set(dir, pkg); + return pkg; + } catch { + /* ignore */ + } +} + +// Based on mlly +function flattenExports( + exports: Exclude = {}, + parentSubpath = "./" +): { subpath: string; fsPath: string; condition?: string }[] { + return Object.entries(exports).flatMap(([key, value]) => { + const [subpath, condition] = key.startsWith(".") + ? [key.slice(1)] + : [undefined, key]; + const _subPath = join(parentSubpath, subpath || ""); + if (typeof value === "string") { + return [ + { subpath: _subPath, fsPath: value.replace(/^\.\//, ""), condition }, + ]; + } + return typeof value === "object" ? flattenExports(value, _subPath) : []; + }); +} diff --git a/src/build/vite/env.ts b/src/build/vite/env.ts index b0d19eaee5..e36313bce9 100644 --- a/src/build/vite/env.ts +++ b/src/build/vite/env.ts @@ -3,7 +3,7 @@ import type { NitroPluginContext, ServiceConfig } from "./types.ts"; import { NodeDevWorker } from "../../dev/worker.ts"; import { join, resolve } from "node:path"; -import { runtimeDependencies, runtimeDir } from "nitro/meta"; +import { runtimeDir } from "nitro/meta"; import { resolveModulePath } from "exsolve"; import { createFetchableDevEnvironment } from "./dev.ts"; import { isAbsolute } from "pathe"; @@ -38,12 +38,7 @@ export function createNitroEnvironment( }, resolve: { noExternal: ctx.nitro!.options.dev - ? [ - ...ctx.rollupConfig!.base.noExternal.filter( - (i) => typeof i === "string" || i instanceof RegExp - ), - ...runtimeDependencies, - ] + ? ctx.rollupConfig!.base.noExternal : true, // production build is standalone conditions: ctx.nitro!.options.exportConditions, externalConditions: ctx.nitro!.options.exportConditions, @@ -75,6 +70,9 @@ export function createServiceEnvironment( emptyOutDir: true, }, resolve: { + noExternal: ctx.nitro!.options.dev + ? ctx.rollupConfig!.base.noExternal + : true, // production build is standalone conditions: ctx.nitro!.options.exportConditions, externalConditions: ctx.nitro!.options.exportConditions, }, diff --git a/src/presets/_nitro/base-worker.ts b/src/presets/_nitro/base-worker.ts index d877d1a9c5..e3f2c64037 100644 --- a/src/presets/_nitro/base-worker.ts +++ b/src/presets/_nitro/base-worker.ts @@ -5,6 +5,7 @@ const baseWorker = defineNitroPreset( entry: null as any, // Abstract node: false, minify: true, + noExternals: true, rollupConfig: { output: { format: "iife", diff --git a/src/types/config.ts b/src/types/config.ts index a01b70b23b..2f9c46dce9 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -241,6 +241,7 @@ export interface NitroOptions extends PresetOptions { replace: Record string)>; commonJS?: RollupCommonJSOptions; exportConditions?: string[]; + noExternals?: boolean; // Advanced typescript: { diff --git a/test/examples.test.ts b/test/examples.test.ts index 4e06733fd8..1ecaa4715f 100644 --- a/test/examples.test.ts +++ b/test/examples.test.ts @@ -18,9 +18,7 @@ const skip = new Set(["websocket"]); const skipDev = new Set(["auto-imports", "cached-handler"]); -const skipProd = new Set([ - "vite-ssr-tss-react", // seroval export condition (only happens to test env) -]); +const skipProd = new Set([]); for (const example of await readdir(examplesDir)) { if (example.startsWith("_")) continue; diff --git a/test/presets/vercel.test.ts b/test/presets/vercel.test.ts index 7c3a9cbf08..8277cd2f6b 100644 --- a/test/presets/vercel.test.ts +++ b/test/presets/vercel.test.ts @@ -1,6 +1,6 @@ import { promises as fsp } from "node:fs"; import { resolve, join, basename } from "pathe"; -import { describe, expect, it, vi, afterAll } from "vitest"; +import { describe, expect, it, vi, beforeAll, afterAll } from "vitest"; import { setupTest, testNitro, fixtureDir } from "../tests.ts"; import { toFetchHandler } from "srvx/node"; @@ -519,8 +519,6 @@ describe("nitro:preset:vercel:bun", async () => { describe.skip("nitro:preset:vercel:bun-verceljson", async () => { const vercelJsonPath = join(fixtureDir, "vercel.json"); - // Need to make sure vercel.json is created before setupTest is called - await fsp.writeFile(vercelJsonPath, JSON.stringify({ bunVersion: "1.x" })); const ctx = await setupTest("vercel", { outDirSuffix: "-bun-verceljson", @@ -529,6 +527,11 @@ describe.skip("nitro:preset:vercel:bun-verceljson", async () => { }, }); + beforeAll(async () => { + // Need to make sure vercel.json is created before setupTest is called + await fsp.writeFile(vercelJsonPath, JSON.stringify({ bunVersion: "1.x" })); + }); + afterAll(async () => { await fsp.unlink(vercelJsonPath).catch(() => {}); }); diff --git a/vitest.config.ts b/vitest.config.ts index 55709c3cd7..6683af6817 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,5 +1,7 @@ import { defineConfig } from "vitest/config"; +process.env.NODE_ENV = "production"; + export default defineConfig({ test: { testTimeout: 30_000, From 7318c30855d39f75bd4727741f6152d55a23f81f Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 1 Dec 2025 12:19:48 +0100 Subject: [PATCH 14/30] remove logs --- src/build/plugins/externals.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index 2470ded96c..469c3949ec 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -100,9 +100,6 @@ export function externals(opts: ExternalsOptions): Plugin { if (!tryResolve(importId, importer)) { const guessed = await guessSubpath(resolvedPath, opts.conditions); if (!guessed) { - console.log( - `[${PLUGIN_NAME}] Unable to trace external: ${id} (resolved to ${resolvedPath})` - ); return resolved; } importId = guessed; @@ -133,7 +130,6 @@ export function externals(opts: ExternalsOptions): Plugin { if (!opts.trace || tracedPaths.size === 0) { return; } - console.log(tracedPaths); const { traceNodeModules } = await import("nf3"); await traceNodeModules([...tracedPaths], { ...opts.trace, From 9e450cb0be83e8679cfc67dd12fefa8f9b0e0dda Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 1 Dec 2025 12:50:23 +0100 Subject: [PATCH 15/30] update --- src/build/config.ts | 61 ++++++++++++++++++++++------------ src/build/plugins/externals.ts | 18 +++------- src/runtime/meta.ts | 3 +- src/types/config.ts | 2 +- 4 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/build/config.ts b/src/build/config.ts index cca74a68b6..fc7fc6917a 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -1,7 +1,6 @@ import type { Nitro, NitroImportMeta } from "nitro/types"; -import { dirname } from "pathe"; import { defineEnv } from "unenv"; -import { runtimeDependencies, presetsDir, runtimeDir } from "nitro/meta"; +import { runtimeDependencies, distDir } from "nitro/meta"; import { escapeRegExp } from "../utils/regex.ts"; export type BaseBuildConfig = ReturnType; @@ -34,25 +33,6 @@ export function baseBuildConfig(nitro: Nitro) { ...nitro.options.replace, }; - const noExternal: (string | RegExp)[] = [ - /^(?:[\0#~.]|virtual:)/, - /^nitro$/, - /nitro\/(dist|app|cache|storage|context|database|task|runtime-config|~internal)/, - runtimeDir, - presetsDir, - !nitro.options.dev && nitro.options.buildDir, - new RegExp(escapeRegExp(nitro.options.rootDir) + "(?!.*node_modules)"), - ...nitro.options.scanDirs, - dirname(nitro.options.entry), - ...(nitro.options.wasm === false ? [] : [/\.wasm$/]), - ...nitro.options.handlers - .map((m) => m.handler) - .filter((i) => typeof i === "string"), - ...(nitro.options.dev || nitro.options.preset === "nitro-prerender" - ? [] - : runtimeDependencies), - ].filter(Boolean) as string[]; - const { env } = defineEnv({ nodeCompat: isNodeless, resolve: true, @@ -64,8 +44,9 @@ export function baseBuildConfig(nitro: Nitro) { const aliases = resolveAliases({ ...env.alias }); + const noExternal: RegExp[] = getNoExternals(nitro); + return { - presetsDir, extensions, isNodeless, replacements, @@ -75,6 +56,42 @@ export function baseBuildConfig(nitro: Nitro) { }; } +function getNoExternals(nitro: Nitro): RegExp[] { + const noExternal = [ + /^(?:[\0#~.]|virtual:)/, + /nitro\/(dist|app|cache|storage|context|database|task|runtime-config|~internal)/, + nitro.options.wasm && /\.wasm$/, + new RegExp("^" + escapeRegExp(distDir)), + new RegExp( + "^" + escapeRegExp(nitro.options.rootDir) + "(?!.*node_modules)" + ), + ...nitro.options.scanDirs, + ...nitro.options.handlers + .map((m) => m.handler) + .filter((i) => typeof i === "string"), + ]; + + if (!nitro.options.dev && nitro.options.preset !== "nitro-prerender") { + noExternal.push( + new RegExp( + `node_modules/${runtimeDependencies.map((dep) => escapeRegExp(dep)).join("|")}` + ) + ); + } + + if (Array.isArray(nitro.options.noExternals)) { + noExternal.push(...nitro.options.noExternals); + } + + return ( + noExternal + .filter(Boolean) + .map((item) => + typeof item === "string" ? new RegExp(escapeRegExp(item)) : item + ) as RegExp[] + ).sort((a, b) => a.source.length - b.source.length); +} + export function resolveAliases(_aliases: Record) { // Sort aliases from specific to general (ie. fs/promises before fs) const aliases = Object.fromEntries( diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index 469c3949ec..3d211e779a 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -10,8 +10,8 @@ import type { PackageJson } from "pkg-types"; export type ExternalsOptions = { rootDir: string; conditions: string[]; - exclude?: (string | RegExp)[]; - include?: (string | RegExp)[]; + exclude?: RegExp[]; + include?: RegExp[]; trace?: | false | Omit< @@ -25,11 +25,11 @@ const PLUGIN_NAME = "nitro:externals"; export function externals(opts: ExternalsOptions): Plugin { const exclude: RegExp[] = [ /^(?:[\0#~.]|[a-z0-9]{2,}:)|\?/, - ...(opts?.exclude || []).map((i) => toRegexFilter(i)), + ...(opts?.exclude || []), ]; const include: RegExp[] | undefined = opts?.include - ? (opts?.include || []).map((i) => toRegexFilter(i)) + ? opts?.include || [] : undefined; const filter = (id: string) => { @@ -149,16 +149,6 @@ export function externals(opts: ExternalsOptions): Plugin { // ---- Internal utils ---- -function toRegexFilter(input: string | RegExp): RegExp { - if (input instanceof RegExp) { - return input; - } - if (typeof input === "string") { - return new RegExp("^" + escapeRegExp(input)); - } - throw new TypeError("Expected a string or RegExp", { cause: input }); -} - const NODE_MODULES_RE = /^(?.+\/node_modules\/)(?[^/@]+|@[^/]+\/[^/]+)(?:\/(?.+))?$/; diff --git a/src/runtime/meta.ts b/src/runtime/meta.ts index c08df47257..aac9d56d55 100644 --- a/src/runtime/meta.ts +++ b/src/runtime/meta.ts @@ -6,9 +6,10 @@ export const version: string = packageJson.version; const resolve = (path: string) => fileURLToPath(new URL(path, import.meta.url)); -export const pkgDir: string = /* @__PURE__ */ resolve("../../"); export const runtimeDir: string = /* @__PURE__ */ resolve("./"); +export const distDir: string = /* @__PURE__ */ resolve("../"); export const presetsDir: string = /* @__PURE__ */ resolve("../presets/"); +export const pkgDir: string = /* @__PURE__ */ resolve("../../"); export const runtimeDependencies: string[] = [ "crossws", // dep diff --git a/src/types/config.ts b/src/types/config.ts index 2f9c46dce9..7e57b6abd5 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -241,7 +241,7 @@ export interface NitroOptions extends PresetOptions { replace: Record string)>; commonJS?: RollupCommonJSOptions; exportConditions?: string[]; - noExternals?: boolean; + noExternals?: boolean | (string | RegExp)[]; // Advanced typescript: { From c17ea9957d0dab75c1b44268f87236f0f4e5f43d Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 1 Dec 2025 13:20:35 +0100 Subject: [PATCH 16/30] `traceDeps` config --- src/build/plugins/externals.ts | 19 ++++++++++--------- src/types/config.ts | 1 + src/utils/regex.ts | 10 ++++++++++ test/fixture/nitro.config.ts | 1 + test/presets/node.test.ts | 3 +-- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index 3d211e779a..9044591840 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -1,17 +1,18 @@ import type { Plugin } from "rollup"; +import type { PackageJson } from "pkg-types"; import type { ExternalsTraceOptions } from "nf3"; + import { pathToFileURL } from "node:url"; import { isAbsolute, join } from "pathe"; import { resolveModulePath } from "exsolve"; -import { escapeRegExp } from "../../utils/regex.ts"; +import { toRegExp } from "../../utils/regex.ts"; import { builtinModules, createRequire } from "node:module"; -import type { PackageJson } from "pkg-types"; export type ExternalsOptions = { rootDir: string; conditions: string[]; - exclude?: RegExp[]; - include?: RegExp[]; + exclude?: (string | RegExp)[]; + include?: (string | RegExp)[]; trace?: | false | Omit< @@ -23,15 +24,15 @@ export type ExternalsOptions = { const PLUGIN_NAME = "nitro:externals"; export function externals(opts: ExternalsOptions): Plugin { + const include: RegExp[] | undefined = opts?.include + ? opts.include.map((p) => toRegExp(p)) + : undefined; + const exclude: RegExp[] = [ /^(?:[\0#~.]|[a-z0-9]{2,}:)|\?/, - ...(opts?.exclude || []), + ...(opts?.exclude || []).map((p) => toRegExp(p)), ]; - const include: RegExp[] | undefined = opts?.include - ? opts?.include || [] - : undefined; - const filter = (id: string) => { // Most match at least one include (if specified) if (include && !include.some((r) => r.test(id))) { diff --git a/src/types/config.ts b/src/types/config.ts index 7e57b6abd5..0a69ee1eac 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -242,6 +242,7 @@ export interface NitroOptions extends PresetOptions { commonJS?: RollupCommonJSOptions; exportConditions?: string[]; noExternals?: boolean | (string | RegExp)[]; + traceDeps?: (string | RegExp)[]; // Advanced typescript: { diff --git a/src/utils/regex.ts b/src/utils/regex.ts index 8dc58e051d..ea6ffa0729 100644 --- a/src/utils/regex.ts +++ b/src/utils/regex.ts @@ -1,3 +1,13 @@ export function escapeRegExp(string: string): string { return string.replace(/[-\\^$*+?.()|[\]{}]/g, String.raw`\$&`); } + +export function toRegExp(input: string | RegExp): RegExp { + if (input instanceof RegExp) { + return input; + } + if (typeof input === "string") { + return new RegExp(escapeRegExp(input)); + } + throw new TypeError("Expected a string or RegExp", { cause: input }); +} diff --git a/test/fixture/nitro.config.ts b/test/fixture/nitro.config.ts index 8008862e7a..2e3dfecfdc 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -51,6 +51,7 @@ export default defineConfig({ ignorePath: true, }, }, + traceDeps: ["@fixture"], alias: { "#fixture-nitro-utils-extra-absolute": fileURLToPath( new URL("node_modules/@fixture/nitro-utils/extra2.mjs", import.meta.url) diff --git a/test/presets/node.test.ts b/test/presets/node.test.ts index 9545f1cc35..757aaaa4ec 100644 --- a/test/presets/node.test.ts +++ b/test/presets/node.test.ts @@ -33,8 +33,7 @@ describe("nitro:preset:node-middleware", async () => { expect(noncached2.headers.get("etag")).toBeNull(); }); - // TODO: Enable when explicit tracedDeps config added - it.todo("should not bundle externals", () => { + it("should trace externals", () => { const serverNodeModules = resolve(ctx.outDir, "server/node_modules"); expect( existsSync(resolve(serverNodeModules, "@fixture/nitro-utils/extra.mjs")) From 19641cd2ea1f631d5532085245973f58367bc79a Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 2 Dec 2025 00:08:52 +0100 Subject: [PATCH 17/30] add missing import --- src/build/plugins/externals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index 9044591840..ab7d952973 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -5,7 +5,7 @@ import type { ExternalsTraceOptions } from "nf3"; import { pathToFileURL } from "node:url"; import { isAbsolute, join } from "pathe"; import { resolveModulePath } from "exsolve"; -import { toRegExp } from "../../utils/regex.ts"; +import { escapeRegExp, toRegExp } from "../../utils/regex.ts"; import { builtinModules, createRequire } from "node:module"; export type ExternalsOptions = { From 67344331ba4e782017a166528d1cff0cb8a1c07d Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 2 Dec 2025 00:14:53 +0100 Subject: [PATCH 18/30] add back traceDeps --- src/build/plugins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/plugins.ts b/src/build/plugins.ts index bfab4d906b..31d39b345d 100644 --- a/src/build/plugins.ts +++ b/src/build/plugins.ts @@ -63,7 +63,7 @@ export function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { rootDir: nitro.options.rootDir, conditions: nitro.options.exportConditions || ["default"], exclude: [...base.noExternal], - include: nitro.options.dev ? undefined : [], + include: nitro.options.dev ? undefined : nitro.options.traceDeps, trace: nitro.options.dev ? false : { outDir: nitro.options.output.serverDir }, From 8dce0529b195ea046ca4d85a0f45efb63d06521e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 10:39:18 +0100 Subject: [PATCH 19/30] windows safety --- src/build/config.ts | 36 ++++++++++++++++------------------ src/build/plugins/externals.ts | 6 +++--- src/build/plugins/virtual.ts | 4 ++-- src/utils/regex.ts | 17 ++++++++++++++-- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/build/config.ts b/src/build/config.ts index fc7fc6917a..ca7b566aac 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -1,7 +1,7 @@ import type { Nitro, NitroImportMeta } from "nitro/types"; import { defineEnv } from "unenv"; import { runtimeDependencies, distDir } from "nitro/meta"; -import { escapeRegExp } from "../utils/regex.ts"; +import { pathRegExp, toPathRegExp } from "../utils/regex.ts"; export type BaseBuildConfig = ReturnType; @@ -57,39 +57,37 @@ export function baseBuildConfig(nitro: Nitro) { } function getNoExternals(nitro: Nitro): RegExp[] { - const noExternal = [ + const noExternal: RegExp[] = [ + /\.[mc]?tsx?$/, /^(?:[\0#~.]|virtual:)/, /nitro\/(dist|app|cache|storage|context|database|task|runtime-config|~internal)/, - nitro.options.wasm && /\.wasm$/, - new RegExp("^" + escapeRegExp(distDir)), - new RegExp( - "^" + escapeRegExp(nitro.options.rootDir) + "(?!.*node_modules)" + new RegExp("^" + pathRegExp(distDir)), + ...[nitro.options.rootDir, ...nitro.options.scanDirs].map( + (dir) => new RegExp("^" + pathRegExp(dir) + "(?!.*node_modules)") ), - ...nitro.options.scanDirs, - ...nitro.options.handlers - .map((m) => m.handler) - .filter((i) => typeof i === "string"), ]; + if (nitro.options.wasm) { + noExternal.push(/\.wasm$/); + } + if (!nitro.options.dev && nitro.options.preset !== "nitro-prerender") { noExternal.push( new RegExp( - `node_modules/${runtimeDependencies.map((dep) => escapeRegExp(dep)).join("|")}` + `${pathRegExp("node_modules/")}${runtimeDependencies.map((dep) => pathRegExp(dep)).join("|")}` ) ); } if (Array.isArray(nitro.options.noExternals)) { - noExternal.push(...nitro.options.noExternals); + noExternal.push( + ...nitro.options.noExternals + .filter(Boolean) + .map((item) => toPathRegExp(item as string | RegExp)) + ); } - return ( - noExternal - .filter(Boolean) - .map((item) => - typeof item === "string" ? new RegExp(escapeRegExp(item)) : item - ) as RegExp[] - ).sort((a, b) => a.source.length - b.source.length); + return noExternal.sort((a, b) => a.source.length - b.source.length); } export function resolveAliases(_aliases: Record) { diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index ab7d952973..ea05aa513e 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -5,7 +5,7 @@ import type { ExternalsTraceOptions } from "nf3"; import { pathToFileURL } from "node:url"; import { isAbsolute, join } from "pathe"; import { resolveModulePath } from "exsolve"; -import { escapeRegExp, toRegExp } from "../../utils/regex.ts"; +import { escapeRegExp, toPathRegExp } from "../../utils/regex.ts"; import { builtinModules, createRequire } from "node:module"; export type ExternalsOptions = { @@ -25,12 +25,12 @@ const PLUGIN_NAME = "nitro:externals"; export function externals(opts: ExternalsOptions): Plugin { const include: RegExp[] | undefined = opts?.include - ? opts.include.map((p) => toRegExp(p)) + ? opts.include.map((p) => toPathRegExp(p)) : undefined; const exclude: RegExp[] = [ /^(?:[\0#~.]|[a-z0-9]{2,}:)|\?/, - ...(opts?.exclude || []).map((p) => toRegExp(p)), + ...(opts?.exclude || []).map((p) => toPathRegExp(p)), ]; const filter = (id: string) => { diff --git a/src/build/plugins/virtual.ts b/src/build/plugins/virtual.ts index ace7e071b4..d0af5be8b8 100644 --- a/src/build/plugins/virtual.ts +++ b/src/build/plugins/virtual.ts @@ -1,5 +1,5 @@ import type { Plugin } from "rollup"; -import { escapeRegExp } from "../../utils/regex.ts"; +import { escapeRegExp, pathRegExp } from "../../utils/regex.ts"; const PREFIX = "\0nitro:virtual:"; @@ -29,7 +29,7 @@ export function virtual(input: VirtualModule[]): Plugin { order: "pre", filter: { id: new RegExp( - `^(${[...modules.keys()].map((id) => escapeRegExp(id)).join("|")})$` + `^(${[...modules.keys()].map((id) => pathRegExp(id)).join("|")})$` ), }, handler: (id) => { diff --git a/src/utils/regex.ts b/src/utils/regex.ts index ea6ffa0729..669d95ef73 100644 --- a/src/utils/regex.ts +++ b/src/utils/regex.ts @@ -1,13 +1,26 @@ +import { isWindows } from "std-env"; + export function escapeRegExp(string: string): string { return string.replace(/[-\\^$*+?.()|[\]{}]/g, String.raw`\$&`); } -export function toRegExp(input: string | RegExp): RegExp { +export function pathRegExp(string: string): string { + if (isWindows) { + string = string.replace(/\\/g, "/"); + } + let escaped = escapeRegExp(string); + if (isWindows) { + escaped = escaped.replace(/\//g, String.raw`[/\\]`); + } + return escaped; +} + +export function toPathRegExp(input: string | RegExp): RegExp { if (input instanceof RegExp) { return input; } if (typeof input === "string") { - return new RegExp(escapeRegExp(input)); + return new RegExp(pathRegExp(input)); } throw new TypeError("Expected a string or RegExp", { cause: input }); } From bc7885ecd7309cfbf01c7c712cd184197a2e483f Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 15:01:15 +0100 Subject: [PATCH 20/30] win/wasm fixes --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- src/build/config.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 5bd790095d..ceb7d568f4 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "unctx": "^2.4.1", "unimport": "^5.5.0", "untyped": "^2.0.0", - "unwasm": "^0.5.1", + "unwasm": "^0.5.2", "vitest": "^4.0.15", "wrangler": "^4.51.0", "xml2js": "^0.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fb0b2da40..2384cda530 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,8 +305,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 unwasm: - specifier: ^0.5.1 - version: 0.5.1 + specifier: ^0.5.2 + version: 0.5.2 vitest: specifier: ^4.0.15 version: 4.0.15(@edge-runtime/vm@5.0.0)(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) @@ -6793,8 +6793,8 @@ packages: resolution: {integrity: sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==} hasBin: true - unwasm@0.5.1: - resolution: {integrity: sha512-tympzQyQWEgUdGr1bZolS/Q6sG4H7eLZ47LQXN8XyRlRYsYoiNGbSWNQnk3SU6w52zKReVucE95uAtnWxYoRew==} + unwasm@0.5.2: + resolution: {integrity: sha512-uWhB7IXQjMC4530uVAeu0lzvYK6P3qHVnmmdQniBi48YybOLN/DqEzcP9BRGk1YTDG3rRWRD8me55nIYoTHyMg==} update-browserslist-db@1.1.4: resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} @@ -13811,7 +13811,7 @@ snapshots: knitwork: 1.3.0 scule: 1.3.0 - unwasm@0.5.1: + unwasm@0.5.2: dependencies: exsolve: 1.0.8 knitwork: 1.3.0 diff --git a/src/build/config.ts b/src/build/config.ts index ca7b566aac..f73e61a636 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -67,7 +67,7 @@ function getNoExternals(nitro: Nitro): RegExp[] { ), ]; - if (nitro.options.wasm) { + if (nitro.options.wasm !== false) { noExternal.push(/\.wasm$/); } From 3d45f28dfe1371e32355920b885f7bb582214d69 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 15:12:35 +0100 Subject: [PATCH 21/30] fix defaults --- src/config/defaults.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 4cc49057c4..767ecb44b2 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -33,6 +33,7 @@ export const NitroDefaults: NitroConfig = { virtual: {}, compressPublicAssets: false, ignore: [], + wasm: {}, // Dev dev: false, @@ -71,6 +72,7 @@ export const NitroDefaults: NitroConfig = { replace: {}, node: true, sourcemap: false, + traceDeps: [], // Advanced typescript: { From 08f7c9809ae5b89ddec9af4f7b7a2b43089e7ef7 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 15:57:30 +0100 Subject: [PATCH 22/30] perf: skip nested rollup node resolutions --- src/build/plugins/externals.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index ea05aa513e..2d6fc4373b 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -3,10 +3,10 @@ import type { PackageJson } from "pkg-types"; import type { ExternalsTraceOptions } from "nf3"; import { pathToFileURL } from "node:url"; +import { builtinModules, createRequire } from "node:module"; import { isAbsolute, join } from "pathe"; import { resolveModulePath } from "exsolve"; import { escapeRegExp, toPathRegExp } from "../../utils/regex.ts"; -import { builtinModules, createRequire } from "node:module"; export type ExternalsOptions = { rootDir: string; @@ -69,6 +69,11 @@ export function externals(opts: ExternalsOptions): Plugin { }; } + // Skip nested rollup-node resolutions + if (rOpts.custom?.["node-resolve"]) { + return null; + } + // Resolve by other resolvers let resolved = await this.resolve(id, importer, rOpts); From 5752b3942ecb95d4e7011b77a1e4fe94d140dec5 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 16:12:23 +0100 Subject: [PATCH 23/30] enable rolldown test for tracing+absolute --- test/presets/node.test.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/test/presets/node.test.ts b/test/presets/node.test.ts index 757aaaa4ec..d61510e23b 100644 --- a/test/presets/node.test.ts +++ b/test/presets/node.test.ts @@ -38,13 +38,8 @@ describe("nitro:preset:node-middleware", async () => { expect( existsSync(resolve(serverNodeModules, "@fixture/nitro-utils/extra.mjs")) ).toBe(true); - if (ctx.nitro?.options.builder !== "rolldown") { - // TODO: Rolldown bundles absolute aliases? - expect( - existsSync( - resolve(serverNodeModules, "@fixture/nitro-utils/extra2.mjs") - ) - ).toBe(true); - } + expect( + existsSync(resolve(serverNodeModules, "@fixture/nitro-utils/extra2.mjs")) + ).toBe(true); }); }); From 25a14fd39a18a486861fa1f5da98cf32f3625942 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 16:38:52 +0100 Subject: [PATCH 24/30] fix prerender externals --- src/build/config.ts | 4 +++- src/build/plugins.ts | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/build/config.ts b/src/build/config.ts index f73e61a636..cde535e40b 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -71,7 +71,9 @@ function getNoExternals(nitro: Nitro): RegExp[] { noExternal.push(/\.wasm$/); } - if (!nitro.options.dev && nitro.options.preset !== "nitro-prerender") { + const isDevOrPrerender = + nitro.options.dev || nitro.options.preset === "nitro-prerender"; + if (!isDevOrPrerender) { noExternal.push( new RegExp( `${pathRegExp("node_modules/")}${runtimeDependencies.map((dep) => pathRegExp(dep)).join("|")}` diff --git a/src/build/plugins.ts b/src/build/plugins.ts index 31d39b345d..ad5ee31ec6 100644 --- a/src/build/plugins.ts +++ b/src/build/plugins.ts @@ -58,13 +58,15 @@ export function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { // Externals (require Node.js compatible resolution) if (nitro.options.node && nitro.options.noExternals !== true) { + const isDevOrPrerender = + nitro.options.dev || nitro.options.preset === "nitro-prerender"; plugins.push( externals({ rootDir: nitro.options.rootDir, conditions: nitro.options.exportConditions || ["default"], exclude: [...base.noExternal], - include: nitro.options.dev ? undefined : nitro.options.traceDeps, - trace: nitro.options.dev + include: isDevOrPrerender ? undefined : nitro.options.traceDeps, + trace: isDevOrPrerender ? false : { outDir: nitro.options.output.serverDir }, }) From 6836c85da88661a675c3307aaec02bb413ff21ab Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 16:40:19 +0100 Subject: [PATCH 25/30] remove absolute external test --- test/fixture/nitro.config.ts | 5 ----- test/fixture/server/routes/modules.ts | 3 --- test/presets/node.test.ts | 3 --- 3 files changed, 11 deletions(-) diff --git a/test/fixture/nitro.config.ts b/test/fixture/nitro.config.ts index 2e3dfecfdc..a61737d657 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -52,11 +52,6 @@ export default defineConfig({ }, }, traceDeps: ["@fixture"], - alias: { - "#fixture-nitro-utils-extra-absolute": fileURLToPath( - new URL("node_modules/@fixture/nitro-utils/extra2.mjs", import.meta.url) - ), - }, serverAssets: [ { baseName: "files", diff --git a/test/fixture/server/routes/modules.ts b/test/fixture/server/routes/modules.ts index b451232265..b311cefb2e 100644 --- a/test/fixture/server/routes/modules.ts +++ b/test/fixture/server/routes/modules.ts @@ -8,8 +8,6 @@ import depLib from "@fixture/nitro-lib"; import subpathLib from "@fixture/nitro-lib/subpath"; // @ts-ignore import extraUtils from "@fixture/nitro-utils/extra"; -// @ts-ignore -import extraUtilsAbsolute from "#fixture-nitro-utils-extra-absolute"; export default () => { return { @@ -18,6 +16,5 @@ export default () => { depLib, // expected to all be 2.0.0 subpathLib, // expected to 2.0.0 extraUtils, - extraUtilsAbsolute, }; }; diff --git a/test/presets/node.test.ts b/test/presets/node.test.ts index d61510e23b..26f88a8ce1 100644 --- a/test/presets/node.test.ts +++ b/test/presets/node.test.ts @@ -38,8 +38,5 @@ describe("nitro:preset:node-middleware", async () => { expect( existsSync(resolve(serverNodeModules, "@fixture/nitro-utils/extra.mjs")) ).toBe(true); - expect( - existsSync(resolve(serverNodeModules, "@fixture/nitro-utils/extra2.mjs")) - ).toBe(true); }); }); From 51d2da3095355fe465bebcf6f75cfa6846a90dd5 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 16:40:58 +0100 Subject: [PATCH 26/30] remove unused import --- test/fixture/nitro.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/fixture/nitro.config.ts b/test/fixture/nitro.config.ts index a61737d657..4c437c578a 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -1,6 +1,5 @@ import { defineConfig } from "nitro"; -import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; import { existsSync } from "node:fs"; From 6dd1dcc0ceb5aacfbd96fa8c333f4a31e0fac1d6 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 18:01:48 +0100 Subject: [PATCH 27/30] hot fixes for vite (e2e) --- src/build/vite/env.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/build/vite/env.ts b/src/build/vite/env.ts index e36313bce9..f051cba8ec 100644 --- a/src/build/vite/env.ts +++ b/src/build/vite/env.ts @@ -3,7 +3,7 @@ import type { NitroPluginContext, ServiceConfig } from "./types.ts"; import { NodeDevWorker } from "../../dev/worker.ts"; import { join, resolve } from "node:path"; -import { runtimeDir } from "nitro/meta"; +import { runtimeDependencies, runtimeDir } from "nitro/meta"; import { resolveModulePath } from "exsolve"; import { createFetchableDevEnvironment } from "./dev.ts"; import { isAbsolute } from "pathe"; @@ -38,7 +38,11 @@ export function createNitroEnvironment( }, resolve: { noExternal: ctx.nitro!.options.dev - ? ctx.rollupConfig!.base.noExternal + ? [ + /^nitro$/, // i have absolutely no idea why and how it fixes issues! + new RegExp(`^(${runtimeDependencies.join("|")})$`), // virtual resolutions in vite skip plugin hooks + ...ctx.rollupConfig!.base.noExternal, + ] : true, // production build is standalone conditions: ctx.nitro!.options.exportConditions, externalConditions: ctx.nitro!.options.exportConditions, From 23a87a02b0c19ae09143951142987c3a406cc806 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 18:56:54 +0100 Subject: [PATCH 28/30] less noExternal matchers --- src/build/config.ts | 15 +++++++++------ src/runtime/meta.ts | 1 - 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/build/config.ts b/src/build/config.ts index cde535e40b..41a40f5ea4 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -1,6 +1,6 @@ import type { Nitro, NitroImportMeta } from "nitro/types"; import { defineEnv } from "unenv"; -import { runtimeDependencies, distDir } from "nitro/meta"; +import { runtimeDependencies, pkgDir } from "nitro/meta"; import { pathRegExp, toPathRegExp } from "../utils/regex.ts"; export type BaseBuildConfig = ReturnType; @@ -60,11 +60,14 @@ function getNoExternals(nitro: Nitro): RegExp[] { const noExternal: RegExp[] = [ /\.[mc]?tsx?$/, /^(?:[\0#~.]|virtual:)/, - /nitro\/(dist|app|cache|storage|context|database|task|runtime-config|~internal)/, - new RegExp("^" + pathRegExp(distDir)), - ...[nitro.options.rootDir, ...nitro.options.scanDirs].map( - (dir) => new RegExp("^" + pathRegExp(dir) + "(?!.*node_modules)") - ), + new RegExp("^" + pathRegExp(pkgDir) + "(?!.*node_modules)"), + ...[ + nitro.options.rootDir, + ...nitro.options.scanDirs.filter( + (dir) => + dir.includes("node_modules") || !dir.startsWith(nitro.options.rootDir) + ), + ].map((dir) => new RegExp("^" + pathRegExp(dir) + "(?!.*node_modules)")), ]; if (nitro.options.wasm !== false) { diff --git a/src/runtime/meta.ts b/src/runtime/meta.ts index aac9d56d55..ec0eee7f7e 100644 --- a/src/runtime/meta.ts +++ b/src/runtime/meta.ts @@ -7,7 +7,6 @@ export const version: string = packageJson.version; const resolve = (path: string) => fileURLToPath(new URL(path, import.meta.url)); export const runtimeDir: string = /* @__PURE__ */ resolve("./"); -export const distDir: string = /* @__PURE__ */ resolve("../"); export const presetsDir: string = /* @__PURE__ */ resolve("../presets/"); export const pkgDir: string = /* @__PURE__ */ resolve("../../"); From 083438047c1cce5e8db613b868b219989cc887ff Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 18:59:40 +0100 Subject: [PATCH 29/30] remove explicit runtimeDependencies rule --- src/build/config.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/build/config.ts b/src/build/config.ts index 41a40f5ea4..b7bce887f9 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -1,6 +1,6 @@ import type { Nitro, NitroImportMeta } from "nitro/types"; import { defineEnv } from "unenv"; -import { runtimeDependencies, pkgDir } from "nitro/meta"; +import { pkgDir } from "nitro/meta"; import { pathRegExp, toPathRegExp } from "../utils/regex.ts"; export type BaseBuildConfig = ReturnType; @@ -74,16 +74,6 @@ function getNoExternals(nitro: Nitro): RegExp[] { noExternal.push(/\.wasm$/); } - const isDevOrPrerender = - nitro.options.dev || nitro.options.preset === "nitro-prerender"; - if (!isDevOrPrerender) { - noExternal.push( - new RegExp( - `${pathRegExp("node_modules/")}${runtimeDependencies.map((dep) => pathRegExp(dep)).join("|")}` - ) - ); - } - if (Array.isArray(nitro.options.noExternals)) { noExternal.push( ...nitro.options.noExternals From 11174c21182cebe862cd726e3644dfe819da97ed Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 3 Dec 2025 22:40:38 +0100 Subject: [PATCH 30/30] optional dep --- package.json | 8 +++-- pnpm-lock.yaml | 6 ++-- src/build/plugins/externals.ts | 7 ++++- src/utils/dep.ts | 53 ++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 src/utils/dep.ts diff --git a/package.json b/package.json index ceb7d568f4..a505c8c711 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,8 @@ "release": "pnpm test && pnpm build && changelogen --release --prerelease --push", "stub": "obuild --stub", "test": "pnpm lint && pnpm test:types && pnpm test:rollup && pnpm test:rolldown", - "test:rollup": "NITRO_BUILDER=rollup pnpm vitest", "test:rolldown": "NITRO_BUILDER=rolldown pnpm vitest", + "test:rollup": "NITRO_BUILDER=rollup pnpm vitest", "test:types": "tsc --noEmit" }, "resolutions": { @@ -63,7 +63,6 @@ "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", - "nf3": "^0.1.12", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.101.0", @@ -129,6 +128,7 @@ "mime": "^4.1.0", "miniflare": "^4.20251128.0", "mlly": "^1.8.0", + "nf3": "^0.1.12", "nypm": "^0.6.2", "obuild": "^0.4.4", "pathe": "^2.0.3", @@ -164,12 +164,16 @@ "youch-core": "^0.3.3" }, "peerDependencies": { + "nf3": ">=0.2", "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "peerDependenciesMeta": { + "nf3": { + "optional": true + }, "rolldown": { "optional": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2384cda530..5d7a49ef06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,9 +27,6 @@ importers: jiti: specifier: ^2.6.1 version: 2.6.1 - nf3: - specifier: ^0.1.12 - version: 0.1.12 ofetch: specifier: ^2.0.0-alpha.3 version: 2.0.0-alpha.3 @@ -223,6 +220,9 @@ importers: mlly: specifier: ^1.8.0 version: 1.8.0 + nf3: + specifier: ^0.1.12 + version: 0.1.12 nypm: specifier: ^0.6.2 version: 0.6.2 diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index 2d6fc4373b..83e324c77d 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -7,6 +7,7 @@ import { builtinModules, createRequire } from "node:module"; import { isAbsolute, join } from "pathe"; import { resolveModulePath } from "exsolve"; import { escapeRegExp, toPathRegExp } from "../../utils/regex.ts"; +import { importDep } from "../../utils/dep.ts"; export type ExternalsOptions = { rootDir: string; @@ -136,7 +137,11 @@ export function externals(opts: ExternalsOptions): Plugin { if (!opts.trace || tracedPaths.size === 0) { return; } - const { traceNodeModules } = await import("nf3"); + const { traceNodeModules } = await importDep({ + id: "nf3", + dir: opts.rootDir, + reason: "tracing external dependencies", + }); await traceNodeModules([...tracedPaths], { ...opts.trace, exportConditions: opts.conditions, diff --git a/src/utils/dep.ts b/src/utils/dep.ts new file mode 100644 index 0000000000..046b8b7ae0 --- /dev/null +++ b/src/utils/dep.ts @@ -0,0 +1,53 @@ +import { consola } from "consola"; +import { resolveModulePath } from "exsolve"; +import { isCI, isTest } from "std-env"; + +export async function importDep( + opts: { + id: string; + dir: string; + reason: string; + }, + _retry?: boolean +): Promise { + const resolved = resolveModulePath(opts.id, { + from: [opts.dir, import.meta.url], + cache: _retry ? false : true, + try: true, + }); + + if (resolved) { + return (await import(resolved)) as Promise; + } + + let shouldInstall: boolean | undefined; + if (_retry || isTest) { + shouldInstall = false; // Do not install dependencies in test mode + } else if (isCI) { + consola.info( + `\`${opts.id}\` is required for ${opts.reason}. Installing automatically in CI environment...` + ); + shouldInstall = true; // Auto install in CI environments + } else { + shouldInstall = await consola.prompt( + `\`${opts.id}\` is required for ${opts.reason}, but it is not installed. Would you like to install it?`, + { type: "confirm", default: true, cancel: "undefined" } + ); + } + + if (!shouldInstall) { + throw new Error( + `\`${opts.id}\` is not installed. Please add it to your dependencies for ${opts.reason}.` + ); + } + + const start = Date.now(); + consola.start(`Installing \`${opts.id}\` in \`${opts.dir}\`...`); + const { addDevDependency } = await import("nypm"); + await addDevDependency(opts.id, { cwd: opts.dir }); + consola.success( + `Installed \`${opts.id}\` in ${opts.dir} (${Date.now() - start}ms).` + ); + + return importDep(opts, true); +}