@withl5e/l5e 1.1.0-alpha.1 → 1.1.0-alpha.2

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,2 +1,2 @@
1
- import { t as e } from "./vite-plugin-R3LFxv9a.js";
1
+ import { t as e } from "./vite-plugin-BOuzxmb2.js";
2
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.1.0-alpha.1",
3
+ "version": "1.1.0-alpha.2",
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",
@@ -47,10 +47,12 @@ export interface ScriptBundleOptions {
47
47
  compact?: boolean;
48
48
  islands?: Array<{ key: string; script: string }>;
49
49
  renderer?: string;
50
- name?: 'global' | 'bundle' | 'shared';
50
+ name?: 'global' | 'bundle' | 'shared' | 'renderer' | 'island';
51
+ inlineStaticRootClosure?: boolean;
51
52
  sharedUrl?: string;
52
53
  globalUrl?: string;
53
54
  exportScripts?: string[];
55
+ externalScripts?: Record<string, string>;
54
56
  }
55
57
 
56
58
  const EMPTY_RESULT: BundleResult = { hash: '', filename: '', content: '' };
@@ -134,6 +136,8 @@ function compactBuildPlugin(
134
136
  policy: ScriptBundlePolicy,
135
137
  sharedUrl?: string,
136
138
  globalUrl?: string,
139
+ eagerFiles = new Set<string>(),
140
+ externalFiles = new Map<string, string>(),
137
141
  ): Plugin {
138
142
  return {
139
143
  name: 'compact-build-graph',
@@ -147,10 +151,14 @@ function compactBuildPlugin(
147
151
  ? path.resolve(path.dirname(importer), source)
148
152
  : null;
149
153
  if (!file) return null;
150
- return policy.isShared(file) && sharedUrl
154
+ return externalFiles.has(file)
155
+ ? { id: externalFiles.get(file)!, external: 'absolute' }
156
+ : policy.isShared(file) && sharedUrl
151
157
  ? { id: sharedUrl, external: 'absolute' }
152
158
  : policy.isGlobalOwned(file) && globalUrl
153
159
  ? { id: globalUrl, external: 'absolute' }
160
+ : policy.isLazy(file) && !eagerFiles.has(file)
161
+ ? { id: policy.assetUrl(file), external: 'absolute' }
154
162
  : { id: file };
155
163
  },
156
164
  };
@@ -175,23 +183,26 @@ async function runScriptBundle(
175
183
  }));
176
184
  const renderer = options.renderer ? policy.fileForScript(options.renderer) : undefined;
177
185
  const exportFiles = (options.exportScripts || []).map((script) => policy.fileForScript(script));
186
+ const eagerFiles = options.inlineStaticRootClosure
187
+ ? policy.staticClosure(roots)
188
+ : new Set<string>();
189
+ const externalFiles = new Map(
190
+ Object.entries(options.externalScripts || {}).map(([script, url]) => [
191
+ policy.fileForScript(script),
192
+ url,
193
+ ]),
194
+ );
178
195
  const entryContent = [
179
196
  ...roots.map((filePath) =>
180
- options.name === 'shared'
197
+ options.name === 'renderer'
198
+ ? `export { createElement, reactDomClient } from ${JSON.stringify(filePath)};`
199
+ : options.name === 'island'
200
+ ? `import * as __island from ${JSON.stringify(filePath)}; export * from ${JSON.stringify(filePath)}; export default __island.default;`
201
+ : options.name === 'shared'
181
202
  ? `export * from ${JSON.stringify(filePath)};`
182
203
  : `import ${JSON.stringify(filePath)};`,
183
204
  ),
184
205
  ...exportFiles.map((filePath) => `export * from ${JSON.stringify(filePath)};`),
185
- ...(islands.length
186
- ? [
187
- 'globalThis.__L5E_ISLANDS__ ||= {};',
188
- ...islands.map(
189
- ({ key, file }) =>
190
- `globalThis.__L5E_ISLANDS__[${JSON.stringify(key)}] = async () => { const [renderer, module] = await Promise.all([import(${JSON.stringify(renderer)}), import(${JSON.stringify(file)})]); return { reactDomClient: renderer.reactDomClient, createElement: renderer.createElement, module }; };`,
191
- ),
192
- 'globalThis.__L5E_BOOT_ISLANDS__?.();',
193
- ]
194
- : []),
195
206
  ].join('\n');
196
207
 
197
208
  const rolldownOptions: InputOptions = {
@@ -199,7 +210,7 @@ async function runScriptBundle(
199
210
  plugins: [
200
211
  virtualEntryPlugin(entryContent),
201
212
  compact
202
- ? compactBuildPlugin(policy, options.sharedUrl, options.globalUrl)
213
+ ? compactBuildPlugin(policy, options.sharedUrl, options.globalUrl, eagerFiles, externalFiles)
203
214
  : preserveBuildChunksPlugin(policy),
204
215
  ],
205
216
  platform: 'neutral',
@@ -244,9 +255,10 @@ async function runScriptBundle(
244
255
  if (chunk.type !== 'chunk') {
245
256
  continue;
246
257
  }
258
+ const code = (chunk.code || '').replace(/^\/\/#(?:end)?region[^\r\n]*(?:\r?\n)?/gm, '');
247
259
  bundledFilesMap.set(chunk.fileName, {
248
- content: chunk.code || '',
249
- hash: generateHash(chunk.code || ''),
260
+ content: code,
261
+ hash: generateHash(code),
250
262
  filename: chunk.fileName,
251
263
  mimeType: 'application/javascript',
252
264
  });
@@ -259,11 +271,12 @@ async function runScriptBundle(
259
271
  if (!entryChunk) {
260
272
  throw new Error('[bundler] rolldown produced no entry chunk');
261
273
  }
274
+ const entryFile = bundledFilesMap.get(entryChunk.fileName)!;
262
275
 
263
276
  return {
264
- hash: generateHash(entryChunk.code || ''),
277
+ hash: entryFile.hash,
265
278
  filename: entryChunk.fileName,
266
- content: entryChunk.code || '',
279
+ content: entryFile.content,
267
280
  };
268
281
  }
269
282
 
@@ -20,6 +20,8 @@ export interface ChunkingOptions {
20
20
  */
21
21
  mode?: 'compact';
22
22
  shared?: SharedChunkGroup[];
23
+ /** Static UI/runtime roots that join React's canonical lazy activation chunk. */
24
+ islandRuntime?: Pick<SharedChunkGroup, 'packages' | 'modules'>;
23
25
  }
24
26
 
25
27
  export interface CoreViteOptions {
@@ -43,6 +45,14 @@ type Decision = { group: string | null; rule?: string; consumers: string[]; reas
43
45
  /** One build-time plan drives placement and the diagnostic report. Runtime reads Vite's manifest. */
44
46
  export function createChunkPlanner(options: ChunkingOptions = {}) {
45
47
  const rules = options.shared || [];
48
+ const islandRuntimeRule = options.islandRuntime;
49
+ if (
50
+ islandRuntimeRule &&
51
+ !islandRuntimeRule.packages?.length &&
52
+ !islandRuntimeRule.modules?.length
53
+ ) {
54
+ throw new Error('[l5e chunking] islandRuntime needs packages or modules.');
55
+ }
46
56
  if (options.mode === 'compact' && rules.length > 1) {
47
57
  throw new Error('[l5e chunking] Compact mode accepts one canonical shared group.');
48
58
  }
@@ -66,6 +76,7 @@ export function createChunkPlanner(options: ChunkingOptions = {}) {
66
76
  }
67
77
  let root = '';
68
78
  const resolvedRules = new Map<SharedChunkGroup, Set<string>>();
79
+ let resolvedIslandRuntime = new Set<string>();
69
80
  const decisions = new Map<string, Decision>();
70
81
  const warnings = new Set<string>();
71
82
  const portable = (id: string) => {
@@ -81,6 +92,7 @@ export function createChunkPlanner(options: ChunkingOptions = {}) {
81
92
  ) {
82
93
  root = projectRoot;
83
94
  resolvedRules.clear();
95
+ resolvedIslandRuntime = new Set();
84
96
  decisions.clear();
85
97
  warnings.clear();
86
98
  for (const rule of rules) {
@@ -92,6 +104,13 @@ export function createChunkPlanner(options: ChunkingOptions = {}) {
92
104
  }
93
105
  resolvedRules.set(rule, ids);
94
106
  }
107
+ if (islandRuntimeRule) {
108
+ for (const specifier of islandRuntimeRule.modules || []) {
109
+ const id = await resolveId(specifier, path.join(root, 'index.html'));
110
+ if (!id) throw new Error(`[l5e chunking] Cannot resolve ${specifier} in islandRuntime`);
111
+ resolvedIslandRuntime.add(id);
112
+ }
113
+ }
95
114
  },
96
115
  analyze(modules: ModuleInfo[]) {
97
116
  decisions.clear();
@@ -149,6 +168,7 @@ export function createChunkPlanner(options: ChunkingOptions = {}) {
149
168
  const mod = graph.get(id)!;
150
169
  if (!mod.isEntry) return 3;
151
170
  if (normalize(id).endsWith('/src/client.global.ts')) return 0;
171
+ if (normalize(id).includes('virtual:l5e-compact-renderer')) return 2;
152
172
  if (/\/react\//.test(normalize(id)) && !packageName(id)) return 2;
153
173
  return 1;
154
174
  };
@@ -170,7 +190,26 @@ export function createChunkPlanner(options: ChunkingOptions = {}) {
170
190
  );
171
191
  let group: string | null = null;
172
192
  let reason = 'Private dependency stays with its entry or dynamic import.';
173
- if (!mod.isEntry && users.length && matches.length) {
193
+ const pkg = packageName(mod.id);
194
+ const islandRuntimeMatch =
195
+ resolvedIslandRuntime.has(mod.id) ||
196
+ islandRuntimeRule?.packages?.includes(pkg || '') === true;
197
+ if (
198
+ options.mode === 'compact' &&
199
+ normalize(mod.id).includes('virtual:l5e-compact-renderer')
200
+ ) {
201
+ group = 'lazy-react-runtime';
202
+ reason = 'Renderer joins the canonical React runtime activation.';
203
+ } else if (
204
+ options.mode === 'compact' &&
205
+ !mod.isEntry &&
206
+ (pkg === 'react' || pkg === 'react-dom' || pkg === 'scheduler') &&
207
+ users.length > 0 &&
208
+ users.every((id) => rank(id) >= 2)
209
+ ) {
210
+ group = 'lazy-react-runtime';
211
+ reason = 'Canonical React runtime is fetched when the first island activates.';
212
+ } else if (!mod.isEntry && users.length && matches.length) {
174
213
  // Explicit groups can combine consumers at the same activation tier.
175
214
  // Never promote island-only code into global/page, or dynamic-only code
176
215
  // into a static entry. Distinct dynamic roots remain separate as well.
@@ -181,6 +220,9 @@ export function createChunkPlanner(options: ChunkingOptions = {}) {
181
220
  : ['global', 'page', 'island'][tier];
182
221
  group = options.mode === 'compact' ? 'shared' : `shared-${matches[0].name}-${activation}`;
183
222
  reason = `Configured group ${matches[0].name}; activation ${activation}.`;
223
+ } else if (options.mode === 'compact' && !mod.isEntry && islandRuntimeMatch) {
224
+ group = 'lazy-react-runtime';
225
+ reason = 'Configured canonical island runtime is fetched with the first island consumer.';
184
226
  } else if (
185
227
  options.mode === 'compact' &&
186
228
  !mod.isEntry &&
@@ -212,7 +254,15 @@ export function createChunkPlanner(options: ChunkingOptions = {}) {
212
254
  includeDependenciesRecursively: options.mode === 'compact',
213
255
  })),
214
256
  {
215
- name: (id: string) => decisions.get(id)?.group || null,
257
+ name: (id: string) =>
258
+ decisions.get(id)?.group === 'lazy-react-runtime' ? 'lazy-react-runtime' : null,
259
+ includeDependenciesRecursively: true,
260
+ },
261
+ {
262
+ name: (id: string) => {
263
+ const group = decisions.get(id)?.group;
264
+ return group === 'lazy-react-runtime' ? null : group || null;
265
+ },
216
266
  includeDependenciesRecursively: false,
217
267
  },
218
268
  ];
@@ -28,6 +28,7 @@ export function createScriptBundlePolicy(
28
28
  const files = new Set<string>();
29
29
  const preserved = new Set<string>();
30
30
  const staticImports = new Map<string, string[]>();
31
+ const lazy = new Set<string>();
31
32
  const jsEntries = Object.entries(manifest).filter(([, entry]) => /\.[cm]?js$/.test(entry.file));
32
33
 
33
34
  function absoluteFile(file: string) {
@@ -83,6 +84,34 @@ export function createScriptBundlePolicy(
83
84
  );
84
85
  }
85
86
 
87
+
88
+ // A runtime page bundle keeps dynamic roots and their static closure as emitted
89
+ // assets. This preserves download/init boundaries while allowing the page's
90
+ // transformed preload helper to start the known closure in parallel.
91
+ const staticallyInitial = new Set<string>();
92
+ const visitInitial = (key: string) => {
93
+ const entry = manifest[key];
94
+ if (!entry || !/\.[cm]?js$/.test(entry.file)) return;
95
+ const file = absoluteFile(entry.file);
96
+ if (staticallyInitial.has(file)) return;
97
+ staticallyInitial.add(file);
98
+ for (const dependency of entry.imports || []) visitInitial(dependency);
99
+ };
100
+ for (const [key, entry] of jsEntries) {
101
+ if (entry.isEntry && !entry.isDynamicEntry) visitInitial(key);
102
+ }
103
+ const visitLazy = (key: string) => {
104
+ const entry = manifest[key];
105
+ if (!entry || !/\.[cm]?js$/.test(entry.file)) return;
106
+ const file = absoluteFile(entry.file);
107
+ if (lazy.has(file) || staticallyInitial.has(file)) return;
108
+ lazy.add(file);
109
+ for (const dependency of entry.imports || []) visitLazy(dependency);
110
+ };
111
+ for (const [, entry] of jsEntries) {
112
+ for (const key of entry.dynamicImports || []) visitLazy(key);
113
+ }
114
+
86
115
  const cacheKey = createHash('sha256')
87
116
  .update(JSON.stringify([directory, assetDirectory, base, manifest]))
88
117
  .digest('hex');
@@ -117,6 +146,19 @@ export function createScriptBundlePolicy(
117
146
  isGlobalOwned(file: string) {
118
147
  return globalOwned.has(file);
119
148
  },
149
+ isLazy(file: string) {
150
+ return lazy.has(file);
151
+ },
152
+ staticClosure(roots: string[]) {
153
+ const closure = new Set<string>();
154
+ const visit = (file: string) => {
155
+ if (closure.has(file)) return;
156
+ closure.add(file);
157
+ for (const dependency of staticImports.get(file) || []) visit(dependency);
158
+ };
159
+ roots.forEach(visit);
160
+ return closure;
161
+ },
120
162
  sharedScripts() {
121
163
  return [...shared].map((file) =>
122
164
  withAssetBase(base, path.relative(assetDirectory, file).replace(/\\/g, '/')),
@@ -321,6 +321,18 @@ async function createPageResponse({
321
321
  compactGlobalUrl = withAssetBase(assetBase, bundledGlobal.filename);
322
322
  globalScripts.push(compactGlobalUrl);
323
323
  }
324
+ const imports: Record<string, string> = {};
325
+ if (compactSharedUrl) {
326
+ for (const original of sharedScripts) imports[original] = compactSharedUrl;
327
+ }
328
+ if (compactGlobalUrl) {
329
+ for (const original of scriptPolicy.globalOwnedScripts()) {
330
+ imports[original] = compactGlobalUrl;
331
+ }
332
+ }
333
+ if (Object.keys(imports).length > 0) {
334
+ extraHead += `<script type="importmap">${serialize({ imports })}</script>`;
335
+ }
324
336
  }
325
337
  scriptSrcList = scriptSrcList.filter(
326
338
  (src) => src.replace(/^\//, '') !== 'src/client.global.ts',
@@ -345,6 +357,28 @@ async function createPageResponse({
345
357
  return { file: entry.file };
346
358
  }
347
359
 
360
+ function activationPlan(entryKeys: string[], excluded = new Set<string>()) {
361
+ const js = new Set<string>();
362
+ const css = new Set<string>();
363
+ const visited = new Set<string>();
364
+ const visit = (entryKey: string) => {
365
+ if (visited.has(entryKey)) return;
366
+ visited.add(entryKey);
367
+ const entry = manifest![entryKey];
368
+ if (!entry) return;
369
+ for (const file of entry.css || []) css.add(withAssetBase(assetBase, file));
370
+ if (!excluded.has(entryKey) && entry.file && /\.[cm]?js$/.test(entry.file)) {
371
+ const absolute = scriptPolicy!.fileForScript(`/${entry.file}`);
372
+ if (!scriptPolicy!.isShared(absolute) && !scriptPolicy!.isGlobalOwned(absolute)) {
373
+ js.add(withAssetBase(assetBase, entry.file));
374
+ }
375
+ }
376
+ for (const dependency of entry.imports || []) visit(dependency);
377
+ };
378
+ entryKeys.forEach(visit);
379
+ return { js: [...js], css: [...css] };
380
+ }
381
+
348
382
  const mappedScripts: string[] = [];
349
383
  for (const src of scriptSrcList) {
350
384
  const entryKey = src.replace(/^\//, '');
@@ -362,26 +396,11 @@ async function createPageResponse({
362
396
  }
363
397
  }
364
398
 
365
- if (mappedScripts.length > 0 || (scriptPolicy?.compact && islandEntries.length > 0)) {
366
- const compactIslands = scriptPolicy?.compact
367
- ? islandEntries.flatMap((island) => {
368
- const entry = manifest[island.src];
369
- return entry?.file ? [{ key: island.key, script: `/${entry.file}` }] : [];
370
- })
371
- : [];
372
- const compactRenderer = scriptPolicy?.compact
373
- ? Object.entries(manifest).find(([key]) => key.includes('l5e-compact-renderer'))?.[1]
374
- ?.file
375
- : undefined;
376
- if (scriptPolicy?.compact && compactIslands.length > 0 && !compactRenderer) {
377
- throw new Error('[l5e chunking] Compact renderer entry is missing from the manifest.');
378
- }
399
+ if (mappedScripts.length > 0) {
379
400
  const bundledScript = await bundleScripts(mappedScripts, distClientDir, scriptPolicy, {
380
401
  compact: scriptPolicy?.compact,
381
- islands: compactIslands,
382
402
  name: 'bundle',
383
403
  sharedUrl: compactSharedUrl,
384
- renderer: compactRenderer ? `/${compactRenderer}` : undefined,
385
404
  globalUrl: compactGlobalUrl,
386
405
  });
387
406
  scriptSrcList = bundledScript.filename
@@ -389,6 +408,86 @@ async function createPageResponse({
389
408
  : mappedScripts.map((file) => withAssetBase(assetBase, file));
390
409
  }
391
410
 
411
+ if (scriptPolicy?.compact && islandEntries.length > 0) {
412
+ const rendererEntry = Object.entries(manifest).find(
413
+ ([key, entry]) => entry.isEntry && key.includes('l5e-compact-renderer'),
414
+ );
415
+ if (!rendererEntry?.[1].file) {
416
+ throw new Error('[l5e chunking] Compact renderer entry is missing from the manifest.');
417
+ }
418
+ const rendererOwnedKeys = new Set<string>();
419
+ rendererOwnedKeys.add(rendererEntry[0]);
420
+ const rendererDependencies = (rendererEntry[1].imports || []).filter((dependency: string) =>
421
+ manifest[dependency]?.file?.includes('compact-renderer'),
422
+ );
423
+ if (rendererDependencies.length !== 1) {
424
+ throw new Error(
425
+ `[l5e chunking] Compact renderer expected one canonical export chunk; found ${rendererDependencies.length}. Multiple chunks can have colliding emitted export aliases.`,
426
+ );
427
+ }
428
+ rendererOwnedKeys.add(rendererDependencies[0]);
429
+ const bundledRenderer = await bundleScripts(
430
+ [`/${rendererEntry[1].file}`],
431
+ distClientDir,
432
+ scriptPolicy,
433
+ {
434
+ compact: true,
435
+ name: 'renderer',
436
+ sharedUrl: compactSharedUrl,
437
+ globalUrl: compactGlobalUrl,
438
+ inlineStaticRootClosure: true,
439
+ exportScripts: [...rendererOwnedKeys]
440
+ .filter((key) => key !== rendererEntry[0])
441
+ .map((key) => `/${manifest[key].file}`),
442
+ },
443
+ );
444
+ if (!bundledRenderer.filename) {
445
+ throw new Error('[l5e chunking] Compact renderer bundle failed.');
446
+ }
447
+ const rendererUrl = withAssetBase(assetBase, bundledRenderer.filename);
448
+ const rendererAliases = Object.fromEntries(
449
+ [...rendererOwnedKeys]
450
+ .filter((key) => manifest[key]?.file)
451
+ .map((key) => [`/${manifest[key].file}`, rendererUrl]),
452
+ );
453
+ const rendererImports = Object.fromEntries(
454
+ [...rendererOwnedKeys]
455
+ .filter((key) => manifest[key]?.file)
456
+ .map((key) => [withAssetBase(assetBase, manifest[key].file), rendererUrl]),
457
+ );
458
+ if (Object.keys(rendererImports).length > 0) {
459
+ extraHead += `<script type="importmap">${serialize({ imports: rendererImports })}</script>`;
460
+ }
461
+ const islandMap: Record<
462
+ string,
463
+ { module: string; renderer: string; js: string[]; css: string[] }
464
+ > = {};
465
+ for (const island of islandEntries) {
466
+ const entry = manifest[island.src];
467
+ if (!entry?.file) continue;
468
+ const bundledIsland = await bundleScripts([`/${entry.file}`], distClientDir, scriptPolicy, {
469
+ compact: true,
470
+ name: 'island',
471
+ sharedUrl: compactSharedUrl,
472
+ globalUrl: compactGlobalUrl,
473
+ inlineStaticRootClosure: true,
474
+ externalScripts: rendererAliases,
475
+ });
476
+ if (!bundledIsland.filename) {
477
+ throw new Error(`[l5e chunking] Compact island bundle failed: ${island.src}`);
478
+ }
479
+ const islandUrl = withAssetBase(assetBase, bundledIsland.filename);
480
+ const plan = activationPlan([island.src], rendererOwnedKeys);
481
+ islandMap[island.key] = {
482
+ module: islandUrl,
483
+ renderer: rendererUrl,
484
+ js: [rendererUrl, islandUrl],
485
+ css: plan.css,
486
+ };
487
+ }
488
+ islandRegistryScript = `<script>Object.assign(window.__L5E_ISLANDS__||={},${serialize(islandMap)});window.__L5E_BOOT_ISLANDS__?.()</script>`;
489
+ }
490
+
392
491
  if (cssFiles.size > 0) {
393
492
  const bundledCss = await bundleCss(
394
493
  [...cssFiles].map((file) => `/${file}`),
@@ -616,6 +715,8 @@ export async function createServer(options: ServerOptions = {}): Promise<ServerC
616
715
  withAssetBase(base, 'global-:hash.:ext'),
617
716
  withAssetBase(base, 'bundle-:hash.:ext'),
618
717
  withAssetBase(base, 'shared-:hash.:ext'),
718
+ withAssetBase(base, 'renderer-:hash.:ext'),
719
+ withAssetBase(base, 'island-:hash.:ext'),
619
720
  ],
620
721
  async (req: ExpressRequest, res: ExpressResponse) => {
621
722
  try {
@@ -585,8 +585,16 @@ export function coreVite(options: CoreViteOptions = {}): Plugin {
585
585
  ...(options.chunking && options.chunking.mode === 'compact'
586
586
  ? {
587
587
  modulePreload: {
588
- resolveDependencies: (_filename: string, dependencies: string[]) =>
589
- dependencies.filter((dependency) => !/\.[cm]?js(?:\?|$)/.test(dependency)),
588
+ resolveDependencies: (
589
+ _filename: string,
590
+ dependencies: string[],
591
+ context: { hostType: 'html' | 'js' },
592
+ ) =>
593
+ context.hostType === 'html'
594
+ ? dependencies.filter(
595
+ (dependency) => !/\.[cm]?js(?:\?|$)/.test(dependency),
596
+ )
597
+ : dependencies,
590
598
  },
591
599
  }
592
600
  : {}),
@@ -618,6 +626,8 @@ export function coreVite(options: CoreViteOptions = {}): Plugin {
618
626
  ? 'assets/[name]-[hash].js'
619
627
  : chunk.name === 'shared'
620
628
  ? 'assets/shared-[hash].js'
629
+ : chunk.name === 'lazy-react-runtime'
630
+ ? 'assets/lazy-react-runtime-[hash].js'
621
631
  : chunk.isDynamicEntry
622
632
  ? 'assets/bundle-[name]-[hash].js'
623
633
  : 'assets/shared-[name]-[hash].js',
@@ -41,8 +41,8 @@ type CompactIslandModule = {
41
41
  createElement: typeof import('react').createElement;
42
42
  module: Record<string, any>;
43
43
  };
44
- const registry: Record<string, () => Promise<CompactIslandModule>> = ((window as any)
45
- .__L5E_ISLANDS__ ||= {});
44
+ type CompactIslandPlan = { module: string; renderer: string; js: string[]; css: string[] };
45
+ const registry: Record<string, CompactIslandPlan> = ((window as any).__L5E_ISLANDS__ ||= {});
46
46
  let islandData: unknown[] | null = null;
47
47
  const scheduled = new WeakSet<Element>();
48
48
 
@@ -79,13 +79,51 @@ function mount(island: IslandMeta) {
79
79
  return async () => {
80
80
  if (mounted) return;
81
81
  mounted = true;
82
- const loader = registry[island.registryKey];
83
- if (!loader) {
82
+ const plan = registry[island.registryKey];
83
+ if (!plan) {
84
84
  console.error(`[l5e-island] Component "${island.registryKey}" not found in page registry.`);
85
85
  return;
86
86
  }
87
87
  try {
88
- const { reactDomClient, createElement, module } = await loader();
88
+ // Schedule the complete known static closure before awaiting any response.
89
+ // Modulepreload fetches without evaluating; native import retains ESM order.
90
+ for (const href of plan.js) {
91
+ if (document.querySelector(`link[rel="modulepreload"][href="${CSS.escape(href)}"]`))
92
+ continue;
93
+ const link = document.createElement('link');
94
+ link.rel = 'modulepreload';
95
+ link.crossOrigin = 'anonymous';
96
+ link.href = href;
97
+ document.head.append(link);
98
+ }
99
+ await Promise.all(
100
+ plan.css.map(
101
+ (href) =>
102
+ new Promise<void>((resolve, reject) => {
103
+ const existing = document.querySelector<HTMLLinkElement>(
104
+ `link[rel="stylesheet"][href="${CSS.escape(href)}"]`,
105
+ );
106
+ if (existing?.sheet) return resolve();
107
+ const link = existing || document.createElement('link');
108
+ link.rel = 'stylesheet';
109
+ link.crossOrigin = 'anonymous';
110
+ link.href = href;
111
+ link.addEventListener('load', () => resolve(), { once: true });
112
+ link.addEventListener('error', () => reject(new Error(`Failed to load ${href}`)), {
113
+ once: true,
114
+ });
115
+ if (!existing) document.head.append(link);
116
+ }),
117
+ ),
118
+ );
119
+ const [renderer, module] = await Promise.all([
120
+ import(/* @vite-ignore */ plan.renderer),
121
+ import(/* @vite-ignore */ plan.module),
122
+ ]);
123
+ const { reactDomClient, createElement } = renderer as Omit<
124
+ CompactIslandModule,
125
+ 'module'
126
+ >;
89
127
  const Component = module.default || module[island.exportName];
90
128
  if (!Component) throw new Error(`No export "default" or "${island.exportName}" in module`);
91
129
  if (island.ssr) {
@@ -103,8 +141,7 @@ function mount(island: IslandMeta) {
103
141
  function boot() {
104
142
  for (const island of discover()) {
105
143
  if (scheduled.has(island.element)) continue;
106
- // The compact global can execute before the page bundle registers its loaders.
107
- // The page bundle calls boot again after registration.
144
+ // A fragment swap can expose an element before its activation plan is installed.
108
145
  if (!registry[island.registryKey]) continue;
109
146
  const strategy = strategies.get(island.mount);
110
147
  if (!strategy) {