@withl5e/l5e 1.0.1-rc.0 → 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-rc.0",
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",
@@ -124,12 +124,14 @@ async function runScriptBundle(
124
124
  uniquePaths: string[],
125
125
  policy: ScriptBundlePolicy,
126
126
  ): Promise<BundleResult> {
127
- const entryContent = uniquePaths
128
- .map((p) => {
129
- const filePath = policy.fileForScript(p);
130
- return `import ${JSON.stringify(filePath)};`;
131
- })
132
- .join('\n');
127
+ const roots = uniquePaths.map((p) => policy.fileForScript(p));
128
+ const suffix = policy.inlineableRoots(roots);
129
+ const prefixChunks = new Map(
130
+ roots.flatMap((file, index) =>
131
+ !policy.isPreserved(file) && !suffix.has(file) ? [[file, `entry-${index}`] as const] : [],
132
+ ),
133
+ );
134
+ const entryContent = roots.map((filePath) => `import ${JSON.stringify(filePath)};`).join('\n');
133
135
 
134
136
  const rolldownOptions: InputOptions = {
135
137
  input: VIRTUAL_ENTRY_ID,
@@ -154,10 +156,10 @@ async function runScriptBundle(
154
156
 
155
157
  const outputOptions: OutputOptions = {
156
158
  format: 'es',
157
- codeSplitting: true,
159
+ codeSplitting: { groups: [{ name: (id) => prefixChunks.get(id) ?? null }] },
158
160
  minify: false,
159
161
  entryFileNames: 'bundle-[hash].js',
160
- chunkFileNames: 'bundle-[hash].js',
162
+ chunkFileNames: `bundle-${generateHash(JSON.stringify(roots))}-[hash].js`,
161
163
  };
162
164
 
163
165
  const { rolldown } = await loadRolldown();
@@ -214,7 +216,7 @@ export async function bundleScripts(
214
216
  return EMPTY_RESULT;
215
217
  }
216
218
 
217
- const uniquePaths = [...new Set(scriptPaths)].sort();
219
+ const uniquePaths = [...new Set(scriptPaths)];
218
220
  const cacheKey = `scripts:${policy.cacheKey}:${JSON.stringify(uniquePaths)}`;
219
221
 
220
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
+ }
@@ -12,6 +12,7 @@ export function createScriptBundlePolicy(manifest: Manifest, distClientDir: stri
12
12
  const assetDirectory = realpathSync(directory);
13
13
  const files = new Set<string>();
14
14
  const preserved = new Set<string>();
15
+ const staticImports = new Map<string, string[]>();
15
16
  const jsEntries = Object.entries(manifest).filter(([, entry]) => /\.[cm]?js$/.test(entry.file));
16
17
 
17
18
  function absoluteFile(file: string) {
@@ -50,6 +51,10 @@ export function createScriptBundlePolicy(manifest: Manifest, distClientDir: stri
50
51
  }
51
52
  preserved.add(absoluteFile(dependency.file));
52
53
  }
54
+ staticImports.set(
55
+ absoluteFile(entry.file),
56
+ (entry.imports || []).map((key) => absoluteFile(manifest[key].file)),
57
+ );
53
58
  }
54
59
 
55
60
  const cacheKey = createHash('sha256')
@@ -67,6 +72,24 @@ export function createScriptBundlePolicy(manifest: Manifest, distClientDir: stri
67
72
  isPreserved(file: string) {
68
73
  return preserved.has(file);
69
74
  },
75
+ inlineableRoots(roots: string[]) {
76
+ // External ESM imports execute before any inlined body. Isolate prefix
77
+ // roots in runtime chunks when a later root introduces a dependency effect;
78
+ // only the suffix after the last such boundary can share the entry chunk.
79
+ const visited = new Set<string>();
80
+ let suffixStart = 0;
81
+ function visit(file: string, rootIndex: number) {
82
+ if (visited.has(file)) return;
83
+ visited.add(file);
84
+ for (const dependency of staticImports.get(file) || []) visit(dependency, rootIndex);
85
+ if (preserved.has(file)) suffixStart = rootIndex;
86
+ }
87
+ roots.forEach((file, index) => {
88
+ visit(file, index);
89
+ if (preserved.has(file)) suffixStart = index + 1;
90
+ });
91
+ return new Set(roots.slice(suffixStart).filter((file) => !preserved.has(file)));
92
+ },
70
93
  assetUrl(file: string) {
71
94
  if (!files.has(file)) throw new Error(`[bundler] Import is missing from manifest: ${file}`);
72
95
  return withAssetBase(base, path.relative(assetDirectory, file).replace(/\\/g, '/'));