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

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.
@@ -361,6 +361,10 @@ const patchRegisteredComponents = (
361
361
  return { allPatched, patchedAny };
362
362
  };
363
363
 
364
+ type FastPatchWindow = Window & {
365
+ __ANGULAR_HMR_FAST_PATCH__?: boolean;
366
+ };
367
+
364
368
  const attemptFastPatch = async (
365
369
  indexPath: string,
366
370
  registry: Map<string, unknown>,
@@ -368,11 +372,21 @@ const attemptFastPatch = async (
368
372
  sourceFile: string,
369
373
  origWarn: typeof console.warn
370
374
  ) => {
375
+ // The bundled page chunk's top-level code re-bootstraps the Angular app
376
+ // (destroy + bootstrapApplication). For fast-patch we just need to read
377
+ // the freshly-built component classes — not re-bootstrap. Setting this
378
+ // flag tells the chunk to skip its bootstrap section and only run the
379
+ // `export * from '<page-module>'` line. Paired with the guard added in
380
+ // `src/build/compileAngular.ts` HMR template.
381
+ const w = window as FastPatchWindow;
382
+ w.__ANGULAR_HMR_FAST_PATCH__ = true;
371
383
  try {
372
384
  const newModule = await import(`${indexPath}?t=${Date.now()}`);
373
385
 
374
- console.warn = origWarn;
375
-
386
+ // NG0912 warnings fire during `applyUpdate` (Angular re-registers
387
+ // the new component class while the old one is still live). Keep
388
+ // the suppression active through the patch, restore right before
389
+ // `refresh()` so any non-NG0912 warnings during `tick()` surface.
376
390
  const { allPatched, patchedAny } = patchRegisteredComponents(
377
391
  newModule,
378
392
  registry,
@@ -380,6 +394,8 @@ const attemptFastPatch = async (
380
394
  sourceFile
381
395
  );
382
396
 
397
+ console.warn = origWarn;
398
+
383
399
  if (!patchedAny) return false;
384
400
  if (!allPatched) return false;
385
401
 
@@ -391,11 +407,21 @@ const attemptFastPatch = async (
391
407
  console.warn('[HMR] Angular fast update failed, falling back:', err);
392
408
 
393
409
  return false;
410
+ } finally {
411
+ delete w.__ANGULAR_HMR_FAST_PATCH__;
394
412
  }
395
413
  };
396
414
 
397
- // handleFastUpdate is kept for future use when the fast path is re-enabled.
398
- const _handleFastUpdate = async (message: HMRMessage) => {
415
+ /* Fast update patch live component prototypes without destroying the app.
416
+ Returns true when at least one registered component was successfully
417
+ patched (and no patch failed); false means we couldn't fast-patch and
418
+ the caller should fall back to a full re-bootstrap.
419
+ Failures we explicitly fall back on:
420
+ - file's source isn't tracked in the component registry yet
421
+ - changed file has no Angular components (e.g. a service or routes file)
422
+ - any component's `applyUpdate` returned false (provider change, etc.)
423
+ - dynamic import failed */
424
+ const handleFastUpdate = async (message: HMRMessage) => {
399
425
  const hmr = window.__ANGULAR_HMR__;
400
426
  if (!hmr || !hmr.getRegistry) return false;
401
427
 
@@ -434,6 +460,45 @@ const _handleFastUpdate = async (message: HMRMessage) => {
434
460
  // MAIN ENTRY POINT
435
461
  // ============================================================
436
462
 
463
+ /* HMR updates are serialized through a single in-flight slot. While one
464
+ update is running (fast or full), additional incoming updates collapse
465
+ into one pending slot — only the latest matters because each rebuild
466
+ produces a chunk that supersedes prior ones for the same source file.
467
+ Without this, two rapid edits could:
468
+ - run two `startViewTransition`s and have the browser abort the first
469
+ mid-callback (the original "Transition was skipped" symptom), or
470
+ - run two `attemptFastPatch`s that both call `applyUpdate` on the same
471
+ registry entries, racing on prototype swaps. */
472
+ let activeMessage: Promise<void> | null = null;
473
+ let pendingMessage: HMRMessage | null = null;
474
+
475
+ const processMessage = async (message: HMRMessage) => {
476
+ const updateType = message.data.updateType || 'logic';
477
+
478
+ if (updateType === 'full') {
479
+ // Server signalled this requires a full reload — skip fast path.
480
+ await handleFullUpdate(message);
481
+
482
+ return;
483
+ }
484
+
485
+ // Default 'logic' path: try fast-patch, fall back to full reload.
486
+ try {
487
+ const patched = await handleFastUpdate(message);
488
+ if (patched) return;
489
+ } catch (err) {
490
+ console.warn(
491
+ '[HMR] Angular fast update threw, falling back to full reload:',
492
+ err
493
+ );
494
+ }
495
+
496
+ // Fast path didn't apply — full re-bootstrap (loses in-memory app state
497
+ // like auth tokens; only happens when the fast path can't handle the
498
+ // change, e.g. routes/providers/services or a never-seen component).
499
+ await handleFullUpdate(message);
500
+ };
501
+
437
502
  export const handleAngularUpdate = (message: HMRMessage) => {
438
503
  if (detectCurrentFramework() !== 'angular') return;
439
504
 
@@ -443,6 +508,7 @@ export const handleAngularUpdate = (message: HMRMessage) => {
443
508
  (updateType === 'style' || updateType === 'css-only') &&
444
509
  message.data.cssUrl
445
510
  ) {
511
+ // CSS-only updates can run in parallel without breaking anything.
446
512
  swapStylesheet(
447
513
  message.data.cssUrl,
448
514
  message.data.cssBaseName || '',
@@ -452,7 +518,22 @@ export const handleAngularUpdate = (message: HMRMessage) => {
452
518
  return;
453
519
  }
454
520
 
455
- handleFullUpdate(message);
521
+ if (activeMessage) {
522
+ // Coalesce: an update is in flight, queue this one (replacing any
523
+ // earlier queued update, which is now stale).
524
+ pendingMessage = message;
525
+
526
+ return;
527
+ }
528
+
529
+ activeMessage = processMessage(message).finally(() => {
530
+ activeMessage = null;
531
+ if (pendingMessage) {
532
+ const next = pendingMessage;
533
+ pendingMessage = null;
534
+ handleAngularUpdate(next);
535
+ }
536
+ });
456
537
  };
457
538
 
458
539
  // ============================================================
@@ -511,19 +592,13 @@ const tickAngularApp = () => {
511
592
  }
512
593
  };
513
594
 
514
- /* HMR updates must be serialized.
515
- If a second `startViewTransition` runs while a previous one is still in
516
- flight, the browser aborts the old transition — but the old update
517
- callback (destroyApp + async bootstrap) keeps running. Two updateFns
518
- then race over the DOM and Angular state, corrupting HMR and forcing a
519
- page refresh.
520
- We avoid that by queuing later updates until the current one finishes.
521
- If multiple updates queue up, only the latest matters (intermediate ones
522
- are superseded), so we collapse to a single pending entry. */
523
- let activeUpdate: Promise<void> | null = null;
524
- let pendingUpdate: (() => Promise<void>) | null = null;
525
-
526
- const runOneUpdate = async (updateFn: () => Promise<void>) => {
595
+ /* `runWithViewTransition` wraps a callback in `document.startViewTransition`
596
+ for a smooth crossfade across full re-bootstraps. Queueing is NOT needed
597
+ here because `handleAngularUpdate` already serializes incoming messages
598
+ through the outer `activeMessage`/`pendingMessage` slots only one
599
+ update runs at a time, so a new `startViewTransition` never aborts an
600
+ in-flight one mid-callback. */
601
+ const runWithViewTransition = async (updateFn: () => Promise<void>) => {
527
602
  const doc: ViewTransitionDocument = document;
528
603
 
529
604
  if (typeof doc.startViewTransition !== 'function') {
@@ -580,25 +655,7 @@ const runOneUpdate = async (updateFn: () => Promise<void>) => {
580
655
  }
581
656
  };
582
657
 
583
- const runWithViewTransition = (updateFn: () => Promise<void>) => {
584
- if (activeUpdate) {
585
- // Supersede any earlier queued update — only the latest matters.
586
- pendingUpdate = updateFn;
587
-
588
- return;
589
- }
590
-
591
- activeUpdate = runOneUpdate(updateFn).finally(() => {
592
- activeUpdate = null;
593
- if (pendingUpdate) {
594
- const next = pendingUpdate;
595
- pendingUpdate = null;
596
- runWithViewTransition(next);
597
- }
598
- });
599
- };
600
-
601
- const handleFullUpdate = (message: HMRMessage) => {
658
+ const handleFullUpdate = async (message: HMRMessage) => {
602
659
  const componentState = captureComponentState();
603
660
  const scrollState = saveScrollState();
604
661
  const formState = saveFormState();
@@ -630,5 +687,5 @@ const handleFullUpdate = (message: HMRMessage) => {
630
687
  restoreScrollState(scrollState);
631
688
  };
632
689
 
633
- runWithViewTransition(doUpdate);
690
+ await runWithViewTransition(doUpdate);
634
691
  };
@@ -48,8 +48,29 @@ type AngularHmrStats = {
48
48
  readonly updateCount: number;
49
49
  };
50
50
 
51
- const componentRegistry = new Map<string, RegistryEntry>();
52
- let globalUpdateCount = 0;
51
+ /* The component registry MUST persist across chunk imports.
52
+ Each compiled page chunk inlines this `angularRuntime.ts` module — when
53
+ the HMR fast-patch dynamically `import()`s a new chunk, that chunk's
54
+ inlined runtime evaluates again. Without a window-level singleton, each
55
+ re-import would create a fresh `componentRegistry` Map, wipe out
56
+ prior registrations, and break subsequent fast-patches (the second
57
+ patch wouldn't find any registered components).
58
+ We anchor the registry on `globalThis.__ANGULAR_HMR_REGISTRY__` so every
59
+ chunk sees the same Map. */
60
+ type GlobalRegistryWindow = typeof globalThis & {
61
+ __ANGULAR_HMR_REGISTRY__?: Map<string, RegistryEntry>;
62
+ __ANGULAR_HMR_UPDATE_COUNT__?: { value: number };
63
+ };
64
+
65
+ const globalScope = globalThis as GlobalRegistryWindow;
66
+
67
+ const componentRegistry: Map<string, RegistryEntry> =
68
+ globalScope.__ANGULAR_HMR_REGISTRY__ ??
69
+ (globalScope.__ANGULAR_HMR_REGISTRY__ = new Map<string, RegistryEntry>());
70
+
71
+ const updateCounter: { value: number } =
72
+ globalScope.__ANGULAR_HMR_UPDATE_COUNT__ ??
73
+ (globalScope.__ANGULAR_HMR_UPDATE_COUNT__ = { value: 0 });
53
74
 
54
75
  const hasInjectorProviderChanges = (
55
76
  oldCtor: ComponentCtor,
@@ -149,7 +170,7 @@ const patchConstructor = (entry: RegistryEntry, newCtor: ComponentCtor) => {
149
170
  throw new Error('Cannot patch non-configurable Angular metadata');
150
171
  }
151
172
 
152
- globalUpdateCount++;
173
+ updateCounter.value++;
153
174
  entry.updateCount++;
154
175
  entry.registeredAt = Date.now();
155
176
  };
@@ -211,7 +232,7 @@ const angularHmrStats: AngularHmrStats = {
211
232
  return componentRegistry.size;
212
233
  },
213
234
  get updateCount() {
214
- return globalUpdateCount;
235
+ return updateCounter.value;
215
236
  }
216
237
  };
217
238
 
package/dist/index.js CHANGED
@@ -44589,46 +44589,57 @@ var absoluteHttpTransferCacheOptions = {
44589
44589
  }
44590
44590
  };
44591
44591
 
44592
- // Re-Bootstrap HMR with View Transitions API
44593
- if (window.__ANGULAR_APP__) {
44594
- try { window.__ANGULAR_APP__.destroy(); } catch (_err) { /* ignore */ }
44595
- window.__ANGULAR_APP__ = null;
44596
- }
44597
-
44598
- // Ensure root element exists after destroy (Angular removes it)
44599
- var _sel = ${componentClassName}.\u0275cmp?.selectors?.[0]?.[0] || 'ng-app';
44600
- if (!document.querySelector(_sel)) {
44601
- (document.getElementById('root') || document.body).appendChild(document.createElement(_sel));
44602
- }
44603
-
44604
- var providers = [provideZonelessChangeDetection()];
44605
- if (!window.__HMR_SKIP_HYDRATION__ && !pageHasIslands) {
44606
- providers.push(provideClientHydration(withHttpTransferCacheOptions(absoluteHttpTransferCacheOptions)));
44607
- }
44608
- delete window.__HMR_SKIP_HYDRATION__;
44609
- providers.push.apply(providers, pageProviders);
44610
- providers.push.apply(providers, propProviders);
44611
- window.__ABS_SLOT_HYDRATION_PENDING__ = pageHasRawStreamingSlots;
44612
-
44613
- if (pageHasRawStreamingSlots) {
44614
- window.__ABS_SLOT_HYDRATION_PENDING__ = false;
44615
- if (typeof window.__ABS_SLOT_FLUSH__ === 'function') {
44616
- requestAnimationFrame(function() {
44617
- window.__ABS_SLOT_FLUSH__();
44618
- });
44619
- }
44620
- } else {
44621
- bootstrapApplication(${componentClassName}, {
44622
- providers: providers
44623
- }).then(function (appRef) {
44624
- window.__ANGULAR_APP__ = appRef;
44592
+ // Re-export the page module so HMR fast-patch (in handlers/angular.ts) can
44593
+ // dynamically import this chunk and discover the freshly-built component
44594
+ // classes without needing a separate build artifact.
44595
+ export * from '${normalizedImportPath}';
44596
+
44597
+ // Re-Bootstrap HMR with View Transitions API.
44598
+ // Skipped during fast-patch: the HMR client sets
44599
+ // window.__ANGULAR_HMR_FAST_PATCH__ = true before \`import()\`-ing this
44600
+ // chunk so it can read the new component classes via \`export *\` above
44601
+ // without destroying the running app.
44602
+ if (!window.__ANGULAR_HMR_FAST_PATCH__) {
44603
+ if (window.__ANGULAR_APP__) {
44604
+ try { window.__ANGULAR_APP__.destroy(); } catch (_err) { /* ignore */ }
44605
+ window.__ANGULAR_APP__ = null;
44606
+ }
44607
+
44608
+ // Ensure root element exists after destroy (Angular removes it)
44609
+ var _sel = ${componentClassName}.\u0275cmp?.selectors?.[0]?.[0] || 'ng-app';
44610
+ if (!document.querySelector(_sel)) {
44611
+ (document.getElementById('root') || document.body).appendChild(document.createElement(_sel));
44612
+ }
44613
+
44614
+ var providers = [provideZonelessChangeDetection()];
44615
+ if (!window.__HMR_SKIP_HYDRATION__ && !pageHasIslands) {
44616
+ providers.push(provideClientHydration(withHttpTransferCacheOptions(absoluteHttpTransferCacheOptions)));
44617
+ }
44618
+ delete window.__HMR_SKIP_HYDRATION__;
44619
+ providers.push.apply(providers, pageProviders);
44620
+ providers.push.apply(providers, propProviders);
44621
+ window.__ABS_SLOT_HYDRATION_PENDING__ = pageHasRawStreamingSlots;
44622
+
44623
+ if (pageHasRawStreamingSlots) {
44625
44624
  window.__ABS_SLOT_HYDRATION_PENDING__ = false;
44626
44625
  if (typeof window.__ABS_SLOT_FLUSH__ === 'function') {
44627
44626
  requestAnimationFrame(function() {
44628
44627
  window.__ABS_SLOT_FLUSH__();
44629
44628
  });
44630
44629
  }
44631
- });
44630
+ } else {
44631
+ bootstrapApplication(${componentClassName}, {
44632
+ providers: providers
44633
+ }).then(function (appRef) {
44634
+ window.__ANGULAR_APP__ = appRef;
44635
+ window.__ABS_SLOT_HYDRATION_PENDING__ = false;
44636
+ if (typeof window.__ABS_SLOT_FLUSH__ === 'function') {
44637
+ requestAnimationFrame(function() {
44638
+ window.__ABS_SLOT_FLUSH__();
44639
+ });
44640
+ }
44641
+ });
44642
+ }
44632
44643
  }
44633
44644
  `.trim() : `
44634
44645
  import '@angular/compiler';
@@ -58538,5 +58549,5 @@ export {
58538
58549
  ANGULAR_INIT_TIMEOUT_MS
58539
58550
  };
58540
58551
 
58541
- //# debugId=01DB8BB408B0E96D64756E2164756E21
58552
+ //# debugId=3B8AFA4EC6C4DEFD64756E2164756E21
58542
58553
  //# sourceMappingURL=index.js.map