wu-framework 1.1.15 → 1.1.16

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.
Files changed (90) hide show
  1. package/LICENSE +39 -39
  2. package/README.md +408 -408
  3. package/dist/wu-framework.cjs.js.map +1 -1
  4. package/dist/wu-framework.dev.js +15151 -15151
  5. package/dist/wu-framework.dev.js.map +1 -1
  6. package/dist/wu-framework.esm.js.map +1 -1
  7. package/dist/wu-framework.umd.js.map +1 -1
  8. package/integrations/astro/README.md +127 -127
  9. package/integrations/astro/WuApp.astro +63 -63
  10. package/integrations/astro/WuShell.astro +39 -39
  11. package/integrations/astro/index.js +68 -68
  12. package/integrations/astro/package.json +38 -38
  13. package/integrations/astro/types.d.ts +53 -53
  14. package/package.json +161 -161
  15. package/src/adapters/angular/ai.js +30 -30
  16. package/src/adapters/angular/index.d.ts +154 -154
  17. package/src/adapters/angular/index.js +932 -932
  18. package/src/adapters/angular.d.ts +3 -3
  19. package/src/adapters/angular.js +3 -3
  20. package/src/adapters/index.js +168 -168
  21. package/src/adapters/lit/ai.js +20 -20
  22. package/src/adapters/lit/index.d.ts +120 -120
  23. package/src/adapters/lit/index.js +721 -721
  24. package/src/adapters/lit.d.ts +3 -3
  25. package/src/adapters/lit.js +3 -3
  26. package/src/adapters/preact/ai.js +33 -33
  27. package/src/adapters/preact/index.d.ts +108 -108
  28. package/src/adapters/preact/index.js +661 -661
  29. package/src/adapters/preact.d.ts +3 -3
  30. package/src/adapters/preact.js +3 -3
  31. package/src/adapters/react/index.js +48 -54
  32. package/src/adapters/react.d.ts +3 -3
  33. package/src/adapters/react.js +3 -3
  34. package/src/adapters/shared.js +64 -64
  35. package/src/adapters/solid/ai.js +32 -32
  36. package/src/adapters/solid/index.d.ts +101 -101
  37. package/src/adapters/solid/index.js +586 -586
  38. package/src/adapters/solid.d.ts +3 -3
  39. package/src/adapters/solid.js +3 -3
  40. package/src/adapters/svelte/ai.js +31 -31
  41. package/src/adapters/svelte/index.d.ts +166 -166
  42. package/src/adapters/svelte/index.js +798 -798
  43. package/src/adapters/svelte.d.ts +3 -3
  44. package/src/adapters/svelte.js +3 -3
  45. package/src/adapters/vanilla/ai.js +30 -30
  46. package/src/adapters/vanilla/index.d.ts +179 -179
  47. package/src/adapters/vanilla/index.js +785 -785
  48. package/src/adapters/vanilla.d.ts +3 -3
  49. package/src/adapters/vanilla.js +3 -3
  50. package/src/adapters/vue/ai.js +52 -52
  51. package/src/adapters/vue/index.d.ts +299 -299
  52. package/src/adapters/vue/index.js +610 -610
  53. package/src/adapters/vue.d.ts +3 -3
  54. package/src/adapters/vue.js +3 -3
  55. package/src/ai/wu-ai-actions.js +261 -261
  56. package/src/ai/wu-ai-agent.js +546 -546
  57. package/src/ai/wu-ai-browser-primitives.js +354 -354
  58. package/src/ai/wu-ai-browser.js +380 -380
  59. package/src/ai/wu-ai-context.js +332 -332
  60. package/src/ai/wu-ai-conversation.js +613 -613
  61. package/src/ai/wu-ai-orchestrate.js +1021 -1021
  62. package/src/ai/wu-ai-permissions.js +381 -381
  63. package/src/ai/wu-ai-provider.js +700 -700
  64. package/src/ai/wu-ai-schema.js +225 -225
  65. package/src/ai/wu-ai-triggers.js +396 -396
  66. package/src/ai/wu-ai.js +804 -804
  67. package/src/core/wu-app.js +236 -236
  68. package/src/core/wu-cache.js +477 -477
  69. package/src/core/wu-core.js +1398 -1398
  70. package/src/core/wu-error-boundary.js +382 -382
  71. package/src/core/wu-event-bus.js +348 -348
  72. package/src/core/wu-hooks.js +350 -350
  73. package/src/core/wu-html-parser.js +190 -190
  74. package/src/core/wu-iframe-sandbox.js +328 -328
  75. package/src/core/wu-loader.js +272 -272
  76. package/src/core/wu-logger.js +134 -134
  77. package/src/core/wu-manifest.js +509 -509
  78. package/src/core/wu-mcp-bridge.js +432 -432
  79. package/src/core/wu-overrides.js +510 -510
  80. package/src/core/wu-performance.js +228 -228
  81. package/src/core/wu-plugin.js +348 -348
  82. package/src/core/wu-prefetch.js +414 -414
  83. package/src/core/wu-proxy-sandbox.js +476 -476
  84. package/src/core/wu-sandbox.js +779 -779
  85. package/src/core/wu-script-executor.js +113 -113
  86. package/src/core/wu-snapshot-sandbox.js +227 -227
  87. package/src/core/wu-strategies.js +256 -256
  88. package/src/core/wu-style-bridge.js +477 -477
  89. package/src/index.js +224 -224
  90. package/src/utils/dependency-resolver.js +327 -327
@@ -1,414 +1,414 @@
1
- /**
2
- * WU-PREFETCH: Intelligent Prefetching with Speculation Rules API
3
- *
4
- * Prefetches microfrontend modules BEFORE they're needed using:
5
- * 1. Speculation Rules API (Chrome 121+) — browser-native prerender/prefetch
6
- * 2. <link rel="modulepreload"> — ES module prefetch (all modern browsers)
7
- * 3. <link rel="prefetch"> — generic fallback
8
- *
9
- * Trigger modes:
10
- * - immediate: prefetch now
11
- * - hover: prefetch when user hovers a target element
12
- * - visible: prefetch when target element enters viewport (IntersectionObserver)
13
- * - idle: prefetch during browser idle time (requestIdleCallback)
14
- *
15
- * @example
16
- * // Prefetch immediately
17
- * wu.prefetch('cart');
18
- *
19
- * // Prefetch when user hovers the cart link
20
- * wu.prefetch('cart', { on: 'hover', target: '#cart-link' });
21
- *
22
- * // Prefetch when the section becomes visible
23
- * wu.prefetch('cart', { on: 'visible', target: '#cart-section' });
24
- *
25
- * // Prefetch during idle time
26
- * wu.prefetch('cart', { on: 'idle' });
27
- *
28
- * // Prefetch multiple apps with eagerness control
29
- * wu.prefetch(['cart', 'profile'], { eagerness: 'moderate' });
30
- */
31
-
32
- import { logger } from './wu-logger.js';
33
-
34
- export class WuPrefetch {
35
- constructor(core) {
36
- this.core = core;
37
-
38
- // Track what we've already prefetched to avoid duplicates
39
- this.prefetched = new Set();
40
-
41
- // Active observers and listeners (for cleanup)
42
- this._observers = new Map();
43
- this._listeners = [];
44
-
45
- // Speculation Rules script element (one per page, updated dynamically)
46
- this._speculationScript = null;
47
- this._speculationRules = { prefetch: [], prerender: [] };
48
-
49
- // Detect browser support
50
- this.supportsSpeculationRules = this._detectSpeculationRules();
51
- this.supportsModulePreload = this._detectModulePreload();
52
-
53
- logger.wuDebug(
54
- `[WuPrefetch] Initialized — ` +
55
- `Speculation Rules: ${this.supportsSpeculationRules ? 'yes' : 'no'}, ` +
56
- `Module Preload: ${this.supportsModulePreload ? 'yes' : 'no'}`
57
- );
58
- }
59
-
60
- // ─── Detection ───────────────────────────────────────────────
61
-
62
- _detectSpeculationRules() {
63
- if (typeof HTMLScriptElement === 'undefined') return false;
64
- return HTMLScriptElement.supports?.('speculationrules') ?? false;
65
- }
66
-
67
- _detectModulePreload() {
68
- if (typeof document === 'undefined') return false;
69
- const link = document.createElement('link');
70
- return link.relList?.supports?.('modulepreload') ?? false;
71
- }
72
-
73
- // ─── Main API ────────────────────────────────────────────────
74
-
75
- /**
76
- * Prefetch one or more apps.
77
- *
78
- * @param {string|string[]} appNames - App name(s) to prefetch
79
- * @param {Object} [options]
80
- * @param {'immediate'|'hover'|'visible'|'idle'} [options.on='immediate'] - Trigger mode
81
- * @param {string|Element} [options.target] - CSS selector or element (for hover/visible)
82
- * @param {'conservative'|'moderate'|'eager'} [options.eagerness='moderate'] - Speculation Rules eagerness
83
- * @returns {Promise<void>|Function} Promise for immediate, cleanup function for deferred
84
- */
85
- async prefetch(appNames, options = {}) {
86
- const names = Array.isArray(appNames) ? appNames : [appNames];
87
- const trigger = options.on || 'immediate';
88
-
89
- switch (trigger) {
90
- case 'immediate':
91
- return this._prefetchImmediate(names, options);
92
-
93
- case 'hover':
94
- return this._prefetchOnHover(names, options);
95
-
96
- case 'visible':
97
- return this._prefetchOnVisible(names, options);
98
-
99
- case 'idle':
100
- return this._prefetchOnIdle(names, options);
101
-
102
- default:
103
- logger.wuWarn(`[WuPrefetch] Unknown trigger "${trigger}", using immediate`);
104
- return this._prefetchImmediate(names, options);
105
- }
106
- }
107
-
108
- // ─── Immediate Prefetch ──────────────────────────────────────
109
-
110
- async _prefetchImmediate(appNames, options) {
111
- const urls = await this._resolveAppUrls(appNames);
112
- if (urls.length === 0) return;
113
-
114
- // Mark all as prefetched by name (prevents duplicate resolution)
115
- urls.forEach(({ name }) => this.prefetched.add(name));
116
-
117
- // Strategy 1: Speculation Rules API (Chrome 121+)
118
- if (this.supportsSpeculationRules) {
119
- this._addSpeculationRules(urls, options.eagerness || 'moderate');
120
- return;
121
- }
122
-
123
- // Strategy 2: <link rel="modulepreload"> for ES modules
124
- if (this.supportsModulePreload) {
125
- urls.forEach(({ url }) => this._injectModulePreload(url));
126
- return;
127
- }
128
-
129
- // Strategy 3: <link rel="prefetch"> fallback
130
- urls.forEach(({ url }) => this._injectPrefetch(url));
131
- }
132
-
133
- // ─── Hover Trigger ───────────────────────────────────────────
134
-
135
- _prefetchOnHover(appNames, options) {
136
- const target = this._resolveTarget(options.target);
137
- if (!target) {
138
- logger.wuWarn('[WuPrefetch] hover trigger requires a target element or selector');
139
- return () => {};
140
- }
141
-
142
- let done = false;
143
-
144
- const handler = () => {
145
- if (done) return;
146
- done = true;
147
- this._prefetchImmediate(appNames, options);
148
- // One-shot: remove after first trigger
149
- target.removeEventListener('mouseenter', handler);
150
- target.removeEventListener('focusin', handler);
151
- };
152
-
153
- // Mouse hover OR keyboard focus
154
- target.addEventListener('mouseenter', handler, { passive: true });
155
- target.addEventListener('focusin', handler, { passive: true });
156
-
157
- const cleanup = () => {
158
- target.removeEventListener('mouseenter', handler);
159
- target.removeEventListener('focusin', handler);
160
- };
161
-
162
- this._listeners.push(cleanup);
163
- return cleanup;
164
- }
165
-
166
- // ─── Visibility Trigger (IntersectionObserver) ───────────────
167
-
168
- _prefetchOnVisible(appNames, options) {
169
- const target = this._resolveTarget(options.target);
170
- if (!target) {
171
- logger.wuWarn('[WuPrefetch] visible trigger requires a target element or selector');
172
- return () => {};
173
- }
174
-
175
- if (typeof IntersectionObserver === 'undefined') {
176
- // No IntersectionObserver → prefetch immediately
177
- this._prefetchImmediate(appNames, options);
178
- return () => {};
179
- }
180
-
181
- const observer = new IntersectionObserver(
182
- (entries) => {
183
- for (const entry of entries) {
184
- if (entry.isIntersecting) {
185
- this._prefetchImmediate(appNames, options);
186
- observer.disconnect();
187
- this._observers.delete(target);
188
- break;
189
- }
190
- }
191
- },
192
- { rootMargin: '200px' } // Start prefetching 200px before visible
193
- );
194
-
195
- observer.observe(target);
196
- this._observers.set(target, observer);
197
-
198
- const cleanup = () => {
199
- observer.disconnect();
200
- this._observers.delete(target);
201
- };
202
-
203
- return cleanup;
204
- }
205
-
206
- // ─── Idle Trigger ────────────────────────────────────────────
207
-
208
- _prefetchOnIdle(appNames, options) {
209
- const callback = () => this._prefetchImmediate(appNames, options);
210
-
211
- if ('requestIdleCallback' in window) {
212
- const id = requestIdleCallback(callback, { timeout: 3000 });
213
- const cleanup = () => cancelIdleCallback(id);
214
- this._listeners.push(cleanup);
215
- return cleanup;
216
- }
217
-
218
- // Fallback: setTimeout 2s
219
- const id = setTimeout(callback, 2000);
220
- const cleanup = () => clearTimeout(id);
221
- this._listeners.push(cleanup);
222
- return cleanup;
223
- }
224
-
225
- // ─── Speculation Rules API ───────────────────────────────────
226
-
227
- _addSpeculationRules(urls, eagerness) {
228
- const newEntries = urls.filter(({ name }) => !this.prefetched.has(name));
229
- if (newEntries.length === 0) return;
230
-
231
- // Mark as prefetched
232
- newEntries.forEach(({ name }) => this.prefetched.add(name));
233
-
234
- // Build URL list for speculation rules
235
- const urlList = newEntries.map(({ url }) => url);
236
-
237
- // Add prefetch rule
238
- this._speculationRules.prefetch.push({
239
- source: 'list',
240
- urls: urlList,
241
- eagerness
242
- });
243
-
244
- // Inject or update the speculation rules script
245
- this._updateSpeculationScript();
246
-
247
- logger.wuDebug(
248
- `[WuPrefetch] Speculation Rules: prefetch ${newEntries.map(e => e.name).join(', ')} ` +
249
- `(eagerness: ${eagerness})`
250
- );
251
- }
252
-
253
- _updateSpeculationScript() {
254
- // Remove existing script (spec requires replacing, not updating)
255
- if (this._speculationScript) {
256
- this._speculationScript.remove();
257
- }
258
-
259
- const script = document.createElement('script');
260
- script.type = 'speculationrules';
261
- script.textContent = JSON.stringify(this._speculationRules);
262
- document.head.appendChild(script);
263
-
264
- this._speculationScript = script;
265
- }
266
-
267
- // ─── Module Preload ──────────────────────────────────────────
268
-
269
- _injectModulePreload(url) {
270
- if (this.prefetched.has(url)) return;
271
- this.prefetched.add(url);
272
-
273
- const link = document.createElement('link');
274
- link.rel = 'modulepreload';
275
- link.href = url;
276
- document.head.appendChild(link);
277
-
278
- logger.wuDebug(`[WuPrefetch] modulepreload: ${url}`);
279
- }
280
-
281
- // ─── Generic Prefetch ────────────────────────────────────────
282
-
283
- _injectPrefetch(url) {
284
- if (this.prefetched.has(url)) return;
285
- this.prefetched.add(url);
286
-
287
- const link = document.createElement('link');
288
- link.rel = 'prefetch';
289
- link.href = url;
290
- link.as = 'script';
291
- document.head.appendChild(link);
292
-
293
- logger.wuDebug(`[WuPrefetch] prefetch: ${url}`);
294
- }
295
-
296
- // ─── URL Resolution ──────────────────────────────────────────
297
-
298
- /**
299
- * Resolve app names to their module URLs.
300
- * Skips apps that are already mounted, already prefetched, or not registered.
301
- */
302
- async _resolveAppUrls(appNames) {
303
- const results = [];
304
-
305
- for (const name of appNames) {
306
- // Skip if already prefetched
307
- if (this.prefetched.has(name)) {
308
- logger.wuDebug(`[WuPrefetch] ${name} already prefetched, skipping`);
309
- continue;
310
- }
311
-
312
- // Skip if already mounted (no need to prefetch)
313
- if (this.core.mounted.has(name)) {
314
- logger.wuDebug(`[WuPrefetch] ${name} already mounted, skipping`);
315
- continue;
316
- }
317
-
318
- // Skip if already loaded (definition exists)
319
- if (this.core.definitions.has(name)) {
320
- logger.wuDebug(`[WuPrefetch] ${name} already defined, skipping`);
321
- continue;
322
- }
323
-
324
- const app = this.core.apps.get(name);
325
- if (!app) {
326
- logger.wuWarn(`[WuPrefetch] App "${name}" not registered, cannot prefetch`);
327
- continue;
328
- }
329
-
330
- try {
331
- const url = await this.core.resolveModulePath(app);
332
- results.push({ name, url });
333
- } catch (error) {
334
- logger.wuWarn(`[WuPrefetch] Failed to resolve URL for "${name}":`, error.message);
335
- }
336
- }
337
-
338
- return results;
339
- }
340
-
341
- // ─── Helpers ─────────────────────────────────────────────────
342
-
343
- _resolveTarget(target) {
344
- if (!target) return null;
345
- if (typeof target === 'string') return document.querySelector(target);
346
- if (target instanceof Element) return target;
347
- return null;
348
- }
349
-
350
- // ─── Prefetch All Registered (utility) ───────────────────────
351
-
352
- /**
353
- * Prefetch all registered but not-yet-mounted apps.
354
- * Useful for aggressive prefetching after initial load.
355
- *
356
- * @param {Object} [options] - Same options as prefetch()
357
- */
358
- async prefetchAll(options = {}) {
359
- const unmountedApps = [];
360
- for (const [name] of this.core.apps) {
361
- if (!this.core.mounted.has(name) && !this.prefetched.has(name)) {
362
- unmountedApps.push(name);
363
- }
364
- }
365
-
366
- if (unmountedApps.length === 0) {
367
- logger.wuDebug('[WuPrefetch] No apps to prefetch');
368
- return;
369
- }
370
-
371
- logger.wuDebug(`[WuPrefetch] Prefetching all: ${unmountedApps.join(', ')}`);
372
- return this.prefetch(unmountedApps, options);
373
- }
374
-
375
- // ─── Stats ───────────────────────────────────────────────────
376
-
377
- getStats() {
378
- return {
379
- prefetched: [...this.prefetched],
380
- activeObservers: this._observers.size,
381
- activeListeners: this._listeners.length,
382
- speculationRulesSupported: this.supportsSpeculationRules,
383
- modulePreloadSupported: this.supportsModulePreload,
384
- speculationRules: this._speculationRules
385
- };
386
- }
387
-
388
- // ─── Cleanup ─────────────────────────────────────────────────
389
-
390
- cleanup() {
391
- // Disconnect all IntersectionObservers
392
- for (const [, observer] of this._observers) {
393
- observer.disconnect();
394
- }
395
- this._observers.clear();
396
-
397
- // Remove all event listeners
398
- for (const cleanup of this._listeners) {
399
- cleanup();
400
- }
401
- this._listeners = [];
402
-
403
- // Remove speculation rules script
404
- if (this._speculationScript) {
405
- this._speculationScript.remove();
406
- this._speculationScript = null;
407
- }
408
-
409
- this._speculationRules = { prefetch: [], prerender: [] };
410
- this.prefetched.clear();
411
-
412
- logger.wuDebug('[WuPrefetch] Cleaned up');
413
- }
414
- }
1
+ /**
2
+ * WU-PREFETCH: Intelligent Prefetching with Speculation Rules API
3
+ *
4
+ * Prefetches microfrontend modules BEFORE they're needed using:
5
+ * 1. Speculation Rules API (Chrome 121+) — browser-native prerender/prefetch
6
+ * 2. <link rel="modulepreload"> — ES module prefetch (all modern browsers)
7
+ * 3. <link rel="prefetch"> — generic fallback
8
+ *
9
+ * Trigger modes:
10
+ * - immediate: prefetch now
11
+ * - hover: prefetch when user hovers a target element
12
+ * - visible: prefetch when target element enters viewport (IntersectionObserver)
13
+ * - idle: prefetch during browser idle time (requestIdleCallback)
14
+ *
15
+ * @example
16
+ * // Prefetch immediately
17
+ * wu.prefetch('cart');
18
+ *
19
+ * // Prefetch when user hovers the cart link
20
+ * wu.prefetch('cart', { on: 'hover', target: '#cart-link' });
21
+ *
22
+ * // Prefetch when the section becomes visible
23
+ * wu.prefetch('cart', { on: 'visible', target: '#cart-section' });
24
+ *
25
+ * // Prefetch during idle time
26
+ * wu.prefetch('cart', { on: 'idle' });
27
+ *
28
+ * // Prefetch multiple apps with eagerness control
29
+ * wu.prefetch(['cart', 'profile'], { eagerness: 'moderate' });
30
+ */
31
+
32
+ import { logger } from './wu-logger.js';
33
+
34
+ export class WuPrefetch {
35
+ constructor(core) {
36
+ this.core = core;
37
+
38
+ // Track what we've already prefetched to avoid duplicates
39
+ this.prefetched = new Set();
40
+
41
+ // Active observers and listeners (for cleanup)
42
+ this._observers = new Map();
43
+ this._listeners = [];
44
+
45
+ // Speculation Rules script element (one per page, updated dynamically)
46
+ this._speculationScript = null;
47
+ this._speculationRules = { prefetch: [], prerender: [] };
48
+
49
+ // Detect browser support
50
+ this.supportsSpeculationRules = this._detectSpeculationRules();
51
+ this.supportsModulePreload = this._detectModulePreload();
52
+
53
+ logger.wuDebug(
54
+ `[WuPrefetch] Initialized — ` +
55
+ `Speculation Rules: ${this.supportsSpeculationRules ? 'yes' : 'no'}, ` +
56
+ `Module Preload: ${this.supportsModulePreload ? 'yes' : 'no'}`
57
+ );
58
+ }
59
+
60
+ // ─── Detection ───────────────────────────────────────────────
61
+
62
+ _detectSpeculationRules() {
63
+ if (typeof HTMLScriptElement === 'undefined') return false;
64
+ return HTMLScriptElement.supports?.('speculationrules') ?? false;
65
+ }
66
+
67
+ _detectModulePreload() {
68
+ if (typeof document === 'undefined') return false;
69
+ const link = document.createElement('link');
70
+ return link.relList?.supports?.('modulepreload') ?? false;
71
+ }
72
+
73
+ // ─── Main API ────────────────────────────────────────────────
74
+
75
+ /**
76
+ * Prefetch one or more apps.
77
+ *
78
+ * @param {string|string[]} appNames - App name(s) to prefetch
79
+ * @param {Object} [options]
80
+ * @param {'immediate'|'hover'|'visible'|'idle'} [options.on='immediate'] - Trigger mode
81
+ * @param {string|Element} [options.target] - CSS selector or element (for hover/visible)
82
+ * @param {'conservative'|'moderate'|'eager'} [options.eagerness='moderate'] - Speculation Rules eagerness
83
+ * @returns {Promise<void>|Function} Promise for immediate, cleanup function for deferred
84
+ */
85
+ async prefetch(appNames, options = {}) {
86
+ const names = Array.isArray(appNames) ? appNames : [appNames];
87
+ const trigger = options.on || 'immediate';
88
+
89
+ switch (trigger) {
90
+ case 'immediate':
91
+ return this._prefetchImmediate(names, options);
92
+
93
+ case 'hover':
94
+ return this._prefetchOnHover(names, options);
95
+
96
+ case 'visible':
97
+ return this._prefetchOnVisible(names, options);
98
+
99
+ case 'idle':
100
+ return this._prefetchOnIdle(names, options);
101
+
102
+ default:
103
+ logger.wuWarn(`[WuPrefetch] Unknown trigger "${trigger}", using immediate`);
104
+ return this._prefetchImmediate(names, options);
105
+ }
106
+ }
107
+
108
+ // ─── Immediate Prefetch ──────────────────────────────────────
109
+
110
+ async _prefetchImmediate(appNames, options) {
111
+ const urls = await this._resolveAppUrls(appNames);
112
+ if (urls.length === 0) return;
113
+
114
+ // Mark all as prefetched by name (prevents duplicate resolution)
115
+ urls.forEach(({ name }) => this.prefetched.add(name));
116
+
117
+ // Strategy 1: Speculation Rules API (Chrome 121+)
118
+ if (this.supportsSpeculationRules) {
119
+ this._addSpeculationRules(urls, options.eagerness || 'moderate');
120
+ return;
121
+ }
122
+
123
+ // Strategy 2: <link rel="modulepreload"> for ES modules
124
+ if (this.supportsModulePreload) {
125
+ urls.forEach(({ url }) => this._injectModulePreload(url));
126
+ return;
127
+ }
128
+
129
+ // Strategy 3: <link rel="prefetch"> fallback
130
+ urls.forEach(({ url }) => this._injectPrefetch(url));
131
+ }
132
+
133
+ // ─── Hover Trigger ───────────────────────────────────────────
134
+
135
+ _prefetchOnHover(appNames, options) {
136
+ const target = this._resolveTarget(options.target);
137
+ if (!target) {
138
+ logger.wuWarn('[WuPrefetch] hover trigger requires a target element or selector');
139
+ return () => {};
140
+ }
141
+
142
+ let done = false;
143
+
144
+ const handler = () => {
145
+ if (done) return;
146
+ done = true;
147
+ this._prefetchImmediate(appNames, options);
148
+ // One-shot: remove after first trigger
149
+ target.removeEventListener('mouseenter', handler);
150
+ target.removeEventListener('focusin', handler);
151
+ };
152
+
153
+ // Mouse hover OR keyboard focus
154
+ target.addEventListener('mouseenter', handler, { passive: true });
155
+ target.addEventListener('focusin', handler, { passive: true });
156
+
157
+ const cleanup = () => {
158
+ target.removeEventListener('mouseenter', handler);
159
+ target.removeEventListener('focusin', handler);
160
+ };
161
+
162
+ this._listeners.push(cleanup);
163
+ return cleanup;
164
+ }
165
+
166
+ // ─── Visibility Trigger (IntersectionObserver) ───────────────
167
+
168
+ _prefetchOnVisible(appNames, options) {
169
+ const target = this._resolveTarget(options.target);
170
+ if (!target) {
171
+ logger.wuWarn('[WuPrefetch] visible trigger requires a target element or selector');
172
+ return () => {};
173
+ }
174
+
175
+ if (typeof IntersectionObserver === 'undefined') {
176
+ // No IntersectionObserver → prefetch immediately
177
+ this._prefetchImmediate(appNames, options);
178
+ return () => {};
179
+ }
180
+
181
+ const observer = new IntersectionObserver(
182
+ (entries) => {
183
+ for (const entry of entries) {
184
+ if (entry.isIntersecting) {
185
+ this._prefetchImmediate(appNames, options);
186
+ observer.disconnect();
187
+ this._observers.delete(target);
188
+ break;
189
+ }
190
+ }
191
+ },
192
+ { rootMargin: '200px' } // Start prefetching 200px before visible
193
+ );
194
+
195
+ observer.observe(target);
196
+ this._observers.set(target, observer);
197
+
198
+ const cleanup = () => {
199
+ observer.disconnect();
200
+ this._observers.delete(target);
201
+ };
202
+
203
+ return cleanup;
204
+ }
205
+
206
+ // ─── Idle Trigger ────────────────────────────────────────────
207
+
208
+ _prefetchOnIdle(appNames, options) {
209
+ const callback = () => this._prefetchImmediate(appNames, options);
210
+
211
+ if ('requestIdleCallback' in window) {
212
+ const id = requestIdleCallback(callback, { timeout: 3000 });
213
+ const cleanup = () => cancelIdleCallback(id);
214
+ this._listeners.push(cleanup);
215
+ return cleanup;
216
+ }
217
+
218
+ // Fallback: setTimeout 2s
219
+ const id = setTimeout(callback, 2000);
220
+ const cleanup = () => clearTimeout(id);
221
+ this._listeners.push(cleanup);
222
+ return cleanup;
223
+ }
224
+
225
+ // ─── Speculation Rules API ───────────────────────────────────
226
+
227
+ _addSpeculationRules(urls, eagerness) {
228
+ const newEntries = urls.filter(({ name }) => !this.prefetched.has(name));
229
+ if (newEntries.length === 0) return;
230
+
231
+ // Mark as prefetched
232
+ newEntries.forEach(({ name }) => this.prefetched.add(name));
233
+
234
+ // Build URL list for speculation rules
235
+ const urlList = newEntries.map(({ url }) => url);
236
+
237
+ // Add prefetch rule
238
+ this._speculationRules.prefetch.push({
239
+ source: 'list',
240
+ urls: urlList,
241
+ eagerness
242
+ });
243
+
244
+ // Inject or update the speculation rules script
245
+ this._updateSpeculationScript();
246
+
247
+ logger.wuDebug(
248
+ `[WuPrefetch] Speculation Rules: prefetch ${newEntries.map(e => e.name).join(', ')} ` +
249
+ `(eagerness: ${eagerness})`
250
+ );
251
+ }
252
+
253
+ _updateSpeculationScript() {
254
+ // Remove existing script (spec requires replacing, not updating)
255
+ if (this._speculationScript) {
256
+ this._speculationScript.remove();
257
+ }
258
+
259
+ const script = document.createElement('script');
260
+ script.type = 'speculationrules';
261
+ script.textContent = JSON.stringify(this._speculationRules);
262
+ document.head.appendChild(script);
263
+
264
+ this._speculationScript = script;
265
+ }
266
+
267
+ // ─── Module Preload ──────────────────────────────────────────
268
+
269
+ _injectModulePreload(url) {
270
+ if (this.prefetched.has(url)) return;
271
+ this.prefetched.add(url);
272
+
273
+ const link = document.createElement('link');
274
+ link.rel = 'modulepreload';
275
+ link.href = url;
276
+ document.head.appendChild(link);
277
+
278
+ logger.wuDebug(`[WuPrefetch] modulepreload: ${url}`);
279
+ }
280
+
281
+ // ─── Generic Prefetch ────────────────────────────────────────
282
+
283
+ _injectPrefetch(url) {
284
+ if (this.prefetched.has(url)) return;
285
+ this.prefetched.add(url);
286
+
287
+ const link = document.createElement('link');
288
+ link.rel = 'prefetch';
289
+ link.href = url;
290
+ link.as = 'script';
291
+ document.head.appendChild(link);
292
+
293
+ logger.wuDebug(`[WuPrefetch] prefetch: ${url}`);
294
+ }
295
+
296
+ // ─── URL Resolution ──────────────────────────────────────────
297
+
298
+ /**
299
+ * Resolve app names to their module URLs.
300
+ * Skips apps that are already mounted, already prefetched, or not registered.
301
+ */
302
+ async _resolveAppUrls(appNames) {
303
+ const results = [];
304
+
305
+ for (const name of appNames) {
306
+ // Skip if already prefetched
307
+ if (this.prefetched.has(name)) {
308
+ logger.wuDebug(`[WuPrefetch] ${name} already prefetched, skipping`);
309
+ continue;
310
+ }
311
+
312
+ // Skip if already mounted (no need to prefetch)
313
+ if (this.core.mounted.has(name)) {
314
+ logger.wuDebug(`[WuPrefetch] ${name} already mounted, skipping`);
315
+ continue;
316
+ }
317
+
318
+ // Skip if already loaded (definition exists)
319
+ if (this.core.definitions.has(name)) {
320
+ logger.wuDebug(`[WuPrefetch] ${name} already defined, skipping`);
321
+ continue;
322
+ }
323
+
324
+ const app = this.core.apps.get(name);
325
+ if (!app) {
326
+ logger.wuWarn(`[WuPrefetch] App "${name}" not registered, cannot prefetch`);
327
+ continue;
328
+ }
329
+
330
+ try {
331
+ const url = await this.core.resolveModulePath(app);
332
+ results.push({ name, url });
333
+ } catch (error) {
334
+ logger.wuWarn(`[WuPrefetch] Failed to resolve URL for "${name}":`, error.message);
335
+ }
336
+ }
337
+
338
+ return results;
339
+ }
340
+
341
+ // ─── Helpers ─────────────────────────────────────────────────
342
+
343
+ _resolveTarget(target) {
344
+ if (!target) return null;
345
+ if (typeof target === 'string') return document.querySelector(target);
346
+ if (target instanceof Element) return target;
347
+ return null;
348
+ }
349
+
350
+ // ─── Prefetch All Registered (utility) ───────────────────────
351
+
352
+ /**
353
+ * Prefetch all registered but not-yet-mounted apps.
354
+ * Useful for aggressive prefetching after initial load.
355
+ *
356
+ * @param {Object} [options] - Same options as prefetch()
357
+ */
358
+ async prefetchAll(options = {}) {
359
+ const unmountedApps = [];
360
+ for (const [name] of this.core.apps) {
361
+ if (!this.core.mounted.has(name) && !this.prefetched.has(name)) {
362
+ unmountedApps.push(name);
363
+ }
364
+ }
365
+
366
+ if (unmountedApps.length === 0) {
367
+ logger.wuDebug('[WuPrefetch] No apps to prefetch');
368
+ return;
369
+ }
370
+
371
+ logger.wuDebug(`[WuPrefetch] Prefetching all: ${unmountedApps.join(', ')}`);
372
+ return this.prefetch(unmountedApps, options);
373
+ }
374
+
375
+ // ─── Stats ───────────────────────────────────────────────────
376
+
377
+ getStats() {
378
+ return {
379
+ prefetched: [...this.prefetched],
380
+ activeObservers: this._observers.size,
381
+ activeListeners: this._listeners.length,
382
+ speculationRulesSupported: this.supportsSpeculationRules,
383
+ modulePreloadSupported: this.supportsModulePreload,
384
+ speculationRules: this._speculationRules
385
+ };
386
+ }
387
+
388
+ // ─── Cleanup ─────────────────────────────────────────────────
389
+
390
+ cleanup() {
391
+ // Disconnect all IntersectionObservers
392
+ for (const [, observer] of this._observers) {
393
+ observer.disconnect();
394
+ }
395
+ this._observers.clear();
396
+
397
+ // Remove all event listeners
398
+ for (const cleanup of this._listeners) {
399
+ cleanup();
400
+ }
401
+ this._listeners = [];
402
+
403
+ // Remove speculation rules script
404
+ if (this._speculationScript) {
405
+ this._speculationScript.remove();
406
+ this._speculationScript = null;
407
+ }
408
+
409
+ this._speculationRules = { prefetch: [], prerender: [] };
410
+ this.prefetched.clear();
411
+
412
+ logger.wuDebug('[WuPrefetch] Cleaned up');
413
+ }
414
+ }