@absolutejs/absolute 0.19.0-beta.747 → 0.19.0-beta.749

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.
@@ -50,6 +50,7 @@ type AngularHmrApi = {
50
50
  applyUpdate: (id: string, newCtor: unknown) => boolean;
51
51
  getRegistry?: () => Map<string, unknown>;
52
52
  refresh: () => void;
53
+ hasPageExportsChanged?: (sourceId: string) => boolean;
53
54
  };
54
55
 
55
56
  type ViewTransitionDocument = Document & {
@@ -383,6 +384,19 @@ const attemptFastPatch = async (
383
384
  try {
384
385
  const newModule = await import(`${indexPath}?t=${Date.now()}`);
385
386
 
387
+ // Page-level `routes` / `providers` changed? Those values are read
388
+ // once during `bootstrapApplication`; an in-place component patch
389
+ // won't re-wire the running router or root injector. The chunk
390
+ // records its current fingerprint each time it evaluates (initial
391
+ // bootstrap + every fast-patch import), so a change between the
392
+ // previous and current evaluation means we need to fall back to a
393
+ // full re-bootstrap.
394
+ if (hmr.hasPageExportsChanged?.(sourceFile)) {
395
+ console.warn = origWarn;
396
+
397
+ return false;
398
+ }
399
+
386
400
  // NG0912 warnings fire during `applyUpdate` (Angular re-registers
387
401
  // the new component class while the old one is still live). Keep
388
402
  // the suppression active through the patch, restore right before
@@ -679,6 +693,13 @@ const handleFullUpdate = async (message: HMRMessage) => {
679
693
  if (!indexPath) return;
680
694
 
681
695
  const doUpdate = async () => {
696
+ // Snapshot every instance that opted into `preserveAcrossHmr(this)`
697
+ // before destroying the app. The new instances created during
698
+ // bootstrap will read these values back via the same helper, so
699
+ // service state (auth tokens, cached data, subscriptions held by
700
+ // reference) survives a full re-bootstrap. Generic by class name
701
+ // — no per-app configuration here.
702
+ captureHmrPreservedInstanceStates();
682
703
  destroyAngularApp();
683
704
  await bootstrapAngularModule(indexPath, rootSelector, rootContainer);
684
705
  restoreComponentState(componentState);
@@ -689,3 +710,72 @@ const handleFullUpdate = async (message: HMRMessage) => {
689
710
 
690
711
  await runWithViewTransition(doUpdate);
691
712
  };
713
+
714
+ /* Snapshot every WeakRef-tracked instance's *preservable* own
715
+ properties into the shared cache. The runtime helper
716
+ `preserveAcrossHmr(this)` from `@absolutejs/absolute/angular`
717
+ populates the tracker, and on the constructor of the next-bootstrapped
718
+ instance reads back from the cache. We talk to the same `globalThis`-
719
+ anchored Map/Set as that helper rather than importing it directly so
720
+ this dev-client bundle doesn't pull in the Angular runtime.
721
+
722
+ Only primitives, plain `{}` objects, and arrays of those are
723
+ preserved. Class instances (HttpClient, BehaviorSubject, etc.) are
724
+ *not* preserved because the new instance must get those from its own
725
+ injector — restoring an OLD-injector reference onto a NEW instance
726
+ would corrupt the new app. */
727
+ type HmrPreserveScope = typeof globalThis & {
728
+ __ABS_HMR_INSTANCE_STATE__?: Map<string, Record<string, unknown>>;
729
+ __ABS_HMR_TRACKED_INSTANCES__?: Set<WeakRef<object>>;
730
+ };
731
+
732
+ const isPreservableValue = (value: unknown, depth = 0): boolean => {
733
+ if (depth > 8) return false;
734
+ if (value === null || value === undefined) return true;
735
+ const t = typeof value;
736
+ if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint')
737
+ return true;
738
+ if (t === 'function' || t === 'symbol') return false;
739
+ if (Array.isArray(value)) {
740
+ return value.every((item) => isPreservableValue(item, depth + 1));
741
+ }
742
+ if (t === 'object') {
743
+ const proto = Object.getPrototypeOf(value);
744
+ if (proto !== Object.prototype && proto !== null) return false;
745
+
746
+ return Object.values(value as object).every((v) =>
747
+ isPreservableValue(v, depth + 1)
748
+ );
749
+ }
750
+
751
+ return false;
752
+ };
753
+
754
+ const captureHmrPreservedInstanceStates = () => {
755
+ const scope = globalThis as HmrPreserveScope;
756
+ const tracker = scope.__ABS_HMR_TRACKED_INSTANCES__;
757
+ if (!tracker || tracker.size === 0) return;
758
+
759
+ const cache = (scope.__ABS_HMR_INSTANCE_STATE__ ??= new Map());
760
+ const dead: WeakRef<object>[] = [];
761
+
762
+ for (const ref of tracker) {
763
+ const instance = ref.deref();
764
+ if (!instance) {
765
+ dead.push(ref);
766
+ continue;
767
+ }
768
+
769
+ const key = instance.constructor?.name;
770
+ if (!key || key === 'Object') continue;
771
+
772
+ const props: Record<string, unknown> = {};
773
+ for (const prop of Object.keys(instance)) {
774
+ const value = (instance as Record<string, unknown>)[prop];
775
+ if (isPreservableValue(value)) props[prop] = value;
776
+ }
777
+ cache.set(key, props);
778
+ }
779
+
780
+ dead.forEach((ref) => tracker.delete(ref));
781
+ };
@@ -238,11 +238,89 @@ const angularHmrStats: AngularHmrStats = {
238
238
 
239
239
  const getAngularHmrStats = () => angularHmrStats;
240
240
 
241
+ /* Page-level export fingerprints — detect when `routes` or `providers`
242
+ change in a page file so the HMR fast-patch can fall back to a full
243
+ re-bootstrap. Without this, a component-level fast-patch silently
244
+ succeeds while the route/provider change is left dangling — the
245
+ running router/injector still uses the values from the initial
246
+ bootstrap. The page chunk template calls `recordPageExports` on every
247
+ evaluation (initial bootstrap and HMR re-imports); the fast-patch
248
+ handler then checks `hasPageExportsChanged` to decide whether to
249
+ force a full re-bootstrap.
250
+ Function references are treated as opaque — they change on every
251
+ module reload but the static config (`path`, `pathMatch`, provider
252
+ token, useValue, etc.) is what we care about. */
253
+
254
+ type PageFingerprint = {
255
+ routes: string | null;
256
+ providers: string | null;
257
+ };
258
+
259
+ type PageExportRecord = {
260
+ current: PageFingerprint;
261
+ previous: PageFingerprint | null;
262
+ };
263
+
264
+ const pageExportRecords = ((
265
+ globalThis as { __ABS_HMR_PAGE_EXPORTS__?: Map<string, PageExportRecord> }
266
+ ).__ABS_HMR_PAGE_EXPORTS__ ??= new Map<string, PageExportRecord>());
267
+
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
+ const recordPageExports = (
292
+ sourceId: string,
293
+ routes: unknown,
294
+ providers: unknown
295
+ ) => {
296
+ const next: PageFingerprint = {
297
+ routes: routes === undefined ? null : fingerprint(routes),
298
+ providers: providers === undefined ? null : fingerprint(providers)
299
+ };
300
+ const existing = pageExportRecords.get(sourceId);
301
+ pageExportRecords.set(sourceId, {
302
+ current: next,
303
+ previous: existing?.current ?? null
304
+ });
305
+ };
306
+
307
+ const hasPageExportsChanged = (sourceId: string): boolean => {
308
+ const record = pageExportRecords.get(sourceId);
309
+ if (!record || !record.previous) return false;
310
+
311
+ return (
312
+ record.previous.routes !== record.current.routes ||
313
+ record.previous.providers !== record.current.providers
314
+ );
315
+ };
316
+
241
317
  export const installAngularHMRRuntime = () => {
242
318
  if (typeof window === 'undefined') return;
243
319
  window.__ANGULAR_HMR__ = {
244
320
  applyUpdate,
245
321
  getStats: getAngularHmrStats,
322
+ hasPageExportsChanged,
323
+ recordPageExports,
246
324
  refresh,
247
325
  register,
248
326
  getRegistry: () => componentRegistry
package/dist/index.js CHANGED
@@ -44594,6 +44594,17 @@ var absoluteHttpTransferCacheOptions = {
44594
44594
  // classes without needing a separate build artifact.
44595
44595
  export * from '${normalizedImportPath}';
44596
44596
 
44597
+ // Record this evaluation's \`routes\` and \`providers\` exports for the
44598
+ // HMR fast-patch to compare against on the next reload. If they change
44599
+ // (a new route was added, a provider was edited), fast-patch falls back
44600
+ // to a full re-bootstrap because those values are consumed once at
44601
+ // bootstrap and won't propagate to the running router/injector via an
44602
+ // in-place component patch.
44603
+ if (typeof window !== 'undefined' && window.__ANGULAR_HMR__ && typeof window.__ANGULAR_HMR__.recordPageExports === 'function') {
44604
+ var __abs_hmr_routes = Reflect.get(pageModule, 'routes');
44605
+ window.__ANGULAR_HMR__.recordPageExports('${resolvedEntry}', __abs_hmr_routes, maybePageProviders);
44606
+ }
44607
+
44597
44608
  // Re-Bootstrap HMR with View Transitions API.
44598
44609
  // Skipped during fast-patch: the HMR client sets
44599
44610
  // window.__ANGULAR_HMR_FAST_PATCH__ = true before \`import()\`-ing this
@@ -58549,5 +58560,5 @@ export {
58549
58560
  ANGULAR_INIT_TIMEOUT_MS
58550
58561
  };
58551
58562
 
58552
- //# debugId=3B8AFA4EC6C4DEFD64756E2164756E21
58563
+ //# debugId=101D5DB96668BF8564756E2164756E21
58553
58564
  //# sourceMappingURL=index.js.map