wu-framework 1.1.15 → 1.1.17

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 (88) hide show
  1. package/README.md +52 -20
  2. package/dist/wu-framework.cjs.js +1 -1
  3. package/dist/wu-framework.cjs.js.map +1 -1
  4. package/dist/wu-framework.dev.js +15511 -15146
  5. package/dist/wu-framework.dev.js.map +1 -1
  6. package/dist/wu-framework.esm.js +1 -1
  7. package/dist/wu-framework.esm.js.map +1 -1
  8. package/dist/wu-framework.umd.js +1 -1
  9. package/dist/wu-framework.umd.js.map +1 -1
  10. package/package.json +166 -161
  11. package/src/adapters/angular/ai.js +30 -30
  12. package/src/adapters/angular/index.d.ts +154 -154
  13. package/src/adapters/angular/index.js +932 -932
  14. package/src/adapters/angular.d.ts +3 -3
  15. package/src/adapters/angular.js +3 -3
  16. package/src/adapters/index.js +168 -168
  17. package/src/adapters/lit/ai.js +20 -20
  18. package/src/adapters/lit/index.d.ts +120 -120
  19. package/src/adapters/lit/index.js +721 -721
  20. package/src/adapters/lit.d.ts +3 -3
  21. package/src/adapters/lit.js +3 -3
  22. package/src/adapters/preact/ai.js +33 -33
  23. package/src/adapters/preact/index.d.ts +108 -108
  24. package/src/adapters/preact/index.js +661 -661
  25. package/src/adapters/preact.d.ts +3 -3
  26. package/src/adapters/preact.js +3 -3
  27. package/src/adapters/react/index.js +48 -54
  28. package/src/adapters/react.d.ts +3 -3
  29. package/src/adapters/react.js +3 -3
  30. package/src/adapters/shared.js +64 -64
  31. package/src/adapters/solid/ai.js +32 -32
  32. package/src/adapters/solid/index.d.ts +101 -101
  33. package/src/adapters/solid/index.js +586 -586
  34. package/src/adapters/solid.d.ts +3 -3
  35. package/src/adapters/solid.js +3 -3
  36. package/src/adapters/svelte/ai.js +31 -31
  37. package/src/adapters/svelte/index.d.ts +166 -166
  38. package/src/adapters/svelte/index.js +798 -798
  39. package/src/adapters/svelte.d.ts +3 -3
  40. package/src/adapters/svelte.js +3 -3
  41. package/src/adapters/vanilla/ai.js +30 -30
  42. package/src/adapters/vanilla/index.d.ts +179 -179
  43. package/src/adapters/vanilla/index.js +785 -785
  44. package/src/adapters/vanilla.d.ts +3 -3
  45. package/src/adapters/vanilla.js +3 -3
  46. package/src/adapters/vue/ai.js +52 -52
  47. package/src/adapters/vue/index.d.ts +299 -299
  48. package/src/adapters/vue/index.js +610 -610
  49. package/src/adapters/vue.d.ts +3 -3
  50. package/src/adapters/vue.js +3 -3
  51. package/src/ai/wu-ai-actions.js +261 -261
  52. package/src/ai/wu-ai-agent.js +546 -546
  53. package/src/ai/wu-ai-browser-primitives.js +354 -354
  54. package/src/ai/wu-ai-browser.js +380 -380
  55. package/src/ai/wu-ai-context.js +332 -332
  56. package/src/ai/wu-ai-conversation.js +613 -613
  57. package/src/ai/wu-ai-orchestrate.js +1021 -1021
  58. package/src/ai/wu-ai-permissions.js +381 -381
  59. package/src/ai/wu-ai-provider.js +700 -700
  60. package/src/ai/wu-ai-schema.js +225 -225
  61. package/src/ai/wu-ai-triggers.js +396 -396
  62. package/src/ai/wu-ai.js +804 -804
  63. package/src/core/wu-app.js +236 -236
  64. package/src/core/wu-cache.js +498 -477
  65. package/src/core/wu-core.js +1412 -1398
  66. package/src/core/wu-error-boundary.js +396 -382
  67. package/src/core/wu-event-bus.js +390 -348
  68. package/src/core/wu-hooks.js +350 -350
  69. package/src/core/wu-html-parser.js +199 -190
  70. package/src/core/wu-iframe-sandbox.js +328 -328
  71. package/src/core/wu-loader.js +385 -273
  72. package/src/core/wu-logger.js +142 -134
  73. package/src/core/wu-manifest.js +532 -509
  74. package/src/core/wu-mcp-bridge.js +432 -432
  75. package/src/core/wu-overrides.js +510 -510
  76. package/src/core/wu-performance.js +228 -228
  77. package/src/core/wu-plugin.js +401 -348
  78. package/src/core/wu-prefetch.js +414 -414
  79. package/src/core/wu-proxy-sandbox.js +477 -476
  80. package/src/core/wu-sandbox.js +779 -779
  81. package/src/core/wu-script-executor.js +161 -113
  82. package/src/core/wu-snapshot-sandbox.js +227 -227
  83. package/src/core/wu-store.js +13 -3
  84. package/src/core/wu-strategies.js +256 -256
  85. package/src/core/wu-style-bridge.js +477 -477
  86. package/src/index.d.ts +317 -0
  87. package/src/index.js +234 -224
  88. 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
+ }