@withl5e/l5e 1.0.1 → 1.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,333 +1,2 @@
1
- import { t as e } from "./global-style-Cgno-yNl.js";
2
- import { existsSync as t, mkdirSync as n, readFileSync as r, readdirSync as i, rmSync as a, statSync as o, writeFileSync as s } from "fs";
3
- import { transform as c } from "esbuild";
4
- import { dirname as l, join as u, relative as d, resolve as f } from "path";
5
- //#region src/core/vite-plugin.ts
6
- var p = "virtual:l5e-views", m = "virtual:l5e-route", h = "virtual:l5e-ssr-entry", g = "virtual:l5e-global-loader", _ = "virtual:l5e-island-strategies", v = "virtual:l5e-islands", y = "virtual:l5e-actions", b = "virtual:l5e-middleware", x = /* @__PURE__ */ new Set([
7
- "MODE",
8
- "BASE_URL",
9
- "PROD",
10
- "DEV",
11
- "SSR",
12
- "LEGACY"
13
- ]);
14
- function S(e, t = []) {
15
- let n = i(e);
16
- for (let r of n) {
17
- let n = u(e, r);
18
- if (o(n).isDirectory()) {
19
- if (r === "node_modules" || r === "dist" || r === ".git") continue;
20
- S(n, t);
21
- } else (r.endsWith(".tsx") || r.endsWith(".ts")) && t.push(n);
22
- }
23
- return t;
24
- }
25
- function C(e) {
26
- let t = 0;
27
- for (let n = 0; n < e.length; n++) t = (t << 5) - t + e.charCodeAt(n), t &= t;
28
- return Math.abs(t).toString(16).slice(0, 4);
29
- }
30
- function w(e) {
31
- let t = e.split("/"), n = t[t.length - 1];
32
- return n = n.replace(/\.(tsx?|jsx?)$/, ""), n;
33
- }
34
- function T(e) {
35
- return `${w(e)}_${C(e)}`;
36
- }
37
- function E(e, n) {
38
- let r = e.replace(/^\//, ""), i = f(n, r);
39
- for (let e of [
40
- ".tsx",
41
- ".ts",
42
- ".jsx",
43
- ".js"
44
- ]) if (t(i + e)) return r + e;
45
- return t(i) ? r : null;
46
- }
47
- function D(e, t) {
48
- let n = r(e, "utf-8"), i = [], a = /<ClientIsland\s[^>]*?from\s*=\s*"([^"]+)"/g, o = /* @__PURE__ */ new Set(), s;
49
- for (; (s = a.exec(n)) !== null;) {
50
- let n = s[1], r;
51
- if (n.startsWith("~/")) r = "/src/" + n.substring(2);
52
- else if (n.startsWith("/src/")) r = n;
53
- else if (n.startsWith("./") || n.startsWith("../")) {
54
- let i = f(e, "..", n);
55
- r = "/" + d(t, i).replace(/\\/g, "/");
56
- } else r = n;
57
- let a = E(r, t);
58
- if (!a) {
59
- console.warn(`[l5e] Island component not found: ${r} (from ${n} in ${e})`);
60
- continue;
61
- }
62
- let c = T(r);
63
- o.has(c) || (o.add(c), i.push({
64
- component: w(r),
65
- resolvedPath: r,
66
- src: a,
67
- key: c
68
- }));
69
- }
70
- return i;
71
- }
72
- function O(e, t) {
73
- let n = r(e, "utf-8"), i = [], a = [], o = /useCss\s*\(\s*['"]([^'"]+)['"]\s*,?\s*\)/g, s = /useClientJs\s*\(\s*['"]([^'"]+)['"]\s*,?\s*\)/g, c;
74
- for (; (c = o.exec(n)) !== null;) {
75
- let n = c[1];
76
- if (n.startsWith("/src/")) i.push(n);
77
- else if (n.startsWith("./") || n.startsWith("../")) {
78
- let r = f(e, "..", n), a = "/" + d(t, r).replace(/\\/g, "/");
79
- i.push(a);
80
- } else i.push(n);
81
- }
82
- for (; (c = s.exec(n)) !== null;) {
83
- let n = c[1];
84
- if (n.startsWith("/src/")) a.push(n);
85
- else if (n.startsWith("./") || n.startsWith("../")) {
86
- let r = f(e, "..", n), i = "/" + d(t, r).replace(/\\/g, "/");
87
- a.push(i);
88
- } else a.push(n);
89
- }
90
- return {
91
- css: i,
92
- js: a
93
- };
94
- }
95
- function k(n) {
96
- let i = {}, a = u(n, "src"), s = /* @__PURE__ */ new Map(), c = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map(), m = /* @__PURE__ */ new Map();
97
- try {
98
- if (!o(a).isDirectory()) return {
99
- input: i,
100
- islandRegistry: s,
101
- pathToKey: c,
102
- keyToSrc: p,
103
- actionRegistry: m
104
- };
105
- let h = u(n, "src", "client.global.ts");
106
- t(h) && (i.global = h, console.log("[l5e] Detected src/client.global.ts and added as entry"));
107
- let g = e(n);
108
- g && (i["global-style"] = g, console.log("[l5e] Detected src/global.css and added as global stylesheet"));
109
- let _ = S(a), v = /* @__PURE__ */ new Set(), y = /* @__PURE__ */ new Set();
110
- for (let e of _) {
111
- let { css: t, js: i } = O(e, n);
112
- t.forEach((e) => v.add(e)), i.forEach((e) => y.add(e));
113
- let o = D(e, n);
114
- for (let e of o) s.set(e.key, e.resolvedPath), c.set(e.resolvedPath, e.key), p.set(e.key, e.src);
115
- let u = e.replace(/\\/g, "/");
116
- if (/\/actions\.(ts|tsx)$/.test(u)) {
117
- let t = r(e, "utf-8"), n = /export\s+const\s+(\w+)\s*=\s*defineAction\s*\(/g, i, o = d(a, l(e)).replace(/\\/g, "/") || ".";
118
- for (; (i = n.exec(t)) !== null;) {
119
- let e = i[1], t = `${e}_${C(o)}`;
120
- m.set(t, {
121
- modulePath: o,
122
- actionName: e
123
- });
124
- }
125
- }
126
- }
127
- for (let e of v) {
128
- let r = e.startsWith("/src/") || e.startsWith("/") ? e.substring(1) : e, a = f(n, r);
129
- if (g && f(a) === f(g)) continue;
130
- if (!t(a)) {
131
- console.warn(`[l5e] CSS file not found: ${a} (from ${e})`);
132
- continue;
133
- }
134
- let o = r.replace(/^src\//, "").replace(/\.css$/, "").replace(/\//g, "-").replace(/\\/g, "-");
135
- i[o] = a;
136
- }
137
- for (let e of y) {
138
- let r = e.startsWith("/src/") || e.startsWith("/") ? e.substring(1) : e, a = f(n, r);
139
- if (!t(a)) {
140
- console.warn(`[l5e] JS/TS file not found: ${a} (from ${e})`);
141
- continue;
142
- }
143
- let o = r.replace(/^src\//, "").replace(/\.(ts|tsx|js|jsx)$/, "").replace(/\//g, "-").replace(/\\/g, "-");
144
- i[o] = a;
145
- }
146
- for (let [e, t] of p) {
147
- let r = f(n, t);
148
- i[`island-${e}`] = r;
149
- }
150
- s.size > 0 && console.log(`[l5e] Detected ${s.size} island(s)`), m.size > 0 && console.log(`[l5e] Detected ${m.size} action(s)`);
151
- } catch (e) {
152
- console.warn("[l5e] Failed to discover bundler input:", e);
153
- }
154
- return {
155
- input: i,
156
- islandRegistry: s,
157
- pathToKey: c,
158
- keyToSrc: p,
159
- actionRegistry: m
160
- };
161
- }
162
- function A(e, t, n, r, i) {
163
- let a = [], o = 0, s = /ClientIsland\s*,\s*\{/g, c;
164
- for (; (c = s.exec(e)) !== null;) {
165
- let s = c.index + c[0].length, l = e.substring(s, s + 500), u = /from:\s*"([^"]+)"/.exec(l);
166
- if (!u) continue;
167
- let p = u[1], m;
168
- if (p.startsWith("~/")) m = "/src/" + p.substring(2);
169
- else if (p.startsWith("/src/")) m = p;
170
- else if (p.startsWith("./") || p.startsWith("../")) {
171
- let e = f(t, "..", p);
172
- m = "/" + d(n, e).replace(/\\/g, "/");
173
- } else m = p;
174
- let h = r.get(m);
175
- if (!h) continue;
176
- let g = i.get(h);
177
- if (!g) continue;
178
- let _ = s + u.index + u[0].length;
179
- a.push(e.substring(o, _)), a.push(`, __key: "${h}", __src: "${g}"`), o = _;
180
- }
181
- return a.push(e.substring(o)), a.join("");
182
- }
183
- function j() {
184
- let e = process.cwd(), r = /* @__PURE__ */ new Map(), i = /* @__PURE__ */ new Map(), o = /* @__PURE__ */ new Map();
185
- return {
186
- name: "l5e-jsx-classic",
187
- enforce: "pre",
188
- configResolved(t) {
189
- e = t.root || process.cwd();
190
- },
191
- handleHotUpdate({ file: t, server: n }) {
192
- let r = d(e, t);
193
- if (/actions\.(ts|tsx)$/.test(t)) {
194
- o = k(e).actionRegistry;
195
- let t = n.moduleGraph.getModuleById("\0virtual:l5e-actions");
196
- t && n.moduleGraph.invalidateModule(t), console.log(`[l5e] Action file changed: ${r} — re-scanned ${o.size} action(s)`);
197
- }
198
- if (t.endsWith(".css")) {
199
- console.log(`[l5e] CSS changed: ${r} - using Vite HMR`);
200
- return;
201
- }
202
- if (t.includes("client.ts")) {
203
- console.log(`[l5e] Client JS changed: ${r} - using Vite HMR`);
204
- return;
205
- }
206
- if (t.includes("/src/") || t.includes("\\src\\")) return console.log(`[l5e] SSR file changed: ${r} - triggering full reload`), n.ws.send({
207
- type: "full-reload",
208
- path: "*"
209
- }), [];
210
- },
211
- buildEnd() {
212
- let n = u(e, ".l5e-temp");
213
- if (t(n)) try {
214
- a(n, {
215
- recursive: !0,
216
- force: !0
217
- }), console.log("[l5e] Cleaned up temporary files");
218
- } catch (e) {
219
- console.warn("[l5e] Failed to cleanup temporary files:", e);
220
- }
221
- },
222
- writeBundle(r, i) {
223
- if (o.size > 0) {
224
- let r = u(e, "dist", "server");
225
- t(r) || n(r, { recursive: !0 });
226
- let i = Object.fromEntries(o);
227
- s(u(r, "action-registry.json"), JSON.stringify(i), "utf-8"), console.log(`[l5e] Wrote action-registry.json (${o.size} actions)`);
228
- }
229
- },
230
- config(e) {
231
- let t = k(e.root || process.cwd());
232
- t.islandRegistry, r = t.pathToKey, i = t.keyToSrc, o = t.actionRegistry;
233
- let n = e.build?.rolldownOptions?.input || {}, a = typeof n == "object" && !Array.isArray(n) ? {
234
- ...t.input,
235
- ...n
236
- } : t.input;
237
- return { build: { rolldownOptions: {
238
- ...e.build?.rolldownOptions,
239
- input: Object.keys(a).length > 0 ? a : void 0,
240
- preserveEntrySignatures: "exports-only"
241
- } } };
242
- },
243
- resolveId(e) {
244
- return e === p ? "\0virtual:l5e-views" : e === m ? "\0virtual:l5e-route" : e === h ? "\0virtual:l5e-ssr-entry" : e === g ? "\0virtual:l5e-global-loader" : e === _ ? "\0virtual:l5e-island-strategies" : e === v ? "\0virtual:l5e-islands" : e === y ? "\0virtual:l5e-actions" : e === b ? "\0virtual:l5e-middleware" : null;
245
- },
246
- async transform(t, n, a) {
247
- if (n.replace(/\\/g, "/").endsWith("src/client.global.ts") && !a?.ssr) return {
248
- code: `import '@withl5e/l5e/island/runtime';\n${t}`,
249
- map: null
250
- };
251
- if (!a?.ssr) {
252
- let e = n.replace(/\\/g, "/").match(/\/src\/(.+)\/actions\.(ts|tsx)$/);
253
- if (e) {
254
- let n = e[1], r = /export\s+const\s+(\w+)\s*=\s*defineAction\s*\(/g, i = [], a;
255
- for (; (a = r.exec(t)) !== null;) {
256
- let e = a[1], n = t.substring(a.index, a.index + 500).match(/method:\s*['"](\w+)['"]/), r = n ? n[1].toUpperCase() : "GET";
257
- i.push({
258
- name: e,
259
- method: r
260
- });
261
- }
262
- if (i.length > 0) return {
263
- code: i.map(({ name: e, method: t }) => {
264
- let r = `${e}_${C(n)}`;
265
- return t === "GET" ? `export async function ${e}(params) {
266
- const res = await fetch('/_l5e/action/${r}?' + new URLSearchParams(params));
267
- if (!res.ok) throw Object.assign(new Error('HTTP ' + res.status), { status: res.status });
268
- return res;
269
- }` : `export async function ${e}(body) {
270
- const res = await fetch('/_l5e/action/${r}', {
271
- method: '${t}',
272
- headers: { 'Content-Type': 'application/json' },
273
- body: JSON.stringify(body),
274
- });
275
- if (!res.ok) throw Object.assign(new Error('HTTP ' + res.status), { status: res.status });
276
- return res;
277
- }`;
278
- }).join("\n\n"),
279
- map: null
280
- };
281
- }
282
- }
283
- if (n.includes("node_modules") || n.includes("/react/") || n.includes("\\react\\")) return null;
284
- if (/\.(tsx|jsx)$/.test(n)) try {
285
- let a = `import { Fragment as __Fragment, jsxFactory } from "@withl5e/l5e/jsx-runtime"\n${t}`, o = await c(a, {
286
- loader: n.endsWith(".tsx") ? "tsx" : "jsx",
287
- jsx: "transform",
288
- jsxFactory: "jsxFactory",
289
- jsxFragment: "__Fragment",
290
- sourcemap: !0,
291
- sourcefile: n,
292
- target: "es2020"
293
- }), s = o.code;
294
- return s.includes("ClientIsland") && (s = A(s, n, e, r, i)), {
295
- code: s,
296
- map: o.map || null
297
- };
298
- } catch (e) {
299
- throw console.error(`[l5e] Failed to transform JSX in ${n}:`, e), e;
300
- }
301
- if (a?.ssr) {
302
- let e = t, n = !1;
303
- if (e = e.replace(/import\.meta\.env\.([a-zA-Z_][a-zA-Z0-9_]*)/g, (e, t) => x.has(t) ? e : (n = !0, `process.env.${t}`)), e = e.replace(/import\.meta\.env\[(['"`])([^'"`]+)\1\]/g, (e, t, r) => x.has(r) ? e : (n = !0, `process.env[${t}${r}${t}]`)), n) return {
304
- code: e,
305
- map: null
306
- };
307
- }
308
- return null;
309
- },
310
- load(n) {
311
- if (n === "\0virtual:l5e-views") return "\nexport const viewLoaders = import.meta.glob('/src/views/*/loader.{ts,tsx}');\nexport const viewComponents = import.meta.glob('/src/views/*/index.tsx');\n";
312
- if (n === "\0virtual:l5e-route") return "export default function routeHandler(requestInfo) {\n return import('/src/route.ts').then((mod) => mod.default(requestInfo));\n}";
313
- if (n === "\0virtual:l5e-ssr-entry") return "export { render } from '@withl5e/l5e/entry-server';\nexport { viewActions } from 'virtual:l5e-actions';";
314
- if (n === "\0virtual:l5e-global-loader") return "export const globalLoader = import.meta.glob('/src/global-loader.{ts,tsx}', { eager: false });";
315
- if (n === "\0virtual:l5e-actions") {
316
- let e = Object.fromEntries(o);
317
- return `export const viewActions = import.meta.glob('/src/**/actions.{ts,tsx}');
318
- export const actionRegistry = ${JSON.stringify(e)};`;
319
- }
320
- if (n === "\0virtual:l5e-middleware") return "\nconst middlewareModules = import.meta.glob([\n '/src/middleware.{ts,tsx,js,jsx}',\n '/src/middleware/index.{ts,tsx,js,jsx}',\n]);\n\nconst middlewarePaths = [\n '/src/middleware.ts',\n '/src/middleware.tsx',\n '/src/middleware.js',\n '/src/middleware.jsx',\n '/src/middleware/index.ts',\n '/src/middleware/index.tsx',\n '/src/middleware/index.js',\n '/src/middleware/index.jsx',\n];\n\nexport async function loadMiddleware() {\n const middlewarePath = middlewarePaths.find((path) => middlewareModules[path]);\n if (!middlewarePath) return undefined;\n\n const mod = await middlewareModules[middlewarePath]();\n return mod.onRequest;\n}\n";
321
- if (n === "\0virtual:l5e-islands") return "export const islandModules = import.meta.glob('/src/**/react/*.{tsx,jsx}');";
322
- if (n === "\0virtual:l5e-island-strategies") {
323
- let n = u(e, "src", "island-strategies.ts");
324
- return t(n) ? "import '/src/island-strategies.ts';" : "/* no custom island strategies */";
325
- }
326
- return null;
327
- }
328
- };
329
- }
330
- //#endregion
331
- export { j as coreVite, j as default };
332
-
333
- //# sourceMappingURL=vite-plugin.js.map
1
+ import { t as e } from "./vite-plugin-D9qTnleZ.js";
2
+ export { e as coreVite, e as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withl5e/l5e",
3
- "version": "1.0.1",
3
+ "version": "1.1.0-alpha.0",
4
4
  "description": "HTML-first SSR MPA framework with loaders, middleware, islands, actions, swap, SEO and cache controls.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -216,7 +216,7 @@ export async function bundleScripts(
216
216
  return EMPTY_RESULT;
217
217
  }
218
218
 
219
- const uniquePaths = [...new Set(scriptPaths)].sort();
219
+ const uniquePaths = [...new Set(scriptPaths)];
220
220
  const cacheKey = `scripts:${policy.cacheKey}:${JSON.stringify(uniquePaths)}`;
221
221
 
222
222
  try {
@@ -0,0 +1,199 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import type { CodeSplittingGroup, ModuleInfo, OutputBundle } from 'rolldown';
4
+
5
+ export interface SharedChunkGroup {
6
+ name: string;
7
+ /** Exact package names, including their subpath imports. */
8
+ packages?: string[];
9
+ /** Module specifiers resolved by Vite, including aliases. */
10
+ modules?: string[];
11
+ priority?: number;
12
+ /** Approximate uncompressed maximum; not a hard network byte budget. */
13
+ maxSize?: number;
14
+ }
15
+
16
+ export interface ChunkingOptions {
17
+ shared?: SharedChunkGroup[];
18
+ }
19
+
20
+ export interface CoreViteOptions {
21
+ /** false leaves chunk placement to Vite; runtime identity protection stays enabled. */
22
+ chunking?: ChunkingOptions | false;
23
+ }
24
+
25
+ const normalize = (id: string) => id.replace(/\\/g, '/');
26
+ const digest = (value: string) => createHash('sha256').update(value).digest('hex').slice(0, 8);
27
+
28
+ /** The last node_modules segment also handles pnpm's nested package layout. */
29
+ function packageName(id: string): string | undefined {
30
+ const suffix = normalize(id).split('/node_modules/').at(-1);
31
+ if (suffix === normalize(id)) return undefined;
32
+ const parts = suffix!.split('/');
33
+ return parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
34
+ }
35
+
36
+ type Decision = { group: string | null; rule?: string; consumers: string[]; reason: string };
37
+
38
+ /** One build-time plan drives placement and the diagnostic report. Runtime reads Vite's manifest. */
39
+ export function createChunkPlanner(options: ChunkingOptions = {}) {
40
+ const rules = options.shared || [];
41
+ const names = new Set<string>();
42
+ for (const rule of rules) {
43
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(rule.name) || names.has(rule.name)) {
44
+ throw new Error(`[l5e chunking] Invalid or duplicate shared group: ${rule.name}`);
45
+ }
46
+ if (!rule.packages?.length && !rule.modules?.length) {
47
+ throw new Error(`[l5e chunking] Group ${rule.name} needs packages or modules.`);
48
+ }
49
+ if (rule.maxSize !== undefined && (!Number.isFinite(rule.maxSize) || rule.maxSize <= 0)) {
50
+ throw new Error(`[l5e chunking] Invalid maxSize for ${rule.name}`);
51
+ }
52
+ names.add(rule.name);
53
+ }
54
+ let root = '';
55
+ const resolvedRules = new Map<SharedChunkGroup, Set<string>>();
56
+ const decisions = new Map<string, Decision>();
57
+ const warnings = new Set<string>();
58
+ const portable = (id: string) => {
59
+ const pkg = packageName(id);
60
+ if (pkg) return `npm:${normalize(id).split('/node_modules/').at(-1)}`;
61
+ return normalize(path.relative(root, id));
62
+ };
63
+
64
+ return {
65
+ async resolve(
66
+ projectRoot: string,
67
+ resolveId: (id: string, importer: string) => Promise<string | null>,
68
+ ) {
69
+ root = projectRoot;
70
+ resolvedRules.clear();
71
+ decisions.clear();
72
+ warnings.clear();
73
+ for (const rule of rules) {
74
+ const ids = new Set<string>();
75
+ for (const specifier of rule.modules || []) {
76
+ const id = await resolveId(specifier, path.join(root, 'index.html'));
77
+ if (!id) throw new Error(`[l5e chunking] Cannot resolve ${specifier} in ${rule.name}`);
78
+ ids.add(id);
79
+ }
80
+ resolvedRules.set(rule, ids);
81
+ }
82
+ },
83
+ analyze(modules: ModuleInfo[]) {
84
+ decisions.clear();
85
+ const graph = new Map(modules.map((mod) => [mod.id, mod]));
86
+ const roots = modules.filter((mod) => mod.isEntry || mod.dynamicImporters.length > 0);
87
+ const consumers = new Map<string, Set<string>>();
88
+ for (const entry of roots) {
89
+ const visited = new Set<string>();
90
+ const visit = (id: string) => {
91
+ if (visited.has(id)) return;
92
+ visited.add(id);
93
+ const mod = graph.get(id);
94
+ if (!mod) return;
95
+ const users = consumers.get(id) || new Set<string>();
96
+ users.add(entry.id);
97
+ consumers.set(id, users);
98
+ mod.importedIds.forEach(visit);
99
+ };
100
+ visit(entry.id);
101
+ }
102
+ const rank = (id: string) => {
103
+ const mod = graph.get(id)!;
104
+ if (!mod.isEntry) return 3;
105
+ if (normalize(id).endsWith('/src/client.global.ts')) return 0;
106
+ if (/\/react\//.test(normalize(id)) && !packageName(id)) return 2;
107
+ return 1;
108
+ };
109
+ const matched = new Set<SharedChunkGroup>();
110
+ for (const mod of modules) {
111
+ const users = [...(consumers.get(mod.id) || [])].sort();
112
+ const labels = users.map(portable).sort();
113
+ const matches = rules
114
+ .filter(
115
+ (rule) =>
116
+ resolvedRules.get(rule)?.has(mod.id) ||
117
+ rule.packages?.includes(packageName(mod.id) || ''),
118
+ )
119
+ .sort((a, b) => (b.priority || 0) - (a.priority || 0));
120
+ matches.forEach((rule) => matched.add(rule));
121
+ if (matches.length > 1)
122
+ warnings.add(
123
+ `${portable(mod.id)} matches ${matches.map((r) => r.name).join(', ')}; using ${matches[0].name}.`,
124
+ );
125
+ let group: string | null = null;
126
+ let reason = 'Private dependency stays with its entry or dynamic import.';
127
+ if (!mod.isEntry && users.length && matches.length) {
128
+ // Explicit groups can combine consumers at the same activation tier.
129
+ // Never promote island-only code into global/page, or dynamic-only code
130
+ // into a static entry. Distinct dynamic roots remain separate as well.
131
+ const tier = Math.min(...users.map(rank));
132
+ const activation =
133
+ tier === 3
134
+ ? `lazy-${digest(JSON.stringify(labels))}`
135
+ : ['global', 'page', 'island'][tier];
136
+ group = `shared-${matches[0].name}-${activation}`;
137
+ reason = `Configured group ${matches[0].name}; activation ${activation}.`;
138
+ } else if (!mod.isEntry && users.length > 1) {
139
+ group = `shared-auto-${digest(JSON.stringify(labels))}`;
140
+ reason = 'Shared by the same static consumers; preserves dynamic import roots.';
141
+ }
142
+ decisions.set(mod.id, { group, rule: matches[0]?.name, consumers: labels, reason });
143
+ }
144
+ for (const rule of rules) {
145
+ if (!matched.has(rule))
146
+ warnings.add(`Group ${rule.name} matched no modules in this client build.`);
147
+ }
148
+ return [...warnings];
149
+ },
150
+ groups(): CodeSplittingGroup[] {
151
+ return [
152
+ ...rules.map((rule) => ({
153
+ name: (id: string) => {
154
+ const decision = decisions.get(id);
155
+ return decision?.rule === rule.name ? decision.group : null;
156
+ },
157
+ maxSize: rule.maxSize,
158
+ includeDependenciesRecursively: false,
159
+ })),
160
+ {
161
+ name: (id: string) => decisions.get(id)?.group || null,
162
+ includeDependenciesRecursively: false,
163
+ },
164
+ ];
165
+ },
166
+ report(bundle: OutputBundle) {
167
+ return {
168
+ version: 1,
169
+ warnings: [...warnings],
170
+ chunks: Object.values(bundle).flatMap((chunk) => {
171
+ if (chunk.type !== 'chunk') return [];
172
+ const source = chunk.facadeModuleId ? portable(chunk.facadeModuleId) : null;
173
+ const kind =
174
+ source === 'src/client.global.ts'
175
+ ? 'global'
176
+ : source && /(?:^|\/)react\//.test(source)
177
+ ? 'island'
178
+ : chunk.isEntry || chunk.isDynamicEntry
179
+ ? 'page'
180
+ : 'shared';
181
+ return [
182
+ {
183
+ file: chunk.fileName,
184
+ kind,
185
+ source,
186
+ bytes: Buffer.byteLength(chunk.code),
187
+ imports: chunk.imports,
188
+ dynamicImports: chunk.dynamicImports,
189
+ modules: Object.keys(chunk.modules).map((id) => ({
190
+ id: portable(id),
191
+ ...decisions.get(id),
192
+ })),
193
+ },
194
+ ];
195
+ }),
196
+ };
197
+ },
198
+ };
199
+ }
@@ -289,28 +289,18 @@ async function createPageResponse({
289
289
  const cssFiles = new Set<string>();
290
290
  const preloadFiles = new Set<string>();
291
291
 
292
+ const visitedEntries = new Set<string>();
292
293
  function collectFromEntry(entryKey: string): { file: string | null } {
293
294
  const entry = manifest![entryKey];
294
295
  if (!entry) return { file: null };
295
-
296
- if (entry.css) entry.css.forEach((css: string) => cssFiles.add(css));
297
- if (entry.imports) {
298
- entry.imports.forEach((importKey: string) => {
299
- const importedChunk = manifest![importKey];
300
- if (importedChunk?.file) preloadFiles.add(importedChunk.file);
301
- if (importedChunk?.css) {
302
- importedChunk.css.forEach((css: string) => cssFiles.add(css));
303
- }
304
- if (importedChunk?.imports) {
305
- importedChunk.imports.forEach((key: string) => {
306
- const chunk = manifest![key];
307
- if (chunk?.file) preloadFiles.add(chunk.file);
308
- if (chunk?.css) chunk.css.forEach((css: string) => cssFiles.add(css));
309
- });
310
- }
311
- });
296
+ if (!visitedEntries.has(entryKey)) {
297
+ visitedEntries.add(entryKey);
298
+ for (const css of entry.css || []) cssFiles.add(css);
299
+ for (const key of entry.imports || []) {
300
+ const dependency = collectFromEntry(key);
301
+ if (dependency.file) preloadFiles.add(dependency.file);
302
+ }
312
303
  }
313
-
314
304
  return { file: entry.file };
315
305
  }
316
306
 
@@ -338,14 +328,20 @@ async function createPageResponse({
338
328
  : mappedScripts.map((file) => withAssetBase(assetBase, file));
339
329
  }
340
330
 
341
- if (mappedCssFiles.length > 0) {
342
- const bundledCss = await bundleCss(mappedCssFiles, distClientDir);
331
+ if (cssFiles.size > 0) {
332
+ const bundledCss = await bundleCss(
333
+ [...cssFiles].map((file) => `/${file}`),
334
+ distClientDir,
335
+ );
343
336
  if (bundledCss.filename) {
344
337
  cssSrcList = [withAssetBase(assetBase, bundledCss.filename)];
345
338
  }
346
339
  }
347
340
 
341
+ cssFiles.clear();
348
342
  const globalEntry = manifest['src/client.global.ts'];
343
+ collectFromEntry('src/client.global.ts');
344
+ for (const cssFile of cssFiles) appendStylesheet(withAssetBase(assetBase, cssFile), true);
349
345
  if (globalEntry) {
350
346
  if (globalEntry.css && globalEntry.css.length > 0) {
351
347
  globalEntry.css.forEach((cssFile: string) => {
@@ -357,6 +353,10 @@ async function createPageResponse({
357
353
  }
358
354
  }
359
355
 
356
+ for (const file of preloadFiles) {
357
+ extraHead += `<link rel="modulepreload" crossorigin href="${escapeProp(withAssetBase(assetBase, file))}">`;
358
+ }
359
+
360
360
  if (islandEntries.length > 0) {
361
361
  const islandMap: Record<string, string> = {};
362
362
  for (const island of islandEntries) {