@absolutejs/absolute 0.19.0-beta.867 → 0.19.0-beta.869
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/build.js +76 -7
- package/dist/build.js.map +5 -5
- package/dist/dev/client/handlers/angularHmrShim.ts +9 -1
- package/dist/dev/client/handlers/angularRemount.ts +265 -0
- package/dist/dev/client/handlers/angularRemountWiring.ts +55 -0
- package/dist/dev/client/hmrClient.ts +29 -1
- package/dist/dev/client/vendor/lview/lViewOps.ts +194 -0
- package/dist/dev/client/vendor/lview/slotConstants.ts +44 -0
- package/dist/index.js +76 -7
- package/dist/index.js.map +5 -5
- package/dist/src/dev/angular/fastHmrCompiler.d.ts +1 -0
- package/package.json +1 -1
|
@@ -13,7 +13,9 @@ import type {} from '../../../types/globals';
|
|
|
13
13
|
* shim setup synchronous + at module scope so it's installed during
|
|
14
14
|
* `hmrClient.ts`'s import-evaluation pass, before page chunks load. */
|
|
15
15
|
|
|
16
|
-
export type AngularHmrEvent =
|
|
16
|
+
export type AngularHmrEvent =
|
|
17
|
+
| 'angular:component-update'
|
|
18
|
+
| 'angular:component-remount';
|
|
17
19
|
export type AngularComponentUpdate = {
|
|
18
20
|
id: string;
|
|
19
21
|
timestamp: number;
|
|
@@ -75,3 +77,9 @@ export const dispatchAngularComponentUpdate = (
|
|
|
75
77
|
) => {
|
|
76
78
|
globalThis.__angularHmr?.dispatch('angular:component-update', data);
|
|
77
79
|
};
|
|
80
|
+
|
|
81
|
+
export const dispatchAngularComponentRemount = (
|
|
82
|
+
data: AngularComponentUpdate
|
|
83
|
+
) => {
|
|
84
|
+
globalThis.__angularHmr?.dispatch('angular:component-remount', data);
|
|
85
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type {} from '../../../types/globals';
|
|
2
|
+
/* Per-component Tier 1 remount.
|
|
3
|
+
*
|
|
4
|
+
* When fastHmr reports a structural change for a component class,
|
|
5
|
+
* full app rebootstrap loses all sibling component state. Instead, we
|
|
6
|
+
* remount only the affected components: destroy each live instance +
|
|
7
|
+
* recreate at the same DOM host with the new factory.
|
|
8
|
+
*
|
|
9
|
+
* This uses public `createComponent` for the heavy lifting (it runs
|
|
10
|
+
* the new constructor, sets up DI, fires lifecycle hooks, renders the
|
|
11
|
+
* template, attaches change detection). We supplement with vendored
|
|
12
|
+
* LView slot manipulation to (a) find each live instance's parent
|
|
13
|
+
* LView slot and (b) splice the freshly-created LView into that slot
|
|
14
|
+
* so it participates in the parent's view tree instead of being a
|
|
15
|
+
* detached root.
|
|
16
|
+
*
|
|
17
|
+
* Caveats baked into this approach:
|
|
18
|
+
* • The new LView starts as a "root" view (createComponent attaches
|
|
19
|
+
* it to ApplicationRef). After splice, it's a child of the
|
|
20
|
+
* original parent. We need to detach from ApplicationRef so it's
|
|
21
|
+
* not double-tracked.
|
|
22
|
+
* • Old @Input bindings from the parent are NOT re-applied. The
|
|
23
|
+
* parent's template flow runs at parent-CD time and wires inputs
|
|
24
|
+
* then; until then the new instance sees default values. In
|
|
25
|
+
* practice this matches Tier 1 rebootstrap behavior — no worse.
|
|
26
|
+
* • Old projection content (ng-content) doesn't transfer. If the
|
|
27
|
+
* parent injected a child via ng-content, the new instance has an
|
|
28
|
+
* empty projection slot until parent re-renders. Logged as a
|
|
29
|
+
* known limitation in ANGULAR_PER_COMPONENT_REMOUNT_RESEARCH.md. */
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
CONTEXT,
|
|
33
|
+
HOST,
|
|
34
|
+
PARENT,
|
|
35
|
+
T_HOST,
|
|
36
|
+
TVIEW
|
|
37
|
+
} from '../vendor/lview/slotConstants';
|
|
38
|
+
import {
|
|
39
|
+
executeOnDestroys,
|
|
40
|
+
isLView,
|
|
41
|
+
markLViewDestroyed,
|
|
42
|
+
processCleanups,
|
|
43
|
+
replaceLViewInTree,
|
|
44
|
+
type LView,
|
|
45
|
+
type TView,
|
|
46
|
+
type TNode
|
|
47
|
+
} from '../vendor/lview/lViewOps';
|
|
48
|
+
|
|
49
|
+
type AngularCoreNamespace = {
|
|
50
|
+
createComponent: (
|
|
51
|
+
type: unknown,
|
|
52
|
+
options: {
|
|
53
|
+
hostElement?: Element;
|
|
54
|
+
environmentInjector: unknown;
|
|
55
|
+
}
|
|
56
|
+
) => {
|
|
57
|
+
instance: unknown;
|
|
58
|
+
hostView: { _lView?: LView; detectChanges?: () => void };
|
|
59
|
+
destroy: () => void;
|
|
60
|
+
};
|
|
61
|
+
ApplicationRef?: unknown;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type ComponentClass = new (...args: unknown[]) => unknown;
|
|
65
|
+
|
|
66
|
+
type LiveInstance = {
|
|
67
|
+
host: Element;
|
|
68
|
+
oldLView: LView;
|
|
69
|
+
parentLView: LView;
|
|
70
|
+
slotIndex: number;
|
|
71
|
+
tNode: TNode;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/* Walk the DOM looking for elements whose component instance is of
|
|
75
|
+
* `Class`. Each match resolves to its parent LView + slot index via
|
|
76
|
+
* the LContext stored on the host element under `__ngContext__`.
|
|
77
|
+
*
|
|
78
|
+
* We walk DOM (not Angular's TRACKED_LVIEWS map) because (a)
|
|
79
|
+
* TRACKED_LVIEWS isn't exported and (b) the DOM walk is bounded by
|
|
80
|
+
* page size, which is fast enough for HMR. */
|
|
81
|
+
const findLiveInstances = (Class: ComponentClass): LiveInstance[] => {
|
|
82
|
+
const results: LiveInstance[] = [];
|
|
83
|
+
const elements = document.querySelectorAll('*');
|
|
84
|
+
for (const el of Array.from(elements)) {
|
|
85
|
+
const ctx = (el as unknown as Record<string, unknown>).__ngContext__;
|
|
86
|
+
if (typeof ctx !== 'object' || ctx === null) continue;
|
|
87
|
+
const lContext = ctx as { lView?: LView; nodeIndex?: number };
|
|
88
|
+
if (!lContext.lView || lContext.nodeIndex === undefined) continue;
|
|
89
|
+
|
|
90
|
+
const slot = lContext.lView[lContext.nodeIndex];
|
|
91
|
+
if (!isLView(slot)) continue;
|
|
92
|
+
const ownLView = slot as LView;
|
|
93
|
+
const instance = ownLView[CONTEXT];
|
|
94
|
+
if (!(instance instanceof Class)) continue;
|
|
95
|
+
|
|
96
|
+
const tNode = ownLView[T_HOST] as TNode | null;
|
|
97
|
+
const host = ownLView[HOST] as Element | null;
|
|
98
|
+
if (!tNode || !host) continue;
|
|
99
|
+
|
|
100
|
+
// Avoid double-recording the same LView (multiple DOM elements
|
|
101
|
+
// can land in the same component, all sharing __ngContext__)
|
|
102
|
+
if (results.some((r) => r.oldLView === ownLView)) continue;
|
|
103
|
+
|
|
104
|
+
results.push({
|
|
105
|
+
host,
|
|
106
|
+
oldLView: ownLView,
|
|
107
|
+
parentLView: lContext.lView,
|
|
108
|
+
slotIndex: lContext.nodeIndex,
|
|
109
|
+
tNode
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return results;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/* Run a public `createComponent` call to instantiate `Class` at
|
|
116
|
+
* `hostElement`. Pulls ApplicationRef + EnvironmentInjector through
|
|
117
|
+
* the live app's injector exposed on `window.__ANGULAR_APP__`. */
|
|
118
|
+
const createFreshAt = (
|
|
119
|
+
Class: ComponentClass,
|
|
120
|
+
hostElement: Element,
|
|
121
|
+
core: AngularCoreNamespace
|
|
122
|
+
): {
|
|
123
|
+
instance: unknown;
|
|
124
|
+
newLView: LView;
|
|
125
|
+
componentRef: ReturnType<AngularCoreNamespace['createComponent']>;
|
|
126
|
+
} | null => {
|
|
127
|
+
const w = window as unknown as {
|
|
128
|
+
__ANGULAR_APP__?: { injector: unknown };
|
|
129
|
+
};
|
|
130
|
+
const envInjector = w.__ANGULAR_APP__?.injector;
|
|
131
|
+
if (!envInjector) return null;
|
|
132
|
+
|
|
133
|
+
const ref = core.createComponent(Class, {
|
|
134
|
+
hostElement,
|
|
135
|
+
environmentInjector: envInjector
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const newLView = ref.hostView._lView;
|
|
139
|
+
if (!newLView) {
|
|
140
|
+
// Should never happen — _lView is always populated by Angular's
|
|
141
|
+
// internal createComponent path. If it is missing, our slot
|
|
142
|
+
// constants might be off; bail to caller for fallback.
|
|
143
|
+
ref.destroy();
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { instance: ref.instance, newLView, componentRef: ref };
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/* Splice `newLView` into `parentLView` at `slotIndex`, replacing
|
|
151
|
+
* `oldLView`. After the splice, the new LView lives in the parent's
|
|
152
|
+
* view tree; the old one is detached. */
|
|
153
|
+
const spliceLViewIntoParent = (
|
|
154
|
+
target: LiveInstance,
|
|
155
|
+
newLView: LView
|
|
156
|
+
): void => {
|
|
157
|
+
const { parentLView, oldLView, slotIndex, tNode } = target;
|
|
158
|
+
replaceLViewInTree(parentLView, oldLView, newLView, slotIndex);
|
|
159
|
+
newLView[PARENT] = parentLView;
|
|
160
|
+
newLView[T_HOST] = tNode;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/* Fire onDestroy + cleanup on the OLD LView so subscriptions, event
|
|
164
|
+
* listeners, and `inject(DestroyRef).onDestroy(...)` callbacks all
|
|
165
|
+
* fire. Then mark the LView as destroyed so any subsequent
|
|
166
|
+
* tree-walk skips it. */
|
|
167
|
+
const teardownOldLView = (oldLView: LView): void => {
|
|
168
|
+
const oldTView = oldLView[TVIEW] as TView | null;
|
|
169
|
+
if (oldTView) {
|
|
170
|
+
executeOnDestroys(oldTView, oldLView);
|
|
171
|
+
processCleanups(oldTView, oldLView);
|
|
172
|
+
}
|
|
173
|
+
markLViewDestroyed(oldLView);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/* The fresh ComponentRef is registered as a root view on
|
|
177
|
+
* ApplicationRef. We don't want it tracked there — its parent in the
|
|
178
|
+
* view tree is the original parent LView. Detach it. */
|
|
179
|
+
const detachFromApplicationRoot = (
|
|
180
|
+
componentRef: { hostView: unknown },
|
|
181
|
+
core: AngularCoreNamespace
|
|
182
|
+
): void => {
|
|
183
|
+
if (!core.ApplicationRef) return;
|
|
184
|
+
const w = window as unknown as {
|
|
185
|
+
__ANGULAR_APP__?: { detachView?: (view: unknown) => void };
|
|
186
|
+
};
|
|
187
|
+
w.__ANGULAR_APP__?.detachView?.(componentRef.hostView);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type RemountResult = {
|
|
191
|
+
className: string;
|
|
192
|
+
remounted: number;
|
|
193
|
+
skipped: number;
|
|
194
|
+
error?: string;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/* Public entry. Called by the bundle's HMR listener block when an
|
|
198
|
+
* `angular:component-remount` event arrives for this class.
|
|
199
|
+
*
|
|
200
|
+
* applyMetadata is the surgical module's default export — it patches
|
|
201
|
+
* `Class.ɵcmp` with the new component definition. We call it BEFORE
|
|
202
|
+
* createComponent so the fresh instance picks up the new template,
|
|
203
|
+
* dependencies, etc.
|
|
204
|
+
*
|
|
205
|
+
* locals + namespaces match `ɵɵreplaceMetadata`'s contract — passed
|
|
206
|
+
* through to applyMetadata. We're not using ɵɵreplaceMetadata here
|
|
207
|
+
* (it preserves instance state, defeating the point), but we mirror
|
|
208
|
+
* the calling convention so bundle-level code stays consistent. */
|
|
209
|
+
export const remountComponentClass = async (
|
|
210
|
+
Class: ComponentClass,
|
|
211
|
+
applyMetadata: (
|
|
212
|
+
Class: unknown,
|
|
213
|
+
namespaces: unknown[],
|
|
214
|
+
...locals: unknown[]
|
|
215
|
+
) => void,
|
|
216
|
+
namespaces: unknown[],
|
|
217
|
+
locals: unknown[],
|
|
218
|
+
core: AngularCoreNamespace,
|
|
219
|
+
className: string
|
|
220
|
+
): Promise<RemountResult> => {
|
|
221
|
+
try {
|
|
222
|
+
applyMetadata.apply(null, [Class, namespaces, ...locals]);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
return {
|
|
225
|
+
className,
|
|
226
|
+
error: `applyMetadata threw: ${(err as Error).message}`,
|
|
227
|
+
remounted: 0,
|
|
228
|
+
skipped: 0
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const targets = findLiveInstances(Class);
|
|
233
|
+
if (targets.length === 0) {
|
|
234
|
+
return { className, remounted: 0, skipped: 0 };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let remounted = 0;
|
|
238
|
+
let skipped = 0;
|
|
239
|
+
|
|
240
|
+
for (const target of targets) {
|
|
241
|
+
try {
|
|
242
|
+
const fresh = createFreshAt(Class, target.host, core);
|
|
243
|
+
if (!fresh) {
|
|
244
|
+
skipped++;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
spliceLViewIntoParent(target, fresh.newLView);
|
|
249
|
+
detachFromApplicationRoot(fresh.componentRef, core);
|
|
250
|
+
teardownOldLView(target.oldLView);
|
|
251
|
+
|
|
252
|
+
fresh.componentRef.hostView.detectChanges?.();
|
|
253
|
+
remounted++;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error(
|
|
256
|
+
`[absolutejs] remount of ${className} failed at`,
|
|
257
|
+
target.host,
|
|
258
|
+
err
|
|
259
|
+
);
|
|
260
|
+
skipped++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { className, remounted, skipped };
|
|
265
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {} from '../../../types/globals';
|
|
2
|
+
/* Wire `globalThis.__absAngularRemount` so the injected
|
|
3
|
+
* `__ng_hmr_remount` blocks (in `hmrInjectionPlugin.ts`) can call into
|
|
4
|
+
* the shared remount implementation. The bundle's listener captures
|
|
5
|
+
* the class via closure and the metadata via dynamic import; everything
|
|
6
|
+
* else is generic, so the implementation is shared rather than baked
|
|
7
|
+
* into every component bundle. */
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
remountComponentClass,
|
|
11
|
+
type RemountResult
|
|
12
|
+
} from './angularRemount';
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
// eslint-disable-next-line no-var
|
|
16
|
+
var __absAngularRemount:
|
|
17
|
+
| ((
|
|
18
|
+
Class: new (...args: unknown[]) => unknown,
|
|
19
|
+
applyMetadata: (
|
|
20
|
+
Class: unknown,
|
|
21
|
+
namespaces: unknown[],
|
|
22
|
+
...locals: unknown[]
|
|
23
|
+
) => void,
|
|
24
|
+
namespaces: unknown[],
|
|
25
|
+
locals: unknown[],
|
|
26
|
+
core: {
|
|
27
|
+
createComponent: (
|
|
28
|
+
type: unknown,
|
|
29
|
+
options: {
|
|
30
|
+
hostElement?: Element;
|
|
31
|
+
environmentInjector: unknown;
|
|
32
|
+
}
|
|
33
|
+
) => {
|
|
34
|
+
instance: unknown;
|
|
35
|
+
hostView: {
|
|
36
|
+
_lView?: unknown[];
|
|
37
|
+
detectChanges?: () => void;
|
|
38
|
+
};
|
|
39
|
+
destroy: () => void;
|
|
40
|
+
};
|
|
41
|
+
ApplicationRef?: unknown;
|
|
42
|
+
},
|
|
43
|
+
className: string
|
|
44
|
+
) => Promise<RemountResult>)
|
|
45
|
+
| undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let installed = false;
|
|
49
|
+
|
|
50
|
+
export const installAngularRemountGlobal = (): void => {
|
|
51
|
+
if (installed) return;
|
|
52
|
+
if (typeof globalThis === 'undefined') return;
|
|
53
|
+
globalThis.__absAngularRemount = remountComponentClass;
|
|
54
|
+
installed = true;
|
|
55
|
+
};
|
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
} from './constants';
|
|
14
14
|
import { detectCurrentFramework } from './frameworkDetect';
|
|
15
15
|
import { hideErrorOverlay, showErrorOverlay } from './errorOverlay';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
dispatchAngularComponentRemount,
|
|
18
|
+
dispatchAngularComponentUpdate
|
|
19
|
+
} from './handlers/angularHmrShim';
|
|
20
|
+
import { installAngularRemountGlobal } from './handlers/angularRemountWiring';
|
|
17
21
|
import { handleReactUpdate } from './handlers/react';
|
|
18
22
|
import { handleHTMLUpdate, handleScriptUpdate } from './handlers/html';
|
|
19
23
|
import { handleHTMXUpdate } from './handlers/htmx';
|
|
@@ -30,6 +34,7 @@ import {
|
|
|
30
34
|
|
|
31
35
|
// Initialize HMR globals
|
|
32
36
|
if (typeof window !== 'undefined') {
|
|
37
|
+
installAngularRemountGlobal();
|
|
33
38
|
if (!window.__HMR_MANIFEST__) {
|
|
34
39
|
window.__HMR_MANIFEST__ = {};
|
|
35
40
|
}
|
|
@@ -69,6 +74,7 @@ window.addEventListener('unhandledrejection', (evt) => {
|
|
|
69
74
|
|
|
70
75
|
const hmrUpdateTypes = new Set([
|
|
71
76
|
'angular:component-update',
|
|
77
|
+
'angular:component-remount',
|
|
72
78
|
'angular:rebootstrap',
|
|
73
79
|
'react-update',
|
|
74
80
|
'html-update',
|
|
@@ -169,6 +175,28 @@ const handleHMRMessage = (message: HMRMessage) => {
|
|
|
169
175
|
}
|
|
170
176
|
break;
|
|
171
177
|
}
|
|
178
|
+
case 'angular:component-remount': {
|
|
179
|
+
// Tier 1a per-component remount. Structural change
|
|
180
|
+
// detected in fastHmr — the existing instance lacks new
|
|
181
|
+
// fields / DI / providers, so we destroy + recreate just
|
|
182
|
+
// this component (vs. full app rebootstrap). The injected
|
|
183
|
+
// `__ng_hmr_remount` listener handles the splice via the
|
|
184
|
+
// `__absAngularRemount` global wired in
|
|
185
|
+
// `installAngularRemountGlobal`.
|
|
186
|
+
const data = message.data as
|
|
187
|
+
| { id?: string; timestamp?: number }
|
|
188
|
+
| undefined;
|
|
189
|
+
if (data && typeof data.id === 'string') {
|
|
190
|
+
dispatchAngularComponentRemount({
|
|
191
|
+
id: data.id,
|
|
192
|
+
timestamp:
|
|
193
|
+
typeof data.timestamp === 'number'
|
|
194
|
+
? data.timestamp
|
|
195
|
+
: Date.now()
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
172
200
|
case 'angular:rebootstrap': {
|
|
173
201
|
// Tier 1 fallback. The user's edit changed structure
|
|
174
202
|
// the surgical path can't safely apply
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type {} from '../../../../types/globals';
|
|
2
|
+
/* Vendored LView slot operations. Direct port from
|
|
3
|
+
* `@angular/core/fesm2022/_debug_node-chunk.mjs` of the small-and-pure
|
|
4
|
+
* helpers we need for per-component remount. The big helpers
|
|
5
|
+
* (renderView / refreshView / destroyLView's full DOM-removal path)
|
|
6
|
+
* stay in Angular — we invoke them indirectly via public
|
|
7
|
+
* `createComponent`. The ones here are slot-manipulation primitives
|
|
8
|
+
* with no transitive dependencies, so vendoring them is safe.
|
|
9
|
+
*
|
|
10
|
+
* Per-Angular-version chore: re-diff against the upstream functions
|
|
11
|
+
* after each minor bump. They've been stable since v17 — the algorithm
|
|
12
|
+
* shape hasn't changed since the LView FLAGS reshuffle. */
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
CHILD_HEAD,
|
|
16
|
+
CHILD_TAIL,
|
|
17
|
+
CLEANUP,
|
|
18
|
+
FLAGS,
|
|
19
|
+
HEADER_OFFSET,
|
|
20
|
+
LFLAG_DESTROYED,
|
|
21
|
+
NEXT,
|
|
22
|
+
ON_DESTROY_HOOKS,
|
|
23
|
+
TVIEW
|
|
24
|
+
} from './slotConstants';
|
|
25
|
+
|
|
26
|
+
export type LView = unknown[];
|
|
27
|
+
export type LContainer = unknown[];
|
|
28
|
+
export type TView = {
|
|
29
|
+
bindingStartIndex: number;
|
|
30
|
+
cleanup: unknown[] | null;
|
|
31
|
+
destroyHooks: unknown[] | null;
|
|
32
|
+
};
|
|
33
|
+
export type TNode = { index: number };
|
|
34
|
+
|
|
35
|
+
/* `isLView` / `isLContainer` shape checks. The runtime distinguishes
|
|
36
|
+
* by whether slot 1 (TVIEW) is an object or undefined — LContainer
|
|
37
|
+
* doesn't have a TView. */
|
|
38
|
+
export const isLView = (v: unknown): v is LView =>
|
|
39
|
+
Array.isArray(v) && typeof (v as unknown[])[TVIEW] === 'object';
|
|
40
|
+
|
|
41
|
+
export const isLContainer = (v: unknown): v is LContainer =>
|
|
42
|
+
Array.isArray(v) && (v as unknown[])[TVIEW] === undefined;
|
|
43
|
+
|
|
44
|
+
export const isDestroyed = (lView: LView): boolean =>
|
|
45
|
+
((lView[FLAGS] as number) & LFLAG_DESTROYED) !== 0;
|
|
46
|
+
|
|
47
|
+
/* Vendored from `replaceLViewInTree(parentLView, oldLView, newLView, index)`.
|
|
48
|
+
* Walks parent's slots looking for the LView/LContainer whose NEXT
|
|
49
|
+
* pointer is `oldLView` and rewires it to `newLView`, then patches
|
|
50
|
+
* CHILD_HEAD / CHILD_TAIL if `oldLView` was at either end, and finally
|
|
51
|
+
* places `newLView` at the indexed slot.
|
|
52
|
+
*
|
|
53
|
+
* Verbatim port — keep it that way to make diff-against-upstream cheap. */
|
|
54
|
+
export const replaceLViewInTree = (
|
|
55
|
+
parentLView: LView,
|
|
56
|
+
oldLView: LView,
|
|
57
|
+
newLView: LView,
|
|
58
|
+
index: number
|
|
59
|
+
): void => {
|
|
60
|
+
const parentTView = parentLView[TVIEW] as TView;
|
|
61
|
+
for (let i = HEADER_OFFSET; i < parentTView.bindingStartIndex; i++) {
|
|
62
|
+
const current = parentLView[i];
|
|
63
|
+
if (
|
|
64
|
+
(isLView(current) || isLContainer(current)) &&
|
|
65
|
+
(current as LView)[NEXT] === oldLView
|
|
66
|
+
) {
|
|
67
|
+
(current as LView)[NEXT] = newLView;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (parentLView[CHILD_HEAD] === oldLView) parentLView[CHILD_HEAD] = newLView;
|
|
72
|
+
if (parentLView[CHILD_TAIL] === oldLView) parentLView[CHILD_TAIL] = newLView;
|
|
73
|
+
newLView[NEXT] = oldLView[NEXT];
|
|
74
|
+
oldLView[NEXT] = null;
|
|
75
|
+
parentLView[index] = newLView;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/* Vendored from `executeOnDestroys(tView, lView)`. tView.destroyHooks
|
|
79
|
+
* is laid out as `[slotIdx, hook | hookList, slotIdx, hook | hookList, ...]`.
|
|
80
|
+
* Each `hook` is either a function (called with `lView[slotIdx]` as
|
|
81
|
+
* `this`) or an array of `[propertyKey, fn]` pairs (one per directive
|
|
82
|
+
* sharing the slot). NodeInjectorFactory contexts are skipped; they
|
|
83
|
+
* represent injector providers, not directive instances. */
|
|
84
|
+
type NodeInjectorFactoryLike = { multi?: unknown };
|
|
85
|
+
|
|
86
|
+
const isNodeInjectorFactoryLike = (
|
|
87
|
+
value: unknown
|
|
88
|
+
): value is NodeInjectorFactoryLike =>
|
|
89
|
+
typeof value === 'object' &&
|
|
90
|
+
value !== null &&
|
|
91
|
+
value.constructor !== undefined &&
|
|
92
|
+
value.constructor.name === 'NodeInjectorFactory';
|
|
93
|
+
|
|
94
|
+
export const executeOnDestroys = (tView: TView, lView: LView): void => {
|
|
95
|
+
const destroyHooks = tView.destroyHooks;
|
|
96
|
+
if (destroyHooks == null) return;
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < destroyHooks.length; i += 2) {
|
|
99
|
+
const slotIdx = destroyHooks[i] as number;
|
|
100
|
+
const context = lView[slotIdx];
|
|
101
|
+
if (isNodeInjectorFactoryLike(context)) continue;
|
|
102
|
+
|
|
103
|
+
const toCall = destroyHooks[i + 1];
|
|
104
|
+
if (Array.isArray(toCall)) {
|
|
105
|
+
for (let j = 0; j < toCall.length; j += 2) {
|
|
106
|
+
const propKey = toCall[j] as string;
|
|
107
|
+
const hook = toCall[j + 1] as () => void;
|
|
108
|
+
const callContext = (context as Record<string, unknown>)[propKey];
|
|
109
|
+
try {
|
|
110
|
+
hook.call(callContext);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('[absolutejs] onDestroy hook threw', err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} else if (typeof toCall === 'function') {
|
|
116
|
+
try {
|
|
117
|
+
(toCall as (this: unknown) => void).call(context);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error('[absolutejs] onDestroy hook threw', err);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/* Vendored from `processCleanups(tView, lView)`. Walks tView.cleanup which
|
|
126
|
+
* is laid out as either:
|
|
127
|
+
* [eventName(string), targetIdx, listenerIdx, indirectIdx, ...]
|
|
128
|
+
* — DOM event listener; lCleanup[indirectIdx] is the unregister fn
|
|
129
|
+
* (or, if indirectIdx is negative, lCleanup[-indirectIdx] is a
|
|
130
|
+
* Subscription whose .unsubscribe() we call)
|
|
131
|
+
* [hookFn(function), contextSlotIdx, ...]
|
|
132
|
+
* — directive output / cleanup callback; call hookFn with
|
|
133
|
+
* lCleanup[contextSlotIdx] as `this`
|
|
134
|
+
* Then walks lView[ON_DESTROY_HOOKS] (component-level destroy hooks,
|
|
135
|
+
* registered via `inject(DestroyRef).onDestroy(...)` etc.) and fires
|
|
136
|
+
* each one. */
|
|
137
|
+
export const processCleanups = (tView: TView, lView: LView): void => {
|
|
138
|
+
const tCleanup = tView.cleanup;
|
|
139
|
+
const lCleanup = lView[CLEANUP] as unknown[] | null;
|
|
140
|
+
|
|
141
|
+
if (tCleanup !== null && lCleanup !== null) {
|
|
142
|
+
for (let i = 0; i < tCleanup.length - 1; i += 2) {
|
|
143
|
+
const entry = tCleanup[i];
|
|
144
|
+
if (typeof entry === 'string') {
|
|
145
|
+
const targetIdx = tCleanup[i + 3] as number;
|
|
146
|
+
try {
|
|
147
|
+
if (targetIdx >= 0) {
|
|
148
|
+
(lCleanup[targetIdx] as () => void)();
|
|
149
|
+
} else {
|
|
150
|
+
(
|
|
151
|
+
lCleanup[-targetIdx] as { unsubscribe: () => void }
|
|
152
|
+
).unsubscribe();
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error('[absolutejs] DOM cleanup threw', err);
|
|
156
|
+
}
|
|
157
|
+
i += 2;
|
|
158
|
+
} else if (typeof entry === 'function') {
|
|
159
|
+
const ctxIdx = tCleanup[i + 1] as number;
|
|
160
|
+
try {
|
|
161
|
+
(entry as (this: unknown) => void).call(lCleanup[ctxIdx]);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error('[absolutejs] cleanup callback threw', err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (lCleanup !== null) {
|
|
170
|
+
lView[CLEANUP] = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const onDestroyHooks = lView[ON_DESTROY_HOOKS] as
|
|
174
|
+
| Array<() => void>
|
|
175
|
+
| null;
|
|
176
|
+
if (onDestroyHooks !== null) {
|
|
177
|
+
lView[ON_DESTROY_HOOKS] = null;
|
|
178
|
+
for (const hook of onDestroyHooks) {
|
|
179
|
+
try {
|
|
180
|
+
hook();
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error('[absolutejs] DestroyRef hook threw', err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/* Mark an LView as destroyed so any later
|
|
189
|
+
* destroyLView/cleanUpView no-ops it. Without this flag the LView
|
|
190
|
+
* could get walked twice (e.g. if Angular's tree-walk later finds
|
|
191
|
+
* a stale reference). */
|
|
192
|
+
export const markLViewDestroyed = (lView: LView): void => {
|
|
193
|
+
lView[FLAGS] = ((lView[FLAGS] as number) | LFLAG_DESTROYED) >>> 0;
|
|
194
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {} from '../../../../types/globals';
|
|
2
|
+
/* Vendored LView slot indices from `@angular/core`. The runtime represents
|
|
3
|
+
* each LView as a flat array; these constants name the structural slots
|
|
4
|
+
* before the per-template slots start at HEADER_OFFSET.
|
|
5
|
+
*
|
|
6
|
+
* Source: `node_modules/@angular/core/fesm2022/_effect-chunk2.mjs`
|
|
7
|
+
* (search for `const HOST = 0;`). These are NOT exported and Angular keeps
|
|
8
|
+
* them tightly held — but they have not shifted since the v9 ivy rewrite,
|
|
9
|
+
* so the maintenance cost is verifying once per Angular minor that
|
|
10
|
+
* `_effect-chunk2.mjs:HOST === 0` etc. still holds.
|
|
11
|
+
*
|
|
12
|
+
* If Angular reorders these, our LView traversal returns wrong slots
|
|
13
|
+
* (e.g. reading PARENT might yield CONTEXT). Symptom: per-component
|
|
14
|
+
* remount throws or silently swaps the wrong subtree. Verify at the
|
|
15
|
+
* top of `angularRemount.ts` via shape checks before doing anything
|
|
16
|
+
* destructive. */
|
|
17
|
+
|
|
18
|
+
export const HOST = 0;
|
|
19
|
+
export const TVIEW = 1;
|
|
20
|
+
export const FLAGS = 2;
|
|
21
|
+
export const PARENT = 3;
|
|
22
|
+
export const NEXT = 4;
|
|
23
|
+
export const T_HOST = 5;
|
|
24
|
+
export const HYDRATION = 6;
|
|
25
|
+
export const CLEANUP = 7;
|
|
26
|
+
export const CONTEXT = 8;
|
|
27
|
+
export const INJECTOR = 9;
|
|
28
|
+
export const ENVIRONMENT = 10;
|
|
29
|
+
export const RENDERER = 11;
|
|
30
|
+
export const CHILD_HEAD = 12;
|
|
31
|
+
export const CHILD_TAIL = 13;
|
|
32
|
+
export const DECLARATION_VIEW = 14;
|
|
33
|
+
export const DECLARATION_COMPONENT_VIEW = 15;
|
|
34
|
+
export const DECLARATION_LCONTAINER = 16;
|
|
35
|
+
export const PREORDER_HOOK_FLAGS = 17;
|
|
36
|
+
export const QUERIES = 18;
|
|
37
|
+
export const ID = 19;
|
|
38
|
+
export const EMBEDDED_VIEW_INJECTOR = 20;
|
|
39
|
+
export const ON_DESTROY_HOOKS = 21;
|
|
40
|
+
export const HEADER_OFFSET = 27;
|
|
41
|
+
|
|
42
|
+
/* LView FLAGS bitfield bits (from same source). We only care about
|
|
43
|
+
* the destroyed bit so that double-destroy is a no-op. */
|
|
44
|
+
export const LFLAG_DESTROYED = 256;
|