@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.
- package/dist/index.js +2 -2
- package/dist/{server-DXNRPyGJ.js → server-CvsOU9cg.js} +35 -27
- 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 +1 -1
- package/src/core/chunking.ts +199 -0
- package/src/core/server.ts +20 -20
- package/src/core/vite-plugin.ts +68 -3
- package/dist/server-DXNRPyGJ.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
|
@@ -216,7 +216,7 @@ export async function bundleScripts(
|
|
|
216
216
|
return EMPTY_RESULT;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
const uniquePaths = [...new Set(scriptPaths)]
|
|
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
|
+
}
|
package/src/core/server.ts
CHANGED
|
@@ -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
|
-
|
|
297
|
-
|
|
298
|
-
entry.imports
|
|
299
|
-
const
|
|
300
|
-
if (
|
|
301
|
-
|
|
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 (
|
|
342
|
-
const bundledCss = await bundleCss(
|
|
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) {
|