@absolutejs/absolute 0.19.0-beta.750 → 0.19.0-beta.751
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 +59 -6
- package/dist/dev/client/handlers/angularRuntime.ts +44 -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
|
|
@@ -815,6 +855,7 @@ const captureHmrPreservedInstanceStates = () => {
|
|
|
815
855
|
if (isPreservableValue(value)) props[prop] = value;
|
|
816
856
|
}
|
|
817
857
|
cache.set(fullKey, props);
|
|
858
|
+
stats.captured++;
|
|
818
859
|
}
|
|
819
860
|
|
|
820
861
|
dead.forEach((ref) => tracker.delete(ref));
|
|
@@ -826,4 +867,16 @@ const endHmrPreservedReboot = () => {
|
|
|
826
867
|
const scope = globalThis as HmrPreserveScope;
|
|
827
868
|
const flag = scope.__ABS_HMR_REBOOT_IN_PROGRESS__;
|
|
828
869
|
if (flag) flag.value = false;
|
|
870
|
+
|
|
871
|
+
const stats = scope.__ABS_HMR_REBOOT_STATS__;
|
|
872
|
+
if (stats && stats.captured > 0) {
|
|
873
|
+
const restored = Array.from(stats.restoredKeys)
|
|
874
|
+
.map((k) => k.replace(/:$/, ''))
|
|
875
|
+
.sort();
|
|
876
|
+
console.info(
|
|
877
|
+
`[HMR] Full re-bootstrap: restored state for ${restored.length}/${stats.captured} tracked instance(s)${
|
|
878
|
+
restored.length > 0 ? ` — ${restored.join(', ')}` : ''
|
|
879
|
+
}. Components without preservation reset to defaults; opt in via \`preserveAcrossHmr(this)\`.`
|
|
880
|
+
);
|
|
881
|
+
}
|
|
829
882
|
};
|
|
@@ -72,6 +72,35 @@ const updateCounter: { value: number } =
|
|
|
72
72
|
globalScope.__ANGULAR_HMR_UPDATE_COUNT__ ??
|
|
73
73
|
(globalScope.__ANGULAR_HMR_UPDATE_COUNT__ = { value: 0 });
|
|
74
74
|
|
|
75
|
+
/* Cheap structural fingerprint. Functions render as 'fn' (treated as
|
|
76
|
+
opaque — they change on every module reload but the static config
|
|
77
|
+
like provider tokens, useValue payloads, etc. is what we care
|
|
78
|
+
about). Objects walk depth-bounded with sorted keys so key order
|
|
79
|
+
doesn't cause spurious diffs. Used both for component-level
|
|
80
|
+
provider arrays and for page-level `routes`/`providers` exports. */
|
|
81
|
+
const fingerprint = (value: unknown, depth = 0): string => {
|
|
82
|
+
if (depth > 6) return '~deep~';
|
|
83
|
+
if (value === null) return 'null';
|
|
84
|
+
if (value === undefined) return 'undef';
|
|
85
|
+
if (typeof value === 'function') return 'fn';
|
|
86
|
+
if (typeof value === 'symbol') return value.toString();
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
return (
|
|
89
|
+
'[' + value.map((v) => fingerprint(v, depth + 1)).join(',') + ']'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (typeof value === 'object') {
|
|
93
|
+
const obj = value as Record<string, unknown>;
|
|
94
|
+
const entries = Object.entries(obj)
|
|
95
|
+
.map(([k, v]): [string, string] => [k, fingerprint(v, depth + 1)])
|
|
96
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
97
|
+
|
|
98
|
+
return '{' + entries.map(([k, v]) => `${k}:${v}`).join(',') + '}';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return JSON.stringify(value);
|
|
102
|
+
};
|
|
103
|
+
|
|
75
104
|
const hasInjectorProviderChanges = (
|
|
76
105
|
oldCtor: ComponentCtor,
|
|
77
106
|
newCtor: ComponentCtor
|
|
@@ -81,7 +110,7 @@ const hasInjectorProviderChanges = (
|
|
|
81
110
|
const newP = newCtor.ɵinj?.providers;
|
|
82
111
|
if (!Array.isArray(oldP) || !Array.isArray(newP)) return false;
|
|
83
112
|
|
|
84
|
-
return oldP
|
|
113
|
+
return fingerprint(oldP) !== fingerprint(newP);
|
|
85
114
|
};
|
|
86
115
|
|
|
87
116
|
const hasComponentProviderChanges = (
|
|
@@ -91,8 +120,21 @@ const hasComponentProviderChanges = (
|
|
|
91
120
|
if (!oldCtor.ɵcmp || !newCtor.ɵcmp) return false;
|
|
92
121
|
const oldResolver = oldCtor.ɵcmp.providersResolver;
|
|
93
122
|
const newResolver = newCtor.ɵcmp.providersResolver;
|
|
123
|
+
// Defined-ness flip — added/removed `providers: [...]` entirely.
|
|
124
|
+
if ((oldResolver === undefined) !== (newResolver === undefined))
|
|
125
|
+
return true;
|
|
126
|
+
if (typeof oldResolver !== 'function' || typeof newResolver !== 'function')
|
|
127
|
+
return false;
|
|
94
128
|
|
|
95
|
-
|
|
129
|
+
// `providersResolver` is the function the Angular compiler emits to
|
|
130
|
+
// merge a component's `providers` array into the element injector.
|
|
131
|
+
// Its source body inlines the provider tokens and useValue/useFactory
|
|
132
|
+
// references, so a change to the user's `providers: [...]` array
|
|
133
|
+
// produces a different function body. Comparing `toString()` catches
|
|
134
|
+
// content changes that the old length/defined-ness check missed —
|
|
135
|
+
// e.g. swapping `useValue: 'foo'` for `useValue: 'bar'` while
|
|
136
|
+
// keeping the array length identical.
|
|
137
|
+
return oldResolver.toString() !== newResolver.toString();
|
|
96
138
|
};
|
|
97
139
|
|
|
98
140
|
const hasProviderChanges = (oldCtor: ComponentCtor, newCtor: ComponentCtor) => {
|
|
@@ -265,29 +307,6 @@ const pageExportRecords = ((
|
|
|
265
307
|
globalThis as { __ABS_HMR_PAGE_EXPORTS__?: Map<string, PageExportRecord> }
|
|
266
308
|
).__ABS_HMR_PAGE_EXPORTS__ ??= new Map<string, PageExportRecord>());
|
|
267
309
|
|
|
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
310
|
const recordPageExports = (
|
|
292
311
|
sourceId: string,
|
|
293
312
|
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