@absolutejs/absolute 0.19.0-beta.706 → 0.19.0-beta.708

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 (106) hide show
  1. package/dist/angular/browser.js +1 -19
  2. package/dist/angular/browser.js.map +3 -3
  3. package/dist/angular/components/constants.js +78 -0
  4. package/dist/angular/components/core/streamingSlotRegistrar.js +58 -0
  5. package/dist/angular/components/core/streamingSlotRegistry.js +114 -0
  6. package/dist/angular/components/defer-slot-payload.js +6 -0
  7. package/dist/angular/components/defer-slot-templates.directive.js +44 -0
  8. package/dist/angular/components/defer-slot.component.js +149 -0
  9. package/dist/angular/components/image.component.js +202 -0
  10. package/dist/angular/components/index.js +4 -0
  11. package/dist/angular/components/stream-slot.component.js +103 -0
  12. package/dist/angular/index.js +91 -36
  13. package/dist/angular/index.js.map +6 -6
  14. package/dist/angular/server.js +91 -36
  15. package/dist/angular/server.js.map +6 -6
  16. package/dist/build.js +242 -162
  17. package/dist/build.js.map +12 -12
  18. package/dist/cli/index.js +214 -142
  19. package/dist/client/index.js +86 -31
  20. package/dist/client/index.js.map +4 -4
  21. package/dist/core/streamingSlotRegistrar.js +1 -19
  22. package/dist/core/streamingSlotRegistrar.js.map +2 -2
  23. package/dist/core/streamingSlotRegistry.js +1 -19
  24. package/dist/core/streamingSlotRegistry.js.map +2 -2
  25. package/dist/dev/client/constants.ts +26 -0
  26. package/dist/dev/client/cssUtils.ts +307 -0
  27. package/dist/dev/client/domDiff.ts +226 -0
  28. package/dist/dev/client/domState.ts +421 -0
  29. package/dist/dev/client/domTracker.ts +61 -0
  30. package/dist/dev/client/errorOverlay.ts +184 -0
  31. package/dist/dev/client/frameworkDetect.ts +63 -0
  32. package/dist/dev/client/handlers/angular.ts +578 -0
  33. package/dist/dev/client/handlers/angularRuntime.ts +231 -0
  34. package/dist/dev/client/handlers/html.ts +364 -0
  35. package/dist/dev/client/handlers/htmx.ts +278 -0
  36. package/dist/dev/client/handlers/react.ts +108 -0
  37. package/dist/dev/client/handlers/rebuild.ts +153 -0
  38. package/dist/dev/client/handlers/svelte.ts +334 -0
  39. package/dist/dev/client/handlers/vue.ts +292 -0
  40. package/dist/dev/client/headPatch.ts +233 -0
  41. package/dist/dev/client/hmrClient.ts +273 -0
  42. package/dist/dev/client/hmrState.ts +14 -0
  43. package/dist/dev/client/moduleVersions.ts +62 -0
  44. package/dist/dev/client/reactRefreshSetup.ts +31 -0
  45. package/dist/index.js +282 -187
  46. package/dist/index.js.map +15 -15
  47. package/dist/islands/browser.js +1 -19
  48. package/dist/islands/browser.js.map +2 -2
  49. package/dist/islands/index.js +80 -26
  50. package/dist/islands/index.js.map +5 -5
  51. package/dist/react/browser.js +7 -25
  52. package/dist/react/browser.js.map +2 -2
  53. package/dist/react/components/browser/index.js +101 -101
  54. package/dist/react/components/index.js +104 -122
  55. package/dist/react/components/index.js.map +3 -3
  56. package/dist/react/hooks/index.js +1 -19
  57. package/dist/react/hooks/index.js.map +2 -2
  58. package/dist/react/index.js +101 -46
  59. package/dist/react/index.js.map +6 -6
  60. package/dist/react/jsxDevRuntimeCompat.js +1 -19
  61. package/dist/react/jsxDevRuntimeCompat.js.map +2 -2
  62. package/dist/react/server.js +13 -30
  63. package/dist/react/server.js.map +4 -4
  64. package/dist/src/angular/components/constants.d.ts +75 -0
  65. package/dist/src/angular/components/defer-slot-templates.directive.d.ts +7 -0
  66. package/dist/src/angular/components/defer-slot.component.d.ts +5 -2
  67. package/dist/src/angular/components/image.component.d.ts +5 -2
  68. package/dist/src/angular/components/index.d.ts +4 -4
  69. package/dist/src/angular/components/stream-slot.component.d.ts +3 -0
  70. package/dist/src/client/streamSwap.d.ts +0 -10
  71. package/dist/src/constants.d.ts +1 -0
  72. package/dist/src/dev/rebuildTrigger.d.ts +1 -1
  73. package/dist/src/svelte/renderToPipeableStream.d.ts +2 -2
  74. package/dist/src/svelte/renderToReadableStream.d.ts +2 -2
  75. package/dist/src/svelte/renderToString.d.ts +2 -2
  76. package/dist/src/vue/components/Image.d.ts +3 -3
  77. package/dist/svelte/browser.js +1 -19
  78. package/dist/svelte/browser.js.map +2 -2
  79. package/dist/svelte/components/AwaitSlot.svelte +39 -0
  80. package/dist/svelte/components/AwaitSlot.svelte.d.ts +2 -0
  81. package/dist/svelte/components/Head.svelte +144 -0
  82. package/dist/svelte/components/Head.svelte.d.ts +2 -0
  83. package/dist/svelte/components/Image.svelte +164 -0
  84. package/dist/svelte/components/Image.svelte.d.ts +5 -0
  85. package/dist/svelte/components/Island.svelte +71 -0
  86. package/dist/svelte/components/Island.svelte.d.ts +5 -0
  87. package/dist/svelte/components/JsonLd.svelte +21 -0
  88. package/dist/svelte/components/JsonLd.svelte.d.ts +2 -0
  89. package/dist/svelte/components/StreamSlot.svelte +41 -0
  90. package/dist/svelte/components/StreamSlot.svelte.d.ts +2 -0
  91. package/dist/svelte/index.js +93 -37
  92. package/dist/svelte/index.js.map +7 -7
  93. package/dist/svelte/server.js +16 -32
  94. package/dist/svelte/server.js.map +5 -5
  95. package/dist/types/globals.d.ts +130 -0
  96. package/dist/vue/browser.js +1 -19
  97. package/dist/vue/browser.js.map +2 -2
  98. package/dist/vue/components/Image.js +1 -19
  99. package/dist/vue/components/Image.js.map +3 -3
  100. package/dist/vue/components/index.js +1 -19
  101. package/dist/vue/components/index.js.map +3 -3
  102. package/dist/vue/index.js +91 -36
  103. package/dist/vue/index.js.map +7 -7
  104. package/dist/vue/server.js +13 -30
  105. package/dist/vue/server.js.map +4 -4
  106. package/package.json +1 -1
@@ -0,0 +1,63 @@
1
+ import type {} from '../../types/globals';
2
+ /* Framework detection and manifest lookup utilities */
3
+
4
+ export const detectCurrentFramework = () => {
5
+ if (window.__HMR_FRAMEWORK__) return window.__HMR_FRAMEWORK__;
6
+ if (window.__REACT_ROOT__) return 'react';
7
+ const path = window.location.pathname;
8
+ if (path === '/vue' || path.startsWith('/vue/')) return 'vue';
9
+ if (path === '/svelte' || path.startsWith('/svelte/')) return 'svelte';
10
+ if (path === '/angular' || path.startsWith('/angular/')) return 'angular';
11
+ if (path === '/htmx' || path.startsWith('/htmx/')) return 'htmx';
12
+ if (path === '/html' || path.startsWith('/html/')) return 'html';
13
+ if (path === '/react' || path.startsWith('/react/')) return 'react';
14
+
15
+ return null;
16
+ };
17
+ export const findIndexPath = (
18
+ manifest: Record<string, string> | undefined,
19
+ sourceFile: string | undefined,
20
+ framework: string
21
+ ) => {
22
+ if (!manifest) return null;
23
+
24
+ if (sourceFile) {
25
+ const componentName = getComponentNameFromPath(sourceFile);
26
+ const indexKey = componentName ? `${componentName}Index` : null;
27
+ if (indexKey && manifest[indexKey]) {
28
+ return manifest[indexKey];
29
+ }
30
+ }
31
+
32
+ const frameworkPatterns: Record<string, RegExp> = {
33
+ angular: /angular/i,
34
+ react: /react/i,
35
+ svelte: /svelte/i,
36
+ vue: /vue/i
37
+ };
38
+ const pattern = frameworkPatterns[framework];
39
+
40
+ for (const key in manifest) {
41
+ const value = manifest[key];
42
+ if (
43
+ key.endsWith('Index') &&
44
+ value &&
45
+ (!pattern || pattern.test(key) || value.includes(`/${framework}/`))
46
+ ) {
47
+ return value;
48
+ }
49
+ }
50
+
51
+ return null;
52
+ };
53
+ export const getComponentNameFromPath = (filePath: string) => {
54
+ if (!filePath) return null;
55
+ const parts = filePath.replace(/\\/g, '/').split('/');
56
+ const fileName = parts[parts.length - 1] || '';
57
+ const baseName = fileName.replace(/\.(tsx?|jsx?|vue|svelte|html)$/, '');
58
+
59
+ return baseName
60
+ .split(/[-_]/)
61
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
62
+ .join('');
63
+ };
@@ -0,0 +1,578 @@
1
+ import type {} from '../../../types/globals';
2
+ /* Angular HMR — Re-Bootstrap with View Transitions API (Zero Flicker)
3
+ DEV MODE ONLY — never active in production.
4
+
5
+ Strategy:
6
+ 1. Capture component state (ng.getComponent) + DOM state
7
+ 2. Use document.startViewTransition() — browser captures a screenshot
8
+ 3. Destroy old app, recreate root element, import new module
9
+ 4. bootstrapApplication() renders new content (behind the screenshot)
10
+ 5. After bootstrap: restore state via ng.getComponent + ng.applyChanges
11
+ 6. View transition resolves — browser smoothly crossfades to new content
12
+
13
+ document.startViewTransition() is the native browser API for page
14
+ transitions. It captures a screenshot before the callback, runs the
15
+ callback (which can be async), and crossfades when the callback finishes.
16
+ The user never sees empty/default state — only the before and after. */
17
+
18
+ import { ANGULAR_INIT_TIMEOUT_MS } from '../constants';
19
+ import {
20
+ saveFormState,
21
+ restoreFormState,
22
+ saveScrollState,
23
+ restoreScrollState
24
+ } from '../domState';
25
+ import { detectCurrentFramework, findIndexPath } from '../frameworkDetect';
26
+
27
+ type HMRMessage = {
28
+ data: {
29
+ cssBaseName?: string;
30
+ cssUrl?: string;
31
+ html?: string;
32
+ manifest?: Record<string, string>;
33
+ pageModuleUrl?: string;
34
+ serverDuration?: number;
35
+ sourceFile?: string;
36
+ updateType?: string;
37
+ };
38
+ };
39
+
40
+ type NgApi = {
41
+ applyChanges?: (component: unknown) => void;
42
+ getComponent?: (element: Element) => unknown;
43
+ };
44
+
45
+ type AngularClientWindow = Window & {
46
+ ng?: NgApi;
47
+ };
48
+
49
+ type AngularHmrApi = {
50
+ applyUpdate: (id: string, newCtor: unknown) => boolean;
51
+ getRegistry?: () => Map<string, unknown>;
52
+ refresh: () => void;
53
+ };
54
+
55
+ type ViewTransitionDocument = Document & {
56
+ startViewTransition?: (updateCallback: () => Promise<void>) => {
57
+ finished: Promise<void>;
58
+ };
59
+ };
60
+
61
+ type AngularComponentExport = ((...args: unknown[]) => unknown) & {
62
+ ɵcmp?: unknown;
63
+ };
64
+
65
+ const isAngularComponentExport = (
66
+ value: unknown
67
+ ): value is AngularComponentExport => {
68
+ if (typeof value !== 'function') {
69
+ return false;
70
+ }
71
+
72
+ return 'ɵcmp' in value && Boolean(value.ɵcmp);
73
+ };
74
+
75
+ const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
76
+ Boolean(value) && typeof value === 'object';
77
+
78
+ const swapStylesheet = (
79
+ cssUrl: string,
80
+ cssBaseName: string,
81
+ framework: string
82
+ ) => {
83
+ let existingLink: HTMLLinkElement | null = null;
84
+ document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
85
+ const linkEl = link instanceof HTMLLinkElement ? link : null;
86
+ const href = linkEl?.getAttribute('href') ?? '';
87
+ if (href.includes(cssBaseName) || href.includes(framework)) {
88
+ existingLink = linkEl;
89
+ }
90
+ });
91
+ if (!existingLink) return;
92
+
93
+ const capturedExisting: HTMLLinkElement = existingLink;
94
+ const newLink = document.createElement('link');
95
+ newLink.rel = 'stylesheet';
96
+ newLink.href = `${cssUrl}?t=${Date.now()}`;
97
+ newLink.onload = function () {
98
+ if (capturedExisting && capturedExisting.parentNode)
99
+ capturedExisting.remove();
100
+ };
101
+ document.head.appendChild(newLink);
102
+ };
103
+
104
+ // ─── State Capture/Restore via ng.getComponent ──────────────
105
+
106
+ type StateSnapshot = {
107
+ selector: string;
108
+ index: number;
109
+ properties: Record<string, unknown>;
110
+ };
111
+
112
+ const readDomCounter = (
113
+ element: Element,
114
+ properties: Record<string, unknown>
115
+ ) => {
116
+ element
117
+ .querySelectorAll('[class*="value"], [class*="count"]')
118
+ .forEach((stateEl) => {
119
+ const text = stateEl.textContent;
120
+ if (text === null || text.trim() === '') return;
121
+ const num = parseInt(text.trim(), 10);
122
+ if (!isNaN(num)) properties['__dom_counter'] = num;
123
+ });
124
+ };
125
+
126
+ const copyInstanceProperty = (
127
+ instance: Record<string, unknown>,
128
+ key: string,
129
+ properties: Record<string, unknown>
130
+ ) => {
131
+ if (key.startsWith('ɵ') || key.startsWith('__')) return;
132
+ const val = instance[key];
133
+ if (typeof val === 'function') return;
134
+ properties[key] = val;
135
+ };
136
+
137
+ const captureInstanceProperties = (
138
+ ngApi: NgApi | undefined,
139
+ element: Element,
140
+ properties: Record<string, unknown>
141
+ ) => {
142
+ if (!ngApi || typeof ngApi.getComponent !== 'function') return;
143
+
144
+ try {
145
+ const instance = ngApi.getComponent(element);
146
+ if (!isObjectRecord(instance)) return;
147
+
148
+ Object.keys(instance).forEach((key) => {
149
+ copyInstanceProperty(instance, key, properties);
150
+ });
151
+ } catch {
152
+ /* ignored */
153
+ }
154
+ };
155
+
156
+ const captureComponentState = () => {
157
+ const snapshots: StateSnapshot[] = [];
158
+ const selectorCounts = new Map<string, number>();
159
+ const angularWindow: AngularClientWindow = window;
160
+ const ngApi = angularWindow.ng;
161
+
162
+ document.querySelectorAll('*').forEach((elem) => {
163
+ const tagName = elem.tagName.toLowerCase();
164
+ if (!tagName.includes('-')) return;
165
+
166
+ const count = selectorCounts.get(tagName) || 0;
167
+ selectorCounts.set(tagName, count + 1);
168
+
169
+ const properties: Record<string, unknown> = {};
170
+ readDomCounter(elem, properties);
171
+ captureInstanceProperties(ngApi, elem, properties);
172
+
173
+ if (Object.keys(properties).length > 0) {
174
+ snapshots.push({ index: count, properties, selector: tagName });
175
+ }
176
+ });
177
+
178
+ return snapshots;
179
+ };
180
+
181
+ const safeSetProperty = (
182
+ instance: Record<string, unknown>,
183
+ key: string,
184
+ value: unknown
185
+ ) => {
186
+ try {
187
+ instance[key] = value;
188
+ } catch {
189
+ /* ignored */
190
+ }
191
+ };
192
+
193
+ const restoreInstanceProperties = (
194
+ instance: Record<string, unknown>,
195
+ snap: StateSnapshot
196
+ ) => {
197
+ const domCounter = snap.properties['__dom_counter'];
198
+ Object.entries(snap.properties).forEach(([key, value]) => {
199
+ if (key === '__dom_counter') return;
200
+ safeSetProperty(instance, key, value);
201
+ });
202
+ if (
203
+ domCounter !== undefined &&
204
+ typeof domCounter === 'number' &&
205
+ 'count' in instance
206
+ ) {
207
+ instance['count'] = domCounter;
208
+ }
209
+ };
210
+
211
+ const restoreViaInstance = (
212
+ ngApi: NgApi | undefined,
213
+ element: Element,
214
+ snap: StateSnapshot
215
+ ) => {
216
+ if (!ngApi || typeof ngApi.getComponent !== 'function') return false;
217
+
218
+ try {
219
+ const instance = ngApi.getComponent(element);
220
+ if (!isObjectRecord(instance)) return false;
221
+
222
+ restoreInstanceProperties(instance, snap);
223
+ if (typeof ngApi.applyChanges === 'function')
224
+ ngApi.applyChanges(element);
225
+
226
+ return true;
227
+ } catch {
228
+ return false;
229
+ }
230
+ };
231
+
232
+ const restoreDomFallback = (element: Element, snap: StateSnapshot) => {
233
+ const domCounter = snap.properties['__dom_counter'];
234
+ if (domCounter === undefined) return;
235
+
236
+ element
237
+ .querySelectorAll('[class*="value"], [class*="count"]')
238
+ .forEach((counterEl) => {
239
+ counterEl.textContent = String(domCounter);
240
+ });
241
+ };
242
+
243
+ const restoreComponentState = (snapshots: StateSnapshot[]) => {
244
+ const angularWindow: AngularClientWindow = window;
245
+ const ngApi = angularWindow.ng;
246
+ if (snapshots.length === 0) return;
247
+
248
+ const bySelector = new Map<string, StateSnapshot[]>();
249
+ for (const snap of snapshots) {
250
+ const list = bySelector.get(snap.selector) || [];
251
+ list.push(snap);
252
+ bySelector.set(snap.selector, list);
253
+ }
254
+
255
+ bySelector.forEach((snaps, selector) => {
256
+ const elements = document.querySelectorAll(selector);
257
+ snaps.forEach((snap) => {
258
+ const element = elements[snap.index];
259
+ if (!element) return;
260
+
261
+ const restored = restoreViaInstance(ngApi, element, snap);
262
+ if (!restored) restoreDomFallback(element, snap);
263
+ });
264
+ });
265
+ };
266
+
267
+ // ─── Wait for Angular bootstrap (event-based, no polling) ───
268
+ // Installs a property setter trap on window.__ANGULAR_APP__ that
269
+ // resolves the promise the instant the bootstrap code writes to it.
270
+ // Falls back to a short timeout in case the setter is bypassed.
271
+
272
+ const waitForAngularApp = () => {
273
+ if (window.__ANGULAR_APP__) return Promise.resolve();
274
+
275
+ const { promise, resolve } = Promise.withResolvers<void>();
276
+ const timeout = setTimeout(resolve, ANGULAR_INIT_TIMEOUT_MS);
277
+
278
+ let stored = window.__ANGULAR_APP__;
279
+
280
+ Object.defineProperty(window, '__ANGULAR_APP__', {
281
+ configurable: true,
282
+ enumerable: true,
283
+ get() {
284
+ return stored;
285
+ },
286
+ set(val) {
287
+ stored = val;
288
+ Object.defineProperty(window, '__ANGULAR_APP__', {
289
+ configurable: true,
290
+ enumerable: true,
291
+ value: val,
292
+ writable: true
293
+ });
294
+ clearTimeout(timeout);
295
+ resolve();
296
+ }
297
+ });
298
+
299
+ return promise;
300
+ };
301
+
302
+ // ============================================================
303
+ // FAST UPDATE — Runtime patching without destroy/re-bootstrap
304
+ // ============================================================
305
+
306
+ const suppressNg0912 = () => {
307
+ const origWarn = console.warn;
308
+ console.warn = function (...args: unknown[]) {
309
+ if (typeof args[0] === 'string' && args[0].includes('NG0912')) return;
310
+ origWarn.apply(console, args);
311
+ };
312
+
313
+ return origWarn;
314
+ };
315
+
316
+ const tryPatchExport = (
317
+ exportName: string,
318
+ newModule: Record<string, unknown>,
319
+ registry: Map<string, unknown>,
320
+ hmr: AngularHmrApi,
321
+ sourceFile: string
322
+ ) => {
323
+ const exported = newModule[exportName];
324
+ if (!isAngularComponentExport(exported)) return 'skip';
325
+
326
+ const registryId = `${sourceFile}#${exportName}`;
327
+ if (!registry.has(registryId)) return 'skip';
328
+
329
+ const success = hmr.applyUpdate(registryId, exported);
330
+ if (!success) return 'fail';
331
+
332
+ return 'patched';
333
+ };
334
+
335
+ const patchRegisteredComponents = (
336
+ newModule: Record<string, unknown>,
337
+ registry: Map<string, unknown>,
338
+ hmr: AngularHmrApi,
339
+ sourceFile: string
340
+ ) => {
341
+ let patchedAny = false;
342
+ const allPatched = Object.keys(newModule).every((exportName) => {
343
+ const result = tryPatchExport(
344
+ exportName,
345
+ newModule,
346
+ registry,
347
+ hmr,
348
+ sourceFile
349
+ );
350
+ if (result === 'skip') {
351
+ return true;
352
+ }
353
+ if (result === 'fail') {
354
+ return false;
355
+ }
356
+ patchedAny = true;
357
+
358
+ return true;
359
+ });
360
+
361
+ return { allPatched, patchedAny };
362
+ };
363
+
364
+ const attemptFastPatch = async (
365
+ indexPath: string,
366
+ registry: Map<string, unknown>,
367
+ hmr: AngularHmrApi,
368
+ sourceFile: string,
369
+ origWarn: typeof console.warn
370
+ ) => {
371
+ try {
372
+ const newModule = await import(`${indexPath}?t=${Date.now()}`);
373
+
374
+ console.warn = origWarn;
375
+
376
+ const { allPatched, patchedAny } = patchRegisteredComponents(
377
+ newModule,
378
+ registry,
379
+ hmr,
380
+ sourceFile
381
+ );
382
+
383
+ if (!patchedAny) return false;
384
+ if (!allPatched) return false;
385
+
386
+ hmr.refresh();
387
+
388
+ return true;
389
+ } catch (err) {
390
+ console.warn = origWarn;
391
+ console.warn('[HMR] Angular fast update failed, falling back:', err);
392
+
393
+ return false;
394
+ }
395
+ };
396
+
397
+ // handleFastUpdate is kept for future use when the fast path is re-enabled.
398
+ const _handleFastUpdate = async (message: HMRMessage) => {
399
+ const hmr = window.__ANGULAR_HMR__;
400
+ if (!hmr || !hmr.getRegistry) return false;
401
+
402
+ const registry = hmr.getRegistry();
403
+ if (registry.size === 0) return false;
404
+
405
+ const indexPath = findIndexPath(
406
+ message.data.manifest,
407
+ message.data.sourceFile,
408
+ 'angular'
409
+ );
410
+ if (!indexPath) return false;
411
+
412
+ const origWarn = suppressNg0912();
413
+
414
+ const patched = await attemptFastPatch(
415
+ indexPath,
416
+ registry,
417
+ hmr,
418
+ message.data.sourceFile || '',
419
+ origWarn
420
+ );
421
+
422
+ if (patched && message.data.cssUrl) {
423
+ swapStylesheet(
424
+ message.data.cssUrl,
425
+ message.data.cssBaseName || '',
426
+ 'angular'
427
+ );
428
+ }
429
+
430
+ return patched;
431
+ };
432
+
433
+ // ============================================================
434
+ // MAIN ENTRY POINT
435
+ // ============================================================
436
+
437
+ export const handleAngularUpdate = (message: HMRMessage) => {
438
+ if (detectCurrentFramework() !== 'angular') return;
439
+
440
+ const updateType = message.data.updateType || 'logic';
441
+
442
+ if (
443
+ (updateType === 'style' || updateType === 'css-only') &&
444
+ message.data.cssUrl
445
+ ) {
446
+ swapStylesheet(
447
+ message.data.cssUrl,
448
+ message.data.cssBaseName || '',
449
+ 'angular'
450
+ );
451
+
452
+ return;
453
+ }
454
+
455
+ handleFullUpdate(message);
456
+ };
457
+
458
+ // ============================================================
459
+ // RE-BOOTSTRAP WITH VIEW TRANSITIONS API
460
+ // ============================================================
461
+
462
+ const findRootSelector = (container: Element) => {
463
+ const candidates = container.querySelectorAll('*');
464
+ for (let idx = 0; idx < candidates.length; idx++) {
465
+ const candidate = candidates[idx];
466
+ if (!candidate) continue;
467
+ const tag = candidate.tagName.toLowerCase();
468
+ if (tag.includes('-')) return tag;
469
+ }
470
+
471
+ return null;
472
+ };
473
+
474
+ const destroyAngularApp = () => {
475
+ if (!window.__ANGULAR_APP__) return;
476
+
477
+ try {
478
+ window.__ANGULAR_APP__.destroy();
479
+ } catch {
480
+ /* ignored */
481
+ }
482
+ window.__ANGULAR_APP__ = null;
483
+ };
484
+
485
+ const bootstrapAngularModule = async (
486
+ indexPath: string,
487
+ rootSelector: string | null,
488
+ rootContainer: Element
489
+ ) => {
490
+ if (rootSelector && !rootContainer.querySelector(rootSelector)) {
491
+ rootContainer.appendChild(document.createElement(rootSelector));
492
+ }
493
+
494
+ window.__HMR_SKIP_HYDRATION__ = true;
495
+
496
+ const origWarn = suppressNg0912();
497
+
498
+ await import(`${indexPath}?t=${Date.now()}`);
499
+ await waitForAngularApp();
500
+
501
+ console.warn = origWarn;
502
+ };
503
+
504
+ const tickAngularApp = () => {
505
+ if (!window.__ANGULAR_APP__) return;
506
+
507
+ try {
508
+ window.__ANGULAR_APP__.tick();
509
+ } catch {
510
+ /* ignored */
511
+ }
512
+ };
513
+
514
+ const runWithViewTransition = (updateFn: () => Promise<void>) => {
515
+ const doc: ViewTransitionDocument = document;
516
+ if (typeof doc.startViewTransition !== 'function') {
517
+ updateFn().catch((err: unknown) => {
518
+ console.warn('[HMR] Angular update failed (non-fatal):', err);
519
+ });
520
+
521
+ return;
522
+ }
523
+
524
+ let styleEl: HTMLStyleElement | null = null;
525
+ try {
526
+ styleEl = document.createElement('style');
527
+ styleEl.textContent =
528
+ '::view-transition-old(root),::view-transition-new(root){animation:none!important}';
529
+ document.head.appendChild(styleEl);
530
+ } catch {
531
+ /* ignored */
532
+ }
533
+
534
+ const removeStyle = () => {
535
+ if (styleEl && styleEl.parentNode) styleEl.remove();
536
+ };
537
+
538
+ doc.startViewTransition(async () => {
539
+ await updateFn();
540
+ })
541
+ .finished.then(removeStyle)
542
+ .catch(removeStyle);
543
+ };
544
+
545
+ const handleFullUpdate = (message: HMRMessage) => {
546
+ const componentState = captureComponentState();
547
+ const scrollState = saveScrollState();
548
+ const formState = saveFormState();
549
+
550
+ if (message.data.cssUrl) {
551
+ swapStylesheet(
552
+ message.data.cssUrl,
553
+ message.data.cssBaseName || '',
554
+ 'angular'
555
+ );
556
+ }
557
+
558
+ const rootContainer = document.getElementById('root') || document.body;
559
+ const rootSelector = findRootSelector(rootContainer);
560
+
561
+ const indexPath = findIndexPath(
562
+ message.data.manifest,
563
+ message.data.sourceFile,
564
+ 'angular'
565
+ );
566
+ if (!indexPath) return;
567
+
568
+ const doUpdate = async () => {
569
+ destroyAngularApp();
570
+ await bootstrapAngularModule(indexPath, rootSelector, rootContainer);
571
+ restoreComponentState(componentState);
572
+ tickAngularApp();
573
+ restoreFormState(formState);
574
+ restoreScrollState(scrollState);
575
+ };
576
+
577
+ runWithViewTransition(doUpdate);
578
+ };