@absolutejs/absolute 0.19.0-beta.750 → 0.19.0-beta.752
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/browser.js +31 -15
- package/dist/angular/browser.js.map +3 -3
- package/dist/angular/index.js +24 -8
- package/dist/angular/index.js.map +3 -3
- package/dist/dev/client/handlers/angular.ts +70 -12
- package/dist/dev/client/handlers/angularRuntime.ts +110 -25
- package/dist/src/angular/preserveAcrossHmr.d.ts +9 -5
- package/dist/types/globals.d.ts +5 -1
- package/package.json +1 -1
|
@@ -606,6 +606,34 @@ const tickAngularApp = () => {
|
|
|
606
606
|
}
|
|
607
607
|
};
|
|
608
608
|
|
|
609
|
+
/* Resolve when Angular reports the application is stable: no pending
|
|
610
|
+
microtasks, scheduled CD, or in-flight lazy chunk loads. Used to gate
|
|
611
|
+
the close of the HMR restoration window so lazy-route components get
|
|
612
|
+
a chance to construct (and call `preserveAcrossHmr`) before
|
|
613
|
+
`rebootInProgress` flips back to false. Falls back after a generous
|
|
614
|
+
ceiling in the unlikely case `whenStable` never resolves (e.g. an
|
|
615
|
+
infinite retry on a service the new app never finishes initializing) —
|
|
616
|
+
we'd rather close the window than leave HMR wedged forever. */
|
|
617
|
+
const APP_STABLE_FALLBACK_MS = 10_000;
|
|
618
|
+
|
|
619
|
+
const waitForAppStable = async () => {
|
|
620
|
+
const app = window.__ANGULAR_APP__;
|
|
621
|
+
if (!app || typeof app.whenStable !== 'function') return;
|
|
622
|
+
|
|
623
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
624
|
+
const fallback = new Promise<void>((resolve) => {
|
|
625
|
+
timer = setTimeout(resolve, APP_STABLE_FALLBACK_MS);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
await Promise.race([app.whenStable(), fallback]);
|
|
630
|
+
} catch {
|
|
631
|
+
/* ignored — fallback timer still resolves */
|
|
632
|
+
} finally {
|
|
633
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
609
637
|
/* `runWithViewTransition` wraps a callback in `document.startViewTransition`
|
|
610
638
|
for a smooth crossfade across full re-bootstraps. Queueing is NOT needed
|
|
611
639
|
here because `handleAngularUpdate` already serializes incoming messages
|
|
@@ -714,12 +742,14 @@ const handleFullUpdate = async (message: HMRMessage) => {
|
|
|
714
742
|
// `bootstrapAngularModule` returns — the route activation
|
|
715
743
|
// chain (loadComponent → dynamic import → instantiate) runs
|
|
716
744
|
// asynchronously after the root app reports bootstrapped.
|
|
717
|
-
//
|
|
718
|
-
// components
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
|
|
745
|
+
// Wait for the application to become stable so those lazy
|
|
746
|
+
// components have constructed and called `preserveAcrossHmr`
|
|
747
|
+
// before we close the restoration window. `whenStable`
|
|
748
|
+
// resolves when there are no pending tasks (lazy chunk
|
|
749
|
+
// loads, microtasks, scheduled CD) — strictly event-based,
|
|
750
|
+
// no fixed timer needed.
|
|
751
|
+
await waitForAppStable();
|
|
752
|
+
endHmrPreservedReboot();
|
|
723
753
|
}
|
|
724
754
|
};
|
|
725
755
|
|
|
@@ -744,6 +774,7 @@ type HmrPreserveScope = typeof globalThis & {
|
|
|
744
774
|
__ABS_HMR_TRACKED_INSTANCES__?: Set<WeakRef<object>>;
|
|
745
775
|
__ABS_HMR_INSTANCE_KEYS__?: WeakMap<object, string>;
|
|
746
776
|
__ABS_HMR_REBOOT_IN_PROGRESS__?: { value: boolean };
|
|
777
|
+
__ABS_HMR_REBOOT_STATS__?: { captured: number; restoredKeys: Set<string> };
|
|
747
778
|
};
|
|
748
779
|
|
|
749
780
|
const isPreservableValue = (value: unknown, depth = 0): boolean => {
|
|
@@ -768,11 +799,20 @@ const isPreservableValue = (value: unknown, depth = 0): boolean => {
|
|
|
768
799
|
return false;
|
|
769
800
|
};
|
|
770
801
|
|
|
802
|
+
const getHmrRebootStats = (scope: HmrPreserveScope) =>
|
|
803
|
+
(scope.__ABS_HMR_REBOOT_STATS__ ??= {
|
|
804
|
+
captured: 0,
|
|
805
|
+
restoredKeys: new Set<string>()
|
|
806
|
+
});
|
|
807
|
+
|
|
771
808
|
const captureHmrPreservedInstanceStates = () => {
|
|
772
809
|
const scope = globalThis as HmrPreserveScope;
|
|
773
810
|
const tracker = scope.__ABS_HMR_TRACKED_INSTANCES__;
|
|
774
811
|
|
|
775
812
|
const flag = (scope.__ABS_HMR_REBOOT_IN_PROGRESS__ ??= { value: false });
|
|
813
|
+
const stats = getHmrRebootStats(scope);
|
|
814
|
+
stats.captured = 0;
|
|
815
|
+
stats.restoredKeys.clear();
|
|
776
816
|
|
|
777
817
|
if (!tracker || tracker.size === 0) {
|
|
778
818
|
// Nothing tracked, but still flip the flag so any service/component
|
|
@@ -786,17 +826,15 @@ const captureHmrPreservedInstanceStates = () => {
|
|
|
786
826
|
|
|
787
827
|
const cache = (scope.__ABS_HMR_INSTANCE_STATE__ ??= new Map());
|
|
788
828
|
const keyMap = scope.__ABS_HMR_INSTANCE_KEYS__;
|
|
789
|
-
const dead: WeakRef<object>[] = [];
|
|
790
829
|
const seen = new Set<string>();
|
|
791
830
|
|
|
792
831
|
cache.clear();
|
|
793
832
|
|
|
794
833
|
for (const ref of tracker) {
|
|
795
834
|
const instance = ref.deref();
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
835
|
+
// Skip already-GC'd refs. We don't bother bookkeeping a "dead"
|
|
836
|
+
// list because the entire tracker is cleared after this loop.
|
|
837
|
+
if (!instance) continue;
|
|
800
838
|
|
|
801
839
|
const className = instance.constructor?.name;
|
|
802
840
|
if (!className || className === 'Object') continue;
|
|
@@ -815,9 +853,17 @@ const captureHmrPreservedInstanceStates = () => {
|
|
|
815
853
|
if (isPreservableValue(value)) props[prop] = value;
|
|
816
854
|
}
|
|
817
855
|
cache.set(fullKey, props);
|
|
856
|
+
stats.captured++;
|
|
818
857
|
}
|
|
819
858
|
|
|
820
|
-
|
|
859
|
+
// Every instance just captured is about to die: `destroyAngularApp()`
|
|
860
|
+
// runs immediately after this. New instances from the next bootstrap
|
|
861
|
+
// repopulate the tracker via their own `preserveAcrossHmr(this)`
|
|
862
|
+
// calls. If we left existing WeakRefs in place, the JS engine often
|
|
863
|
+
// won't have GC'd the old objects yet at the next capture — those
|
|
864
|
+
// zombies would inflate the captured count and trigger spurious
|
|
865
|
+
// collision warnings against the new generation's instances.
|
|
866
|
+
tracker.clear();
|
|
821
867
|
|
|
822
868
|
flag.value = true;
|
|
823
869
|
};
|
|
@@ -826,4 +872,16 @@ const endHmrPreservedReboot = () => {
|
|
|
826
872
|
const scope = globalThis as HmrPreserveScope;
|
|
827
873
|
const flag = scope.__ABS_HMR_REBOOT_IN_PROGRESS__;
|
|
828
874
|
if (flag) flag.value = false;
|
|
875
|
+
|
|
876
|
+
const stats = scope.__ABS_HMR_REBOOT_STATS__;
|
|
877
|
+
if (stats && stats.captured > 0) {
|
|
878
|
+
const restored = Array.from(stats.restoredKeys)
|
|
879
|
+
.map((k) => k.replace(/:$/, ''))
|
|
880
|
+
.sort();
|
|
881
|
+
console.info(
|
|
882
|
+
`[HMR] Full re-bootstrap: restored state for ${restored.length}/${stats.captured} tracked instance(s)${
|
|
883
|
+
restored.length > 0 ? ` — ${restored.join(', ')}` : ''
|
|
884
|
+
}. Components without preservation reset to defaults; opt in via \`preserveAcrossHmr(this)\`.`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
829
887
|
};
|
|
@@ -25,6 +25,7 @@ import type {} from '../../../types/globals';
|
|
|
25
25
|
type AngularComponentDefinition = {
|
|
26
26
|
providers?: unknown;
|
|
27
27
|
providersResolver?: unknown;
|
|
28
|
+
selectors?: unknown[];
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
type ComponentCtor = (abstract new (...args: never[]) => unknown) & {
|
|
@@ -72,6 +73,35 @@ const updateCounter: { value: number } =
|
|
|
72
73
|
globalScope.__ANGULAR_HMR_UPDATE_COUNT__ ??
|
|
73
74
|
(globalScope.__ANGULAR_HMR_UPDATE_COUNT__ = { value: 0 });
|
|
74
75
|
|
|
76
|
+
/* Cheap structural fingerprint. Functions render as 'fn' (treated as
|
|
77
|
+
opaque — they change on every module reload but the static config
|
|
78
|
+
like provider tokens, useValue payloads, etc. is what we care
|
|
79
|
+
about). Objects walk depth-bounded with sorted keys so key order
|
|
80
|
+
doesn't cause spurious diffs. Used both for component-level
|
|
81
|
+
provider arrays and for page-level `routes`/`providers` exports. */
|
|
82
|
+
const fingerprint = (value: unknown, depth = 0): string => {
|
|
83
|
+
if (depth > 6) return '~deep~';
|
|
84
|
+
if (value === null) return 'null';
|
|
85
|
+
if (value === undefined) return 'undef';
|
|
86
|
+
if (typeof value === 'function') return 'fn';
|
|
87
|
+
if (typeof value === 'symbol') return value.toString();
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
return (
|
|
90
|
+
'[' + value.map((v) => fingerprint(v, depth + 1)).join(',') + ']'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === 'object') {
|
|
94
|
+
const obj = value as Record<string, unknown>;
|
|
95
|
+
const entries = Object.entries(obj)
|
|
96
|
+
.map(([k, v]): [string, string] => [k, fingerprint(v, depth + 1)])
|
|
97
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
98
|
+
|
|
99
|
+
return '{' + entries.map(([k, v]) => `${k}:${v}`).join(',') + '}';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return JSON.stringify(value);
|
|
103
|
+
};
|
|
104
|
+
|
|
75
105
|
const hasInjectorProviderChanges = (
|
|
76
106
|
oldCtor: ComponentCtor,
|
|
77
107
|
newCtor: ComponentCtor
|
|
@@ -81,7 +111,7 @@ const hasInjectorProviderChanges = (
|
|
|
81
111
|
const newP = newCtor.ɵinj?.providers;
|
|
82
112
|
if (!Array.isArray(oldP) || !Array.isArray(newP)) return false;
|
|
83
113
|
|
|
84
|
-
return oldP
|
|
114
|
+
return fingerprint(oldP) !== fingerprint(newP);
|
|
85
115
|
};
|
|
86
116
|
|
|
87
117
|
const hasComponentProviderChanges = (
|
|
@@ -91,8 +121,21 @@ const hasComponentProviderChanges = (
|
|
|
91
121
|
if (!oldCtor.ɵcmp || !newCtor.ɵcmp) return false;
|
|
92
122
|
const oldResolver = oldCtor.ɵcmp.providersResolver;
|
|
93
123
|
const newResolver = newCtor.ɵcmp.providersResolver;
|
|
124
|
+
// Defined-ness flip — added/removed `providers: [...]` entirely.
|
|
125
|
+
if ((oldResolver === undefined) !== (newResolver === undefined))
|
|
126
|
+
return true;
|
|
127
|
+
if (typeof oldResolver !== 'function' || typeof newResolver !== 'function')
|
|
128
|
+
return false;
|
|
94
129
|
|
|
95
|
-
|
|
130
|
+
// `providersResolver` is the function the Angular compiler emits to
|
|
131
|
+
// merge a component's `providers` array into the element injector.
|
|
132
|
+
// Its source body inlines the provider tokens and useValue/useFactory
|
|
133
|
+
// references, so a change to the user's `providers: [...]` array
|
|
134
|
+
// produces a different function body. Comparing `toString()` catches
|
|
135
|
+
// content changes that the old length/defined-ness check missed —
|
|
136
|
+
// e.g. swapping `useValue: 'foo'` for `useValue: 'bar'` while
|
|
137
|
+
// keeping the array length identical.
|
|
138
|
+
return oldResolver.toString() !== newResolver.toString();
|
|
96
139
|
};
|
|
97
140
|
|
|
98
141
|
const hasProviderChanges = (oldCtor: ComponentCtor, newCtor: ComponentCtor) => {
|
|
@@ -175,6 +218,55 @@ const patchConstructor = (entry: RegistryEntry, newCtor: ComponentCtor) => {
|
|
|
175
218
|
entry.registeredAt = Date.now();
|
|
176
219
|
};
|
|
177
220
|
|
|
221
|
+
/* The fast-patch swap of `ɵcmp` and prototype methods doesn't mark
|
|
222
|
+
live OnPush components as dirty — `applicationRef.tick()` alone
|
|
223
|
+
only checks views that are already marked dirty. So a template
|
|
224
|
+
edit on an OnPush component would silently fail to render until
|
|
225
|
+
the user clicked something that triggered a markForCheck.
|
|
226
|
+
We collect every successfully-patched ctor here, then `refresh()`
|
|
227
|
+
walks the DOM for each ctor's selector, gets the live instance via
|
|
228
|
+
the `ng` debug API, and calls `applyChanges` on it (which marks
|
|
229
|
+
the view dirty AND runs CD on its subtree). */
|
|
230
|
+
const pendingFastPatchRefresh: Set<ComponentCtor> = new Set();
|
|
231
|
+
|
|
232
|
+
type AngularDebugWindow = Window & {
|
|
233
|
+
ng?: {
|
|
234
|
+
applyChanges?: (component: unknown) => void;
|
|
235
|
+
getComponent?: (element: Element) => unknown;
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const componentTagSelectors = (ctor: ComponentCtor): string[] => {
|
|
240
|
+
const selectors = ctor.ɵcmp?.selectors;
|
|
241
|
+
if (!Array.isArray(selectors)) return [];
|
|
242
|
+
const tags: string[] = [];
|
|
243
|
+
for (const tuple of selectors) {
|
|
244
|
+
if (!Array.isArray(tuple)) continue;
|
|
245
|
+
const head = tuple[0];
|
|
246
|
+
// Component selectors lead with the tag name (a hyphenated
|
|
247
|
+
// element name); attribute selectors lead with `''`. Skip the
|
|
248
|
+
// attribute case — those are directives, not OnPush views.
|
|
249
|
+
if (typeof head === 'string' && head.includes('-')) tags.push(head);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return tags;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const markPatchedDirty = (ctor: ComponentCtor) => {
|
|
256
|
+
const ng = (window as AngularDebugWindow).ng;
|
|
257
|
+
if (!ng?.getComponent || !ng?.applyChanges) return;
|
|
258
|
+
for (const tag of componentTagSelectors(ctor)) {
|
|
259
|
+
document.querySelectorAll(tag).forEach((el) => {
|
|
260
|
+
try {
|
|
261
|
+
const instance = ng.getComponent?.(el);
|
|
262
|
+
if (instance) ng.applyChanges?.(instance);
|
|
263
|
+
} catch {
|
|
264
|
+
/* dev-only debug API — ignore failures */
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
178
270
|
const applyUpdate = (id: string, newCtor: unknown) => {
|
|
179
271
|
if (!isComponentCtor(newCtor)) return false;
|
|
180
272
|
|
|
@@ -209,6 +301,13 @@ const applyUpdate = (id: string, newCtor: unknown) => {
|
|
|
209
301
|
|
|
210
302
|
try {
|
|
211
303
|
patchConstructor(entry, newCtor);
|
|
304
|
+
// Queue this ctor for `refresh()` to mark its live instances
|
|
305
|
+
// dirty — the patch swapped metadata in place, but OnPush
|
|
306
|
+
// components need an explicit markForCheck to re-render.
|
|
307
|
+
// `liveCtor` is the on-page constructor (we patched into it);
|
|
308
|
+
// we use that for selector lookup since the swap may have
|
|
309
|
+
// updated `ɵcmp` on `liveCtor` itself.
|
|
310
|
+
pendingFastPatchRefresh.add(liveCtor);
|
|
212
311
|
|
|
213
312
|
return true;
|
|
214
313
|
} catch (err) {
|
|
@@ -220,6 +319,15 @@ const applyUpdate = (id: string, newCtor: unknown) => {
|
|
|
220
319
|
|
|
221
320
|
const refresh = () => {
|
|
222
321
|
if (!window.__ANGULAR_APP__) return;
|
|
322
|
+
// Mark every live instance of every patched component dirty before
|
|
323
|
+
// ticking. `tick()` alone wouldn't re-render OnPush components,
|
|
324
|
+
// since they only re-check on `markForCheck`. `applyChanges` marks
|
|
325
|
+
// the view dirty and runs CD on its subtree — covers both OnPush
|
|
326
|
+
// and Default change-detection components correctly.
|
|
327
|
+
for (const ctor of pendingFastPatchRefresh) {
|
|
328
|
+
markPatchedDirty(ctor);
|
|
329
|
+
}
|
|
330
|
+
pendingFastPatchRefresh.clear();
|
|
223
331
|
try {
|
|
224
332
|
window.__ANGULAR_APP__.tick();
|
|
225
333
|
} catch (err) {
|
|
@@ -265,29 +373,6 @@ const pageExportRecords = ((
|
|
|
265
373
|
globalThis as { __ABS_HMR_PAGE_EXPORTS__?: Map<string, PageExportRecord> }
|
|
266
374
|
).__ABS_HMR_PAGE_EXPORTS__ ??= new Map<string, PageExportRecord>());
|
|
267
375
|
|
|
268
|
-
const fingerprint = (value: unknown, depth = 0): string => {
|
|
269
|
-
if (depth > 6) return '~deep~';
|
|
270
|
-
if (value === null) return 'null';
|
|
271
|
-
if (value === undefined) return 'undef';
|
|
272
|
-
if (typeof value === 'function') return 'fn';
|
|
273
|
-
if (typeof value === 'symbol') return value.toString();
|
|
274
|
-
if (Array.isArray(value)) {
|
|
275
|
-
return (
|
|
276
|
-
'[' + value.map((v) => fingerprint(v, depth + 1)).join(',') + ']'
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
if (typeof value === 'object') {
|
|
280
|
-
const obj = value as Record<string, unknown>;
|
|
281
|
-
const entries = Object.entries(obj)
|
|
282
|
-
.map(([k, v]): [string, string] => [k, fingerprint(v, depth + 1)])
|
|
283
|
-
.sort(([a], [b]) => a.localeCompare(b));
|
|
284
|
-
|
|
285
|
-
return '{' + entries.map(([k, v]) => `${k}:${v}`).join(',') + '}';
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return JSON.stringify(value);
|
|
289
|
-
};
|
|
290
|
-
|
|
291
376
|
const recordPageExports = (
|
|
292
377
|
sourceId: string,
|
|
293
378
|
routes: unknown,
|
|
@@ -17,9 +17,13 @@ export declare const preserveAcrossHmr: (instance: object, key?: string | number
|
|
|
17
17
|
* are skipped — the new instance gets fresh ones from its new
|
|
18
18
|
* injector, just like at first bootstrap. */
|
|
19
19
|
export declare const captureTrackedInstanceStates: () => void;
|
|
20
|
-
/** Clear the active-reboot flag
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
/** Clear the active-reboot flag and emit a one-line summary so
|
|
21
|
+
* developers can see at-a-glance which classes had state preserved.
|
|
22
|
+
* Helps surface the existence of `preserveAcrossHmr` to anyone whose
|
|
23
|
+
* state was reset because they hadn't opted in.
|
|
24
|
+
* Called by the HMR client after the new app has finished
|
|
25
|
+
* bootstrapping (success or failure — wrap in `try/finally`). After
|
|
26
|
+
* this, `preserveAcrossHmr` calls will track but won't restore — so
|
|
27
|
+
* navigating to a route after HMR doesn't resurrect stale state from
|
|
28
|
+
* the last reboot. */
|
|
25
29
|
export declare const endHmrReboot: () => void;
|
package/dist/types/globals.d.ts
CHANGED
|
@@ -56,7 +56,11 @@ declare global {
|
|
|
56
56
|
__SVELTE_COMPONENT__?: Record<string, unknown>;
|
|
57
57
|
__ABS_SVELTE_ISLAND_HTML__?: Record<string, string>;
|
|
58
58
|
__SVELTE_UNMOUNT__?: () => void;
|
|
59
|
-
__ANGULAR_APP__?: {
|
|
59
|
+
__ANGULAR_APP__?: {
|
|
60
|
+
destroy: () => void;
|
|
61
|
+
tick: () => void;
|
|
62
|
+
whenStable: () => Promise<void>;
|
|
63
|
+
} | null;
|
|
60
64
|
__HMR_SKIP_HYDRATION__?: boolean;
|
|
61
65
|
__HMR_NEW_PAGE_CLASS__?: unknown;
|
|
62
66
|
__NG_REPLACE_METADATA__?: (...args: unknown[]) => void;
|
package/package.json
CHANGED