Skip to content

Commit cd0ca19

Browse files
authored
Merge branch 'main' into feat/vercel-cron
2 parents 703ae6d + 09d6aa6 commit cd0ca19

5 files changed

Lines changed: 247 additions & 74 deletions

File tree

src/build/chunks.ts

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import type { Nitro } from "nitro/types";
2-
import { presetsDir, runtimeDir } from "nitro/meta";
2+
3+
// Tests in @test/unit/chunks.test.ts
34

45
const virtualRe = /^(?:\0|#|virtual:)/;
56

6-
export const NODE_MODULES_RE = /node_modules[/\\][^.]/;
7+
export const NODE_MODULES_RE = /node_modules[/\\](?!(?:nitro|nitro-nightly)[/\\])[^.]/;
78

89
export function libChunkName(id: string) {
9-
const pkgName = id.match(
10-
/.*(?:[/\\])node_modules(?:[/\\])(?<package>@[^/\\]+[/\\][^/\\]+|[^/\\.][^/\\]*)/
11-
)?.groups?.package;
12-
return `_libs/${pkgName || "common"}`;
10+
const pkgName = pathToPkgName(id);
11+
return pkgName ? `_libs/${pkgName}` : undefined;
12+
}
13+
14+
export function pathToPkgName(path: string): string | undefined {
15+
let pkgName = path.match(
16+
/.*(?:[/\\])node_modules(?:[/\\])(?<name>@[^/\\]+[/\\][^/\\]+|[^/\\.][^/\\]*)/
17+
)?.groups?.name;
18+
if (pkgName?.endsWith("-nightly")) {
19+
pkgName = pkgName.slice(0, -8);
20+
}
21+
return pkgName;
1322
}
1423

1524
export function getChunkName(chunk: { name: string; moduleIds: string[] }, nitro: Nitro) {
@@ -19,31 +28,19 @@ export function getChunkName(chunk: { name: string; moduleIds: string[] }, nitro
1928
}
2029

2130
// Library chunks
22-
if (chunk.moduleIds.every((id) => /node_modules[/\\]\w/.test(id))) {
23-
const pkgNames = [
24-
...new Set(
25-
chunk.moduleIds
26-
.map(
27-
(id) =>
28-
id.match(/.*[/\\]node_modules[/\\](?<package>@[^/\\]+[/\\][^/\\]+|[^/\\]+)/)?.groups
29-
?.package
30-
)
31-
.filter(Boolean)
32-
.map((name) => name!.split(/[/\\]/).pop()!)
33-
.filter(Boolean)
34-
),
35-
].sort((a, b) => a.length - b.length);
36-
let chunkName = "";
37-
for (const name of pkgNames) {
38-
const separator = chunkName ? "+" : "";
39-
if ((chunkName + separator + name).length > 30) {
40-
return `_libs/_[hash].mjs`;
41-
}
42-
chunkName += separator + name;
31+
if (chunk.moduleIds.every((id) => NODE_MODULES_RE.test(id))) {
32+
const chunkName = joinPkgNames(chunk.moduleIds);
33+
if (chunkName.length > 30) {
34+
return `${chunk.name}+[...].mjs`;
4335
}
4436
return `_libs/${chunkName || "_"}.mjs`;
4537
}
4638

39+
// _ chunks are preserved (should be after library normalization)
40+
if (chunk.name.startsWith("_")) {
41+
return `${chunk.name}.mjs`;
42+
}
43+
4744
// No moduleIds
4845
if (chunk.moduleIds.length === 0) {
4946
return `_chunks/${chunk.name}.mjs`;
@@ -74,11 +71,6 @@ export function getChunkName(chunk: { name: string; moduleIds: string[] }, nitro
7471
return `_build/[name].mjs`;
7572
}
7673

77-
// Only nitro runtime
78-
if (ids.every((id) => id.startsWith(runtimeDir) || id.startsWith(presetsDir))) {
79-
return `_nitro/[name].mjs`;
80-
}
81-
8274
// Try to match user defined routes or tasks
8375
const mainId = ids.at(-1);
8476
if (mainId) {
@@ -100,12 +92,29 @@ export function getChunkName(chunk: { name: string; moduleIds: string[] }, nitro
10092
return `_chunks/[name].mjs`;
10193
}
10294

103-
function routeToFsPath(route: string) {
95+
function joinPkgNames(moduleIds: string[]): string {
96+
const names = [
97+
...new Set(
98+
moduleIds
99+
.map((id) => pathToPkgName(id))
100+
.filter(Boolean)
101+
.map((name) => name!.replace(/^@/, "").replace(/[/\\]/g, "__"))
102+
),
103+
].sort();
104+
return names.join("+");
105+
}
106+
107+
export function routeToFsPath(route: string) {
104108
return (
105109
route
106110
.split("/")
107111
.slice(1)
108-
.map((s) => `${s.replace(/[:*]+/g, "$").replace(/[^$a-zA-Z0-9_.[\]/]/g, "_")}`)
112+
.map((s) =>
113+
s
114+
.replace(/:(\w+)/g, "[$1]")
115+
.replace(/\*+/g, "[...]")
116+
.replace(/[^a-zA-Z0-9_.[\]]/g, "_")
117+
)
109118
.join("/") || "index"
110119
);
111120
}

src/build/plugins.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export async function baseBuildPlugins(nitro: Nitro, base: BaseBuildConfig) {
6464
plugins.push(
6565
externals({
6666
rootDir: nitro.options.rootDir,
67-
conditions: nitro.options.exportConditions || ["default"],
67+
conditions: nitro.options.exportConditions!,
6868
exclude: [...base.noExternal],
6969
include: isDevOrPrerender
7070
? undefined

src/config/resolvers/export-conditions.ts

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,51 +9,22 @@ export async function resolveExportConditionsOptions(options: NitroOptions) {
99
}
1010

1111
function _resolveExportConditions(
12-
conditions: string[],
12+
userConditions: string[],
1313
opts: { dev: boolean; node: boolean; wasm?: boolean }
1414
) {
15-
const resolvedConditions: string[] = [];
15+
const conditions: string[] = [...userConditions.filter((c) => !c.startsWith("!"))];
1616

17-
// 1. Add dev or production
18-
resolvedConditions.push(opts.dev ? "development" : "production");
17+
conditions.push(opts.dev ? "development" : "production");
1918

20-
// 2. Add user specified conditions
21-
resolvedConditions.push(...conditions);
22-
23-
// 3. Add runtime conditions (node or web)
24-
if (opts.node) {
25-
resolvedConditions.push("node");
26-
} else {
27-
// https://runtime-keys.proposal.wintercg.org/
28-
resolvedConditions.push(
29-
"wintercg",
30-
"worker",
31-
"web",
32-
"browser",
33-
"workerd",
34-
"edge-light",
35-
"netlify",
36-
"edge-routine",
37-
"deno"
38-
);
39-
}
40-
41-
// 4. Add unwasm conditions
4219
if (opts.wasm) {
43-
resolvedConditions.push("wasm", "unwasm");
20+
conditions.push("wasm", "unwasm");
4421
}
4522

46-
// 5. Add default conditions
47-
// "module" is NOT A STANDARD CONDITION but widely used in the ecosystem adding helps with compatibility
48-
resolvedConditions.push("import", "default", "module");
49-
50-
// 6. Auto detect bun and deno (builder)
51-
if ("Bun" in globalThis) {
52-
resolvedConditions.push("bun");
53-
} else if ("Deno" in globalThis) {
54-
resolvedConditions.push("deno");
23+
if (opts.node) {
24+
conditions.push("node");
5525
}
5626

57-
// Dedup with preserving order
58-
return resolvedConditions.filter((c, i) => resolvedConditions.indexOf(c) === i);
27+
const negated = new Set(userConditions.filter((c) => c.startsWith("!")).map((c) => c.slice(1)));
28+
29+
return [...new Set(conditions)].filter((c) => !negated.has(c));
5930
}

src/presets/bun/preset.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const bun = defineNitroPreset(
55
entry: "./bun/runtime/bun",
66
serveStatic: true,
77
// https://bun.sh/docs/runtime/modules#resolution
8-
exportConditions: ["bun", "node", "import", "default"],
8+
exportConditions: ["bun"],
99
commands: {
1010
preview: "bun run ./server/index.mjs",
1111
},

test/unit/chunks.test.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { Nitro } from "nitro/types";
3+
import {
4+
NODE_MODULES_RE,
5+
libChunkName,
6+
pathToPkgName,
7+
getChunkName,
8+
routeToFsPath,
9+
} from "../../src/build/chunks.ts";
10+
11+
function createChunk(name: string, moduleIds: string[]): { name: string; moduleIds: string[] } {
12+
return { name, moduleIds };
13+
}
14+
15+
function createNitro(overrides: Partial<Nitro> = {}): Nitro {
16+
return {
17+
options: { buildDir: "/build", tasks: {}, ...overrides.options },
18+
routing: {
19+
routes: { routes: [] },
20+
...overrides.routing,
21+
},
22+
...overrides,
23+
} as unknown as Nitro;
24+
}
25+
26+
describe("NODE_MODULES_RE", () => {
27+
it.each([
28+
["/foo/node_modules/bar/index.js", true],
29+
["node_modules/bar/index.js", true],
30+
["node_modules\\bar\\index.js", true],
31+
["/foo/node_modules/nitro/dist/index.js", false],
32+
["/foo/node_modules/nitro-nightly/dist/index.js", false],
33+
["/foo/node_modules/.nitro", false],
34+
["/foo/node_modules/.cache", false],
35+
["/foo/src/bar.js", false],
36+
])("%s → %s", (path, expected) => {
37+
expect(NODE_MODULES_RE.test(path)).toBe(expected);
38+
});
39+
});
40+
41+
describe("pathToPkgName", () => {
42+
it.each([
43+
["/foo/node_modules/express/index.js", "express"],
44+
["/foo/node_modules/@h3/core/index.js", "@h3/core"],
45+
["C:\\proj\\node_modules\\express\\index.js", "express"],
46+
["C:\\proj\\node_modules\\@h3\\core\\index.js", "@h3\\core"],
47+
["/node_modules/nitro-nightly/dist/index.js", "nitro"],
48+
["/node_modules/a/node_modules/b/index.js", "b"],
49+
["/foo/src/bar.js", undefined],
50+
])("%s → %s", (path, expected) => {
51+
expect(pathToPkgName(path)).toBe(expected);
52+
});
53+
});
54+
55+
describe("libChunkName", () => {
56+
it.each([
57+
["/node_modules/express/index.js", "_libs/express"],
58+
["/node_modules/@h3/core/index.js", "_libs/@h3/core"],
59+
["/src/utils/foo.ts", undefined],
60+
["/node_modules/nitro-nightly/dist/index.js", "_libs/nitro"],
61+
])("%s → %s", (id, expected) => {
62+
expect(libChunkName(id)).toBe(expected);
63+
});
64+
});
65+
66+
describe("routeToFsPath", () => {
67+
it.each([
68+
["/api/hello", "api/hello"],
69+
["/api/users/:id", "api/users/[id]"],
70+
["/", "index"],
71+
["/api/users/:id/posts/*", "api/users/[id]/posts/[...]"],
72+
])("%s → %s", (route, expected) => {
73+
expect(routeToFsPath(route)).toBe(expected);
74+
});
75+
});
76+
77+
describe("getChunkName", () => {
78+
const nitro = createNitro();
79+
80+
it.each<[string, { name: string; moduleIds: string[] }, string]>([
81+
["rolldown-runtime", createChunk("rolldown-runtime", []), "_runtime.mjs"],
82+
["_ chunks are preserved", createChunk("_shared", ["/src/foo.ts"]), "_shared.mjs"],
83+
[
84+
"all node_modules (sorted a-z)",
85+
createChunk("vendor", ["/node_modules/express/index.js", "/node_modules/h3/dist/index.mjs"]),
86+
"_libs/express+h3.mjs",
87+
],
88+
[
89+
"single node_modules package",
90+
createChunk("vendor", ["/node_modules/a/index.js"]),
91+
"_libs/a.mjs",
92+
],
93+
[
94+
"node_modules names exceed 30 chars",
95+
createChunk("_libs/vendor", [
96+
"/node_modules/some-very-long-package-name/index.js",
97+
"/node_modules/another-very-long-name/index.js",
98+
]),
99+
"_libs/vendor+[...].mjs",
100+
],
101+
[
102+
"3 node_modules sorted a-z",
103+
createChunk("vendor", [
104+
"/node_modules/zod/index.js",
105+
"/node_modules/ab/index.js",
106+
"/node_modules/h3/dist/index.mjs",
107+
]),
108+
"_libs/ab+h3+zod.mjs",
109+
],
110+
[
111+
"scoped packages use __ separator",
112+
createChunk("vendor", ["/node_modules/@h3/core/index.js", "/node_modules/defu/index.js"]),
113+
"_libs/defu+h3__core.mjs",
114+
],
115+
["empty moduleIds (vacuous every())", createChunk("my-chunk", []), "_libs/_.mjs"],
116+
[
117+
"virtual:raw modules",
118+
createChunk("raw", ["\0virtual:raw:foo", "#virtual:raw:bar"]),
119+
"_raw/[name].mjs",
120+
],
121+
["all virtual modules", createChunk("virt", ["\0something", "#other"]), "_virtual/[name].mjs"],
122+
["wasm modules", createChunk("wasm", ["/src/module.wasm"]), "_wasm/[name].mjs"],
123+
[
124+
"vite/services modules",
125+
createChunk("ssr", ["/vite/services/component.js"]),
126+
"_ssr/[name].mjs",
127+
],
128+
["buildDir modules", createChunk("build", ["/build/generated.js"]), "_build/[name].mjs"],
129+
[
130+
"mixed virtual + wasm",
131+
createChunk("mixed", ["\0virtual:something", "/src/module.wasm"]),
132+
"_wasm/[name].mjs",
133+
],
134+
["fallback to _chunks", createChunk("misc", ["/src/utils/helper.ts"]), "_chunks/[name].mjs"],
135+
])("%s → %s", (_label, chunk, expected) => {
136+
expect(getChunkName(chunk, nitro)).toBe(expected);
137+
});
138+
139+
it("returns _routes/<path>.mjs for route handler", () => {
140+
const n = createNitro({
141+
routing: {
142+
routes: {
143+
routes: [{ data: [{ route: "/api/hello", handler: "/src/routes/api/hello.ts" }] }],
144+
},
145+
},
146+
} as any);
147+
expect(getChunkName(createChunk("route", ["/src/routes/api/hello.ts"]), n)).toBe(
148+
"_routes/api/hello.mjs"
149+
);
150+
});
151+
152+
it("returns _routes/<path>.mjs for dynamic route", () => {
153+
const n = createNitro({
154+
routing: {
155+
routes: {
156+
routes: [
157+
{
158+
data: [{ route: "/api/users/:id", handler: "/src/routes/api/users/[id].ts" }],
159+
},
160+
],
161+
},
162+
},
163+
} as any);
164+
expect(getChunkName(createChunk("route", ["/src/routes/api/users/[id].ts"]), n)).toBe(
165+
"_routes/api/users/[id].mjs"
166+
);
167+
});
168+
169+
it("returns _routes/index.mjs for root route", () => {
170+
const n = createNitro({
171+
routing: {
172+
routes: {
173+
routes: [{ data: [{ route: "/", handler: "/src/routes/index.ts" }] }],
174+
},
175+
},
176+
} as any);
177+
expect(getChunkName(createChunk("route", ["/src/routes/index.ts"]), n)).toBe(
178+
"_routes/index.mjs"
179+
);
180+
});
181+
182+
it("returns _tasks/[name].mjs for task handler", () => {
183+
const n = createNitro({
184+
options: {
185+
buildDir: "/build",
186+
tasks: { "db:migrate": { handler: "/src/tasks/migrate.ts" } },
187+
},
188+
} as any);
189+
expect(getChunkName(createChunk("task", ["/src/tasks/migrate.ts"]), n)).toBe(
190+
"_tasks/[name].mjs"
191+
);
192+
});
193+
});

0 commit comments

Comments
 (0)