@absolutejs/absolute 0.19.0-beta.845 → 0.19.0-beta.847
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 +45 -23
- package/dist/angular/index.js.map +11 -10
- package/dist/angular/server.js +45 -23
- package/dist/angular/server.js.map +11 -10
- package/dist/build.js +955 -498
- package/dist/build.js.map +16 -13
- package/dist/cli/index.js +547 -286
- package/dist/client/index.js +16 -9
- package/dist/client/index.js.map +6 -5
- package/dist/dev/client/handlers/angular.ts +309 -19
- package/dist/dev/client/handlers/angularRuntime.ts +468 -0
- package/dist/dev/client/hmrToast.ts +150 -0
- package/dist/index.js +1002 -545
- package/dist/index.js.map +17 -14
- package/dist/islands/index.js +32 -11
- package/dist/islands/index.js.map +7 -6
- package/dist/react/index.js +32 -11
- package/dist/react/index.js.map +7 -6
- package/dist/src/build/rewriteImports.d.ts +6 -14
- package/dist/src/build/rewriteImportsPlugin.d.ts +48 -0
- package/dist/src/dev/angular/editTypeDetection.d.ts +8 -0
- package/dist/src/dev/pathUtils.d.ts +3 -0
- package/dist/src/utils/buildDirectoryLock.d.ts +26 -3
- package/dist/src/utils/loadConfig.d.ts +5 -0
- package/dist/src/utils/resolveDevPort.d.ts +21 -0
- package/dist/src/utils/runtimeMode.d.ts +3 -0
- package/dist/svelte/index.js +32 -11
- package/dist/svelte/index.js.map +7 -6
- package/dist/svelte/server.js +17 -3
- package/dist/svelte/server.js.map +3 -3
- package/dist/types/build.d.ts +15 -0
- package/dist/types/globals.d.ts +12 -0
- package/dist/vue/index.js +32 -11
- package/dist/vue/index.js.map +7 -6
- package/package.json +1 -1
|
@@ -26,6 +26,21 @@ 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;
|
|
29
44
|
};
|
|
30
45
|
|
|
31
46
|
type ComponentCtor = (abstract new (...args: never[]) => unknown) & {
|
|
@@ -145,6 +160,45 @@ const hasProviderChanges = (oldCtor: ComponentCtor, newCtor: ComponentCtor) => {
|
|
|
145
160
|
return false;
|
|
146
161
|
};
|
|
147
162
|
|
|
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
|
+
|
|
148
202
|
const register = (id: string, ctor: unknown) => {
|
|
149
203
|
if (!id || !isComponentCtor(ctor)) return;
|
|
150
204
|
if (!componentRegistry.has(id)) {
|
|
@@ -154,6 +208,34 @@ const register = (id: string, ctor: unknown) => {
|
|
|
154
208
|
registeredAt: Date.now(),
|
|
155
209
|
updateCount: 0
|
|
156
210
|
});
|
|
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 });
|
|
157
239
|
}
|
|
158
240
|
};
|
|
159
241
|
|
|
@@ -267,6 +349,383 @@ const markPatchedDirty = (ctor: ComponentCtor) => {
|
|
|
267
349
|
}
|
|
268
350
|
};
|
|
269
351
|
|
|
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
|
+
const hosts = collectStyleHosts();
|
|
457
|
+
const consumed = new Set<HTMLStyleElement>();
|
|
458
|
+
const matches: { tag: HTMLStyleElement; nextContent: string }[] = [];
|
|
459
|
+
|
|
460
|
+
for (let i = 0; i < oldStyles.length; i++) {
|
|
461
|
+
const oldContent = oldStyles[i] ?? '';
|
|
462
|
+
const nextContent = nextStyles[i] ?? '';
|
|
463
|
+
if (oldContent === nextContent) continue;
|
|
464
|
+
const tag = findStyleTagByContent(hosts, oldContent, consumed);
|
|
465
|
+
if (!tag) {
|
|
466
|
+
// Couldn't locate one of the live <style> tags — fall through
|
|
467
|
+
// to reboot rather than leaving the page in a half-updated
|
|
468
|
+
// state.
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
consumed.add(tag);
|
|
472
|
+
matches.push({ tag, nextContent });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Only mutate after we've verified we can update every diffed style.
|
|
476
|
+
for (const { tag, nextContent } of matches) {
|
|
477
|
+
tag.textContent = nextContent;
|
|
478
|
+
}
|
|
479
|
+
liveCmp.styles = nextStyles;
|
|
480
|
+
|
|
481
|
+
updateCounter.value++;
|
|
482
|
+
entry.updateCount++;
|
|
483
|
+
entry.registeredAt = Date.now();
|
|
484
|
+
|
|
485
|
+
return true;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
/* Template HMR — surgical swap of the template-related fields on a
|
|
489
|
+
* registered component's `ɵcmp` so the live instance re-renders with
|
|
490
|
+
* the new template WITHOUT re-instantiating. Inputs, outputs, host
|
|
491
|
+
* bindings, providers, and lifecycle hooks live on the class
|
|
492
|
+
* prototype + ɵcmp, and we leave those alone — only the template
|
|
493
|
+
* factory and the slot counts/queries that depend on it are replaced.
|
|
494
|
+
*
|
|
495
|
+
* Why a defined list of fields and not a full `ɵcmp` swap: a wholesale
|
|
496
|
+
* `Object.assign(liveCmp, newCmp)` would also overwrite `providers /
|
|
497
|
+
* providersResolver` and other class-level metadata. Those changes
|
|
498
|
+
* already require a full reboot (the existing fast-path handler in
|
|
499
|
+
* `angular.ts` checks `hasProviderChanges` and bails). For a pure
|
|
500
|
+
* template edit, restricting the patch to the template subgraph
|
|
501
|
+
* keeps live instances on the same DI tokens, queryList references,
|
|
502
|
+
* input bindings, etc. — only the rendered output changes.
|
|
503
|
+
*
|
|
504
|
+
* After the swap, the component's TView (the cached view layout) is
|
|
505
|
+
* stale because slot counts may have changed. Angular regenerates the
|
|
506
|
+
* TView lazily on the first re-render, but only if the existing one
|
|
507
|
+
* is invalidated — which happens automatically when we walk the live
|
|
508
|
+
* instances and call `applyChanges`. The same `markPatchedDirty`
|
|
509
|
+
* helper used by `applyUpdate` covers OnPush views too. */
|
|
510
|
+
|
|
511
|
+
const TEMPLATE_PATCH_FIELDS = [
|
|
512
|
+
'template',
|
|
513
|
+
'consts',
|
|
514
|
+
'decls',
|
|
515
|
+
'vars',
|
|
516
|
+
'viewQuery',
|
|
517
|
+
'contentQueries',
|
|
518
|
+
'ngContentSelectors',
|
|
519
|
+
'dependencies',
|
|
520
|
+
'hostBindings',
|
|
521
|
+
'hostVars',
|
|
522
|
+
'hostAttrs',
|
|
523
|
+
'inputs',
|
|
524
|
+
'outputs'
|
|
525
|
+
] as const;
|
|
526
|
+
|
|
527
|
+
const applyTemplateUpdate = (id: string, newCtor: unknown) => {
|
|
528
|
+
if (!isComponentCtor(newCtor)) return false;
|
|
529
|
+
|
|
530
|
+
const entry = componentRegistry.get(id);
|
|
531
|
+
if (!entry) {
|
|
532
|
+
register(id, newCtor);
|
|
533
|
+
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const { liveCtor } = entry;
|
|
538
|
+
if (liveCtor === newCtor) return true;
|
|
539
|
+
|
|
540
|
+
const liveCmp = liveCtor.ɵcmp as Record<string, unknown> | undefined;
|
|
541
|
+
const nextCmp = newCtor.ɵcmp as Record<string, unknown> | undefined;
|
|
542
|
+
if (!liveCmp || !nextCmp) return false;
|
|
543
|
+
|
|
544
|
+
// If providers changed, this isn't a pure template edit anymore —
|
|
545
|
+
// fall back to reboot via the caller.
|
|
546
|
+
if (hasProviderChanges(liveCtor, newCtor)) return false;
|
|
547
|
+
|
|
548
|
+
for (const field of TEMPLATE_PATCH_FIELDS) {
|
|
549
|
+
if (Object.prototype.hasOwnProperty.call(nextCmp, field)) {
|
|
550
|
+
liveCmp[field] = nextCmp[field];
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
pendingFastPatchRefresh.add(liveCtor);
|
|
555
|
+
updateCounter.value++;
|
|
556
|
+
entry.updateCount++;
|
|
557
|
+
entry.registeredAt = Date.now();
|
|
558
|
+
|
|
559
|
+
return true;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
type TemplateUpdateMode = typeof globalThis & {
|
|
563
|
+
__ANGULAR_HMR_TEMPLATE_UPDATE_MODE__?: boolean;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const templateUpdateBatch: StyleBatchEntry[] = [];
|
|
567
|
+
|
|
568
|
+
const beginTemplateUpdateBatch = () => {
|
|
569
|
+
templateUpdateBatch.length = 0;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const endTemplateUpdateBatch = (): StyleBatchEntry[] => {
|
|
573
|
+
const out = templateUpdateBatch.slice();
|
|
574
|
+
templateUpdateBatch.length = 0;
|
|
575
|
+
|
|
576
|
+
return out;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
/* Service HMR — Level 3 hybrid:
|
|
580
|
+
* 1. Always swap prototype methods on the live ctor. Reaches every
|
|
581
|
+
* live instance (singletons + transient injectees) because they
|
|
582
|
+
* all share the same prototype.
|
|
583
|
+
* 2. If the live singleton is reachable via the root injector,
|
|
584
|
+
* attempt to instantiate a donor with the new ctor and copy any
|
|
585
|
+
* OWN PROPERTIES that the live singleton is missing — this picks
|
|
586
|
+
* up new class-field initializers without overwriting accumulated
|
|
587
|
+
* runtime state. Donor instantiation is best-effort: services
|
|
588
|
+
* using `inject()` outside of an injection context will throw,
|
|
589
|
+
* and we just skip the field merge in that case (the prototype
|
|
590
|
+
* swap still applies, so method changes take effect).
|
|
591
|
+
* 3. The classifier only routes here for services with NO
|
|
592
|
+
* side-effecting calls in the constructor / field initializers
|
|
593
|
+
* (no `subscribe / setInterval / addEventListener / effect /
|
|
594
|
+
* new Worker / new EventSource / etc.`). Anything that touches
|
|
595
|
+
* external state at construction time falls through to reboot
|
|
596
|
+
* via the server-side classification, never reaching this code
|
|
597
|
+
* path. */
|
|
598
|
+
|
|
599
|
+
type AppRefWithInjector = {
|
|
600
|
+
injector?: { get?: (token: unknown, notFoundValue?: unknown) => unknown };
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const getRootInjector = (): {
|
|
604
|
+
get: (token: unknown, notFoundValue?: unknown) => unknown;
|
|
605
|
+
} | null => {
|
|
606
|
+
const app = window.__ANGULAR_APP__ as AppRefWithInjector | null;
|
|
607
|
+
if (!app || !app.injector || typeof app.injector.get !== 'function') {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return app.injector as {
|
|
612
|
+
get: (token: unknown, notFoundValue?: unknown) => unknown;
|
|
613
|
+
};
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const swapPrototypeMethods = (
|
|
617
|
+
liveCtor: ComponentCtor,
|
|
618
|
+
newCtor: ComponentCtor
|
|
619
|
+
) => {
|
|
620
|
+
const newProto = newCtor.prototype as Record<string, unknown>;
|
|
621
|
+
const liveProto = liveCtor.prototype as Record<string, unknown>;
|
|
622
|
+
Object.getOwnPropertyNames(newProto).forEach((prop) => {
|
|
623
|
+
if (prop === 'constructor') return;
|
|
624
|
+
try {
|
|
625
|
+
const desc = Object.getOwnPropertyDescriptor(newProto, prop);
|
|
626
|
+
if (desc) Object.defineProperty(liveProto, prop, desc);
|
|
627
|
+
} catch {
|
|
628
|
+
/* non-configurable property — skip */
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const tryInstantiateServiceDonor = (newCtor: ComponentCtor): unknown | null => {
|
|
634
|
+
try {
|
|
635
|
+
// `new newCtor()` with no args. Works for services with no
|
|
636
|
+
// constructor params and no `inject()` calls at field-init time.
|
|
637
|
+
// Anything more sophisticated (services that use `inject()`
|
|
638
|
+
// outside an injection context) throws here and we fall back to
|
|
639
|
+
// prototype-only swap.
|
|
640
|
+
return Reflect.construct(newCtor as unknown as new () => unknown, []);
|
|
641
|
+
} catch {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const mergeMissingFields = (
|
|
647
|
+
liveInstance: Record<string, unknown>,
|
|
648
|
+
donor: Record<string, unknown>
|
|
649
|
+
) => {
|
|
650
|
+
let merged = 0;
|
|
651
|
+
Object.getOwnPropertyNames(donor).forEach((prop) => {
|
|
652
|
+
if (Object.prototype.hasOwnProperty.call(liveInstance, prop)) return;
|
|
653
|
+
try {
|
|
654
|
+
const desc = Object.getOwnPropertyDescriptor(donor, prop);
|
|
655
|
+
if (desc) {
|
|
656
|
+
Object.defineProperty(liveInstance, prop, desc);
|
|
657
|
+
merged++;
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
/* defining the property failed — skip */
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
return merged;
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
const applyServiceUpdate = (id: string, newCtor: unknown) => {
|
|
668
|
+
if (!isComponentCtor(newCtor)) return false;
|
|
669
|
+
|
|
670
|
+
const entry = componentRegistry.get(id);
|
|
671
|
+
if (!entry) {
|
|
672
|
+
register(id, newCtor);
|
|
673
|
+
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const { liveCtor } = entry;
|
|
678
|
+
if (liveCtor === newCtor) return true;
|
|
679
|
+
|
|
680
|
+
// Method swap — reaches every live instance.
|
|
681
|
+
swapPrototypeMethods(liveCtor, newCtor);
|
|
682
|
+
|
|
683
|
+
// Best-effort field merge on the live singleton.
|
|
684
|
+
const injector = getRootInjector();
|
|
685
|
+
if (injector) {
|
|
686
|
+
try {
|
|
687
|
+
const liveInstance = injector.get(liveCtor, null) as Record<
|
|
688
|
+
string,
|
|
689
|
+
unknown
|
|
690
|
+
> | null;
|
|
691
|
+
if (liveInstance) {
|
|
692
|
+
const donor = tryInstantiateServiceDonor(newCtor) as Record<
|
|
693
|
+
string,
|
|
694
|
+
unknown
|
|
695
|
+
> | null;
|
|
696
|
+
if (donor) mergeMissingFields(liveInstance, donor);
|
|
697
|
+
}
|
|
698
|
+
} catch {
|
|
699
|
+
/* injector lookup failed — service may not be `providedIn:
|
|
700
|
+
"root"`, or the type-token mismatched. Prototype swap is
|
|
701
|
+
already applied, so methods take effect either way. */
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
updateCounter.value++;
|
|
706
|
+
entry.updateCount++;
|
|
707
|
+
entry.registeredAt = Date.now();
|
|
708
|
+
|
|
709
|
+
return true;
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
type ServiceUpdateMode = typeof globalThis & {
|
|
713
|
+
__ANGULAR_HMR_SERVICE_UPDATE_MODE__?: boolean;
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const serviceUpdateBatch: StyleBatchEntry[] = [];
|
|
717
|
+
|
|
718
|
+
const beginServiceUpdateBatch = () => {
|
|
719
|
+
serviceUpdateBatch.length = 0;
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
const endServiceUpdateBatch = (): StyleBatchEntry[] => {
|
|
723
|
+
const out = serviceUpdateBatch.slice();
|
|
724
|
+
serviceUpdateBatch.length = 0;
|
|
725
|
+
|
|
726
|
+
return out;
|
|
727
|
+
};
|
|
728
|
+
|
|
270
729
|
const applyUpdate = (id: string, newCtor: unknown) => {
|
|
271
730
|
if (!isComponentCtor(newCtor)) return false;
|
|
272
731
|
|
|
@@ -402,7 +861,16 @@ const hasPageExportsChanged = (sourceId: string): boolean => {
|
|
|
402
861
|
export const installAngularHMRRuntime = () => {
|
|
403
862
|
if (typeof window === 'undefined') return;
|
|
404
863
|
window.__ANGULAR_HMR__ = {
|
|
864
|
+
applyServiceUpdate,
|
|
865
|
+
applyStyleUpdate,
|
|
866
|
+
applyTemplateUpdate,
|
|
405
867
|
applyUpdate,
|
|
868
|
+
beginServiceUpdateBatch,
|
|
869
|
+
beginStyleUpdateBatch,
|
|
870
|
+
beginTemplateUpdateBatch,
|
|
871
|
+
endServiceUpdateBatch,
|
|
872
|
+
endStyleUpdateBatch,
|
|
873
|
+
endTemplateUpdateBatch,
|
|
406
874
|
getStats: getAngularHmrStats,
|
|
407
875
|
hasPageExportsChanged,
|
|
408
876
|
recordPageExports,
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {} from '../../types/globals';
|
|
2
|
+
/* HMR notification toast — bottom-right corner, fades in for ~2.5s.
|
|
3
|
+
*
|
|
4
|
+
* Used when the Angular HMR client falls through to a full reboot, so
|
|
5
|
+
* the developer can SEE why a save triggered a reboot instead of
|
|
6
|
+
* silently watching the splash transition. The reason string comes
|
|
7
|
+
* from the server-side classifier (`src/dev/angular/editTypeDetection`),
|
|
8
|
+
* surfaced via the `reason` field on the HMR wire message.
|
|
9
|
+
*
|
|
10
|
+
* Mounts a single shared container the first time it's called and
|
|
11
|
+
* stacks toasts inside it. Each toast removes itself after the visible
|
|
12
|
+
* window expires; the container stays mounted for the session. */
|
|
13
|
+
|
|
14
|
+
const CONTAINER_ID = '__abs_hmr_toast_container__';
|
|
15
|
+
const VISIBLE_DURATION_MS = 2500;
|
|
16
|
+
const FADE_MS = 220;
|
|
17
|
+
|
|
18
|
+
const ensureContainer = (): HTMLDivElement => {
|
|
19
|
+
const existing = document.getElementById(
|
|
20
|
+
CONTAINER_ID
|
|
21
|
+
) as HTMLDivElement | null;
|
|
22
|
+
if (existing) return existing;
|
|
23
|
+
|
|
24
|
+
const container = document.createElement('div');
|
|
25
|
+
container.id = CONTAINER_ID;
|
|
26
|
+
Object.assign(container.style, {
|
|
27
|
+
position: 'fixed',
|
|
28
|
+
bottom: '16px',
|
|
29
|
+
right: '16px',
|
|
30
|
+
display: 'flex',
|
|
31
|
+
flexDirection: 'column',
|
|
32
|
+
gap: '8px',
|
|
33
|
+
zIndex: '2147483646',
|
|
34
|
+
pointerEvents: 'none',
|
|
35
|
+
fontFamily:
|
|
36
|
+
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
37
|
+
fontSize: '12px',
|
|
38
|
+
maxWidth: '420px'
|
|
39
|
+
});
|
|
40
|
+
document.body.appendChild(container);
|
|
41
|
+
|
|
42
|
+
return container;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const accentForType = (updateType: string | undefined): string => {
|
|
46
|
+
switch (updateType) {
|
|
47
|
+
case 'route':
|
|
48
|
+
return '#1d4ed8';
|
|
49
|
+
case 'service-with-side-effects':
|
|
50
|
+
return '#b45309';
|
|
51
|
+
case 'reboot':
|
|
52
|
+
default:
|
|
53
|
+
return '#dd0031';
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type HmrToastInput = {
|
|
58
|
+
updateType?: string;
|
|
59
|
+
reason?: string;
|
|
60
|
+
editSourceFile?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const showHmrToast = ({
|
|
64
|
+
updateType,
|
|
65
|
+
reason,
|
|
66
|
+
editSourceFile
|
|
67
|
+
}: HmrToastInput) => {
|
|
68
|
+
if (typeof document === 'undefined') return;
|
|
69
|
+
const container = ensureContainer();
|
|
70
|
+
|
|
71
|
+
const toast = document.createElement('div');
|
|
72
|
+
const accent = accentForType(updateType);
|
|
73
|
+
Object.assign(toast.style, {
|
|
74
|
+
background: 'rgba(15, 17, 22, 0.94)',
|
|
75
|
+
color: '#f8fafc',
|
|
76
|
+
borderLeft: `3px solid ${accent}`,
|
|
77
|
+
padding: '8px 12px',
|
|
78
|
+
borderRadius: '6px',
|
|
79
|
+
boxShadow: '0 6px 24px rgba(0, 0, 0, 0.35)',
|
|
80
|
+
opacity: '0',
|
|
81
|
+
transform: 'translateY(6px)',
|
|
82
|
+
transition: `opacity ${FADE_MS}ms ease, transform ${FADE_MS}ms ease`,
|
|
83
|
+
pointerEvents: 'auto',
|
|
84
|
+
whiteSpace: 'nowrap',
|
|
85
|
+
overflow: 'hidden',
|
|
86
|
+
textOverflow: 'ellipsis',
|
|
87
|
+
maxWidth: '420px'
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const label = document.createElement('div');
|
|
91
|
+
Object.assign(label.style, {
|
|
92
|
+
color: accent,
|
|
93
|
+
fontWeight: '600',
|
|
94
|
+
marginBottom: '2px',
|
|
95
|
+
letterSpacing: '0.02em'
|
|
96
|
+
});
|
|
97
|
+
label.textContent = `HMR reboot — ${updateType ?? 'unknown'}`;
|
|
98
|
+
toast.appendChild(label);
|
|
99
|
+
|
|
100
|
+
const body = document.createElement('div');
|
|
101
|
+
Object.assign(body.style, {
|
|
102
|
+
color: '#cbd5e1',
|
|
103
|
+
whiteSpace: 'normal',
|
|
104
|
+
wordBreak: 'break-word'
|
|
105
|
+
});
|
|
106
|
+
body.textContent = reason ?? '(no reason given)';
|
|
107
|
+
toast.appendChild(body);
|
|
108
|
+
|
|
109
|
+
if (editSourceFile) {
|
|
110
|
+
const path = document.createElement('div');
|
|
111
|
+
Object.assign(path.style, {
|
|
112
|
+
color: '#64748b',
|
|
113
|
+
marginTop: '2px',
|
|
114
|
+
fontSize: '11px',
|
|
115
|
+
whiteSpace: 'nowrap',
|
|
116
|
+
overflow: 'hidden',
|
|
117
|
+
textOverflow: 'ellipsis'
|
|
118
|
+
});
|
|
119
|
+
// Format like the server logger does: relative + leading slash.
|
|
120
|
+
const cwdLike = editSourceFile.replace(/^.*?(\/src\/|\/pages\/)/, '$1');
|
|
121
|
+
path.textContent = cwdLike;
|
|
122
|
+
toast.appendChild(path);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
container.appendChild(toast);
|
|
126
|
+
|
|
127
|
+
// Trigger CSS transition by deferring to the next paint.
|
|
128
|
+
requestAnimationFrame(() => {
|
|
129
|
+
toast.style.opacity = '1';
|
|
130
|
+
toast.style.transform = 'translateY(0)';
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const removeAt = window.setTimeout(() => {
|
|
134
|
+
toast.style.opacity = '0';
|
|
135
|
+
toast.style.transform = 'translateY(6px)';
|
|
136
|
+
window.setTimeout(() => {
|
|
137
|
+
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
|
138
|
+
}, FADE_MS);
|
|
139
|
+
}, VISIBLE_DURATION_MS);
|
|
140
|
+
|
|
141
|
+
// If the user clicks the toast, dismiss it immediately.
|
|
142
|
+
toast.addEventListener('click', () => {
|
|
143
|
+
window.clearTimeout(removeAt);
|
|
144
|
+
toast.style.opacity = '0';
|
|
145
|
+
toast.style.transform = 'translateY(6px)';
|
|
146
|
+
window.setTimeout(() => {
|
|
147
|
+
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
|
148
|
+
}, FADE_MS);
|
|
149
|
+
});
|
|
150
|
+
};
|