@absolutejs/absolute 0.19.0-beta.851 → 0.19.0-beta.853
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 +3 -1
- package/dist/angular/index.js.map +3 -3
- package/dist/angular/server.js +3 -1
- package/dist/angular/server.js.map +3 -3
- package/dist/build.js +1919 -284
- package/dist/build.js.map +17 -7
- package/dist/dev/client/handlers/angular.ts +17 -234
- package/dist/dev/client/handlers/angularHmrShim.ts +77 -0
- package/dist/dev/client/handlers/angularRuntime.ts +0 -476
- package/dist/dev/client/hmrClient.ts +21 -0
- package/dist/index.js +2006 -329
- package/dist/index.js.map +18 -8
- package/dist/react/browser.js.map +1 -1
- package/dist/react/index.js +3 -1
- package/dist/react/index.js.map +3 -3
- 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 +19 -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/plugins/hmr.d.ts +25 -0
- package/dist/src/react/UniversalRouter.d.ts +3 -1
- 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/types/globals.d.ts +0 -12
- 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
|
@@ -26,21 +26,6 @@ type AngularComponentDefinition = {
|
|
|
26
26
|
providers?: unknown;
|
|
27
27
|
providersResolver?: unknown;
|
|
28
28
|
selectors?: unknown[];
|
|
29
|
-
styles?: string[];
|
|
30
|
-
encapsulation?: number;
|
|
31
|
-
template?: unknown;
|
|
32
|
-
consts?: unknown;
|
|
33
|
-
decls?: number;
|
|
34
|
-
vars?: number;
|
|
35
|
-
viewQuery?: unknown;
|
|
36
|
-
contentQueries?: unknown;
|
|
37
|
-
ngContentSelectors?: unknown;
|
|
38
|
-
dependencies?: unknown;
|
|
39
|
-
hostBindings?: unknown;
|
|
40
|
-
hostVars?: number;
|
|
41
|
-
hostAttrs?: unknown;
|
|
42
|
-
inputs?: unknown;
|
|
43
|
-
outputs?: unknown;
|
|
44
29
|
};
|
|
45
30
|
|
|
46
31
|
type ComponentCtor = (abstract new (...args: never[]) => unknown) & {
|
|
@@ -160,45 +145,6 @@ const hasProviderChanges = (oldCtor: ComponentCtor, newCtor: ComponentCtor) => {
|
|
|
160
145
|
return false;
|
|
161
146
|
};
|
|
162
147
|
|
|
163
|
-
/* Style-update batch buffer.
|
|
164
|
-
*
|
|
165
|
-
* When a component-CSS edit triggers HMR, the rebuilt page chunk
|
|
166
|
-
* re-evaluates with `__ANGULAR_HMR_STYLE_UPDATE_MODE__` set on the
|
|
167
|
-
* window. Inside that mode, every `register(id, newCtor)` call from
|
|
168
|
-
* the chunk's auto-registration block routes its newCtor straight
|
|
169
|
-
* into `applyStyleUpdate(id, newCtor)` instead of being a no-op
|
|
170
|
-
* (which is the default for already-registered IDs).
|
|
171
|
-
*
|
|
172
|
-
* This is the only way to reach CHILD-component classes — the page
|
|
173
|
-
* chunk only `export *`s the page's own module, so a top-level
|
|
174
|
-
* `Object.keys(newModule)` walk wouldn't find imported components.
|
|
175
|
-
* The registration block runs once per compiled file (page + every
|
|
176
|
-
* imported component), so it covers the whole subtree.
|
|
177
|
-
*
|
|
178
|
-
* The batch is consulted by `handleComponentStyleUpdate` after the
|
|
179
|
-
* chunk import resolves: if any registration's update returned false,
|
|
180
|
-
* the orchestrator falls through to a full reboot rather than leaving
|
|
181
|
-
* the page partially restyled. */
|
|
182
|
-
|
|
183
|
-
type StyleUpdateMode = typeof globalThis & {
|
|
184
|
-
__ANGULAR_HMR_STYLE_UPDATE_MODE__?: boolean;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
type StyleBatchEntry = { id: string; ok: boolean };
|
|
188
|
-
|
|
189
|
-
const styleUpdateBatch: StyleBatchEntry[] = [];
|
|
190
|
-
|
|
191
|
-
const beginStyleUpdateBatch = () => {
|
|
192
|
-
styleUpdateBatch.length = 0;
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const endStyleUpdateBatch = (): StyleBatchEntry[] => {
|
|
196
|
-
const out = styleUpdateBatch.slice();
|
|
197
|
-
styleUpdateBatch.length = 0;
|
|
198
|
-
|
|
199
|
-
return out;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
148
|
const register = (id: string, ctor: unknown) => {
|
|
203
149
|
if (!id || !isComponentCtor(ctor)) return;
|
|
204
150
|
if (!componentRegistry.has(id)) {
|
|
@@ -208,34 +154,6 @@ const register = (id: string, ctor: unknown) => {
|
|
|
208
154
|
registeredAt: Date.now(),
|
|
209
155
|
updateCount: 0
|
|
210
156
|
});
|
|
211
|
-
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Already registered. If we're inside an HMR style-update or
|
|
216
|
-
// template-update window, route this re-registration's new ctor
|
|
217
|
-
// through the appropriate surgical patcher. The per-file
|
|
218
|
-
// auto-registration block is the only place to intercept new ctors
|
|
219
|
-
// for CHILD components — the page chunk's `export *` only re-exports
|
|
220
|
-
// the page's own module.
|
|
221
|
-
const styleScope = globalThis as StyleUpdateMode;
|
|
222
|
-
if (styleScope.__ANGULAR_HMR_STYLE_UPDATE_MODE__) {
|
|
223
|
-
const ok = applyStyleUpdate(id, ctor);
|
|
224
|
-
styleUpdateBatch.push({ id, ok });
|
|
225
|
-
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
const tmplScope = globalThis as TemplateUpdateMode;
|
|
229
|
-
if (tmplScope.__ANGULAR_HMR_TEMPLATE_UPDATE_MODE__) {
|
|
230
|
-
const ok = applyTemplateUpdate(id, ctor);
|
|
231
|
-
templateUpdateBatch.push({ id, ok });
|
|
232
|
-
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const svcScope = globalThis as ServiceUpdateMode;
|
|
236
|
-
if (svcScope.__ANGULAR_HMR_SERVICE_UPDATE_MODE__) {
|
|
237
|
-
const ok = applyServiceUpdate(id, ctor);
|
|
238
|
-
serviceUpdateBatch.push({ id, ok });
|
|
239
157
|
}
|
|
240
158
|
};
|
|
241
159
|
|
|
@@ -349,391 +267,6 @@ const markPatchedDirty = (ctor: ComponentCtor) => {
|
|
|
349
267
|
}
|
|
350
268
|
};
|
|
351
269
|
|
|
352
|
-
/* Component-style HMR — swaps `ɵcmp.styles` and replaces matching
|
|
353
|
-
* `<style>` tags in the document so the visible page reflects the new
|
|
354
|
-
* CSS without a re-bootstrap.
|
|
355
|
-
*
|
|
356
|
-
* Why this is safe with Emulated encapsulation (the default): Angular's
|
|
357
|
-
* compiler rewrites the CSS at build time, prefixing every selector
|
|
358
|
-
* with `[_ngcontent-c<scopeId>]`. The scope ID is deterministic per
|
|
359
|
-
* component def — the same source file produces the same scope ID
|
|
360
|
-
* across rebuilds — so the rewritten DOM still matches the new CSS.
|
|
361
|
-
* We only need to update the style *content*; the elements wearing
|
|
362
|
-
* `_ngcontent-c<scopeId>` attributes are still on the page from the
|
|
363
|
-
* initial bootstrap.
|
|
364
|
-
*
|
|
365
|
-
* ShadowDOM encapsulation (3) is not yet handled — each component
|
|
366
|
-
* instance has its own shadow root with its own style tags, requiring
|
|
367
|
-
* a per-instance walk. Falls through to reboot for now.
|
|
368
|
-
*
|
|
369
|
-
* The matching strategy: walk every `<style>` tag in `document.head`
|
|
370
|
-
* and `document.body`, find ones whose `textContent` exactly matches a
|
|
371
|
-
* string in the OLD `ɵcmp.styles` array, and replace it with the
|
|
372
|
-
* corresponding string from the NEW array. Equal-length arrays only —
|
|
373
|
-
* adding or removing a `styleUrl` entry triggers a reboot.
|
|
374
|
-
*
|
|
375
|
-
* Returns true on full success, false if we couldn't safely apply
|
|
376
|
-
* (length mismatch, ShadowDOM, missing styles array, or any old
|
|
377
|
-
* style had no DOM match — meaning we'd leave the page in a partially
|
|
378
|
-
* updated state). */
|
|
379
|
-
|
|
380
|
-
const SHADOW_DOM_ENCAPSULATION = 3;
|
|
381
|
-
|
|
382
|
-
type StyleHost = {
|
|
383
|
-
host: ParentNode;
|
|
384
|
-
tags: HTMLStyleElement[];
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
const collectStyleHosts = (): StyleHost[] => {
|
|
388
|
-
const hosts: StyleHost[] = [];
|
|
389
|
-
const headTags = Array.from(
|
|
390
|
-
document.head.querySelectorAll('style')
|
|
391
|
-
) as HTMLStyleElement[];
|
|
392
|
-
const bodyTags = Array.from(
|
|
393
|
-
document.body.querySelectorAll('style')
|
|
394
|
-
) as HTMLStyleElement[];
|
|
395
|
-
if (headTags.length > 0)
|
|
396
|
-
hosts.push({ host: document.head, tags: headTags });
|
|
397
|
-
if (bodyTags.length > 0)
|
|
398
|
-
hosts.push({ host: document.body, tags: bodyTags });
|
|
399
|
-
|
|
400
|
-
return hosts;
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
const findStyleTagByContent = (
|
|
404
|
-
hosts: StyleHost[],
|
|
405
|
-
content: string,
|
|
406
|
-
consumed: Set<HTMLStyleElement>
|
|
407
|
-
): HTMLStyleElement | null => {
|
|
408
|
-
for (const { tags } of hosts) {
|
|
409
|
-
for (const tag of tags) {
|
|
410
|
-
if (consumed.has(tag)) continue;
|
|
411
|
-
if (tag.textContent === content) return tag;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return null;
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
const applyStyleUpdate = (id: string, newCtor: unknown) => {
|
|
419
|
-
if (!isComponentCtor(newCtor)) return false;
|
|
420
|
-
|
|
421
|
-
const entry = componentRegistry.get(id);
|
|
422
|
-
if (!entry) {
|
|
423
|
-
// First time we've seen this component — register it but no styles
|
|
424
|
-
// to swap yet. The next edit will pick up the now-registered ctor.
|
|
425
|
-
register(id, newCtor);
|
|
426
|
-
|
|
427
|
-
return true;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const { liveCtor } = entry;
|
|
431
|
-
if (liveCtor === newCtor) return true;
|
|
432
|
-
|
|
433
|
-
const liveCmp = liveCtor.ɵcmp;
|
|
434
|
-
const newCmp = newCtor.ɵcmp;
|
|
435
|
-
if (!liveCmp || !newCmp) return false;
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
liveCmp.encapsulation === SHADOW_DOM_ENCAPSULATION ||
|
|
439
|
-
newCmp.encapsulation === SHADOW_DOM_ENCAPSULATION
|
|
440
|
-
) {
|
|
441
|
-
// Shadow DOM scopes styles per-instance — out of scope for v1.
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const oldStyles = liveCmp.styles;
|
|
446
|
-
const nextStyles = newCmp.styles;
|
|
447
|
-
if (!Array.isArray(oldStyles) || !Array.isArray(nextStyles)) return false;
|
|
448
|
-
if (oldStyles.length !== nextStyles.length) return false;
|
|
449
|
-
if (oldStyles.length === 0) {
|
|
450
|
-
// No styles to swap, no work to do — succeed trivially.
|
|
451
|
-
liveCmp.styles = nextStyles;
|
|
452
|
-
|
|
453
|
-
return true;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Always update `ɵcmp.styles` first so future mounts of this
|
|
457
|
-
// component pick up the new content. The DOM `<style>` swap below
|
|
458
|
-
// handles currently-mounted instances; any component that isn't on
|
|
459
|
-
// the page right now (an unopened modal, a route view that hasn't
|
|
460
|
-
// been visited yet) simply has no `<style>` tag to find — and
|
|
461
|
-
// that's fine. The next time it mounts, Angular reads from
|
|
462
|
-
// `ɵcmp.styles` and injects the new content. Treating that case
|
|
463
|
-
// as a failure was the source of the "Transition was skipped"
|
|
464
|
-
// bug: the SPA at /portal/dashboard registered dozens of
|
|
465
|
-
// components whose styles weren't injected (modals, sibling tabs,
|
|
466
|
-
// etc.), and each missing live `<style>` tag forced fallthrough
|
|
467
|
-
// to the reboot path, firing `startViewTransition` while the
|
|
468
|
-
// previous one was still finishing.
|
|
469
|
-
liveCmp.styles = nextStyles;
|
|
470
|
-
|
|
471
|
-
const hosts = collectStyleHosts();
|
|
472
|
-
const consumed = new Set<HTMLStyleElement>();
|
|
473
|
-
|
|
474
|
-
for (let i = 0; i < oldStyles.length; i++) {
|
|
475
|
-
const oldContent = oldStyles[i] ?? '';
|
|
476
|
-
const nextContent = nextStyles[i] ?? '';
|
|
477
|
-
if (oldContent === nextContent) continue;
|
|
478
|
-
const tag = findStyleTagByContent(hosts, oldContent, consumed);
|
|
479
|
-
if (!tag) {
|
|
480
|
-
// Component not currently mounted (or already-swapped style).
|
|
481
|
-
// Skip the DOM swap; `ɵcmp.styles` is already updated above
|
|
482
|
-
// so the next mount picks up the new content.
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
consumed.add(tag);
|
|
486
|
-
tag.textContent = nextContent;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
updateCounter.value++;
|
|
490
|
-
entry.updateCount++;
|
|
491
|
-
entry.registeredAt = Date.now();
|
|
492
|
-
|
|
493
|
-
return true;
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
/* Template HMR — surgical swap of the template-related fields on a
|
|
497
|
-
* registered component's `ɵcmp` so the live instance re-renders with
|
|
498
|
-
* the new template WITHOUT re-instantiating. Inputs, outputs, host
|
|
499
|
-
* bindings, providers, and lifecycle hooks live on the class
|
|
500
|
-
* prototype + ɵcmp, and we leave those alone — only the template
|
|
501
|
-
* factory and the slot counts/queries that depend on it are replaced.
|
|
502
|
-
*
|
|
503
|
-
* Why a defined list of fields and not a full `ɵcmp` swap: a wholesale
|
|
504
|
-
* `Object.assign(liveCmp, newCmp)` would also overwrite `providers /
|
|
505
|
-
* providersResolver` and other class-level metadata. Those changes
|
|
506
|
-
* already require a full reboot (the existing fast-path handler in
|
|
507
|
-
* `angular.ts` checks `hasProviderChanges` and bails). For a pure
|
|
508
|
-
* template edit, restricting the patch to the template subgraph
|
|
509
|
-
* keeps live instances on the same DI tokens, queryList references,
|
|
510
|
-
* input bindings, etc. — only the rendered output changes.
|
|
511
|
-
*
|
|
512
|
-
* After the swap, the component's TView (the cached view layout) is
|
|
513
|
-
* stale because slot counts may have changed. Angular regenerates the
|
|
514
|
-
* TView lazily on the first re-render, but only if the existing one
|
|
515
|
-
* is invalidated — which happens automatically when we walk the live
|
|
516
|
-
* instances and call `applyChanges`. The same `markPatchedDirty`
|
|
517
|
-
* helper used by `applyUpdate` covers OnPush views too. */
|
|
518
|
-
|
|
519
|
-
const TEMPLATE_PATCH_FIELDS = [
|
|
520
|
-
'template',
|
|
521
|
-
'consts',
|
|
522
|
-
'decls',
|
|
523
|
-
'vars',
|
|
524
|
-
'viewQuery',
|
|
525
|
-
'contentQueries',
|
|
526
|
-
'ngContentSelectors',
|
|
527
|
-
'dependencies',
|
|
528
|
-
'hostBindings',
|
|
529
|
-
'hostVars',
|
|
530
|
-
'hostAttrs',
|
|
531
|
-
'inputs',
|
|
532
|
-
'outputs'
|
|
533
|
-
] as const;
|
|
534
|
-
|
|
535
|
-
const applyTemplateUpdate = (id: string, newCtor: unknown) => {
|
|
536
|
-
if (!isComponentCtor(newCtor)) return false;
|
|
537
|
-
|
|
538
|
-
const entry = componentRegistry.get(id);
|
|
539
|
-
if (!entry) {
|
|
540
|
-
register(id, newCtor);
|
|
541
|
-
|
|
542
|
-
return true;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const { liveCtor } = entry;
|
|
546
|
-
if (liveCtor === newCtor) return true;
|
|
547
|
-
|
|
548
|
-
const liveCmp = liveCtor.ɵcmp as Record<string, unknown> | undefined;
|
|
549
|
-
const nextCmp = newCtor.ɵcmp as Record<string, unknown> | undefined;
|
|
550
|
-
if (!liveCmp || !nextCmp) return false;
|
|
551
|
-
|
|
552
|
-
// If providers changed, this isn't a pure template edit anymore —
|
|
553
|
-
// fall back to reboot via the caller.
|
|
554
|
-
if (hasProviderChanges(liveCtor, newCtor)) return false;
|
|
555
|
-
|
|
556
|
-
for (const field of TEMPLATE_PATCH_FIELDS) {
|
|
557
|
-
if (Object.prototype.hasOwnProperty.call(nextCmp, field)) {
|
|
558
|
-
liveCmp[field] = nextCmp[field];
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
pendingFastPatchRefresh.add(liveCtor);
|
|
563
|
-
updateCounter.value++;
|
|
564
|
-
entry.updateCount++;
|
|
565
|
-
entry.registeredAt = Date.now();
|
|
566
|
-
|
|
567
|
-
return true;
|
|
568
|
-
};
|
|
569
|
-
|
|
570
|
-
type TemplateUpdateMode = typeof globalThis & {
|
|
571
|
-
__ANGULAR_HMR_TEMPLATE_UPDATE_MODE__?: boolean;
|
|
572
|
-
};
|
|
573
|
-
|
|
574
|
-
const templateUpdateBatch: StyleBatchEntry[] = [];
|
|
575
|
-
|
|
576
|
-
const beginTemplateUpdateBatch = () => {
|
|
577
|
-
templateUpdateBatch.length = 0;
|
|
578
|
-
};
|
|
579
|
-
|
|
580
|
-
const endTemplateUpdateBatch = (): StyleBatchEntry[] => {
|
|
581
|
-
const out = templateUpdateBatch.slice();
|
|
582
|
-
templateUpdateBatch.length = 0;
|
|
583
|
-
|
|
584
|
-
return out;
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
/* Service HMR — Level 3 hybrid:
|
|
588
|
-
* 1. Always swap prototype methods on the live ctor. Reaches every
|
|
589
|
-
* live instance (singletons + transient injectees) because they
|
|
590
|
-
* all share the same prototype.
|
|
591
|
-
* 2. If the live singleton is reachable via the root injector,
|
|
592
|
-
* attempt to instantiate a donor with the new ctor and copy any
|
|
593
|
-
* OWN PROPERTIES that the live singleton is missing — this picks
|
|
594
|
-
* up new class-field initializers without overwriting accumulated
|
|
595
|
-
* runtime state. Donor instantiation is best-effort: services
|
|
596
|
-
* using `inject()` outside of an injection context will throw,
|
|
597
|
-
* and we just skip the field merge in that case (the prototype
|
|
598
|
-
* swap still applies, so method changes take effect).
|
|
599
|
-
* 3. The classifier only routes here for services with NO
|
|
600
|
-
* side-effecting calls in the constructor / field initializers
|
|
601
|
-
* (no `subscribe / setInterval / addEventListener / effect /
|
|
602
|
-
* new Worker / new EventSource / etc.`). Anything that touches
|
|
603
|
-
* external state at construction time falls through to reboot
|
|
604
|
-
* via the server-side classification, never reaching this code
|
|
605
|
-
* path. */
|
|
606
|
-
|
|
607
|
-
type AppRefWithInjector = {
|
|
608
|
-
injector?: { get?: (token: unknown, notFoundValue?: unknown) => unknown };
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
const getRootInjector = (): {
|
|
612
|
-
get: (token: unknown, notFoundValue?: unknown) => unknown;
|
|
613
|
-
} | null => {
|
|
614
|
-
const app = window.__ANGULAR_APP__ as AppRefWithInjector | null;
|
|
615
|
-
if (!app || !app.injector || typeof app.injector.get !== 'function') {
|
|
616
|
-
return null;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
return app.injector as {
|
|
620
|
-
get: (token: unknown, notFoundValue?: unknown) => unknown;
|
|
621
|
-
};
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
const swapPrototypeMethods = (
|
|
625
|
-
liveCtor: ComponentCtor,
|
|
626
|
-
newCtor: ComponentCtor
|
|
627
|
-
) => {
|
|
628
|
-
const newProto = newCtor.prototype as Record<string, unknown>;
|
|
629
|
-
const liveProto = liveCtor.prototype as Record<string, unknown>;
|
|
630
|
-
Object.getOwnPropertyNames(newProto).forEach((prop) => {
|
|
631
|
-
if (prop === 'constructor') return;
|
|
632
|
-
try {
|
|
633
|
-
const desc = Object.getOwnPropertyDescriptor(newProto, prop);
|
|
634
|
-
if (desc) Object.defineProperty(liveProto, prop, desc);
|
|
635
|
-
} catch {
|
|
636
|
-
/* non-configurable property — skip */
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
const tryInstantiateServiceDonor = (newCtor: ComponentCtor): unknown | null => {
|
|
642
|
-
try {
|
|
643
|
-
// `new newCtor()` with no args. Works for services with no
|
|
644
|
-
// constructor params and no `inject()` calls at field-init time.
|
|
645
|
-
// Anything more sophisticated (services that use `inject()`
|
|
646
|
-
// outside an injection context) throws here and we fall back to
|
|
647
|
-
// prototype-only swap.
|
|
648
|
-
return Reflect.construct(newCtor as unknown as new () => unknown, []);
|
|
649
|
-
} catch {
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
const mergeMissingFields = (
|
|
655
|
-
liveInstance: Record<string, unknown>,
|
|
656
|
-
donor: Record<string, unknown>
|
|
657
|
-
) => {
|
|
658
|
-
let merged = 0;
|
|
659
|
-
Object.getOwnPropertyNames(donor).forEach((prop) => {
|
|
660
|
-
if (Object.prototype.hasOwnProperty.call(liveInstance, prop)) return;
|
|
661
|
-
try {
|
|
662
|
-
const desc = Object.getOwnPropertyDescriptor(donor, prop);
|
|
663
|
-
if (desc) {
|
|
664
|
-
Object.defineProperty(liveInstance, prop, desc);
|
|
665
|
-
merged++;
|
|
666
|
-
}
|
|
667
|
-
} catch {
|
|
668
|
-
/* defining the property failed — skip */
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
return merged;
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
const applyServiceUpdate = (id: string, newCtor: unknown) => {
|
|
676
|
-
if (!isComponentCtor(newCtor)) return false;
|
|
677
|
-
|
|
678
|
-
const entry = componentRegistry.get(id);
|
|
679
|
-
if (!entry) {
|
|
680
|
-
register(id, newCtor);
|
|
681
|
-
|
|
682
|
-
return true;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const { liveCtor } = entry;
|
|
686
|
-
if (liveCtor === newCtor) return true;
|
|
687
|
-
|
|
688
|
-
// Method swap — reaches every live instance.
|
|
689
|
-
swapPrototypeMethods(liveCtor, newCtor);
|
|
690
|
-
|
|
691
|
-
// Best-effort field merge on the live singleton.
|
|
692
|
-
const injector = getRootInjector();
|
|
693
|
-
if (injector) {
|
|
694
|
-
try {
|
|
695
|
-
const liveInstance = injector.get(liveCtor, null) as Record<
|
|
696
|
-
string,
|
|
697
|
-
unknown
|
|
698
|
-
> | null;
|
|
699
|
-
if (liveInstance) {
|
|
700
|
-
const donor = tryInstantiateServiceDonor(newCtor) as Record<
|
|
701
|
-
string,
|
|
702
|
-
unknown
|
|
703
|
-
> | null;
|
|
704
|
-
if (donor) mergeMissingFields(liveInstance, donor);
|
|
705
|
-
}
|
|
706
|
-
} catch {
|
|
707
|
-
/* injector lookup failed — service may not be `providedIn:
|
|
708
|
-
"root"`, or the type-token mismatched. Prototype swap is
|
|
709
|
-
already applied, so methods take effect either way. */
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
updateCounter.value++;
|
|
714
|
-
entry.updateCount++;
|
|
715
|
-
entry.registeredAt = Date.now();
|
|
716
|
-
|
|
717
|
-
return true;
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
type ServiceUpdateMode = typeof globalThis & {
|
|
721
|
-
__ANGULAR_HMR_SERVICE_UPDATE_MODE__?: boolean;
|
|
722
|
-
};
|
|
723
|
-
|
|
724
|
-
const serviceUpdateBatch: StyleBatchEntry[] = [];
|
|
725
|
-
|
|
726
|
-
const beginServiceUpdateBatch = () => {
|
|
727
|
-
serviceUpdateBatch.length = 0;
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
const endServiceUpdateBatch = (): StyleBatchEntry[] => {
|
|
731
|
-
const out = serviceUpdateBatch.slice();
|
|
732
|
-
serviceUpdateBatch.length = 0;
|
|
733
|
-
|
|
734
|
-
return out;
|
|
735
|
-
};
|
|
736
|
-
|
|
737
270
|
const applyUpdate = (id: string, newCtor: unknown) => {
|
|
738
271
|
if (!isComponentCtor(newCtor)) return false;
|
|
739
272
|
|
|
@@ -869,16 +402,7 @@ const hasPageExportsChanged = (sourceId: string): boolean => {
|
|
|
869
402
|
export const installAngularHMRRuntime = () => {
|
|
870
403
|
if (typeof window === 'undefined') return;
|
|
871
404
|
window.__ANGULAR_HMR__ = {
|
|
872
|
-
applyServiceUpdate,
|
|
873
|
-
applyStyleUpdate,
|
|
874
|
-
applyTemplateUpdate,
|
|
875
405
|
applyUpdate,
|
|
876
|
-
beginServiceUpdateBatch,
|
|
877
|
-
beginStyleUpdateBatch,
|
|
878
|
-
beginTemplateUpdateBatch,
|
|
879
|
-
endServiceUpdateBatch,
|
|
880
|
-
endStyleUpdateBatch,
|
|
881
|
-
endTemplateUpdateBatch,
|
|
882
406
|
getStats: getAngularHmrStats,
|
|
883
407
|
hasPageExportsChanged,
|
|
884
408
|
recordPageExports,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { detectCurrentFramework } from './frameworkDetect';
|
|
15
15
|
import { hideErrorOverlay, showErrorOverlay } from './errorOverlay';
|
|
16
16
|
import { handleAngularUpdate } from './handlers/angular';
|
|
17
|
+
import { dispatchAngularComponentUpdate } from './handlers/angularHmrShim';
|
|
17
18
|
import { handleReactUpdate } from './handlers/react';
|
|
18
19
|
import { handleHTMLUpdate, handleScriptUpdate } from './handlers/html';
|
|
19
20
|
import { handleHTMXUpdate } from './handlers/htmx';
|
|
@@ -152,6 +153,26 @@ const handleHMRMessage = (message: HMRMessage) => {
|
|
|
152
153
|
hideErrorOverlay();
|
|
153
154
|
handleAngularUpdate(message);
|
|
154
155
|
break;
|
|
156
|
+
case 'angular:component-update': {
|
|
157
|
+
// Surgical-HMR fast path. Server resolved the changed
|
|
158
|
+
// file → owning component classes and emitted one
|
|
159
|
+
// message per affected component. Our injected
|
|
160
|
+
// `__ng_hmr_load` blocks (see hmrInjectionPlugin.ts)
|
|
161
|
+
// listen here and re-fetch the applyMetadata module.
|
|
162
|
+
const data = message.data as
|
|
163
|
+
| { id?: string; timestamp?: number }
|
|
164
|
+
| undefined;
|
|
165
|
+
if (data && typeof data.id === 'string') {
|
|
166
|
+
dispatchAngularComponentUpdate({
|
|
167
|
+
id: data.id,
|
|
168
|
+
timestamp:
|
|
169
|
+
typeof data.timestamp === 'number'
|
|
170
|
+
? data.timestamp
|
|
171
|
+
: Date.now()
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
155
176
|
case 'rebuild-error':
|
|
156
177
|
handleRebuildError(message);
|
|
157
178
|
break;
|