@absolutejs/absolute 0.19.0-beta.853 → 0.19.0-beta.855

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.
@@ -1,415 +0,0 @@
1
- import type {} from '../../../types/globals';
2
- /* Angular HMR — Zoneless Runtime Preservation
3
- DEV MODE ONLY — never included in production builds.
4
-
5
- Runtime component patching via prototype swap and ɵcmp metadata swap.
6
- State persists naturally via instance continuity — NO serialization.
7
-
8
- Why state serialization was removed:
9
- Angular component + service state lives on JS object instances.
10
- Prototype swapping replaces method implementations without destroying
11
- instances, so all state (properties, injected services, etc.) survives.
12
- Serializing and reassigning state is fragile, lossy, and unnecessary.
13
-
14
- Why zoneless requires manual tick():
15
- With provideZonelessChangeDetection(), there is no Zone.js to
16
- auto-trigger change detection. After swapping prototypes or templates,
17
- we must explicitly call ApplicationRef.tick() to re-render.
18
-
19
- Why this is safe in a multi-framework environment:
20
- This module only touches Angular-specific globals (__ANGULAR_APP__,
21
- __ANGULAR_HMR__). It never modifies document.body, React roots,
22
- Vue instances, or Svelte components. The registry is keyed by
23
- source file path, so name collisions across frameworks are impossible. */
24
-
25
- type AngularComponentDefinition = {
26
- providers?: unknown;
27
- providersResolver?: unknown;
28
- selectors?: unknown[];
29
- };
30
-
31
- type ComponentCtor = (abstract new (...args: never[]) => unknown) & {
32
- ɵcmp?: AngularComponentDefinition;
33
- ɵfac?: unknown;
34
- ɵinj?: AngularComponentDefinition;
35
- };
36
-
37
- const isComponentCtor = (value: unknown): value is ComponentCtor =>
38
- typeof value === 'function';
39
-
40
- type RegistryEntry = {
41
- liveCtor: ComponentCtor;
42
- id: string;
43
- registeredAt: number;
44
- updateCount: number;
45
- };
46
-
47
- type AngularHmrStats = {
48
- readonly componentCount: number;
49
- readonly updateCount: number;
50
- };
51
-
52
- /* The component registry MUST persist across chunk imports.
53
- Each compiled page chunk inlines this `angularRuntime.ts` module — when
54
- the HMR fast-patch dynamically `import()`s a new chunk, that chunk's
55
- inlined runtime evaluates again. Without a window-level singleton, each
56
- re-import would create a fresh `componentRegistry` Map, wipe out
57
- prior registrations, and break subsequent fast-patches (the second
58
- patch wouldn't find any registered components).
59
- We anchor the registry on `globalThis.__ANGULAR_HMR_REGISTRY__` so every
60
- chunk sees the same Map. */
61
- type GlobalRegistryWindow = typeof globalThis & {
62
- __ANGULAR_HMR_REGISTRY__?: Map<string, RegistryEntry>;
63
- __ANGULAR_HMR_UPDATE_COUNT__?: { value: number };
64
- };
65
-
66
- const globalScope = globalThis as GlobalRegistryWindow;
67
-
68
- const componentRegistry: Map<string, RegistryEntry> =
69
- globalScope.__ANGULAR_HMR_REGISTRY__ ??
70
- (globalScope.__ANGULAR_HMR_REGISTRY__ = new Map<string, RegistryEntry>());
71
-
72
- const updateCounter: { value: number } =
73
- globalScope.__ANGULAR_HMR_UPDATE_COUNT__ ??
74
- (globalScope.__ANGULAR_HMR_UPDATE_COUNT__ = { value: 0 });
75
-
76
- /* Cheap structural fingerprint. Functions render as 'fn' (treated as
77
- opaque — they change on every module reload but the static config
78
- like provider tokens, useValue payloads, etc. is what we care
79
- about). Objects walk depth-bounded with sorted keys so key order
80
- doesn't cause spurious diffs. Used both for component-level
81
- provider arrays and for page-level `routes`/`providers` exports. */
82
- const fingerprint = (value: unknown, depth = 0): string => {
83
- if (depth > 6) return '~deep~';
84
- if (value === null) return 'null';
85
- if (value === undefined) return 'undef';
86
- if (typeof value === 'function') return 'fn';
87
- if (typeof value === 'symbol') return value.toString();
88
- if (Array.isArray(value)) {
89
- return (
90
- '[' + value.map((v) => fingerprint(v, depth + 1)).join(',') + ']'
91
- );
92
- }
93
- if (typeof value === 'object') {
94
- const obj = value as Record<string, unknown>;
95
- const entries = Object.entries(obj)
96
- .map(([k, v]): [string, string] => [k, fingerprint(v, depth + 1)])
97
- .sort(([a], [b]) => a.localeCompare(b));
98
-
99
- return '{' + entries.map(([k, v]) => `${k}:${v}`).join(',') + '}';
100
- }
101
-
102
- return JSON.stringify(value);
103
- };
104
-
105
- const hasInjectorProviderChanges = (
106
- oldCtor: ComponentCtor,
107
- newCtor: ComponentCtor
108
- ) => {
109
- if (oldCtor.ɵinj === undefined || newCtor.ɵinj === undefined) return false;
110
- const oldP = oldCtor.ɵinj?.providers;
111
- const newP = newCtor.ɵinj?.providers;
112
- if (!Array.isArray(oldP) || !Array.isArray(newP)) return false;
113
-
114
- return fingerprint(oldP) !== fingerprint(newP);
115
- };
116
-
117
- const hasComponentProviderChanges = (
118
- oldCtor: ComponentCtor,
119
- newCtor: ComponentCtor
120
- ) => {
121
- if (!oldCtor.ɵcmp || !newCtor.ɵcmp) return false;
122
- const oldResolver = oldCtor.ɵcmp.providersResolver;
123
- const newResolver = newCtor.ɵcmp.providersResolver;
124
- // Defined-ness flip — added/removed `providers: [...]` entirely.
125
- if ((oldResolver === undefined) !== (newResolver === undefined))
126
- return true;
127
- if (typeof oldResolver !== 'function' || typeof newResolver !== 'function')
128
- return false;
129
-
130
- // `providersResolver` is the function the Angular compiler emits to
131
- // merge a component's `providers` array into the element injector.
132
- // Its source body inlines the provider tokens and useValue/useFactory
133
- // references, so a change to the user's `providers: [...]` array
134
- // produces a different function body. Comparing `toString()` catches
135
- // content changes that the old length/defined-ness check missed —
136
- // e.g. swapping `useValue: 'foo'` for `useValue: 'bar'` while
137
- // keeping the array length identical.
138
- return oldResolver.toString() !== newResolver.toString();
139
- };
140
-
141
- const hasProviderChanges = (oldCtor: ComponentCtor, newCtor: ComponentCtor) => {
142
- if (hasInjectorProviderChanges(oldCtor, newCtor)) return true;
143
- if (hasComponentProviderChanges(oldCtor, newCtor)) return true;
144
-
145
- return false;
146
- };
147
-
148
- const register = (id: string, ctor: unknown) => {
149
- if (!id || !isComponentCtor(ctor)) return;
150
- if (!componentRegistry.has(id)) {
151
- componentRegistry.set(id, {
152
- id,
153
- liveCtor: ctor,
154
- registeredAt: Date.now(),
155
- updateCount: 0
156
- });
157
- }
158
- };
159
-
160
- const SKIP_STATIC_PROPS = [
161
- 'prototype',
162
- 'length',
163
- 'name',
164
- 'caller',
165
- 'arguments'
166
- ];
167
-
168
- const swapPrototypeProp = (
169
- liveCtor: ComponentCtor,
170
- newProto: ComponentCtor,
171
- prop: string
172
- ) => {
173
- if (prop === 'constructor') return;
174
- try {
175
- const desc = Object.getOwnPropertyDescriptor(newProto, prop);
176
- if (desc) Object.defineProperty(liveCtor.prototype, prop, desc);
177
- } catch {
178
- /* non-configurable */
179
- }
180
- };
181
-
182
- const swapStaticProp = (
183
- liveCtor: ComponentCtor,
184
- newCtor: ComponentCtor,
185
- prop: string
186
- ) => {
187
- if (SKIP_STATIC_PROPS.includes(prop)) return true;
188
- try {
189
- const desc = Object.getOwnPropertyDescriptor(newCtor, prop);
190
- if (!desc) return true;
191
- if (!desc.configurable) return prop !== 'ɵcmp' && prop !== 'ɵfac';
192
- Object.defineProperty(liveCtor, prop, desc);
193
-
194
- return true;
195
- } catch {
196
- return prop !== 'ɵcmp' && prop !== 'ɵfac';
197
- }
198
- };
199
-
200
- const patchConstructor = (entry: RegistryEntry, newCtor: ComponentCtor) => {
201
- const { liveCtor } = entry;
202
-
203
- const newProto = newCtor.prototype;
204
- Object.getOwnPropertyNames(newProto).forEach((prop) => {
205
- swapPrototypeProp(liveCtor, newProto, prop);
206
- });
207
-
208
- const allPatched = Object.getOwnPropertyNames(newCtor).every((prop) =>
209
- swapStaticProp(liveCtor, newCtor, prop)
210
- );
211
-
212
- if (!allPatched) {
213
- throw new Error('Cannot patch non-configurable Angular metadata');
214
- }
215
-
216
- updateCounter.value++;
217
- entry.updateCount++;
218
- entry.registeredAt = Date.now();
219
- };
220
-
221
- /* The fast-patch swap of `ɵcmp` and prototype methods doesn't mark
222
- live OnPush components as dirty — `applicationRef.tick()` alone
223
- only checks views that are already marked dirty. So a template
224
- edit on an OnPush component would silently fail to render until
225
- the user clicked something that triggered a markForCheck.
226
- We collect every successfully-patched ctor here, then `refresh()`
227
- walks the DOM for each ctor's selector, gets the live instance via
228
- the `ng` debug API, and calls `applyChanges` on it (which marks
229
- the view dirty AND runs CD on its subtree). */
230
- const pendingFastPatchRefresh: Set<ComponentCtor> = new Set();
231
-
232
- type AngularDebugWindow = Window & {
233
- ng?: {
234
- applyChanges?: (component: unknown) => void;
235
- getComponent?: (element: Element) => unknown;
236
- };
237
- };
238
-
239
- const componentTagSelectors = (ctor: ComponentCtor): string[] => {
240
- const selectors = ctor.ɵcmp?.selectors;
241
- if (!Array.isArray(selectors)) return [];
242
- const tags: string[] = [];
243
- for (const tuple of selectors) {
244
- if (!Array.isArray(tuple)) continue;
245
- const head = tuple[0];
246
- // Component selectors lead with the tag name (a hyphenated
247
- // element name); attribute selectors lead with `''`. Skip the
248
- // attribute case — those are directives, not OnPush views.
249
- if (typeof head === 'string' && head.includes('-')) tags.push(head);
250
- }
251
-
252
- return tags;
253
- };
254
-
255
- const markPatchedDirty = (ctor: ComponentCtor) => {
256
- const ng = (window as AngularDebugWindow).ng;
257
- if (!ng?.getComponent || !ng?.applyChanges) return;
258
- for (const tag of componentTagSelectors(ctor)) {
259
- document.querySelectorAll(tag).forEach((el) => {
260
- try {
261
- const instance = ng.getComponent?.(el);
262
- if (instance) ng.applyChanges?.(instance);
263
- } catch {
264
- /* dev-only debug API — ignore failures */
265
- }
266
- });
267
- }
268
- };
269
-
270
- const applyUpdate = (id: string, newCtor: unknown) => {
271
- if (!isComponentCtor(newCtor)) return false;
272
-
273
- const entry = componentRegistry.get(id);
274
- if (!entry) {
275
- register(id, newCtor);
276
-
277
- return true;
278
- }
279
-
280
- const { liveCtor } = entry;
281
- if (liveCtor === newCtor) return true;
282
-
283
- if (hasProviderChanges(liveCtor, newCtor)) {
284
- console.warn(
285
- '[HMR] Angular provider change detected for',
286
- id,
287
- '→ full reload'
288
- );
289
-
290
- return false;
291
- }
292
- if (newCtor.ɵcmp === undefined && liveCtor.ɵcmp !== undefined) {
293
- console.warn(
294
- '[HMR] New constructor missing ɵcmp for',
295
- id,
296
- '→ full reload'
297
- );
298
-
299
- return false;
300
- }
301
-
302
- try {
303
- patchConstructor(entry, newCtor);
304
- // Queue this ctor for `refresh()` to mark its live instances
305
- // dirty — the patch swapped metadata in place, but OnPush
306
- // components need an explicit markForCheck to re-render.
307
- // `liveCtor` is the on-page constructor (we patched into it);
308
- // we use that for selector lookup since the swap may have
309
- // updated `ɵcmp` on `liveCtor` itself.
310
- pendingFastPatchRefresh.add(liveCtor);
311
-
312
- return true;
313
- } catch (err) {
314
- console.error('[HMR] Angular runtime patch failed for', id, ':', err);
315
-
316
- return false;
317
- }
318
- };
319
-
320
- const refresh = () => {
321
- if (!window.__ANGULAR_APP__) return;
322
- // Mark every live instance of every patched component dirty before
323
- // ticking. `tick()` alone wouldn't re-render OnPush components,
324
- // since they only re-check on `markForCheck`. `applyChanges` marks
325
- // the view dirty and runs CD on its subtree — covers both OnPush
326
- // and Default change-detection components correctly.
327
- for (const ctor of pendingFastPatchRefresh) {
328
- markPatchedDirty(ctor);
329
- }
330
- pendingFastPatchRefresh.clear();
331
- try {
332
- window.__ANGULAR_APP__.tick();
333
- } catch (err) {
334
- console.warn('[HMR] Angular tick() failed after patch:', err);
335
- }
336
- };
337
-
338
- const angularHmrStats: AngularHmrStats = {
339
- get componentCount() {
340
- return componentRegistry.size;
341
- },
342
- get updateCount() {
343
- return updateCounter.value;
344
- }
345
- };
346
-
347
- const getAngularHmrStats = () => angularHmrStats;
348
-
349
- /* Page-level export fingerprints — detect when `routes` or `providers`
350
- change in a page file so the HMR fast-patch can fall back to a full
351
- re-bootstrap. Without this, a component-level fast-patch silently
352
- succeeds while the route/provider change is left dangling — the
353
- running router/injector still uses the values from the initial
354
- bootstrap. The page chunk template calls `recordPageExports` on every
355
- evaluation (initial bootstrap and HMR re-imports); the fast-patch
356
- handler then checks `hasPageExportsChanged` to decide whether to
357
- force a full re-bootstrap.
358
- Function references are treated as opaque — they change on every
359
- module reload but the static config (`path`, `pathMatch`, provider
360
- token, useValue, etc.) is what we care about. */
361
-
362
- type PageFingerprint = {
363
- routes: string | null;
364
- providers: string | null;
365
- };
366
-
367
- type PageExportRecord = {
368
- current: PageFingerprint;
369
- previous: PageFingerprint | null;
370
- };
371
-
372
- const pageExportRecords = ((
373
- globalThis as { __ABS_HMR_PAGE_EXPORTS__?: Map<string, PageExportRecord> }
374
- ).__ABS_HMR_PAGE_EXPORTS__ ??= new Map<string, PageExportRecord>());
375
-
376
- const recordPageExports = (
377
- sourceId: string,
378
- routes: unknown,
379
- providers: unknown
380
- ) => {
381
- const next: PageFingerprint = {
382
- routes: routes === undefined ? null : fingerprint(routes),
383
- providers: providers === undefined ? null : fingerprint(providers)
384
- };
385
- const existing = pageExportRecords.get(sourceId);
386
- pageExportRecords.set(sourceId, {
387
- current: next,
388
- previous: existing?.current ?? null
389
- });
390
- };
391
-
392
- const hasPageExportsChanged = (sourceId: string): boolean => {
393
- const record = pageExportRecords.get(sourceId);
394
- if (!record || !record.previous) return false;
395
-
396
- return (
397
- record.previous.routes !== record.current.routes ||
398
- record.previous.providers !== record.current.providers
399
- );
400
- };
401
-
402
- export const installAngularHMRRuntime = () => {
403
- if (typeof window === 'undefined') return;
404
- window.__ANGULAR_HMR__ = {
405
- applyUpdate,
406
- getStats: getAngularHmrStats,
407
- hasPageExportsChanged,
408
- recordPageExports,
409
- refresh,
410
- register,
411
- getRegistry: () => componentRegistry
412
- };
413
- };
414
-
415
- installAngularHMRRuntime();
@@ -1,8 +0,0 @@
1
- export type AngularEditType = 'template' | 'style-component' | 'class-component' | 'service-method-only' | 'service-with-side-effects' | 'route' | 'reboot';
2
- export type AngularEditClassification = {
3
- type: AngularEditType;
4
- reason: string;
5
- sourceFile: string;
6
- };
7
- export declare const classifyAngularEdit: (file: string) => AngularEditClassification;
8
- export declare const collapseClassifications: (classifications: AngularEditClassification[]) => AngularEditClassification;