@absolutejs/absolute 0.19.0-beta.852 → 0.19.0-beta.854
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/angular/components/core/streamingSlotRegistrar.js +1 -1
- package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
- package/dist/angular/index.js +52 -76
- package/dist/angular/index.js.map +3 -3
- package/dist/angular/server.js +53 -77
- package/dist/angular/server.js.map +3 -3
- package/dist/build.js +2129 -730
- package/dist/build.js.map +17 -8
- package/dist/dev/client/handlers/angularHmrShim.ts +77 -0
- package/dist/dev/client/hmrClient.ts +55 -5
- package/dist/index.js +2219 -778
- package/dist/index.js.map +18 -9
- package/dist/react/index.js +3 -1
- package/dist/react/index.js.map +2 -2
- package/dist/react/server.js +3 -1
- package/dist/react/server.js.map +2 -2
- package/dist/src/core/prepare.d.ts +25 -0
- package/dist/src/dev/angular/fastHmrCompiler.d.ts +32 -0
- package/dist/src/dev/angular/hmrCompiler.d.ts +18 -0
- package/dist/src/dev/angular/hmrImportGenerator.d.ts +3 -0
- package/dist/src/dev/angular/hmrInjectionPlugin.d.ts +7 -0
- package/dist/src/dev/angular/resolveOwningComponents.d.ts +8 -0
- package/dist/src/dev/angular/vendor/translator/api/ast_factory.d.ts +363 -0
- package/dist/src/dev/angular/vendor/translator/api/import_generator.d.ts +49 -0
- package/dist/src/dev/angular/vendor/translator/context.d.ts +18 -0
- package/dist/src/dev/angular/vendor/translator/translator.d.ts +75 -0
- package/dist/src/dev/angular/vendor/translator/ts_util.d.ts +12 -0
- package/dist/src/dev/angular/vendor/translator/typescript_ast_factory.d.ts +66 -0
- package/dist/src/dev/angular/vendor/translator/typescript_translator.d.ts +13 -0
- package/dist/src/dev/rebuildTrigger.d.ts +1 -0
- package/dist/src/plugins/hmr.d.ts +25 -0
- package/dist/src/vue/components/Image.d.ts +1 -1
- package/dist/svelte/index.js +3 -1
- package/dist/svelte/index.js.map +2 -2
- package/dist/svelte/server.js +3 -1
- package/dist/svelte/server.js.map +2 -2
- package/dist/vue/index.js +3 -1
- package/dist/vue/index.js.map +2 -2
- package/dist/vue/server.js +3 -1
- package/dist/vue/server.js.map +2 -2
- package/package.json +1 -1
- package/dist/dev/client/handlers/angular.ts +0 -684
- package/dist/dev/client/handlers/angularRuntime.ts +0 -415
- package/dist/src/dev/angular/editTypeDetection.d.ts +0 -8
|
@@ -1,684 +0,0 @@
|
|
|
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/service state via `preserveAcrossHmr` opt-ins
|
|
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. New instances restore from cache via `preserveAcrossHmr` in their
|
|
11
|
-
constructors / ngOnInit (gated on rebootInProgress flag)
|
|
12
|
-
6. Wait for `applicationRef.whenStable()` so lazy-route components
|
|
13
|
-
have a chance to construct, then close the restoration window
|
|
14
|
-
7. View transition resolves — browser smoothly crossfades to new
|
|
15
|
-
content
|
|
16
|
-
|
|
17
|
-
document.startViewTransition() is the native browser API for page
|
|
18
|
-
transitions. It captures a screenshot before the callback, runs
|
|
19
|
-
the callback (which can be async), and crossfades when the callback
|
|
20
|
-
finishes. The user never sees empty/default state — only the
|
|
21
|
-
before and after. */
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
captureTrackedInstanceStates,
|
|
25
|
-
endHmrReboot
|
|
26
|
-
} from '../../../angular/hmrPreserveCore';
|
|
27
|
-
import { ANGULAR_INIT_TIMEOUT_MS } from '../constants';
|
|
28
|
-
import {
|
|
29
|
-
saveFormState,
|
|
30
|
-
restoreFormState,
|
|
31
|
-
saveScrollState,
|
|
32
|
-
restoreScrollState
|
|
33
|
-
} from '../domState';
|
|
34
|
-
import { detectCurrentFramework, findIndexPath } from '../frameworkDetect';
|
|
35
|
-
import { showHmrToast } from '../hmrToast';
|
|
36
|
-
|
|
37
|
-
type AngularUpdateType =
|
|
38
|
-
| 'template'
|
|
39
|
-
| 'style-component'
|
|
40
|
-
| 'class-component'
|
|
41
|
-
| 'service-method-only'
|
|
42
|
-
| 'service-with-side-effects'
|
|
43
|
-
| 'route'
|
|
44
|
-
| 'reboot'
|
|
45
|
-
// Legacy types kept for back-compat with global CSS edits and pre-2.1
|
|
46
|
-
// builds. Treated as fast-path triggers (style → swap stylesheet,
|
|
47
|
-
// logic → fast-patch + reboot fallback).
|
|
48
|
-
| 'style'
|
|
49
|
-
| 'css-only'
|
|
50
|
-
| 'logic'
|
|
51
|
-
| 'full';
|
|
52
|
-
|
|
53
|
-
type HMRMessage = {
|
|
54
|
-
data: {
|
|
55
|
-
cssBaseName?: string;
|
|
56
|
-
cssUrl?: string;
|
|
57
|
-
editSourceFile?: string;
|
|
58
|
-
html?: string;
|
|
59
|
-
manifest?: Record<string, string>;
|
|
60
|
-
pageModuleUrl?: string;
|
|
61
|
-
reason?: string;
|
|
62
|
-
serverDuration?: number;
|
|
63
|
-
sourceFile?: string;
|
|
64
|
-
updateType?: AngularUpdateType;
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
type AngularHmrApi = {
|
|
69
|
-
applyUpdate: (id: string, newCtor: unknown) => boolean;
|
|
70
|
-
getRegistry?: () => Map<string, unknown>;
|
|
71
|
-
refresh: () => void;
|
|
72
|
-
hasPageExportsChanged?: (sourceId: string) => boolean;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
type ViewTransitionDocument = Document & {
|
|
76
|
-
startViewTransition?: (updateCallback: () => Promise<void>) => {
|
|
77
|
-
finished: Promise<void>;
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
type AngularComponentExport = ((...args: unknown[]) => unknown) & {
|
|
82
|
-
ɵcmp?: unknown;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const isAngularComponentExport = (
|
|
86
|
-
value: unknown
|
|
87
|
-
): value is AngularComponentExport => {
|
|
88
|
-
if (typeof value !== 'function') {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return 'ɵcmp' in value && Boolean(value.ɵcmp);
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const swapStylesheet = (
|
|
96
|
-
cssUrl: string,
|
|
97
|
-
cssBaseName: string,
|
|
98
|
-
framework: string
|
|
99
|
-
) => {
|
|
100
|
-
let existingLink: HTMLLinkElement | null = null;
|
|
101
|
-
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
|
102
|
-
const linkEl = link instanceof HTMLLinkElement ? link : null;
|
|
103
|
-
const href = linkEl?.getAttribute('href') ?? '';
|
|
104
|
-
if (href.includes(cssBaseName) || href.includes(framework)) {
|
|
105
|
-
existingLink = linkEl;
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
if (!existingLink) return;
|
|
109
|
-
|
|
110
|
-
const capturedExisting: HTMLLinkElement = existingLink;
|
|
111
|
-
const newLink = document.createElement('link');
|
|
112
|
-
newLink.rel = 'stylesheet';
|
|
113
|
-
newLink.href = `${cssUrl}?t=${Date.now()}`;
|
|
114
|
-
newLink.onload = function () {
|
|
115
|
-
if (capturedExisting && capturedExisting.parentNode)
|
|
116
|
-
capturedExisting.remove();
|
|
117
|
-
};
|
|
118
|
-
document.head.appendChild(newLink);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// ─── Wait for Angular bootstrap (event-based, no polling) ───
|
|
122
|
-
// Installs a property setter trap on window.__ANGULAR_APP__ that
|
|
123
|
-
// resolves the promise the instant the bootstrap code writes to it.
|
|
124
|
-
// Falls back to a short timeout in case the setter is bypassed.
|
|
125
|
-
|
|
126
|
-
const waitForAngularApp = () => {
|
|
127
|
-
if (window.__ANGULAR_APP__) return Promise.resolve();
|
|
128
|
-
|
|
129
|
-
const { promise, resolve } = Promise.withResolvers<void>();
|
|
130
|
-
const timeout = setTimeout(resolve, ANGULAR_INIT_TIMEOUT_MS);
|
|
131
|
-
|
|
132
|
-
let stored = window.__ANGULAR_APP__;
|
|
133
|
-
|
|
134
|
-
Object.defineProperty(window, '__ANGULAR_APP__', {
|
|
135
|
-
configurable: true,
|
|
136
|
-
enumerable: true,
|
|
137
|
-
get() {
|
|
138
|
-
return stored;
|
|
139
|
-
},
|
|
140
|
-
set(val) {
|
|
141
|
-
stored = val;
|
|
142
|
-
Object.defineProperty(window, '__ANGULAR_APP__', {
|
|
143
|
-
configurable: true,
|
|
144
|
-
enumerable: true,
|
|
145
|
-
value: val,
|
|
146
|
-
writable: true
|
|
147
|
-
});
|
|
148
|
-
clearTimeout(timeout);
|
|
149
|
-
resolve();
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return promise;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
// ============================================================
|
|
157
|
-
// FAST UPDATE — Runtime patching without destroy/re-bootstrap
|
|
158
|
-
// ============================================================
|
|
159
|
-
|
|
160
|
-
const suppressNg0912 = () => {
|
|
161
|
-
const origWarn = console.warn;
|
|
162
|
-
console.warn = function (...args: unknown[]) {
|
|
163
|
-
if (typeof args[0] === 'string' && args[0].includes('NG0912')) return;
|
|
164
|
-
origWarn.apply(console, args);
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
return origWarn;
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const tryPatchExport = (
|
|
171
|
-
exportName: string,
|
|
172
|
-
newModule: Record<string, unknown>,
|
|
173
|
-
registry: Map<string, unknown>,
|
|
174
|
-
hmr: AngularHmrApi,
|
|
175
|
-
sourceFile: string
|
|
176
|
-
) => {
|
|
177
|
-
const exported = newModule[exportName];
|
|
178
|
-
if (!isAngularComponentExport(exported)) return 'skip';
|
|
179
|
-
|
|
180
|
-
const registryId = `${sourceFile}#${exportName}`;
|
|
181
|
-
if (!registry.has(registryId)) return 'skip';
|
|
182
|
-
|
|
183
|
-
const success = hmr.applyUpdate(registryId, exported);
|
|
184
|
-
if (!success) return 'fail';
|
|
185
|
-
|
|
186
|
-
return 'patched';
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const patchRegisteredComponents = (
|
|
190
|
-
newModule: Record<string, unknown>,
|
|
191
|
-
registry: Map<string, unknown>,
|
|
192
|
-
hmr: AngularHmrApi,
|
|
193
|
-
sourceFile: string
|
|
194
|
-
) => {
|
|
195
|
-
let patchedAny = false;
|
|
196
|
-
const allPatched = Object.keys(newModule).every((exportName) => {
|
|
197
|
-
const result = tryPatchExport(
|
|
198
|
-
exportName,
|
|
199
|
-
newModule,
|
|
200
|
-
registry,
|
|
201
|
-
hmr,
|
|
202
|
-
sourceFile
|
|
203
|
-
);
|
|
204
|
-
if (result === 'skip') {
|
|
205
|
-
return true;
|
|
206
|
-
}
|
|
207
|
-
if (result === 'fail') {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
patchedAny = true;
|
|
211
|
-
|
|
212
|
-
return true;
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
return { allPatched, patchedAny };
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
type FastPatchWindow = Window & {
|
|
219
|
-
__ANGULAR_HMR_FAST_PATCH__?: boolean;
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const attemptFastPatch = async (
|
|
223
|
-
indexPath: string,
|
|
224
|
-
registry: Map<string, unknown>,
|
|
225
|
-
hmr: AngularHmrApi,
|
|
226
|
-
sourceFile: string,
|
|
227
|
-
origWarn: typeof console.warn
|
|
228
|
-
) => {
|
|
229
|
-
// The bundled page chunk's top-level code re-bootstraps the Angular app
|
|
230
|
-
// (destroy + bootstrapApplication). For fast-patch we just need to read
|
|
231
|
-
// the freshly-built component classes — not re-bootstrap. Setting this
|
|
232
|
-
// flag tells the chunk to skip its bootstrap section and only run the
|
|
233
|
-
// `export * from '<page-module>'` line. Paired with the guard added in
|
|
234
|
-
// `src/build/compileAngular.ts` HMR template.
|
|
235
|
-
const w = window as FastPatchWindow;
|
|
236
|
-
w.__ANGULAR_HMR_FAST_PATCH__ = true;
|
|
237
|
-
try {
|
|
238
|
-
const newModule = await import(`${indexPath}?t=${Date.now()}`);
|
|
239
|
-
|
|
240
|
-
// Page-level `routes` / `providers` changed? Those values are read
|
|
241
|
-
// once during `bootstrapApplication`; an in-place component patch
|
|
242
|
-
// won't re-wire the running router or root injector. The chunk
|
|
243
|
-
// records its current fingerprint each time it evaluates (initial
|
|
244
|
-
// bootstrap + every fast-patch import), so a change between the
|
|
245
|
-
// previous and current evaluation means we need to fall back to a
|
|
246
|
-
// full re-bootstrap.
|
|
247
|
-
if (hmr.hasPageExportsChanged?.(sourceFile)) {
|
|
248
|
-
console.warn = origWarn;
|
|
249
|
-
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// NG0912 warnings fire during `applyUpdate` (Angular re-registers
|
|
254
|
-
// the new component class while the old one is still live). Keep
|
|
255
|
-
// the suppression active through the patch, restore right before
|
|
256
|
-
// `refresh()` so any non-NG0912 warnings during `tick()` surface.
|
|
257
|
-
const { allPatched, patchedAny } = patchRegisteredComponents(
|
|
258
|
-
newModule,
|
|
259
|
-
registry,
|
|
260
|
-
hmr,
|
|
261
|
-
sourceFile
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
console.warn = origWarn;
|
|
265
|
-
|
|
266
|
-
if (!patchedAny) return false;
|
|
267
|
-
if (!allPatched) return false;
|
|
268
|
-
|
|
269
|
-
hmr.refresh();
|
|
270
|
-
|
|
271
|
-
return true;
|
|
272
|
-
} catch (err) {
|
|
273
|
-
console.warn = origWarn;
|
|
274
|
-
console.warn('[HMR] Angular fast update failed, falling back:', err);
|
|
275
|
-
|
|
276
|
-
return false;
|
|
277
|
-
} finally {
|
|
278
|
-
delete w.__ANGULAR_HMR_FAST_PATCH__;
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
/* Fast update — patch live component prototypes without destroying the app.
|
|
283
|
-
Returns true when at least one registered component was successfully
|
|
284
|
-
patched (and no patch failed); false means we couldn't fast-patch and
|
|
285
|
-
the caller should fall back to a full re-bootstrap.
|
|
286
|
-
Failures we explicitly fall back on:
|
|
287
|
-
- file's source isn't tracked in the component registry yet
|
|
288
|
-
- changed file has no Angular components (e.g. a service or routes file)
|
|
289
|
-
- any component's `applyUpdate` returned false (provider change, etc.)
|
|
290
|
-
- dynamic import failed */
|
|
291
|
-
const handleFastUpdate = async (message: HMRMessage) => {
|
|
292
|
-
const hmr = window.__ANGULAR_HMR__;
|
|
293
|
-
if (!hmr || !hmr.getRegistry) return false;
|
|
294
|
-
|
|
295
|
-
const registry = hmr.getRegistry();
|
|
296
|
-
if (registry.size === 0) return false;
|
|
297
|
-
|
|
298
|
-
const indexPath = findIndexPath(
|
|
299
|
-
message.data.manifest,
|
|
300
|
-
message.data.sourceFile,
|
|
301
|
-
'angular'
|
|
302
|
-
);
|
|
303
|
-
if (!indexPath) return false;
|
|
304
|
-
|
|
305
|
-
const origWarn = suppressNg0912();
|
|
306
|
-
|
|
307
|
-
const patched = await attemptFastPatch(
|
|
308
|
-
indexPath,
|
|
309
|
-
registry,
|
|
310
|
-
hmr,
|
|
311
|
-
message.data.sourceFile || '',
|
|
312
|
-
origWarn
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
if (patched && message.data.cssUrl) {
|
|
316
|
-
swapStylesheet(
|
|
317
|
-
message.data.cssUrl,
|
|
318
|
-
message.data.cssBaseName || '',
|
|
319
|
-
'angular'
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return patched;
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
// ============================================================
|
|
327
|
-
// MAIN ENTRY POINT
|
|
328
|
-
// ============================================================
|
|
329
|
-
|
|
330
|
-
/* HMR updates are serialized through a single in-flight slot. While one
|
|
331
|
-
update is running (fast or full), additional incoming updates collapse
|
|
332
|
-
into one pending slot — only the latest matters because each rebuild
|
|
333
|
-
produces a chunk that supersedes prior ones for the same source file.
|
|
334
|
-
Without this, two rapid edits could:
|
|
335
|
-
- run two `startViewTransition`s and have the browser abort the first
|
|
336
|
-
mid-callback (the original "Transition was skipped" symptom), or
|
|
337
|
-
- run two `attemptFastPatch`s that both call `applyUpdate` on the same
|
|
338
|
-
registry entries, racing on prototype swaps. */
|
|
339
|
-
let activeMessage: Promise<void> | null = null;
|
|
340
|
-
let pendingMessage: HMRMessage | null = null;
|
|
341
|
-
|
|
342
|
-
/* Surgical fast-path stubs.
|
|
343
|
-
*
|
|
344
|
-
* Phase 2's dynamic-import-based handlers (component-style /
|
|
345
|
-
* template / service-method-only) are intentionally absent here —
|
|
346
|
-
* they were architecturally wrong (dynamic-importing the rebuilt
|
|
347
|
-
* page chunk created a parallel class identity, tripping NG0912
|
|
348
|
-
* collisions and producing scope-ID drift on Emulated styles). The
|
|
349
|
-
* surgical pipeline that replaces them uses Angular'''s
|
|
350
|
-
* `ɵɵreplaceMetadata` primitive — see SURGICAL_HMR.md.
|
|
351
|
-
*
|
|
352
|
-
* Until that pipeline lands, classifications other than
|
|
353
|
-
* `class-component` fall through to the existing reboot path. The
|
|
354
|
-
* toast tells the developer why. */
|
|
355
|
-
const handleTemplateUpdate = async (_message: HMRMessage): Promise<boolean> =>
|
|
356
|
-
false;
|
|
357
|
-
|
|
358
|
-
const handleComponentStyleUpdate = async (
|
|
359
|
-
_message: HMRMessage
|
|
360
|
-
): Promise<boolean> => false;
|
|
361
|
-
|
|
362
|
-
const handleServiceMethodSwap = async (
|
|
363
|
-
_message: HMRMessage
|
|
364
|
-
): Promise<boolean> => false;
|
|
365
|
-
|
|
366
|
-
const logRebootReason = (message: HMRMessage) => {
|
|
367
|
-
const reason = message.data.reason;
|
|
368
|
-
const updateType = message.data.updateType;
|
|
369
|
-
if (!reason && !updateType) return;
|
|
370
|
-
console.info(
|
|
371
|
-
`[HMR] Angular reboot — ${updateType ?? 'unknown'}: ${reason ?? '(no reason given)'}`
|
|
372
|
-
);
|
|
373
|
-
// Surface the same info as a brief on-page toast so the developer
|
|
374
|
-
// sees it without opening devtools. Toasts auto-dismiss after a
|
|
375
|
-
// few seconds.
|
|
376
|
-
showHmrToast({
|
|
377
|
-
editSourceFile: message.data.editSourceFile,
|
|
378
|
-
reason,
|
|
379
|
-
updateType
|
|
380
|
-
});
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
const processMessage = async (message: HMRMessage) => {
|
|
384
|
-
const updateType = message.data.updateType ?? 'logic';
|
|
385
|
-
|
|
386
|
-
switch (updateType) {
|
|
387
|
-
case 'template': {
|
|
388
|
-
const ok = await handleTemplateUpdate(message);
|
|
389
|
-
if (ok) return;
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
case 'style-component': {
|
|
393
|
-
const ok = await handleComponentStyleUpdate(message);
|
|
394
|
-
if (ok) return;
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
397
|
-
case 'service-method-only': {
|
|
398
|
-
const ok = await handleServiceMethodSwap(message);
|
|
399
|
-
if (ok) return;
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
|
-
case 'class-component':
|
|
403
|
-
case 'logic': {
|
|
404
|
-
// Existing prototype-swap fast-patch. Falls through to reboot
|
|
405
|
-
// on any failure (provider change, page-export change, etc.).
|
|
406
|
-
try {
|
|
407
|
-
const patched = await handleFastUpdate(message);
|
|
408
|
-
if (patched) return;
|
|
409
|
-
} catch (err) {
|
|
410
|
-
console.warn(
|
|
411
|
-
'[HMR] Angular fast update threw, falling back to reboot:',
|
|
412
|
-
err
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
case 'service-with-side-effects':
|
|
418
|
-
case 'route':
|
|
419
|
-
case 'reboot':
|
|
420
|
-
case 'full':
|
|
421
|
-
// Explicit reboot signals — skip the fast path entirely and
|
|
422
|
-
// log why so the developer can see the classification.
|
|
423
|
-
break;
|
|
424
|
-
default:
|
|
425
|
-
// Unknown update type — be conservative and reboot.
|
|
426
|
-
break;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Falling through: full re-bootstrap. Components and services that
|
|
430
|
-
// opted into `preserveAcrossHmr(this)` keep their state; anything
|
|
431
|
-
// that didn't opt in is reset to its class-field defaults. The
|
|
432
|
-
// summary log emitted by `endHmrReboot` lists what was preserved.
|
|
433
|
-
logRebootReason(message);
|
|
434
|
-
await handleFullUpdate(message);
|
|
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
|
-
// Global CSS-only updates: swap the stylesheet in place. These
|
|
447
|
-
// run outside the activeMessage queue because they can't conflict
|
|
448
|
-
// with an in-flight component update.
|
|
449
|
-
swapStylesheet(
|
|
450
|
-
message.data.cssUrl,
|
|
451
|
-
message.data.cssBaseName || '',
|
|
452
|
-
'angular'
|
|
453
|
-
);
|
|
454
|
-
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (activeMessage) {
|
|
459
|
-
// Coalesce: an update is in flight, queue this one (replacing any
|
|
460
|
-
// earlier queued update, which is now stale).
|
|
461
|
-
pendingMessage = message;
|
|
462
|
-
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
activeMessage = processMessage(message).finally(() => {
|
|
467
|
-
activeMessage = null;
|
|
468
|
-
if (pendingMessage) {
|
|
469
|
-
const next = pendingMessage;
|
|
470
|
-
pendingMessage = null;
|
|
471
|
-
handleAngularUpdate(next);
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
// ============================================================
|
|
477
|
-
// RE-BOOTSTRAP WITH VIEW TRANSITIONS API
|
|
478
|
-
// ============================================================
|
|
479
|
-
|
|
480
|
-
const findRootSelector = (container: Element) => {
|
|
481
|
-
const candidates = container.querySelectorAll('*');
|
|
482
|
-
for (let idx = 0; idx < candidates.length; idx++) {
|
|
483
|
-
const candidate = candidates[idx];
|
|
484
|
-
if (!candidate) continue;
|
|
485
|
-
const tag = candidate.tagName.toLowerCase();
|
|
486
|
-
if (tag.includes('-')) return tag;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return null;
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
const destroyAngularApp = () => {
|
|
493
|
-
if (!window.__ANGULAR_APP__) return;
|
|
494
|
-
|
|
495
|
-
try {
|
|
496
|
-
window.__ANGULAR_APP__.destroy();
|
|
497
|
-
} catch {
|
|
498
|
-
/* ignored */
|
|
499
|
-
}
|
|
500
|
-
window.__ANGULAR_APP__ = null;
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
const bootstrapAngularModule = async (
|
|
504
|
-
indexPath: string,
|
|
505
|
-
rootSelector: string | null,
|
|
506
|
-
rootContainer: Element
|
|
507
|
-
) => {
|
|
508
|
-
if (rootSelector && !rootContainer.querySelector(rootSelector)) {
|
|
509
|
-
rootContainer.appendChild(document.createElement(rootSelector));
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
window.__HMR_SKIP_HYDRATION__ = true;
|
|
513
|
-
|
|
514
|
-
const origWarn = suppressNg0912();
|
|
515
|
-
|
|
516
|
-
await import(`${indexPath}?t=${Date.now()}`);
|
|
517
|
-
await waitForAngularApp();
|
|
518
|
-
|
|
519
|
-
console.warn = origWarn;
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
const tickAngularApp = () => {
|
|
523
|
-
if (!window.__ANGULAR_APP__) return;
|
|
524
|
-
|
|
525
|
-
try {
|
|
526
|
-
window.__ANGULAR_APP__.tick();
|
|
527
|
-
} catch {
|
|
528
|
-
/* ignored */
|
|
529
|
-
}
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
/* Resolve when Angular reports the application is stable: no pending
|
|
533
|
-
microtasks, scheduled CD, or in-flight lazy chunk loads. Used to gate
|
|
534
|
-
the close of the HMR restoration window so lazy-route components get
|
|
535
|
-
a chance to construct (and call `preserveAcrossHmr`) before
|
|
536
|
-
`rebootInProgress` flips back to false. Falls back after a generous
|
|
537
|
-
ceiling in the unlikely case `whenStable` never resolves (e.g. an
|
|
538
|
-
infinite retry on a service the new app never finishes initializing) —
|
|
539
|
-
we'd rather close the window than leave HMR wedged forever. */
|
|
540
|
-
const APP_STABLE_FALLBACK_MS = 10_000;
|
|
541
|
-
|
|
542
|
-
const waitForAppStable = async () => {
|
|
543
|
-
const app = window.__ANGULAR_APP__;
|
|
544
|
-
if (!app || typeof app.whenStable !== 'function') return;
|
|
545
|
-
|
|
546
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
547
|
-
const fallback = new Promise<void>((resolve) => {
|
|
548
|
-
timer = setTimeout(resolve, APP_STABLE_FALLBACK_MS);
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
try {
|
|
552
|
-
await Promise.race([app.whenStable(), fallback]);
|
|
553
|
-
} catch {
|
|
554
|
-
/* ignored — fallback timer still resolves */
|
|
555
|
-
} finally {
|
|
556
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
/* `runWithViewTransition` wraps a callback in `document.startViewTransition`
|
|
561
|
-
for a smooth crossfade across full re-bootstraps. Queueing is NOT needed
|
|
562
|
-
here because `handleAngularUpdate` already serializes incoming messages
|
|
563
|
-
through the outer `activeMessage`/`pendingMessage` slots — only one
|
|
564
|
-
update runs at a time, so a new `startViewTransition` never aborts an
|
|
565
|
-
in-flight one mid-callback. */
|
|
566
|
-
const runWithViewTransition = async (updateFn: () => Promise<void>) => {
|
|
567
|
-
const doc: ViewTransitionDocument = document;
|
|
568
|
-
|
|
569
|
-
if (typeof doc.startViewTransition !== 'function') {
|
|
570
|
-
try {
|
|
571
|
-
await updateFn();
|
|
572
|
-
} catch (err) {
|
|
573
|
-
console.warn('[HMR] Angular update failed (non-fatal):', err);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
let styleEl: HTMLStyleElement | null = null;
|
|
580
|
-
try {
|
|
581
|
-
styleEl = document.createElement('style');
|
|
582
|
-
styleEl.textContent =
|
|
583
|
-
'::view-transition-old(root),::view-transition-new(root){animation:none!important}';
|
|
584
|
-
document.head.appendChild(styleEl);
|
|
585
|
-
} catch {
|
|
586
|
-
/* ignored */
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
let updatePromise: Promise<void> = Promise.resolve();
|
|
590
|
-
try {
|
|
591
|
-
const transition = doc.startViewTransition(() => {
|
|
592
|
-
updatePromise = updateFn();
|
|
593
|
-
|
|
594
|
-
return updatePromise;
|
|
595
|
-
});
|
|
596
|
-
// Wait for both the visual transition and the update callback.
|
|
597
|
-
// `transition.finished` rejects with AbortError when a new transition
|
|
598
|
-
// supersedes this one — swallow that since we serialize updates so
|
|
599
|
-
// it shouldn't happen, and even if it does we still want to wait
|
|
600
|
-
// for `updateFn` to complete before releasing the next update.
|
|
601
|
-
await Promise.all([
|
|
602
|
-
transition.finished.catch(() => {
|
|
603
|
-
/* skipped */
|
|
604
|
-
}),
|
|
605
|
-
updatePromise.catch((err) => {
|
|
606
|
-
console.warn('[HMR] Angular update failed (non-fatal):', err);
|
|
607
|
-
})
|
|
608
|
-
]);
|
|
609
|
-
} catch (err) {
|
|
610
|
-
console.warn('[HMR] Angular update failed (non-fatal):', err);
|
|
611
|
-
// If startViewTransition itself threw, run the update directly so
|
|
612
|
-
// HMR still applies (loses the crossfade but preserves correctness).
|
|
613
|
-
try {
|
|
614
|
-
await updateFn();
|
|
615
|
-
} catch (innerErr) {
|
|
616
|
-
console.warn('[HMR] Angular update failed (non-fatal):', innerErr);
|
|
617
|
-
}
|
|
618
|
-
} finally {
|
|
619
|
-
if (styleEl && styleEl.parentNode) styleEl.remove();
|
|
620
|
-
}
|
|
621
|
-
};
|
|
622
|
-
|
|
623
|
-
const handleFullUpdate = async (message: HMRMessage) => {
|
|
624
|
-
// DOM-level state — preserved separately from instance state because
|
|
625
|
-
// it lives in the document, not in component fields. Form values and
|
|
626
|
-
// scroll position survive a full re-bootstrap regardless of whether
|
|
627
|
-
// any component opted into `preserveAcrossHmr`.
|
|
628
|
-
const scrollState = saveScrollState();
|
|
629
|
-
const formState = saveFormState();
|
|
630
|
-
|
|
631
|
-
if (message.data.cssUrl) {
|
|
632
|
-
swapStylesheet(
|
|
633
|
-
message.data.cssUrl,
|
|
634
|
-
message.data.cssBaseName || '',
|
|
635
|
-
'angular'
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const rootContainer = document.getElementById('root') || document.body;
|
|
640
|
-
const rootSelector = findRootSelector(rootContainer);
|
|
641
|
-
|
|
642
|
-
const indexPath = findIndexPath(
|
|
643
|
-
message.data.manifest,
|
|
644
|
-
message.data.sourceFile,
|
|
645
|
-
'angular'
|
|
646
|
-
);
|
|
647
|
-
if (!indexPath) return;
|
|
648
|
-
|
|
649
|
-
const doUpdate = async () => {
|
|
650
|
-
// Snapshot every instance that opted into `preserveAcrossHmr(this)`
|
|
651
|
-
// before destroying the app, and flip the reboot-in-progress flag
|
|
652
|
-
// on. The new instances created during bootstrap will read cached
|
|
653
|
-
// state back via the same helper while the flag is on. Both this
|
|
654
|
-
// capture call and the user-facing `preserveAcrossHmr` helper
|
|
655
|
-
// share the same `globalThis`-anchored cache via `hmrPreserveCore`.
|
|
656
|
-
captureTrackedInstanceStates();
|
|
657
|
-
try {
|
|
658
|
-
destroyAngularApp();
|
|
659
|
-
await bootstrapAngularModule(
|
|
660
|
-
indexPath,
|
|
661
|
-
rootSelector,
|
|
662
|
-
rootContainer
|
|
663
|
-
);
|
|
664
|
-
tickAngularApp();
|
|
665
|
-
restoreFormState(formState);
|
|
666
|
-
restoreScrollState(scrollState);
|
|
667
|
-
} finally {
|
|
668
|
-
// Lazy-loaded child route components construct AFTER
|
|
669
|
-
// `bootstrapAngularModule` returns — the route activation
|
|
670
|
-
// chain (loadComponent → dynamic import → instantiate) runs
|
|
671
|
-
// asynchronously after the root app reports bootstrapped.
|
|
672
|
-
// Wait for the application to become stable so those lazy
|
|
673
|
-
// components have constructed and called `preserveAcrossHmr`
|
|
674
|
-
// before we close the restoration window. `whenStable`
|
|
675
|
-
// resolves when there are no pending tasks (lazy chunk
|
|
676
|
-
// loads, microtasks, scheduled CD) — strictly event-based,
|
|
677
|
-
// no fixed timer needed.
|
|
678
|
-
await waitForAppStable();
|
|
679
|
-
endHmrReboot();
|
|
680
|
-
}
|
|
681
|
-
};
|
|
682
|
-
|
|
683
|
-
await runWithViewTransition(doUpdate);
|
|
684
|
-
};
|