@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.
- package/dist/index.js +2 -2
- package/dist/{server-DPBLtja1.js → server-CvsOU9cg.js} +74 -53
- package/dist/server-CvsOU9cg.js.map +1 -0
- package/dist/server.js +1 -1
- package/dist/vite-plugin-D9qTnleZ.js +469 -0
- package/dist/vite-plugin-D9qTnleZ.js.map +1 -0
- package/dist/vite-plugin.js +2 -333
- package/package.json +1 -1
- package/src/core/bundler.ts +11 -9
- package/src/core/chunking.ts +199 -0
- package/src/core/script-bundle-policy.ts +23 -0
- package/src/core/server.ts +20 -20
- package/src/core/vite-plugin.ts +68 -3
- package/dist/server-DPBLtja1.js.map +0 -1
- package/dist/vite-plugin.js.map +0 -1
package/dist/vite-plugin.js
CHANGED
|
@@ -1,333 +1,2 @@
|
|
|
1
|
-
import { t as e } from "./
|
|
2
|
-
|
|
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
package/src/core/bundler.ts
CHANGED
|
@@ -124,12 +124,14 @@ async function runScriptBundle(
|
|
|
124
124
|
uniquePaths: string[],
|
|
125
125
|
policy: ScriptBundlePolicy,
|
|
126
126
|
): Promise<BundleResult> {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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:
|
|
159
|
+
codeSplitting: { groups: [{ name: (id) => prefixChunks.get(id) ?? null }] },
|
|
158
160
|
minify: false,
|
|
159
161
|
entryFileNames: 'bundle-[hash].js',
|
|
160
|
-
chunkFileNames:
|
|
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)]
|
|
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, '/'));
|