From 0029767658fc4e8de7fa1fb5544ceac9a7242de8 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:55:54 +0800 Subject: [PATCH 1/4] feat(builder): add builderless mode for runtime-only bundling --- src/build/build.ts | 4 ++ src/build/plugins.ts | 5 ++ src/build/plugins/user-code-external.ts | 63 ++++++++++++++++ src/build/rewrite.ts | 96 +++++++++++++++++++++++++ src/build/rolldown/prod.ts | 2 + src/build/rollup/prod.ts | 2 + src/config/defaults.ts | 1 + src/presets/cloudflare/utils.ts | 2 +- src/types/config.ts | 7 +- test/minimal/minimal.test.ts | 31 +++++++- 10 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 src/build/plugins/user-code-external.ts create mode 100644 src/build/rewrite.ts diff --git a/src/build/build.ts b/src/build/build.ts index 5fc53a58eb..3f39f1f03f 100644 --- a/src/build/build.ts +++ b/src/build/build.ts @@ -1,6 +1,10 @@ import type { Nitro } from "nitro/types"; export async function build(nitro: Nitro) { + if (nitro.options.builderless && nitro.options.builder === "vite") { + throw new Error("`builderless` is currently supported only with `rollup` and `rolldown`."); + } + switch (nitro.options.builder) { case "rollup": { const { rollupBuild } = await import("./rollup/build.ts"); diff --git a/src/build/plugins.ts b/src/build/plugins.ts index ace6f18f4d..3140b83c0e 100644 --- a/src/build/plugins.ts +++ b/src/build/plugins.ts @@ -11,6 +11,7 @@ import { virtual, virtualDeps } from "./plugins/virtual.ts"; import { sourcemapMinify } from "./plugins/sourcemap-min.ts"; import { raw } from "./plugins/raw.ts"; import { externals } from "./plugins/externals.ts"; +import { userCodeExternal } from "./plugins/user-code-external.ts"; export async function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { const plugins: Plugin[] = []; @@ -20,6 +21,10 @@ export async function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) { nitro.vfs = virtualPlugin.api.modules; plugins.push(virtualPlugin, virtualDeps()); + if (nitro.options.builderless) { + plugins.push(userCodeExternal(nitro)); + } + // Auto imports if (nitro.options.imports) { const unimportPlugin = await import("unimport/unplugin"); diff --git a/src/build/plugins/user-code-external.ts b/src/build/plugins/user-code-external.ts new file mode 100644 index 0000000000..400da1ac2a --- /dev/null +++ b/src/build/plugins/user-code-external.ts @@ -0,0 +1,63 @@ +import type { Plugin } from "rollup"; +import type { Nitro } from "nitro/types"; +import { isAbsolute, relative } from "pathe"; +import { presetsDir, runtimeDir } from "nitro/meta"; + +export const BUILDERLESS_EXTERNAL_MARKER = "nitro:user-code-external"; + +export function userCodeExternal(nitro: Nitro): Plugin { + const includeRoots = [...new Set([nitro.options.rootDir, ...nitro.options.scanDirs])]; + const excludeRoots = [ + runtimeDir, + presetsDir, + nitro.options.buildDir, + nitro.options.output.dir, + nitro.options.output.serverDir, + nitro.options.output.publicDir, + ]; + + const shouldExternalize = (id: string) => { + if (!isAbsolute(id) || isNodeModulesPath(id)) { + return false; + } + if (!includeRoots.some((root) => isSubpath(id, root))) { + return false; + } + return !excludeRoots.some((root) => isSubpath(id, root)); + }; + + return { + name: BUILDERLESS_EXTERNAL_MARKER, + resolveId: { + order: "pre", + handler(id) { + const [path] = splitSpecifier(id); + if (!path || !shouldExternalize(path)) { + return null; + } + return { + id, + external: true, + moduleSideEffects: true, + }; + }, + }, + }; +} + +function splitSpecifier(specifier: string) { + const queryIndex = specifier.indexOf("?"); + if (queryIndex < 0) { + return [specifier, ""] as const; + } + return [specifier.slice(0, queryIndex), specifier.slice(queryIndex)] as const; +} + +function isNodeModulesPath(path: string) { + return /[/\\]node_modules[/\\]/.test(path); +} + +function isSubpath(path: string, parent: string) { + const rel = relative(parent, path); + return !rel || (!rel.startsWith("..") && !isAbsolute(rel)); +} diff --git a/src/build/rewrite.ts b/src/build/rewrite.ts new file mode 100644 index 0000000000..1866c1b00a --- /dev/null +++ b/src/build/rewrite.ts @@ -0,0 +1,96 @@ +import type { Nitro } from "nitro/types"; +import { readFile } from "node:fs/promises"; +import { glob } from "tinyglobby"; +import { dirname, isAbsolute, relative } from "pathe"; +import { writeFile } from "../utils/fs.ts"; +import { presetsDir, runtimeDir } from "nitro/meta"; + +export async function rewriteBuilderlessImports(nitro: Nitro) { + if (!nitro.options.builderless) { + return; + } + + const files = await glob("**/*.{mjs,js}", { + cwd: nitro.options.output.serverDir, + absolute: true, + }); + + await Promise.all( + files.map(async (file) => { + const source = await readFile(file, "utf8"); + const rewritten = rewriteModuleImports(source, file, nitro); + if (rewritten !== source) { + await writeFile(file, rewritten); + } + }) + ); +} + +function rewriteModuleImports(source: string, fromFile: string, nitro: Nitro) { + const rewrite = (specifier: string) => rewriteSpecifier(specifier, fromFile, nitro); + + return source + .replace(/from\s+(['"])([^'"]+)\1/g, (_full, quote: string, specifier: string) => { + return `from ${quote}${rewrite(specifier)}${quote}`; + }) + .replace(/\bimport\s+(['"])([^'"]+)\1/g, (_full, quote: string, specifier: string) => { + return `import ${quote}${rewrite(specifier)}${quote}`; + }) + .replace(/import\(\s*(['"])([^'"]+)\1\s*\)/g, (_full, quote: string, specifier: string) => { + return `import(${quote}${rewrite(specifier)}${quote})`; + }); +} + +function rewriteSpecifier(specifier: string, fromFile: string, nitro: Nitro) { + const [path, query] = splitSpecifier(specifier); + if (!path || !isAbsolute(path) || !shouldRewrite(path, nitro)) { + return specifier; + } + + const relativePath = toPosixPath(relative(dirname(fromFile), path)); + const normalized = relativePath.startsWith(".") ? relativePath : `./${relativePath}`; + return normalized + query; +} + +function shouldRewrite(path: string, nitro: Nitro) { + if (isSubpath(path, runtimeDir) || isSubpath(path, presetsDir)) { + return false; + } + if (isNodeModulesPath(path)) { + return false; + } + + const includeRoots = [...new Set([nitro.options.rootDir, ...nitro.options.scanDirs])]; + const excludeRoots = [ + nitro.options.buildDir, + nitro.options.output.dir, + nitro.options.output.serverDir, + nitro.options.output.publicDir, + ]; + + if (!includeRoots.some((root) => isSubpath(path, root))) { + return false; + } + return !excludeRoots.some((root) => isSubpath(path, root)); +} + +function splitSpecifier(specifier: string) { + const queryIndex = specifier.indexOf("?"); + if (queryIndex < 0) { + return [specifier, ""] as const; + } + return [specifier.slice(0, queryIndex), specifier.slice(queryIndex)] as const; +} + +function isNodeModulesPath(path: string) { + return /[/\\]node_modules[/\\]/.test(path); +} + +function isSubpath(path: string, parent: string) { + const rel = relative(parent, path); + return !rel || (!rel.startsWith("..") && !isAbsolute(rel)); +} + +function toPosixPath(path: string) { + return path.replace(/\\/g, "/"); +} diff --git a/src/build/rolldown/prod.ts b/src/build/rolldown/prod.ts index baa42092e1..e9442e933d 100644 --- a/src/build/rolldown/prod.ts +++ b/src/build/rolldown/prod.ts @@ -7,6 +7,7 @@ import { scanHandlers } from "../../scan.ts"; import { generateFSTree } from "../../utils/fs-tree.ts"; import { writeTypes } from "../types.ts"; import { writeBuildInfo } from "../info.ts"; +import { rewriteBuilderlessImports } from "../rewrite.ts"; import type { RolldownOutput } from "rolldown"; export async function buildProduction(nitro: Nitro, config: RolldownOptions) { @@ -24,6 +25,7 @@ export async function buildProduction(nitro: Nitro, config: RolldownOptions) { ); const build = await rolldown.rolldown(config); output = (await build.write(config.output as OutputOptions)) as RolldownOutput; + await rewriteBuilderlessImports(nitro); } const buildInfo = await writeBuildInfo(nitro, output); diff --git a/src/build/rollup/prod.ts b/src/build/rollup/prod.ts index e67678f635..96e2593d1c 100644 --- a/src/build/rollup/prod.ts +++ b/src/build/rollup/prod.ts @@ -5,6 +5,7 @@ import { scanHandlers } from "../../scan.ts"; import { generateFSTree } from "../../utils/fs-tree.ts"; import { writeTypes } from "../types.ts"; import { writeBuildInfo } from "../info.ts"; +import { rewriteBuilderlessImports } from "../rewrite.ts"; import { formatRollupError } from "./error.ts"; import type { RollupOutput } from "rollup"; @@ -27,6 +28,7 @@ export async function buildProduction(nitro: Nitro, rollupConfig: RollupConfig) }); output = await build.write(rollupConfig.output); + await rewriteBuilderlessImports(nitro); } const buildInfo = await writeBuildInfo(nitro, output); diff --git a/src/config/defaults.ts b/src/config/defaults.ts index f032f77b44..acfe87616d 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -68,6 +68,7 @@ export const NitroDefaults: NitroConfig = { // Builder builder: undefined, + builderless: false, moduleSideEffects: ["unenv/polyfill/"], replace: {}, node: true, diff --git a/src/presets/cloudflare/utils.ts b/src/presets/cloudflare/utils.ts index a6447b92c3..627739c8a8 100644 --- a/src/presets/cloudflare/utils.ts +++ b/src/presets/cloudflare/utils.ts @@ -275,7 +275,7 @@ export async function writeWranglerConfig(nitro: Nitro, cfTarget: "pages" | "mod if (cfTarget === "module") { // Avoid double bundling if (wranglerConfig.no_bundle === undefined) { - wranglerConfig.no_bundle = true; + wranglerConfig.no_bundle = !nitro.options.builderless; } // Scan all server/ chunks diff --git a/src/types/config.ts b/src/types/config.ts index 8e733da763..db5bb27110 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -216,8 +216,13 @@ export interface NitroOptions extends PresetOptions { retryDelay?: number; }; - // Rollup + // Builder builder?: "rollup" | "rolldown" | "vite"; + /** + * Keep Nitro runtime bundling while leaving user handlers/modules unbundled. + * Useful when a downstream platform bundler (e.g. Wrangler) should bundle app code. + */ + builderless?: boolean; rollupConfig?: RollupConfig; rolldownConfig?: RolldownConfig; entry: string; diff --git a/test/minimal/minimal.test.ts b/test/minimal/minimal.test.ts index 80fe1e9f2a..9a3f60e25f 100644 --- a/test/minimal/minimal.test.ts +++ b/test/minimal/minimal.test.ts @@ -2,8 +2,9 @@ import { afterAll, describe, expect, it } from "vitest"; import { createNitro, build, prepare } from "nitro/builder"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { mkdir, rm, stat } from "node:fs/promises"; +import { mkdir, readFile, rm, stat } from "node:fs/promises"; import { glob } from "tinyglobby"; +import escapeRE from "escape-string-regexp"; const fixtureDir = fileURLToPath(new URL("./", import.meta.url)); const tmpDir = fileURLToPath(new URL(".tmp", import.meta.url)); @@ -65,6 +66,34 @@ describe("minimal fixture", () => { } } + describe("builderless", () => { + let outDir: string; + + it("externalizes app code with rolldown", async () => { + outDir = join(tmpDir, "output", "builderless-rolldown"); + await rm(outDir, { recursive: true, force: true }); + await mkdir(outDir, { recursive: true }); + const nitro = await createNitro({ + rootDir: fixtureDir, + output: { dir: outDir }, + builder: "rolldown", + builderless: true, + }); + await prepare(nitro); + await build(nitro); + await nitro.close(); + }); + + it("rewrites absolute imports to relative paths", async () => { + const files = await glob("server/**/*.{mjs,js}", { cwd: outDir, absolute: true }); + const contents = (await Promise.all(files.map((file) => readFile(file, "utf8")))).join("\n"); + const rootDirPattern = new RegExp(escapeRE(fileURLToPath(new URL("./", import.meta.url)))); + + expect(contents).toMatch(/from "(?:\.\.\/)+server\.ts"/); + expect(contents).not.toMatch(rootDirPattern); + }); + }); + if (process.env.TEST_DEBUG) { afterAll(() => { console.table(results); From ad3157fa800af7d32e0925a0225b186a8d75cf3e Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:00:01 +0800 Subject: [PATCH 2/4] feat(builder): support builderless mode with vite --- src/build/build.ts | 4 --- src/build/vite/prod.ts | 2 ++ test/minimal/minimal.test.ts | 56 ++++++++++++++++++++++-------------- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/build/build.ts b/src/build/build.ts index 3f39f1f03f..5fc53a58eb 100644 --- a/src/build/build.ts +++ b/src/build/build.ts @@ -1,10 +1,6 @@ import type { Nitro } from "nitro/types"; export async function build(nitro: Nitro) { - if (nitro.options.builderless && nitro.options.builder === "vite") { - throw new Error("`builderless` is currently supported only with `rollup` and `rolldown`."); - } - switch (nitro.options.builder) { case "rollup": { const { rollupBuild } = await import("./rollup/build.ts"); diff --git a/src/build/vite/prod.ts b/src/build/vite/prod.ts index 127c2fe50b..ae652a95c3 100644 --- a/src/build/vite/prod.ts +++ b/src/build/vite/prod.ts @@ -7,6 +7,7 @@ import { colors as C } from "consola/utils"; import { copyPublicAssets } from "../../builder.ts"; import { existsSync } from "node:fs"; import { writeBuildInfo } from "../info.ts"; +import { rewriteBuilderlessImports } from "../rewrite.ts"; import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { isTest, isCI } from "std-env"; import type { RolldownOutput } from "rolldown"; @@ -109,6 +110,7 @@ export async function buildEnvironments(ctx: NitroPluginContext, builder: ViteBu // Build the Nitro server bundle const output = (await builder.build(builder.environments.nitro)) as RolldownOutput; + await rewriteBuilderlessImports(nitro); // Close the Nitro instance await nitro.close(); diff --git a/test/minimal/minimal.test.ts b/test/minimal/minimal.test.ts index 9a3f60e25f..4d41feddbe 100644 --- a/test/minimal/minimal.test.ts +++ b/test/minimal/minimal.test.ts @@ -67,31 +67,43 @@ describe("minimal fixture", () => { } describe("builderless", () => { - let outDir: string; + const builders = ["rolldown", "vite", "vite7"] as const; - it("externalizes app code with rolldown", async () => { - outDir = join(tmpDir, "output", "builderless-rolldown"); - await rm(outDir, { recursive: true, force: true }); - await mkdir(outDir, { recursive: true }); - const nitro = await createNitro({ - rootDir: fixtureDir, - output: { dir: outDir }, - builder: "rolldown", - builderless: true, - }); - await prepare(nitro); - await build(nitro); - await nitro.close(); - }); + for (const builder of builders) { + describe(builder, () => { + let outDir: string; - it("rewrites absolute imports to relative paths", async () => { - const files = await glob("server/**/*.{mjs,js}", { cwd: outDir, absolute: true }); - const contents = (await Promise.all(files.map((file) => readFile(file, "utf8")))).join("\n"); - const rootDirPattern = new RegExp(escapeRE(fileURLToPath(new URL("./", import.meta.url)))); + it("externalizes app code", async () => { + outDir = join(tmpDir, "output", `builderless-${builder}`); + await rm(outDir, { recursive: true, force: true }); + await mkdir(outDir, { recursive: true }); + const nitro = await createNitro({ + rootDir: fixtureDir, + output: { dir: outDir }, + builder: builder.includes("vite") ? "vite" : "rolldown", + // @ts-expect-error for testing + __vitePkg__: builder.includes("vite") ? builder : undefined, + builderless: true, + }); + await prepare(nitro); + await build(nitro); + await nitro.close(); + }); - expect(contents).toMatch(/from "(?:\.\.\/)+server\.ts"/); - expect(contents).not.toMatch(rootDirPattern); - }); + it("rewrites absolute imports to relative paths", async () => { + const files = await glob("server/**/*.{mjs,js}", { cwd: outDir, absolute: true }); + const contents = (await Promise.all(files.map((file) => readFile(file, "utf8")))).join( + "\n" + ); + const rootDirPattern = new RegExp( + escapeRE(fileURLToPath(new URL("./", import.meta.url))) + ); + + expect(contents).toMatch(/from "(?:\.\.\/)+server\.ts"/); + expect(contents).not.toMatch(rootDirPattern); + }); + }); + } }); if (process.env.TEST_DEBUG) { From b5f850f60eb5eac0f0e02f4043c13bac3303c34d Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:04:44 +0800 Subject: [PATCH 3/4] test(builder): cover builderless mode with rollup --- test/minimal/minimal.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/minimal/minimal.test.ts b/test/minimal/minimal.test.ts index 4d41feddbe..0917c7f24f 100644 --- a/test/minimal/minimal.test.ts +++ b/test/minimal/minimal.test.ts @@ -67,7 +67,7 @@ describe("minimal fixture", () => { } describe("builderless", () => { - const builders = ["rolldown", "vite", "vite7"] as const; + const builders = ["rolldown", "rollup", "vite", "vite7"] as const; for (const builder of builders) { describe(builder, () => { @@ -80,7 +80,7 @@ describe("minimal fixture", () => { const nitro = await createNitro({ rootDir: fixtureDir, output: { dir: outDir }, - builder: builder.includes("vite") ? "vite" : "rolldown", + builder: builder.includes("vite") ? "vite" : (builder as "rolldown" | "rollup"), // @ts-expect-error for testing __vitePkg__: builder.includes("vite") ? builder : undefined, builderless: true, @@ -99,7 +99,7 @@ describe("minimal fixture", () => { escapeRE(fileURLToPath(new URL("./", import.meta.url))) ); - expect(contents).toMatch(/from "(?:\.\.\/)+server\.ts"/); + expect(contents).toMatch(/from ['"](?:\.\.\/)+server\.ts['"]/); expect(contents).not.toMatch(rootDirPattern); }); }); From 5f9fee8b3a32e5c4ed38af09834fd1c241702456 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:33:50 +0800 Subject: [PATCH 4/4] refactor(build): deduplicate builderless path matching --- src/build/plugins/user-code-external.ts | 42 ++--------------------- src/build/rewrite.ts | 45 ++----------------------- src/build/utils/builderless-path.ts | 42 +++++++++++++++++++++++ 3 files changed, 47 insertions(+), 82 deletions(-) create mode 100644 src/build/utils/builderless-path.ts diff --git a/src/build/plugins/user-code-external.ts b/src/build/plugins/user-code-external.ts index 400da1ac2a..717ed958f7 100644 --- a/src/build/plugins/user-code-external.ts +++ b/src/build/plugins/user-code-external.ts @@ -1,38 +1,17 @@ import type { Plugin } from "rollup"; import type { Nitro } from "nitro/types"; -import { isAbsolute, relative } from "pathe"; -import { presetsDir, runtimeDir } from "nitro/meta"; +import { isBuilderlessUserCodePath, splitSpecifier } from "../utils/builderless-path.ts"; export const BUILDERLESS_EXTERNAL_MARKER = "nitro:user-code-external"; export function userCodeExternal(nitro: Nitro): Plugin { - const includeRoots = [...new Set([nitro.options.rootDir, ...nitro.options.scanDirs])]; - const excludeRoots = [ - runtimeDir, - presetsDir, - nitro.options.buildDir, - nitro.options.output.dir, - nitro.options.output.serverDir, - nitro.options.output.publicDir, - ]; - - const shouldExternalize = (id: string) => { - if (!isAbsolute(id) || isNodeModulesPath(id)) { - return false; - } - if (!includeRoots.some((root) => isSubpath(id, root))) { - return false; - } - return !excludeRoots.some((root) => isSubpath(id, root)); - }; - return { name: BUILDERLESS_EXTERNAL_MARKER, resolveId: { order: "pre", handler(id) { const [path] = splitSpecifier(id); - if (!path || !shouldExternalize(path)) { + if (!path || !isBuilderlessUserCodePath(path, nitro)) { return null; } return { @@ -44,20 +23,3 @@ export function userCodeExternal(nitro: Nitro): Plugin { }, }; } - -function splitSpecifier(specifier: string) { - const queryIndex = specifier.indexOf("?"); - if (queryIndex < 0) { - return [specifier, ""] as const; - } - return [specifier.slice(0, queryIndex), specifier.slice(queryIndex)] as const; -} - -function isNodeModulesPath(path: string) { - return /[/\\]node_modules[/\\]/.test(path); -} - -function isSubpath(path: string, parent: string) { - const rel = relative(parent, path); - return !rel || (!rel.startsWith("..") && !isAbsolute(rel)); -} diff --git a/src/build/rewrite.ts b/src/build/rewrite.ts index 1866c1b00a..2892bb57c6 100644 --- a/src/build/rewrite.ts +++ b/src/build/rewrite.ts @@ -1,9 +1,9 @@ import type { Nitro } from "nitro/types"; import { readFile } from "node:fs/promises"; import { glob } from "tinyglobby"; -import { dirname, isAbsolute, relative } from "pathe"; +import { dirname, relative } from "pathe"; import { writeFile } from "../utils/fs.ts"; -import { presetsDir, runtimeDir } from "nitro/meta"; +import { isBuilderlessUserCodePath, splitSpecifier } from "./utils/builderless-path.ts"; export async function rewriteBuilderlessImports(nitro: Nitro) { if (!nitro.options.builderless) { @@ -43,7 +43,7 @@ function rewriteModuleImports(source: string, fromFile: string, nitro: Nitro) { function rewriteSpecifier(specifier: string, fromFile: string, nitro: Nitro) { const [path, query] = splitSpecifier(specifier); - if (!path || !isAbsolute(path) || !shouldRewrite(path, nitro)) { + if (!path || !isBuilderlessUserCodePath(path, nitro)) { return specifier; } @@ -52,45 +52,6 @@ function rewriteSpecifier(specifier: string, fromFile: string, nitro: Nitro) { return normalized + query; } -function shouldRewrite(path: string, nitro: Nitro) { - if (isSubpath(path, runtimeDir) || isSubpath(path, presetsDir)) { - return false; - } - if (isNodeModulesPath(path)) { - return false; - } - - const includeRoots = [...new Set([nitro.options.rootDir, ...nitro.options.scanDirs])]; - const excludeRoots = [ - nitro.options.buildDir, - nitro.options.output.dir, - nitro.options.output.serverDir, - nitro.options.output.publicDir, - ]; - - if (!includeRoots.some((root) => isSubpath(path, root))) { - return false; - } - return !excludeRoots.some((root) => isSubpath(path, root)); -} - -function splitSpecifier(specifier: string) { - const queryIndex = specifier.indexOf("?"); - if (queryIndex < 0) { - return [specifier, ""] as const; - } - return [specifier.slice(0, queryIndex), specifier.slice(queryIndex)] as const; -} - -function isNodeModulesPath(path: string) { - return /[/\\]node_modules[/\\]/.test(path); -} - -function isSubpath(path: string, parent: string) { - const rel = relative(parent, path); - return !rel || (!rel.startsWith("..") && !isAbsolute(rel)); -} - function toPosixPath(path: string) { return path.replace(/\\/g, "/"); } diff --git a/src/build/utils/builderless-path.ts b/src/build/utils/builderless-path.ts new file mode 100644 index 0000000000..a62971f105 --- /dev/null +++ b/src/build/utils/builderless-path.ts @@ -0,0 +1,42 @@ +import type { Nitro } from "nitro/types"; +import { presetsDir, runtimeDir } from "nitro/meta"; +import { isAbsolute, relative } from "pathe"; + +export function splitSpecifier(specifier: string) { + const queryIndex = specifier.indexOf("?"); + if (queryIndex < 0) { + return [specifier, ""] as const; + } + return [specifier.slice(0, queryIndex), specifier.slice(queryIndex)] as const; +} + +export function isNodeModulesPath(path: string) { + return /[/\\]node_modules[/\\]/.test(path); +} + +export function isSubpath(path: string, parent: string) { + const rel = relative(parent, path); + return !rel || (!rel.startsWith("..") && !isAbsolute(rel)); +} + +export function isBuilderlessUserCodePath(path: string, nitro: Nitro) { + if (!isAbsolute(path) || isNodeModulesPath(path)) { + return false; + } + if (isSubpath(path, runtimeDir) || isSubpath(path, presetsDir)) { + return false; + } + + const includeRoots = [...new Set([nitro.options.rootDir, ...nitro.options.scanDirs])]; + const excludeRoots = [ + nitro.options.buildDir, + nitro.options.output.dir, + nitro.options.output.serverDir, + nitro.options.output.publicDir, + ]; + + if (!includeRoots.some((root) => isSubpath(path, root))) { + return false; + } + return !excludeRoots.some((root) => isSubpath(path, root)); +}