@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.
@@ -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 = 'angular:component-update';
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 { dispatchAngularComponentUpdate } from './handlers/angularHmrShim';
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;