@absolutejs/absolute 0.19.0-beta.751 → 0.19.0-beta.752
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.
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
"import type { StreamingSlot } from '../utils/streamingSlots';\n\ntype StreamingSlotRegistrar = (slot: StreamingSlot) => void;\ntype StreamingSlotWarningController = {\n\tmaybeWarn(primitiveName: string): void;\n};\ntype StreamingSlotCollectionController = {\n\tisCollecting(): boolean;\n};\n\nconst STREAMING_SLOT_REGISTRAR_KEY = Symbol.for(\n\t'absolutejs.streamingSlotRegistrar'\n);\nconst STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for(\n\t'absolutejs.streamingSlotWarningController'\n);\nconst STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for(\n\t'absolutejs.streamingSlotCollectionController'\n);\n\nconst getRegisteredStreamingSlotRegistrar = () => {\n\tconst value = Reflect.get(globalThis, STREAMING_SLOT_REGISTRAR_KEY);\n\tif (typeof value === 'function' || value === null) {\n\t\treturn value;\n\t}\n\n\treturn undefined;\n};\n\nconst isObjectRecord = (value: unknown): value is Record<string, unknown> =>\n\tBoolean(value) && typeof value === 'object';\n\nconst isStreamingSlotWarningController = (\n\tvalue: unknown\n): value is StreamingSlotWarningController =>\n\tisObjectRecord(value) &&\n\t'maybeWarn' in value &&\n\ttypeof value.maybeWarn === 'function';\n\nconst isStreamingSlotCollectionController = (\n\tvalue: unknown\n): value is StreamingSlotCollectionController =>\n\tisObjectRecord(value) &&\n\t'isCollecting' in value &&\n\ttypeof value.isCollecting === 'function';\n\nconst getWarningController = () => {\n\tconst value = Reflect.get(globalThis, STREAMING_SLOT_WARNING_STORAGE_KEY);\n\tif (value === null || typeof value === 'undefined') return undefined;\n\n\treturn isStreamingSlotWarningController(value) ? value : undefined;\n};\n\nconst getCollectionController = () => {\n\tconst value = Reflect.get(\n\t\tglobalThis,\n\t\tSTREAMING_SLOT_COLLECTION_STORAGE_KEY\n\t);\n\tif (value === null || typeof value === 'undefined') return undefined;\n\n\treturn isStreamingSlotCollectionController(value) ? value : undefined;\n};\n\nexport const hasRegisteredStreamingSlotRegistrar = () =>\n\ttypeof getRegisteredStreamingSlotRegistrar() === 'function';\nexport const isStreamingSlotCollectionActive = () =>\n\tgetCollectionController()?.isCollecting() === true;\nexport const registerStreamingSlot = (slot: StreamingSlot) => {\n\tgetRegisteredStreamingSlotRegistrar()?.(slot);\n};\nexport const setStreamingSlotCollectionController = (\n\tcontroller: StreamingSlotCollectionController | null\n) => {\n\tReflect.set(globalThis, STREAMING_SLOT_COLLECTION_STORAGE_KEY, controller);\n};\nexport const setStreamingSlotRegistrar = (\n\tnextRegistrar: StreamingSlotRegistrar | null\n) => {\n\tReflect.set(globalThis, STREAMING_SLOT_REGISTRAR_KEY, nextRegistrar);\n};\nexport const setStreamingSlotWarningController = (\n\tcontroller: StreamingSlotWarningController | null\n) => {\n\tReflect.set(globalThis, STREAMING_SLOT_WARNING_STORAGE_KEY, controller);\n};\nexport const warnMissingStreamingSlotCollector = (primitiveName: string) => {\n\tif (\n\t\tprocess.env.NODE_ENV === 'production' ||\n\t\tisStreamingSlotCollectionActive()\n\t) {\n\t\treturn;\n\t}\n\n\tgetWarningController()?.maybeWarn(primitiveName);\n};\n",
|
|
236
236
|
"import { InjectionToken } from '@angular/core';\n\nconst DEFAULT_DETERMINISTIC_SEED = 'absolute-angular';\nconst DEFAULT_DETERMINISTIC_NOW = 0;\nconst HASH_MULTIPLIER = 31;\nconst FNV_OFFSET_BASIS = 2166136261;\nconst FNV_PRIME = 16777619;\nconst XORSHIFT_LEFT_1 = 13;\nconst XORSHIFT_LEFT_2 = 5;\nconst XORSHIFT_RIGHT = 17;\nconst UINT32_MAX = 0x100000000;\n\nexport type DeterministicRandom = () => number;\n\nexport type DeterministicEnvOptions = {\n\tnow?: Date | number | string;\n\tseed?: number | string;\n};\n\nexport const DETERMINISTIC_NOW = new InjectionToken<number>(\n\t'DETERMINISTIC_NOW'\n);\nexport const DETERMINISTIC_RANDOM = new InjectionToken<DeterministicRandom>(\n\t'DETERMINISTIC_RANDOM'\n);\nexport const DETERMINISTIC_SEED = new InjectionToken<string>(\n\t'DETERMINISTIC_SEED'\n);\n\nconst hashSeed = (seed: number | string) => {\n\tconst seedText = String(seed);\n\tlet hash = FNV_OFFSET_BASIS;\n\n\tfor (const char of seedText) {\n\t\thash = Math.imul(hash ^ char.charCodeAt(0), FNV_PRIME);\n\t}\n\n\treturn hash >>> 0 || HASH_MULTIPLIER;\n};\n\nexport const createDeterministicRandom = (\n\tseed: number | string = DEFAULT_DETERMINISTIC_SEED\n) => {\n\tlet state = hashSeed(seed);\n\n\treturn () => {\n\t\tstate ^= state << XORSHIFT_LEFT_1;\n\t\tstate ^= state >>> XORSHIFT_RIGHT;\n\t\tstate ^= state << XORSHIFT_LEFT_2;\n\n\t\treturn (state >>> 0) / UINT32_MAX;\n\t};\n};\n\nconst normalizeNow = (now: Date | number | string | undefined) => {\n\tif (now instanceof Date) return now.getTime();\n\tif (typeof now === 'string') return new Date(now).getTime();\n\tif (typeof now === 'number') return now;\n\n\treturn DEFAULT_DETERMINISTIC_NOW;\n};\n\nexport const provideDeterministicEnv = (\n\toptions: DeterministicEnvOptions = {}\n) => {\n\tconst seed = String(options.seed ?? DEFAULT_DETERMINISTIC_SEED);\n\tconst now = normalizeNow(options.now);\n\n\treturn [\n\t\t{ provide: DETERMINISTIC_SEED, useValue: seed },\n\t\t{ provide: DETERMINISTIC_NOW, useValue: now },\n\t\t{\n\t\t\tdeps: [DETERMINISTIC_SEED],\n\t\t\tprovide: DETERMINISTIC_RANDOM,\n\t\t\tuseFactory: createDeterministicRandom\n\t\t}\n\t];\n};\n",
|
|
237
237
|
"import type { Type } from '@angular/core';\nimport type { AngularPageDefinition } from '../../types/angular';\n\nexport const defineAngularPage = <\n\tProps extends Record<string, unknown> = Record<never, never>\n>(definition: {\n\tcomponent: Type<unknown>;\n}) => definition as AngularPageDefinition<Props>;\n",
|
|
238
|
-
"/* preserveAcrossHmr — keep service AND component instance state alive\n across full Angular re-bootstraps in dev mode.\n\n Why this exists: most HMR updates use fast-patch (in-place prototype\n swap) and never destroy the running app, so instance state is never\n lost. But some changes — route definitions, providers, brand-new\n components, anything in a `providedIn: 'root'` provider list —\n require the HMR client to fall back to a full re-bootstrap. That\n destroys every service and component instance, and the new ones\n start with class field initializers (e.g. `idToken: null`,\n `searchQuery: ''`). Anything held in memory (auth tokens, cached\n query results, form input values, scroll-positioned filters) gets\n wiped, even though the underlying source of truth (Firebase session,\n the URL, the user's intent) hasn't changed.\n\n Usage — opt in once per class:\n\n // Service (singleton, no key needed)\n @Injectable({ providedIn: 'root' })\n export class AuthService {\n idToken: string | null = null;\n\n constructor() {\n preserveAcrossHmr(this);\n }\n }\n\n // Component (one instance per route, no key needed)\n @Component({ ... })\n export class AdminProfilesComponent {\n searchQuery = '';\n currentPage = 0;\n\n constructor() {\n preserveAcrossHmr(this);\n }\n }\n\n // Component with multiple instances on the same page —\n // pass a key derived from @Input. Use ngOnInit because Angular\n // sets @Input properties between constructor and ngOnInit.\n @Component({ ... })\n export class ItemRowComponent implements OnInit {\n @Input() id!: string;\n expanded = false;\n\n ngOnInit() {\n preserveAcrossHmr(this, this.id);\n }\n }\n\n How it works:\n 1. Every call registers the instance for *future* capture (a\n WeakRef so we don't leak), independent of the HMR cycle.\n 2. If an HMR full re-bootstrap is in progress, restores the\n cached preservable own properties onto the new instance.\n 3. Right before the next destroy (called from the HMR client),\n every tracked instance's preservable own properties are\n snapshotted into the cache, keyed by `${className}:${key}`.\n\n Cache lifetime — scoped to the HMR cycle:\n - capture() flips `rebootInProgress` to true, populates cache.\n - bootstrap completes → flag flipped back to false.\n - Outside an HMR reboot, restoration is a no-op. So navigating\n away and back to a route with a previously-captured component\n gets fresh state, not stale state from the last HMR.\n\n Repeated components without a key:\n If two instances of the same class register under the same key\n during a single capture, a console warning fires. The cache\n entry holds the LAST one captured; first-mounting instance wins\n on restore. Pass an explicit `key` (per-row id, etc.) for\n deterministic preservation of multiple instances.\n\n What gets preserved:\n Only primitives, plain `{}` objects, and arrays of those. Class\n instances (HttpClient, BehaviorSubject, Date, Map…) and\n functions are excluded — the new instance must get those from\n its own injector / construction. See `isPreservable` below.\n\n Don't expect this to preserve `@Input`-bound properties: the\n parent component is the source of truth and will overwrite via\n change detection on the new render anyway.\n\n Production: in non-dev (`__DEV__` is undefined or false), the\n helper is a no-op. The cache and tracker only exist in dev. */\n\nimport { ChangeDetectorRef, inject } from '@angular/core';\n\ntype StateCache = Map<string, Record<string, unknown>>;\ntype InstanceTracker = Set<WeakRef<object>>;\ntype InstanceKeyMap = WeakMap<object, string>;\ntype RebootFlag = { value: boolean };\ntype RebootStats = { captured: number; restoredKeys: Set<string> };\n\ntype PreserveScope = typeof globalThis & {\n\t__ABS_HMR_INSTANCE_STATE__?: StateCache;\n\t__ABS_HMR_TRACKED_INSTANCES__?: InstanceTracker;\n\t__ABS_HMR_INSTANCE_KEYS__?: InstanceKeyMap;\n\t__ABS_HMR_REBOOT_IN_PROGRESS__?: RebootFlag;\n\t__ABS_HMR_REBOOT_STATS__?: RebootStats;\n};\n\nconst isDev = (): boolean => {\n\t// SSR safety: globalThis on the server is process-wide and shared\n\t// across requests, so writing to the preservation cache during SSR\n\t// would leak request state between users. Gate strictly on the\n\t// presence of a browser `window` *and* a dev signal — neither is\n\t// true in a production build, so this is a hard no-op there too.\n\tif (typeof window === 'undefined') return false;\n\t// Angular's `ngDevMode` is an object in dev (with hydration counters,\n\t// etc.) and `undefined` in production. Treat any truthy value as dev.\n\tconst scope = globalThis as { __DEV__?: unknown; ngDevMode?: unknown };\n\n\treturn Boolean(scope.__DEV__) || Boolean(scope.ngDevMode);\n};\n\nconst getCache = (): StateCache => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_INSTANCE_STATE__ ??= new Map());\n};\n\nconst getTracker = (): InstanceTracker => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_TRACKED_INSTANCES__ ??= new Set());\n};\n\nconst getKeyMap = (): InstanceKeyMap => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_INSTANCE_KEYS__ ??= new WeakMap());\n};\n\nconst getRebootFlag = (): RebootFlag => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_REBOOT_IN_PROGRESS__ ??= { value: false });\n};\n\nconst getRebootStats = (): RebootStats => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_REBOOT_STATS__ ??= {\n\t\tcaptured: 0,\n\t\trestoredKeys: new Set()\n\t});\n};\n\n/* Determine whether a value is safe to preserve across an HMR full\n re-bootstrap. We snapshot the OLD app's instance state into a cache\n and copy it back onto the NEW instance — but holding references to\n the OLD app's Angular-injected services (HttpClient, ApplicationRef,\n subscriptions tied to the destroyed injector, etc.) and restoring\n them onto the new instance would corrupt the new app: the new\n `HttpClient` from the new injector would be replaced by a stale ref\n pointing at a destroyed graph.\n We accept primitives, plain `{}` objects, and arrays of those, which\n covers the common cases (auth tokens, cached query results, search\n queries, pagination state) without leaking Angular-injected\n dependencies, RxJS subjects, DOM nodes, or other live references. */\nconst isPreservable = (value: unknown, depth = 0): boolean => {\n\tif (depth > 8) return false;\n\tif (value === null || value === undefined) return true;\n\tconst t = typeof value;\n\tif (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint')\n\t\treturn true;\n\tif (t === 'function' || t === 'symbol') return false;\n\tif (Array.isArray(value)) {\n\t\treturn value.every((item) => isPreservable(item, depth + 1));\n\t}\n\tif (t === 'object') {\n\t\tconst proto = Object.getPrototypeOf(value);\n\t\t// Only POJOs — class instances (HttpClient, BehaviorSubject, Date,\n\t\t// Map, etc.) carry runtime identity that the new instance must get\n\t\t// from its own injector / construction.\n\t\tif (proto !== Object.prototype && proto !== null) return false;\n\n\t\treturn Object.values(value as object).every((v) =>\n\t\t\tisPreservable(v, depth + 1)\n\t\t);\n\t}\n\n\treturn false;\n};\n\nconst buildCacheKey = (instance: object, key?: unknown): string | null => {\n\tconst className = instance.constructor?.name;\n\tif (!className || className === 'Object') return null;\n\tconst suffix = key === undefined || key === null ? '' : String(key);\n\n\treturn `${className}:${suffix}`;\n};\n\nconst restoreFromCache = (instance: object, key: string) => {\n\tconst cache = getCache();\n\tconst stored = cache.get(key);\n\tif (!stored) return;\n\n\tfor (const [prop, value] of Object.entries(stored)) {\n\t\ttry {\n\t\t\t(instance as Record<string, unknown>)[prop] = value;\n\t\t} catch {\n\t\t\t/* property is non-writable / has a setter that threw — skip */\n\t\t}\n\t}\n\n\tgetRebootStats().restoredKeys.add(key);\n\n\t// OnPush components don't see direct property assignments — they\n\t// only re-check on `markForCheck()`. The restoration above happens\n\t// before the first CD pass on this instance, so on the *initial*\n\t// render OnPush is fine; but if `preserveAcrossHmr` is called from\n\t// `ngOnInit` (the keyed-component path), the parent's CD pass may\n\t// have already painted defaults. Schedule a markForCheck so the\n\t// restored values are visible without the user remembering to do\n\t// it themselves.\n\t// `inject()` requires an active injection context — true inside\n\t// constructors, factories, and `runInInjectionContext` blocks. It\n\t// throws otherwise (e.g. called from a lifecycle hook), in which\n\t// case we skip cleanly; the component's own first CD pass will\n\t// pick up the values anyway.\n\ttry {\n\t\tconst cdr = inject(ChangeDetectorRef, { optional: true });\n\t\tif (cdr) queueMicrotask(() => cdr.markForCheck());\n\t} catch {\n\t\t/* outside injection context / no CDR available — fine */\n\t}\n};\n\n/** Mark a service or component instance for state preservation across\n * full Angular HMR re-bootstraps. Call once from the constructor or\n * `ngOnInit`. Safe in production (no-op outside dev mode).\n *\n * @param instance Usually `this`. The class name is used as part of\n * the cache key.\n * @param key Optional discriminator when multiple instances of the\n * same class can be alive at once (rows, tabs, etc). Coerced\n * to string. Use `ngOnInit` to call this when the key depends\n * on `@Input` values, since Angular sets inputs between\n * constructor and ngOnInit. */\nexport const preserveAcrossHmr = (\n\tinstance: object,\n\tkey?: string | number\n): void => {\n\tif (!isDev()) return;\n\n\tconst fullKey = buildCacheKey(instance, key);\n\tif (fullKey === null) return;\n\n\t// Always register for future capture — independent of whether a\n\t// reboot is currently in progress. The next capture cycle will\n\t// snapshot whatever's in the tracker. Store the key alongside the\n\t// instance via WeakMap so capture knows which cache slot to use\n\t// without instance pollution.\n\tgetTracker().add(new WeakRef(instance));\n\tgetKeyMap().set(instance, fullKey);\n\n\t// Restoration is HMR-cycle-scoped. Outside an active reboot, the\n\t// new instance keeps its class-field defaults; we don't want stale\n\t// state from a previous HMR to leak into normal navigations.\n\tif (getRebootFlag().value) {\n\t\trestoreFromCache(instance, fullKey);\n\t}\n};\n\n/** Snapshot every tracked instance's preservable own properties into\n * the cache and flip the reboot flag on. Called by the HMR client\n * right before `destroyAngularApp()`. Properties that aren't\n * preservable (Angular-injected services, Subjects, class instances)\n * are skipped — the new instance gets fresh ones from its new\n * injector, just like at first bootstrap. */\nexport const captureTrackedInstanceStates = (): void => {\n\tif (!isDev()) return;\n\n\tconst cache = getCache();\n\tconst tracker = getTracker();\n\tconst keyMap = getKeyMap();\n\tconst stats = getRebootStats();\n\tconst dead: WeakRef<object>[] = [];\n\tconst seen = new Set<string>();\n\n\tcache.clear();\n\tstats.restoredKeys.clear();\n\tstats.captured = 0;\n\n\tfor (const ref of tracker) {\n\t\tconst instance = ref.deref();\n\t\tif (!instance) {\n\t\t\tdead.push(ref);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst fullKey = keyMap.get(instance) ?? buildCacheKey(instance);\n\t\tif (fullKey === null) continue;\n\n\t\t// Warn when two instances would collide on the same cache slot\n\t\t// (same className with no key, or duplicate user-supplied keys).\n\t\t// On collision the second instance's state silently overwrites\n\t\t// the first — pass an explicit `key` to differentiate.\n\t\tif (seen.has(fullKey)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[HMR] preserveAcrossHmr collision on \"${fullKey}\". Two instances would use the same cache slot — the later one will overwrite the earlier one's state on full re-bootstrap. Pass a unique \\`key\\` argument (e.g. an @Input id) to differentiate.`\n\t\t\t);\n\t\t}\n\t\tseen.add(fullKey);\n\n\t\tconst props: Record<string, unknown> = {};\n\t\tfor (const prop of Object.keys(instance)) {\n\t\t\tconst value = (instance as Record<string, unknown>)[prop];\n\t\t\tif (isPreservable(value)) props[prop] = value;\n\t\t}\n\t\tcache.set(fullKey, props);\n\t\tstats.captured++;\n\t}\n\n\tdead.forEach((ref) => tracker.delete(ref));\n\n\tgetRebootFlag().value = true;\n};\n\n/** Clear the active-reboot flag and emit a one-line summary so\n * developers can see at-a-glance which classes had state preserved.\n * Helps surface the existence of `preserveAcrossHmr` to anyone whose\n * state was reset because they hadn't opted in.\n * Called by the HMR client after the new app has finished\n * bootstrapping (success or failure — wrap in `try/finally`). After\n * this, `preserveAcrossHmr` calls will track but won't restore — so\n * navigating to a route after HMR doesn't resurrect stale state from\n * the last reboot. */\nexport const endHmrReboot = (): void => {\n\tif (!isDev()) return;\n\tgetRebootFlag().value = false;\n\n\tconst stats = getRebootStats();\n\tif (stats.captured > 0) {\n\t\tconst restored = Array.from(stats.restoredKeys)\n\t\t\t.map((k) => k.replace(/:$/, ''))\n\t\t\t.sort();\n\t\tconsole.info(\n\t\t\t`[HMR] Full re-bootstrap: restored state for ${restored.length}/${stats.captured} tracked instance(s)${\n\t\t\t\trestored.length > 0 ? ` — ${restored.join(', ')}` : ''\n\t\t\t}. Components without preservation reset to defaults; opt in via \\`preserveAcrossHmr(this)\\`.`\n\t\t);\n\t}\n};\n",
|
|
238
|
+
"/* preserveAcrossHmr — keep service AND component instance state alive\n across full Angular re-bootstraps in dev mode.\n\n Why this exists: most HMR updates use fast-patch (in-place prototype\n swap) and never destroy the running app, so instance state is never\n lost. But some changes — route definitions, providers, brand-new\n components, anything in a `providedIn: 'root'` provider list —\n require the HMR client to fall back to a full re-bootstrap. That\n destroys every service and component instance, and the new ones\n start with class field initializers (e.g. `idToken: null`,\n `searchQuery: ''`). Anything held in memory (auth tokens, cached\n query results, form input values, scroll-positioned filters) gets\n wiped, even though the underlying source of truth (Firebase session,\n the URL, the user's intent) hasn't changed.\n\n Usage — opt in once per class:\n\n // Service (singleton, no key needed)\n @Injectable({ providedIn: 'root' })\n export class AuthService {\n idToken: string | null = null;\n\n constructor() {\n preserveAcrossHmr(this);\n }\n }\n\n // Component (one instance per route, no key needed)\n @Component({ ... })\n export class AdminProfilesComponent {\n searchQuery = '';\n currentPage = 0;\n\n constructor() {\n preserveAcrossHmr(this);\n }\n }\n\n // Component with multiple instances on the same page —\n // pass a key derived from @Input. Use ngOnInit because Angular\n // sets @Input properties between constructor and ngOnInit.\n @Component({ ... })\n export class ItemRowComponent implements OnInit {\n @Input() id!: string;\n expanded = false;\n\n ngOnInit() {\n preserveAcrossHmr(this, this.id);\n }\n }\n\n How it works:\n 1. Every call registers the instance for *future* capture (a\n WeakRef so we don't leak), independent of the HMR cycle.\n 2. If an HMR full re-bootstrap is in progress, restores the\n cached preservable own properties onto the new instance.\n 3. Right before the next destroy (called from the HMR client),\n every tracked instance's preservable own properties are\n snapshotted into the cache, keyed by `${className}:${key}`.\n\n Cache lifetime — scoped to the HMR cycle:\n - capture() flips `rebootInProgress` to true, populates cache.\n - bootstrap completes → flag flipped back to false.\n - Outside an HMR reboot, restoration is a no-op. So navigating\n away and back to a route with a previously-captured component\n gets fresh state, not stale state from the last HMR.\n\n Repeated components without a key:\n If two instances of the same class register under the same key\n during a single capture, a console warning fires. The cache\n entry holds the LAST one captured; first-mounting instance wins\n on restore. Pass an explicit `key` (per-row id, etc.) for\n deterministic preservation of multiple instances.\n\n What gets preserved:\n Only primitives, plain `{}` objects, and arrays of those. Class\n instances (HttpClient, BehaviorSubject, Date, Map…) and\n functions are excluded — the new instance must get those from\n its own injector / construction. See `isPreservable` below.\n\n Don't expect this to preserve `@Input`-bound properties: the\n parent component is the source of truth and will overwrite via\n change detection on the new render anyway.\n\n Production: in non-dev (`__DEV__` is undefined or false), the\n helper is a no-op. The cache and tracker only exist in dev. */\n\nimport { ChangeDetectorRef, inject } from '@angular/core';\n\ntype StateCache = Map<string, Record<string, unknown>>;\ntype InstanceTracker = Set<WeakRef<object>>;\ntype InstanceKeyMap = WeakMap<object, string>;\ntype RebootFlag = { value: boolean };\ntype RebootStats = { captured: number; restoredKeys: Set<string> };\n\ntype PreserveScope = typeof globalThis & {\n\t__ABS_HMR_INSTANCE_STATE__?: StateCache;\n\t__ABS_HMR_TRACKED_INSTANCES__?: InstanceTracker;\n\t__ABS_HMR_INSTANCE_KEYS__?: InstanceKeyMap;\n\t__ABS_HMR_REBOOT_IN_PROGRESS__?: RebootFlag;\n\t__ABS_HMR_REBOOT_STATS__?: RebootStats;\n};\n\nconst isDev = (): boolean => {\n\t// SSR safety: globalThis on the server is process-wide and shared\n\t// across requests, so writing to the preservation cache during SSR\n\t// would leak request state between users. Gate strictly on the\n\t// presence of a browser `window` *and* a dev signal — neither is\n\t// true in a production build, so this is a hard no-op there too.\n\tif (typeof window === 'undefined') return false;\n\t// Angular's `ngDevMode` is an object in dev (with hydration counters,\n\t// etc.) and `undefined` in production. Treat any truthy value as dev.\n\tconst scope = globalThis as { __DEV__?: unknown; ngDevMode?: unknown };\n\n\treturn Boolean(scope.__DEV__) || Boolean(scope.ngDevMode);\n};\n\nconst getCache = (): StateCache => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_INSTANCE_STATE__ ??= new Map());\n};\n\nconst getTracker = (): InstanceTracker => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_TRACKED_INSTANCES__ ??= new Set());\n};\n\nconst getKeyMap = (): InstanceKeyMap => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_INSTANCE_KEYS__ ??= new WeakMap());\n};\n\nconst getRebootFlag = (): RebootFlag => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_REBOOT_IN_PROGRESS__ ??= { value: false });\n};\n\nconst getRebootStats = (): RebootStats => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_REBOOT_STATS__ ??= {\n\t\tcaptured: 0,\n\t\trestoredKeys: new Set()\n\t});\n};\n\n/* Determine whether a value is safe to preserve across an HMR full\n re-bootstrap. We snapshot the OLD app's instance state into a cache\n and copy it back onto the NEW instance — but holding references to\n the OLD app's Angular-injected services (HttpClient, ApplicationRef,\n subscriptions tied to the destroyed injector, etc.) and restoring\n them onto the new instance would corrupt the new app: the new\n `HttpClient` from the new injector would be replaced by a stale ref\n pointing at a destroyed graph.\n We accept primitives, plain `{}` objects, and arrays of those, which\n covers the common cases (auth tokens, cached query results, search\n queries, pagination state) without leaking Angular-injected\n dependencies, RxJS subjects, DOM nodes, or other live references. */\nconst isPreservable = (value: unknown, depth = 0): boolean => {\n\tif (depth > 8) return false;\n\tif (value === null || value === undefined) return true;\n\tconst t = typeof value;\n\tif (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint')\n\t\treturn true;\n\tif (t === 'function' || t === 'symbol') return false;\n\tif (Array.isArray(value)) {\n\t\treturn value.every((item) => isPreservable(item, depth + 1));\n\t}\n\tif (t === 'object') {\n\t\tconst proto = Object.getPrototypeOf(value);\n\t\t// Only POJOs — class instances (HttpClient, BehaviorSubject, Date,\n\t\t// Map, etc.) carry runtime identity that the new instance must get\n\t\t// from its own injector / construction.\n\t\tif (proto !== Object.prototype && proto !== null) return false;\n\n\t\treturn Object.values(value as object).every((v) =>\n\t\t\tisPreservable(v, depth + 1)\n\t\t);\n\t}\n\n\treturn false;\n};\n\nconst buildCacheKey = (instance: object, key?: unknown): string | null => {\n\tconst className = instance.constructor?.name;\n\tif (!className || className === 'Object') return null;\n\tconst suffix = key === undefined || key === null ? '' : String(key);\n\n\treturn `${className}:${suffix}`;\n};\n\nconst restoreFromCache = (instance: object, key: string) => {\n\tconst cache = getCache();\n\tconst stored = cache.get(key);\n\tif (!stored) return;\n\n\tfor (const [prop, value] of Object.entries(stored)) {\n\t\ttry {\n\t\t\t(instance as Record<string, unknown>)[prop] = value;\n\t\t} catch {\n\t\t\t/* property is non-writable / has a setter that threw — skip */\n\t\t}\n\t}\n\n\tgetRebootStats().restoredKeys.add(key);\n\n\t// OnPush components don't see direct property assignments — they\n\t// only re-check on `markForCheck()`. The restoration above happens\n\t// before the first CD pass on this instance, so on the *initial*\n\t// render OnPush is fine; but if `preserveAcrossHmr` is called from\n\t// `ngOnInit` (the keyed-component path), the parent's CD pass may\n\t// have already painted defaults. Schedule a markForCheck so the\n\t// restored values are visible without the user remembering to do\n\t// it themselves.\n\t// `inject()` requires an active injection context — true inside\n\t// constructors, factories, and `runInInjectionContext` blocks. It\n\t// throws otherwise (e.g. called from a lifecycle hook), in which\n\t// case we skip cleanly; the component's own first CD pass will\n\t// pick up the values anyway.\n\ttry {\n\t\tconst cdr = inject(ChangeDetectorRef, { optional: true });\n\t\tif (cdr) queueMicrotask(() => cdr.markForCheck());\n\t} catch {\n\t\t/* outside injection context / no CDR available — fine */\n\t}\n};\n\n/** Mark a service or component instance for state preservation across\n * full Angular HMR re-bootstraps. Call once from the constructor or\n * `ngOnInit`. Safe in production (no-op outside dev mode).\n *\n * @param instance Usually `this`. The class name is used as part of\n * the cache key.\n * @param key Optional discriminator when multiple instances of the\n * same class can be alive at once (rows, tabs, etc). Coerced\n * to string. Use `ngOnInit` to call this when the key depends\n * on `@Input` values, since Angular sets inputs between\n * constructor and ngOnInit. */\nexport const preserveAcrossHmr = (\n\tinstance: object,\n\tkey?: string | number\n): void => {\n\tif (!isDev()) return;\n\n\tconst fullKey = buildCacheKey(instance, key);\n\tif (fullKey === null) return;\n\n\t// Always register for future capture — independent of whether a\n\t// reboot is currently in progress. The next capture cycle will\n\t// snapshot whatever's in the tracker. Store the key alongside the\n\t// instance via WeakMap so capture knows which cache slot to use\n\t// without instance pollution.\n\tgetTracker().add(new WeakRef(instance));\n\tgetKeyMap().set(instance, fullKey);\n\n\t// Restoration is HMR-cycle-scoped. Outside an active reboot, the\n\t// new instance keeps its class-field defaults; we don't want stale\n\t// state from a previous HMR to leak into normal navigations.\n\tif (getRebootFlag().value) {\n\t\trestoreFromCache(instance, fullKey);\n\t}\n};\n\n/** Snapshot every tracked instance's preservable own properties into\n * the cache and flip the reboot flag on. Called by the HMR client\n * right before `destroyAngularApp()`. Properties that aren't\n * preservable (Angular-injected services, Subjects, class instances)\n * are skipped — the new instance gets fresh ones from its new\n * injector, just like at first bootstrap. */\nexport const captureTrackedInstanceStates = (): void => {\n\tif (!isDev()) return;\n\n\tconst cache = getCache();\n\tconst tracker = getTracker();\n\tconst keyMap = getKeyMap();\n\tconst stats = getRebootStats();\n\tconst seen = new Set<string>();\n\n\tcache.clear();\n\tstats.restoredKeys.clear();\n\tstats.captured = 0;\n\n\tfor (const ref of tracker) {\n\t\tconst instance = ref.deref();\n\t\t// Skip already-GC'd refs. We don't bother bookkeeping a \"dead\"\n\t\t// list because the entire tracker is cleared after this loop.\n\t\tif (!instance) continue;\n\n\t\tconst fullKey = keyMap.get(instance) ?? buildCacheKey(instance);\n\t\tif (fullKey === null) continue;\n\n\t\t// Warn when two instances would collide on the same cache slot\n\t\t// (same className with no key, or duplicate user-supplied keys).\n\t\t// On collision the second instance's state silently overwrites\n\t\t// the first — pass an explicit `key` to differentiate.\n\t\tif (seen.has(fullKey)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[HMR] preserveAcrossHmr collision on \"${fullKey}\". Two instances would use the same cache slot — the later one will overwrite the earlier one's state on full re-bootstrap. Pass a unique \\`key\\` argument (e.g. an @Input id) to differentiate.`\n\t\t\t);\n\t\t}\n\t\tseen.add(fullKey);\n\n\t\tconst props: Record<string, unknown> = {};\n\t\tfor (const prop of Object.keys(instance)) {\n\t\t\tconst value = (instance as Record<string, unknown>)[prop];\n\t\t\tif (isPreservable(value)) props[prop] = value;\n\t\t}\n\t\tcache.set(fullKey, props);\n\t\tstats.captured++;\n\t}\n\n\t// Every instance just captured is about to die: `destroyAngularApp()`\n\t// runs immediately after this and tears down the root injector. New\n\t// instances from the next bootstrap repopulate the tracker via their\n\t// own `preserveAcrossHmr(this)` calls. If we left existing WeakRefs\n\t// in place, the JS engine often won't have GC'd the old objects yet\n\t// at the next capture — `weakRef.deref()` would return zombies that\n\t// inflate the captured count and trigger spurious collision warnings\n\t// (since both the prior generation's zombie and the new instance map\n\t// to the same `${className}:` slot).\n\ttracker.clear();\n\n\tgetRebootFlag().value = true;\n};\n\n/** Clear the active-reboot flag and emit a one-line summary so\n * developers can see at-a-glance which classes had state preserved.\n * Helps surface the existence of `preserveAcrossHmr` to anyone whose\n * state was reset because they hadn't opted in.\n * Called by the HMR client after the new app has finished\n * bootstrapping (success or failure — wrap in `try/finally`). After\n * this, `preserveAcrossHmr` calls will track but won't restore — so\n * navigating to a route after HMR doesn't resurrect stale state from\n * the last reboot. */\nexport const endHmrReboot = (): void => {\n\tif (!isDev()) return;\n\tgetRebootFlag().value = false;\n\n\tconst stats = getRebootStats();\n\tif (stats.captured > 0) {\n\t\tconst restored = Array.from(stats.restoredKeys)\n\t\t\t.map((k) => k.replace(/:$/, ''))\n\t\t\t.sort();\n\t\tconsole.info(\n\t\t\t`[HMR] Full re-bootstrap: restored state for ${restored.length}/${stats.captured} tracked instance(s)${\n\t\t\t\trestored.length > 0 ? ` — ${restored.join(', ')}` : ''\n\t\t\t}. Components without preservation reset to defaults; opt in via \\`preserveAcrossHmr(this)\\`.`\n\t\t);\n\t}\n};\n",
|
|
239
239
|
"import '@angular/compiler';\nimport {\n\ttype AfterViewInit,\n\tComponent,\n\tElementRef,\n\tInput,\n\ttype OnChanges,\n\tViewChild\n} from '@angular/core';\nimport type * as i0 from '@angular/core';\nimport type { RuntimeIslandRenderProps } from '../../types/island';\nimport { preserveIslandMarkup } from '../client/preserveIslandMarkup';\nimport { serializeIslandAttributes } from '../core/islandMarkupAttributes';\n\n@Component({\n\tselector: 'absolute-island',\n\tstandalone: true,\n\ttemplate: '<div #container [attr.ngSkipHydration]=\"true\"></div>'\n})\nexport class Island implements AfterViewInit, OnChanges {\n\t@Input() component = '';\n\t@Input() framework: RuntimeIslandRenderProps['framework'] = 'react';\n\t@Input() hydrate: RuntimeIslandRenderProps['hydrate'] = 'load';\n\t@Input() props: RuntimeIslandRenderProps['props'] = {};\n\n\tdeclare static ɵfac: i0.ɵɵFactoryDeclaration<Island, never>;\n\tdeclare static ɵcmp: i0.ɵɵComponentDeclaration<\n\t\tIsland,\n\t\t'absolute-island',\n\t\tnever,\n\t\t{\n\t\t\tcomponent: { alias: 'component'; required: false };\n\t\t\tframework: { alias: 'framework'; required: false };\n\t\t\thydrate: { alias: 'hydrate'; required: false };\n\t\t\tprops: { alias: 'props'; required: false };\n\t\t},\n\t\tRecord<string, string>,\n\t\tnever,\n\t\tnever,\n\t\ttrue,\n\t\tnever\n\t>;\n\n\t@ViewChild('container', { static: true })\n\tprivate readonly container?: ElementRef<HTMLElement>;\n\n\tprivate markup = '';\n\n\tngOnChanges() {\n\t\tconst runtimeProps = {\n\t\t\tcomponent: this.component,\n\t\t\tframework: this.framework,\n\t\t\thydrate: this.hydrate,\n\t\t\tprops: this.props\n\t\t} satisfies RuntimeIslandRenderProps;\n\t\tconst { attributes, innerHTML } = preserveIslandMarkup(runtimeProps);\n\n\t\tthis.markup = `<div ${serializeIslandAttributes(attributes)}>${innerHTML}</div>`;\n\t\tthis.applyMarkup();\n\t}\n\n\tngAfterViewInit() {\n\t\tthis.applyMarkup();\n\t}\n\n\tprivate applyMarkup() {\n\t\tconst container = this.container?.nativeElement;\n\t\tif (!container) return;\n\t\tif (container.innerHTML === this.markup) return;\n\t\tcontainer.innerHTML = this.markup;\n\t}\n}\n",
|
|
240
240
|
"import type { RuntimeIslandRenderProps } from '../../types/island';\nimport { getIslandMarkerAttributes } from '../core/islandMarkupAttributes';\n\ntype PreservedIslandMarkup = {\n\tattributes: Record<string, string>;\n\tinnerHTML: string;\n};\n\ntype IslandMarkerElement = HTMLElement & {\n\tdataset: DOMStringMap & {\n\t\tcomponent?: string;\n\t\tframework?: string;\n\t\thydrate?: string;\n\t\tisland?: string;\n\t\tislandId?: string;\n\t\tprops?: string;\n\t};\n};\n\nconst getClaimMap = () => {\n\tif (typeof window === 'undefined') {\n\t\treturn null;\n\t}\n\n\twindow.__ABS_CLAIMED_ISLAND_MARKUP__ ??= new Map<string, number>();\n\n\treturn window.__ABS_CLAIMED_ISLAND_MARKUP__;\n};\n\nconst getSnapshotMap = () => {\n\tif (typeof window === 'undefined') {\n\t\treturn null;\n\t}\n\n\twindow.__ABS_SERVER_ISLAND_HTML__ ??= new Map<\n\t\tstring,\n\t\tPreservedIslandMarkup[]\n\t>();\n\n\treturn window.__ABS_SERVER_ISLAND_HTML__;\n};\n\nconst getIslandSignature = (props: RuntimeIslandRenderProps) => {\n\tconst attributes = getIslandMarkerAttributes(props);\n\n\treturn [\n\t\tattributes['data-component'],\n\t\tattributes['data-framework'],\n\t\tattributes['data-hydrate'],\n\t\tattributes['data-props']\n\t].join('::');\n};\n\nconst isMatchingIslandElement = (\n\telement: Element,\n\tprops: RuntimeIslandRenderProps\n): element is IslandMarkerElement => {\n\tif (!(element instanceof HTMLElement)) {\n\t\treturn false;\n\t}\n\n\tconst attributes = getIslandMarkerAttributes(props);\n\n\treturn (\n\t\telement.dataset.island === 'true' &&\n\t\telement.dataset.component === attributes['data-component'] &&\n\t\telement.dataset.framework === attributes['data-framework'] &&\n\t\t(element.dataset.hydrate ?? 'load') === attributes['data-hydrate'] &&\n\t\t(element.dataset.props ?? '{}') === attributes['data-props']\n\t);\n};\n\nconst snapshotIslandElement = (\n\telement: HTMLElement,\n\tsnapshotMap: Map<string, PreservedIslandMarkup[]>\n) => {\n\tconst signature = [\n\t\telement.dataset.component,\n\t\telement.dataset.framework,\n\t\telement.dataset.hydrate ?? 'load',\n\t\telement.dataset.props ?? '{}'\n\t].join('::');\n\tconst existing = snapshotMap.get(signature) ?? [];\n\tconst attributes = Object.fromEntries(\n\t\telement\n\t\t\t.getAttributeNames()\n\t\t\t.map((name) => [name, element.getAttribute(name) ?? ''])\n\t);\n\texisting.push({\n\t\tattributes,\n\t\tinnerHTML: element.innerHTML\n\t});\n\tsnapshotMap.set(signature, existing);\n};\n\nexport const initializeIslandMarkupSnapshot = () => {\n\tif (typeof document === 'undefined') {\n\t\treturn;\n\t}\n\n\tconst snapshotMap = getSnapshotMap();\n\tif (!snapshotMap || snapshotMap.size > 0) {\n\t\treturn;\n\t}\n\n\tconst elements = Array.from(\n\t\tdocument.querySelectorAll<HTMLElement>('[data-island=\"true\"]')\n\t);\n\tfor (const element of elements) {\n\t\tsnapshotIslandElement(element, snapshotMap);\n\t}\n};\n\nexport const preserveIslandMarkup = (props: RuntimeIslandRenderProps) => {\n\tif (typeof document === 'undefined') {\n\t\treturn {\n\t\t\tattributes: getIslandMarkerAttributes(props),\n\t\t\tinnerHTML: ''\n\t\t};\n\t}\n\n\tconst claimMap = getClaimMap();\n\tconst snapshotMap = getSnapshotMap();\n\tconst signature = getIslandSignature(props);\n\tconst claimedCount = claimMap?.get(signature) ?? 0;\n\tconst snapshotCandidate = snapshotMap?.get(signature)?.[claimedCount];\n\tconst candidates = Array.from(\n\t\tdocument.querySelectorAll('[data-island=\"true\"]')\n\t).filter((element) => isMatchingIslandElement(element, props));\n\tconst candidate = candidates[claimedCount];\n\tif (claimMap) {\n\t\tclaimMap.set(signature, claimedCount + 1);\n\t}\n\n\treturn {\n\t\tattributes:\n\t\t\tsnapshotCandidate?.attributes ?? getIslandMarkerAttributes(props),\n\t\tinnerHTML: snapshotCandidate?.innerHTML ?? candidate?.innerHTML ?? ''\n\t};\n};\n",
|
|
241
241
|
"import { inject, PendingTasks } from '@angular/core';\n\nexport const withPendingTask = async <Value>(work: () => Promise<Value>) => {\n\tconst removeTask = inject(PendingTasks).add();\n\n\ttry {\n\t\treturn await work();\n\t} finally {\n\t\tremoveTask();\n\t}\n};\n",
|
|
@@ -273,7 +273,7 @@
|
|
|
273
273
|
"import {\n\tappendStreamingSlotPatchesToStream,\n\ttype AppendStreamingSlotsOptions,\n\ttype StreamingSlotPolicy,\n\ttype StreamingSlot\n} from '../utils/streamingSlots';\nimport { runWithStreamingSlotRegistry } from './streamingSlotRegistry';\n\ntype ResponseLike = Response | Promise<Response>;\n\nexport type StreamingSlotEnhancerOptions = Omit<\n\tAppendStreamingSlotsOptions,\n\t'injectRuntime'\n> & {\n\tstreamingSlots?: StreamingSlot[];\n\tpolicy?: StreamingSlotPolicy;\n};\n\nconst toResponse = async (responseLike: ResponseLike) => responseLike;\n\nconst cloneHeaders = (response: Response) => {\n\tconst headers = new Headers(response.headers);\n\n\treturn headers;\n};\n\nexport const enhanceHtmlResponseWithStreamingSlots = (\n\tresponse: Response,\n\t{\n\t\tnonce,\n\t\tonError,\n\t\truntimePlacement,\n\t\truntimePreludeScript,\n\t\tstreamingSlots = [],\n\t\tpolicy\n\t}: StreamingSlotEnhancerOptions = {}\n) => {\n\tif (!response.body || streamingSlots.length === 0) {\n\t\treturn response;\n\t}\n\n\tconst body = appendStreamingSlotPatchesToStream(\n\t\tresponse.body,\n\t\tstreamingSlots,\n\t\t{\n\t\t\tnonce,\n\t\t\tonError,\n\t\t\tpolicy,\n\t\t\truntimePlacement,\n\t\t\truntimePreludeScript\n\t\t}\n\t);\n\n\treturn new Response(body, {\n\t\theaders: cloneHeaders(response),\n\t\tstatus: response.status,\n\t\tstatusText: response.statusText\n\t});\n};\n\nexport const withStreamingSlots = async (\n\tresponseLike: ResponseLike,\n\toptions: StreamingSlotEnhancerOptions = {}\n) =>\n\tenhanceHtmlResponseWithStreamingSlots(\n\t\tawait toResponse(responseLike),\n\t\toptions\n\t);\n\nconst mergeStreamingSlots = (\n\tregistered: StreamingSlot[],\n\texplicit: StreamingSlot[]\n) => {\n\tconst merged = new Map<string, StreamingSlot>();\n\tfor (const slot of registered) merged.set(slot.id, slot);\n\tfor (const slot of explicit) merged.set(slot.id, slot);\n\n\treturn [...merged.values()];\n};\n\nexport const withRegisteredStreamingSlots = async (\n\trenderResponse: () => ResponseLike,\n\toptions: StreamingSlotEnhancerOptions = {}\n) => {\n\tconst { result, slots } =\n\t\tawait runWithStreamingSlotRegistry(renderResponse);\n\tconst explicit = options.streamingSlots ?? [];\n\n\treturn withStreamingSlots(result, {\n\t\t...options,\n\t\tstreamingSlots: mergeStreamingSlots(slots, explicit)\n\t});\n};\n",
|
|
274
274
|
"import { AsyncLocalStorage } from 'node:async_hooks';\nimport { logWarn } from '../utils/logger';\nimport { setStreamingSlotWarningController } from './streamingSlotRegistrar';\n\ntype WarningStore = {\n\thandlerCallsite?: string;\n\thasWarned: boolean;\n};\n\ntype WarningStorage = AsyncLocalStorage<WarningStore>;\n\nconst STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for(\n\t'absolutejs.streamingSlotWarningAsyncLocalStorage'\n);\n\nconst isObjectRecord = (value: unknown): value is Record<string, unknown> =>\n\tBoolean(value) && typeof value === 'object';\n\nconst isAsyncLocalStorage = (value: unknown): value is WarningStorage =>\n\tisObjectRecord(value) &&\n\t'getStore' in value &&\n\ttypeof value.getStore === 'function' &&\n\t'run' in value &&\n\ttypeof value.run === 'function';\n\nconst getWarningStorage = () => {\n\tconst value = Reflect.get(globalThis, STREAMING_SLOT_WARNING_STORAGE_KEY);\n\tif (value === null || typeof value === 'undefined') {\n\t\treturn undefined;\n\t}\n\n\treturn isAsyncLocalStorage(value) ? value : undefined;\n};\n\nconst ensureWarningStorage = () => {\n\tconst existing = getWarningStorage();\n\tif (existing) {\n\t\treturn existing;\n\t}\n\n\tconst storage = new AsyncLocalStorage<WarningStore>();\n\tReflect.set(globalThis, STREAMING_SLOT_WARNING_STORAGE_KEY, storage);\n\n\treturn storage;\n};\n\nconst normalizeCallsitePath = (value: string) =>\n\tvalue\n\t\t.replace(`${process.cwd()}/`, '')\n\t\t.replace(process.cwd(), '')\n\t\t.replace(/^\\.\\/+/, '');\n\nconst formatWarningCallsite = (callsite: string) => {\n\tconst match = callsite.match(/^(.*?)(:\\d+:\\d+)$/);\n\tif (!match) {\n\t\treturn `\\x1b[36m${callsite}\\x1b[0m`;\n\t}\n\n\treturn `\\x1b[36m${match[1]}\\x1b[33m${match[2]}\\x1b[0m`;\n};\n\nconst shouldIgnoreWarningFrame = (frame: string) =>\n\tframe.includes('/node_modules/') ||\n\tframe.includes('/dist/') ||\n\tframe.includes('/src/react/pageHandler.') ||\n\tframe.includes('/src/vue/pageHandler.') ||\n\tframe.includes('/src/svelte/pageHandler.') ||\n\tframe.includes('/src/angular/pageHandler.') ||\n\tframe.includes('/src/core/streamingSlotWarningScope.');\n\nconst getWarningLocation = (frame: string) =>\n\tframe.match(/\\((\\/[^)]+:\\d+:\\d+)\\)$/)?.[1] ??\n\tframe.match(/at (\\/[^ ]+:\\d+:\\d+)$/)?.[1];\n\nconst extractCallsiteFromStack = (stack: string) => {\n\tconst location = stack\n\t\t.split('\\n')\n\t\t.slice(1)\n\t\t.map((line) => line.trim())\n\t\t.filter((frame) => !shouldIgnoreWarningFrame(frame))\n\t\t.map((frame) => getWarningLocation(frame))\n\t\t.find((frameLocation) => frameLocation !== undefined);\n\n\treturn location ? normalizeCallsitePath(location) : undefined;\n};\n\nconst buildMissingCollectorWarning = (\n\tprimitiveName: string,\n\thandlerCallsite?: string\n) =>\n\t`${primitiveName} rendered during SSR without streaming slot collection enabled. Add { collectStreamingSlots: true } to this page handler to enable out-of-order streaming for this route.${handlerCallsite ? ` Update ${formatWarningCallsite(handlerCallsite)}.` : ''}`;\n\nsetStreamingSlotWarningController({\n\tmaybeWarn: (primitiveName: string) => {\n\t\tconst store = getWarningStorage()?.getStore();\n\t\tif (!store || store.hasWarned) {\n\t\t\treturn;\n\t\t}\n\n\t\tstore.hasWarned = true;\n\t\tlogWarn(\n\t\t\tbuildMissingCollectorWarning(primitiveName, store.handlerCallsite)\n\t\t);\n\t}\n});\n\nexport const captureStreamingSlotWarningCallsite = () => {\n\tif (process.env.NODE_ENV === 'production') {\n\t\treturn undefined;\n\t}\n\n\tconst { stack } = new Error();\n\tif (!stack) {\n\t\treturn undefined;\n\t}\n\n\treturn extractCallsiteFromStack(stack);\n};\n\nexport const runWithStreamingSlotWarningScope = <T>(\n\ttask: () => Promise<T> | T,\n\tmetadata?: {\n\t\thandlerCallsite?: string;\n\t}\n) =>\n\tensureWarningStorage().run(\n\t\t{ handlerCallsite: metadata?.handlerCallsite, hasWarned: false },\n\t\ttask\n\t);\n",
|
|
275
275
|
"import type { Type } from '@angular/core';\nimport type { AngularPageDefinition } from '../../types/angular';\n\nexport const defineAngularPage = <\n\tProps extends Record<string, unknown> = Record<never, never>\n>(definition: {\n\tcomponent: Type<unknown>;\n}) => definition as AngularPageDefinition<Props>;\n",
|
|
276
|
-
"/* preserveAcrossHmr — keep service AND component instance state alive\n across full Angular re-bootstraps in dev mode.\n\n Why this exists: most HMR updates use fast-patch (in-place prototype\n swap) and never destroy the running app, so instance state is never\n lost. But some changes — route definitions, providers, brand-new\n components, anything in a `providedIn: 'root'` provider list —\n require the HMR client to fall back to a full re-bootstrap. That\n destroys every service and component instance, and the new ones\n start with class field initializers (e.g. `idToken: null`,\n `searchQuery: ''`). Anything held in memory (auth tokens, cached\n query results, form input values, scroll-positioned filters) gets\n wiped, even though the underlying source of truth (Firebase session,\n the URL, the user's intent) hasn't changed.\n\n Usage — opt in once per class:\n\n // Service (singleton, no key needed)\n @Injectable({ providedIn: 'root' })\n export class AuthService {\n idToken: string | null = null;\n\n constructor() {\n preserveAcrossHmr(this);\n }\n }\n\n // Component (one instance per route, no key needed)\n @Component({ ... })\n export class AdminProfilesComponent {\n searchQuery = '';\n currentPage = 0;\n\n constructor() {\n preserveAcrossHmr(this);\n }\n }\n\n // Component with multiple instances on the same page —\n // pass a key derived from @Input. Use ngOnInit because Angular\n // sets @Input properties between constructor and ngOnInit.\n @Component({ ... })\n export class ItemRowComponent implements OnInit {\n @Input() id!: string;\n expanded = false;\n\n ngOnInit() {\n preserveAcrossHmr(this, this.id);\n }\n }\n\n How it works:\n 1. Every call registers the instance for *future* capture (a\n WeakRef so we don't leak), independent of the HMR cycle.\n 2. If an HMR full re-bootstrap is in progress, restores the\n cached preservable own properties onto the new instance.\n 3. Right before the next destroy (called from the HMR client),\n every tracked instance's preservable own properties are\n snapshotted into the cache, keyed by `${className}:${key}`.\n\n Cache lifetime — scoped to the HMR cycle:\n - capture() flips `rebootInProgress` to true, populates cache.\n - bootstrap completes → flag flipped back to false.\n - Outside an HMR reboot, restoration is a no-op. So navigating\n away and back to a route with a previously-captured component\n gets fresh state, not stale state from the last HMR.\n\n Repeated components without a key:\n If two instances of the same class register under the same key\n during a single capture, a console warning fires. The cache\n entry holds the LAST one captured; first-mounting instance wins\n on restore. Pass an explicit `key` (per-row id, etc.) for\n deterministic preservation of multiple instances.\n\n What gets preserved:\n Only primitives, plain `{}` objects, and arrays of those. Class\n instances (HttpClient, BehaviorSubject, Date, Map…) and\n functions are excluded — the new instance must get those from\n its own injector / construction. See `isPreservable` below.\n\n Don't expect this to preserve `@Input`-bound properties: the\n parent component is the source of truth and will overwrite via\n change detection on the new render anyway.\n\n Production: in non-dev (`__DEV__` is undefined or false), the\n helper is a no-op. The cache and tracker only exist in dev. */\n\nimport { ChangeDetectorRef, inject } from '@angular/core';\n\ntype StateCache = Map<string, Record<string, unknown>>;\ntype InstanceTracker = Set<WeakRef<object>>;\ntype InstanceKeyMap = WeakMap<object, string>;\ntype RebootFlag = { value: boolean };\ntype RebootStats = { captured: number; restoredKeys: Set<string> };\n\ntype PreserveScope = typeof globalThis & {\n\t__ABS_HMR_INSTANCE_STATE__?: StateCache;\n\t__ABS_HMR_TRACKED_INSTANCES__?: InstanceTracker;\n\t__ABS_HMR_INSTANCE_KEYS__?: InstanceKeyMap;\n\t__ABS_HMR_REBOOT_IN_PROGRESS__?: RebootFlag;\n\t__ABS_HMR_REBOOT_STATS__?: RebootStats;\n};\n\nconst isDev = (): boolean => {\n\t// SSR safety: globalThis on the server is process-wide and shared\n\t// across requests, so writing to the preservation cache during SSR\n\t// would leak request state between users. Gate strictly on the\n\t// presence of a browser `window` *and* a dev signal — neither is\n\t// true in a production build, so this is a hard no-op there too.\n\tif (typeof window === 'undefined') return false;\n\t// Angular's `ngDevMode` is an object in dev (with hydration counters,\n\t// etc.) and `undefined` in production. Treat any truthy value as dev.\n\tconst scope = globalThis as { __DEV__?: unknown; ngDevMode?: unknown };\n\n\treturn Boolean(scope.__DEV__) || Boolean(scope.ngDevMode);\n};\n\nconst getCache = (): StateCache => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_INSTANCE_STATE__ ??= new Map());\n};\n\nconst getTracker = (): InstanceTracker => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_TRACKED_INSTANCES__ ??= new Set());\n};\n\nconst getKeyMap = (): InstanceKeyMap => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_INSTANCE_KEYS__ ??= new WeakMap());\n};\n\nconst getRebootFlag = (): RebootFlag => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_REBOOT_IN_PROGRESS__ ??= { value: false });\n};\n\nconst getRebootStats = (): RebootStats => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_REBOOT_STATS__ ??= {\n\t\tcaptured: 0,\n\t\trestoredKeys: new Set()\n\t});\n};\n\n/* Determine whether a value is safe to preserve across an HMR full\n re-bootstrap. We snapshot the OLD app's instance state into a cache\n and copy it back onto the NEW instance — but holding references to\n the OLD app's Angular-injected services (HttpClient, ApplicationRef,\n subscriptions tied to the destroyed injector, etc.) and restoring\n them onto the new instance would corrupt the new app: the new\n `HttpClient` from the new injector would be replaced by a stale ref\n pointing at a destroyed graph.\n We accept primitives, plain `{}` objects, and arrays of those, which\n covers the common cases (auth tokens, cached query results, search\n queries, pagination state) without leaking Angular-injected\n dependencies, RxJS subjects, DOM nodes, or other live references. */\nconst isPreservable = (value: unknown, depth = 0): boolean => {\n\tif (depth > 8) return false;\n\tif (value === null || value === undefined) return true;\n\tconst t = typeof value;\n\tif (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint')\n\t\treturn true;\n\tif (t === 'function' || t === 'symbol') return false;\n\tif (Array.isArray(value)) {\n\t\treturn value.every((item) => isPreservable(item, depth + 1));\n\t}\n\tif (t === 'object') {\n\t\tconst proto = Object.getPrototypeOf(value);\n\t\t// Only POJOs — class instances (HttpClient, BehaviorSubject, Date,\n\t\t// Map, etc.) carry runtime identity that the new instance must get\n\t\t// from its own injector / construction.\n\t\tif (proto !== Object.prototype && proto !== null) return false;\n\n\t\treturn Object.values(value as object).every((v) =>\n\t\t\tisPreservable(v, depth + 1)\n\t\t);\n\t}\n\n\treturn false;\n};\n\nconst buildCacheKey = (instance: object, key?: unknown): string | null => {\n\tconst className = instance.constructor?.name;\n\tif (!className || className === 'Object') return null;\n\tconst suffix = key === undefined || key === null ? '' : String(key);\n\n\treturn `${className}:${suffix}`;\n};\n\nconst restoreFromCache = (instance: object, key: string) => {\n\tconst cache = getCache();\n\tconst stored = cache.get(key);\n\tif (!stored) return;\n\n\tfor (const [prop, value] of Object.entries(stored)) {\n\t\ttry {\n\t\t\t(instance as Record<string, unknown>)[prop] = value;\n\t\t} catch {\n\t\t\t/* property is non-writable / has a setter that threw — skip */\n\t\t}\n\t}\n\n\tgetRebootStats().restoredKeys.add(key);\n\n\t// OnPush components don't see direct property assignments — they\n\t// only re-check on `markForCheck()`. The restoration above happens\n\t// before the first CD pass on this instance, so on the *initial*\n\t// render OnPush is fine; but if `preserveAcrossHmr` is called from\n\t// `ngOnInit` (the keyed-component path), the parent's CD pass may\n\t// have already painted defaults. Schedule a markForCheck so the\n\t// restored values are visible without the user remembering to do\n\t// it themselves.\n\t// `inject()` requires an active injection context — true inside\n\t// constructors, factories, and `runInInjectionContext` blocks. It\n\t// throws otherwise (e.g. called from a lifecycle hook), in which\n\t// case we skip cleanly; the component's own first CD pass will\n\t// pick up the values anyway.\n\ttry {\n\t\tconst cdr = inject(ChangeDetectorRef, { optional: true });\n\t\tif (cdr) queueMicrotask(() => cdr.markForCheck());\n\t} catch {\n\t\t/* outside injection context / no CDR available — fine */\n\t}\n};\n\n/** Mark a service or component instance for state preservation across\n * full Angular HMR re-bootstraps. Call once from the constructor or\n * `ngOnInit`. Safe in production (no-op outside dev mode).\n *\n * @param instance Usually `this`. The class name is used as part of\n * the cache key.\n * @param key Optional discriminator when multiple instances of the\n * same class can be alive at once (rows, tabs, etc). Coerced\n * to string. Use `ngOnInit` to call this when the key depends\n * on `@Input` values, since Angular sets inputs between\n * constructor and ngOnInit. */\nexport const preserveAcrossHmr = (\n\tinstance: object,\n\tkey?: string | number\n): void => {\n\tif (!isDev()) return;\n\n\tconst fullKey = buildCacheKey(instance, key);\n\tif (fullKey === null) return;\n\n\t// Always register for future capture — independent of whether a\n\t// reboot is currently in progress. The next capture cycle will\n\t// snapshot whatever's in the tracker. Store the key alongside the\n\t// instance via WeakMap so capture knows which cache slot to use\n\t// without instance pollution.\n\tgetTracker().add(new WeakRef(instance));\n\tgetKeyMap().set(instance, fullKey);\n\n\t// Restoration is HMR-cycle-scoped. Outside an active reboot, the\n\t// new instance keeps its class-field defaults; we don't want stale\n\t// state from a previous HMR to leak into normal navigations.\n\tif (getRebootFlag().value) {\n\t\trestoreFromCache(instance, fullKey);\n\t}\n};\n\n/** Snapshot every tracked instance's preservable own properties into\n * the cache and flip the reboot flag on. Called by the HMR client\n * right before `destroyAngularApp()`. Properties that aren't\n * preservable (Angular-injected services, Subjects, class instances)\n * are skipped — the new instance gets fresh ones from its new\n * injector, just like at first bootstrap. */\nexport const captureTrackedInstanceStates = (): void => {\n\tif (!isDev()) return;\n\n\tconst cache = getCache();\n\tconst tracker = getTracker();\n\tconst keyMap = getKeyMap();\n\tconst stats = getRebootStats();\n\tconst dead: WeakRef<object>[] = [];\n\tconst seen = new Set<string>();\n\n\tcache.clear();\n\tstats.restoredKeys.clear();\n\tstats.captured = 0;\n\n\tfor (const ref of tracker) {\n\t\tconst instance = ref.deref();\n\t\tif (!instance) {\n\t\t\tdead.push(ref);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst fullKey = keyMap.get(instance) ?? buildCacheKey(instance);\n\t\tif (fullKey === null) continue;\n\n\t\t// Warn when two instances would collide on the same cache slot\n\t\t// (same className with no key, or duplicate user-supplied keys).\n\t\t// On collision the second instance's state silently overwrites\n\t\t// the first — pass an explicit `key` to differentiate.\n\t\tif (seen.has(fullKey)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[HMR] preserveAcrossHmr collision on \"${fullKey}\". Two instances would use the same cache slot — the later one will overwrite the earlier one's state on full re-bootstrap. Pass a unique \\`key\\` argument (e.g. an @Input id) to differentiate.`\n\t\t\t);\n\t\t}\n\t\tseen.add(fullKey);\n\n\t\tconst props: Record<string, unknown> = {};\n\t\tfor (const prop of Object.keys(instance)) {\n\t\t\tconst value = (instance as Record<string, unknown>)[prop];\n\t\t\tif (isPreservable(value)) props[prop] = value;\n\t\t}\n\t\tcache.set(fullKey, props);\n\t\tstats.captured++;\n\t}\n\n\tdead.forEach((ref) => tracker.delete(ref));\n\n\tgetRebootFlag().value = true;\n};\n\n/** Clear the active-reboot flag and emit a one-line summary so\n * developers can see at-a-glance which classes had state preserved.\n * Helps surface the existence of `preserveAcrossHmr` to anyone whose\n * state was reset because they hadn't opted in.\n * Called by the HMR client after the new app has finished\n * bootstrapping (success or failure — wrap in `try/finally`). After\n * this, `preserveAcrossHmr` calls will track but won't restore — so\n * navigating to a route after HMR doesn't resurrect stale state from\n * the last reboot. */\nexport const endHmrReboot = (): void => {\n\tif (!isDev()) return;\n\tgetRebootFlag().value = false;\n\n\tconst stats = getRebootStats();\n\tif (stats.captured > 0) {\n\t\tconst restored = Array.from(stats.restoredKeys)\n\t\t\t.map((k) => k.replace(/:$/, ''))\n\t\t\t.sort();\n\t\tconsole.info(\n\t\t\t`[HMR] Full re-bootstrap: restored state for ${restored.length}/${stats.captured} tracked instance(s)${\n\t\t\t\trestored.length > 0 ? ` — ${restored.join(', ')}` : ''\n\t\t\t}. Components without preservation reset to defaults; opt in via \\`preserveAcrossHmr(this)\\`.`\n\t\t);\n\t}\n};\n",
|
|
276
|
+
"/* preserveAcrossHmr — keep service AND component instance state alive\n across full Angular re-bootstraps in dev mode.\n\n Why this exists: most HMR updates use fast-patch (in-place prototype\n swap) and never destroy the running app, so instance state is never\n lost. But some changes — route definitions, providers, brand-new\n components, anything in a `providedIn: 'root'` provider list —\n require the HMR client to fall back to a full re-bootstrap. That\n destroys every service and component instance, and the new ones\n start with class field initializers (e.g. `idToken: null`,\n `searchQuery: ''`). Anything held in memory (auth tokens, cached\n query results, form input values, scroll-positioned filters) gets\n wiped, even though the underlying source of truth (Firebase session,\n the URL, the user's intent) hasn't changed.\n\n Usage — opt in once per class:\n\n // Service (singleton, no key needed)\n @Injectable({ providedIn: 'root' })\n export class AuthService {\n idToken: string | null = null;\n\n constructor() {\n preserveAcrossHmr(this);\n }\n }\n\n // Component (one instance per route, no key needed)\n @Component({ ... })\n export class AdminProfilesComponent {\n searchQuery = '';\n currentPage = 0;\n\n constructor() {\n preserveAcrossHmr(this);\n }\n }\n\n // Component with multiple instances on the same page —\n // pass a key derived from @Input. Use ngOnInit because Angular\n // sets @Input properties between constructor and ngOnInit.\n @Component({ ... })\n export class ItemRowComponent implements OnInit {\n @Input() id!: string;\n expanded = false;\n\n ngOnInit() {\n preserveAcrossHmr(this, this.id);\n }\n }\n\n How it works:\n 1. Every call registers the instance for *future* capture (a\n WeakRef so we don't leak), independent of the HMR cycle.\n 2. If an HMR full re-bootstrap is in progress, restores the\n cached preservable own properties onto the new instance.\n 3. Right before the next destroy (called from the HMR client),\n every tracked instance's preservable own properties are\n snapshotted into the cache, keyed by `${className}:${key}`.\n\n Cache lifetime — scoped to the HMR cycle:\n - capture() flips `rebootInProgress` to true, populates cache.\n - bootstrap completes → flag flipped back to false.\n - Outside an HMR reboot, restoration is a no-op. So navigating\n away and back to a route with a previously-captured component\n gets fresh state, not stale state from the last HMR.\n\n Repeated components without a key:\n If two instances of the same class register under the same key\n during a single capture, a console warning fires. The cache\n entry holds the LAST one captured; first-mounting instance wins\n on restore. Pass an explicit `key` (per-row id, etc.) for\n deterministic preservation of multiple instances.\n\n What gets preserved:\n Only primitives, plain `{}` objects, and arrays of those. Class\n instances (HttpClient, BehaviorSubject, Date, Map…) and\n functions are excluded — the new instance must get those from\n its own injector / construction. See `isPreservable` below.\n\n Don't expect this to preserve `@Input`-bound properties: the\n parent component is the source of truth and will overwrite via\n change detection on the new render anyway.\n\n Production: in non-dev (`__DEV__` is undefined or false), the\n helper is a no-op. The cache and tracker only exist in dev. */\n\nimport { ChangeDetectorRef, inject } from '@angular/core';\n\ntype StateCache = Map<string, Record<string, unknown>>;\ntype InstanceTracker = Set<WeakRef<object>>;\ntype InstanceKeyMap = WeakMap<object, string>;\ntype RebootFlag = { value: boolean };\ntype RebootStats = { captured: number; restoredKeys: Set<string> };\n\ntype PreserveScope = typeof globalThis & {\n\t__ABS_HMR_INSTANCE_STATE__?: StateCache;\n\t__ABS_HMR_TRACKED_INSTANCES__?: InstanceTracker;\n\t__ABS_HMR_INSTANCE_KEYS__?: InstanceKeyMap;\n\t__ABS_HMR_REBOOT_IN_PROGRESS__?: RebootFlag;\n\t__ABS_HMR_REBOOT_STATS__?: RebootStats;\n};\n\nconst isDev = (): boolean => {\n\t// SSR safety: globalThis on the server is process-wide and shared\n\t// across requests, so writing to the preservation cache during SSR\n\t// would leak request state between users. Gate strictly on the\n\t// presence of a browser `window` *and* a dev signal — neither is\n\t// true in a production build, so this is a hard no-op there too.\n\tif (typeof window === 'undefined') return false;\n\t// Angular's `ngDevMode` is an object in dev (with hydration counters,\n\t// etc.) and `undefined` in production. Treat any truthy value as dev.\n\tconst scope = globalThis as { __DEV__?: unknown; ngDevMode?: unknown };\n\n\treturn Boolean(scope.__DEV__) || Boolean(scope.ngDevMode);\n};\n\nconst getCache = (): StateCache => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_INSTANCE_STATE__ ??= new Map());\n};\n\nconst getTracker = (): InstanceTracker => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_TRACKED_INSTANCES__ ??= new Set());\n};\n\nconst getKeyMap = (): InstanceKeyMap => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_INSTANCE_KEYS__ ??= new WeakMap());\n};\n\nconst getRebootFlag = (): RebootFlag => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_REBOOT_IN_PROGRESS__ ??= { value: false });\n};\n\nconst getRebootStats = (): RebootStats => {\n\tconst scope = globalThis as PreserveScope;\n\n\treturn (scope.__ABS_HMR_REBOOT_STATS__ ??= {\n\t\tcaptured: 0,\n\t\trestoredKeys: new Set()\n\t});\n};\n\n/* Determine whether a value is safe to preserve across an HMR full\n re-bootstrap. We snapshot the OLD app's instance state into a cache\n and copy it back onto the NEW instance — but holding references to\n the OLD app's Angular-injected services (HttpClient, ApplicationRef,\n subscriptions tied to the destroyed injector, etc.) and restoring\n them onto the new instance would corrupt the new app: the new\n `HttpClient` from the new injector would be replaced by a stale ref\n pointing at a destroyed graph.\n We accept primitives, plain `{}` objects, and arrays of those, which\n covers the common cases (auth tokens, cached query results, search\n queries, pagination state) without leaking Angular-injected\n dependencies, RxJS subjects, DOM nodes, or other live references. */\nconst isPreservable = (value: unknown, depth = 0): boolean => {\n\tif (depth > 8) return false;\n\tif (value === null || value === undefined) return true;\n\tconst t = typeof value;\n\tif (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint')\n\t\treturn true;\n\tif (t === 'function' || t === 'symbol') return false;\n\tif (Array.isArray(value)) {\n\t\treturn value.every((item) => isPreservable(item, depth + 1));\n\t}\n\tif (t === 'object') {\n\t\tconst proto = Object.getPrototypeOf(value);\n\t\t// Only POJOs — class instances (HttpClient, BehaviorSubject, Date,\n\t\t// Map, etc.) carry runtime identity that the new instance must get\n\t\t// from its own injector / construction.\n\t\tif (proto !== Object.prototype && proto !== null) return false;\n\n\t\treturn Object.values(value as object).every((v) =>\n\t\t\tisPreservable(v, depth + 1)\n\t\t);\n\t}\n\n\treturn false;\n};\n\nconst buildCacheKey = (instance: object, key?: unknown): string | null => {\n\tconst className = instance.constructor?.name;\n\tif (!className || className === 'Object') return null;\n\tconst suffix = key === undefined || key === null ? '' : String(key);\n\n\treturn `${className}:${suffix}`;\n};\n\nconst restoreFromCache = (instance: object, key: string) => {\n\tconst cache = getCache();\n\tconst stored = cache.get(key);\n\tif (!stored) return;\n\n\tfor (const [prop, value] of Object.entries(stored)) {\n\t\ttry {\n\t\t\t(instance as Record<string, unknown>)[prop] = value;\n\t\t} catch {\n\t\t\t/* property is non-writable / has a setter that threw — skip */\n\t\t}\n\t}\n\n\tgetRebootStats().restoredKeys.add(key);\n\n\t// OnPush components don't see direct property assignments — they\n\t// only re-check on `markForCheck()`. The restoration above happens\n\t// before the first CD pass on this instance, so on the *initial*\n\t// render OnPush is fine; but if `preserveAcrossHmr` is called from\n\t// `ngOnInit` (the keyed-component path), the parent's CD pass may\n\t// have already painted defaults. Schedule a markForCheck so the\n\t// restored values are visible without the user remembering to do\n\t// it themselves.\n\t// `inject()` requires an active injection context — true inside\n\t// constructors, factories, and `runInInjectionContext` blocks. It\n\t// throws otherwise (e.g. called from a lifecycle hook), in which\n\t// case we skip cleanly; the component's own first CD pass will\n\t// pick up the values anyway.\n\ttry {\n\t\tconst cdr = inject(ChangeDetectorRef, { optional: true });\n\t\tif (cdr) queueMicrotask(() => cdr.markForCheck());\n\t} catch {\n\t\t/* outside injection context / no CDR available — fine */\n\t}\n};\n\n/** Mark a service or component instance for state preservation across\n * full Angular HMR re-bootstraps. Call once from the constructor or\n * `ngOnInit`. Safe in production (no-op outside dev mode).\n *\n * @param instance Usually `this`. The class name is used as part of\n * the cache key.\n * @param key Optional discriminator when multiple instances of the\n * same class can be alive at once (rows, tabs, etc). Coerced\n * to string. Use `ngOnInit` to call this when the key depends\n * on `@Input` values, since Angular sets inputs between\n * constructor and ngOnInit. */\nexport const preserveAcrossHmr = (\n\tinstance: object,\n\tkey?: string | number\n): void => {\n\tif (!isDev()) return;\n\n\tconst fullKey = buildCacheKey(instance, key);\n\tif (fullKey === null) return;\n\n\t// Always register for future capture — independent of whether a\n\t// reboot is currently in progress. The next capture cycle will\n\t// snapshot whatever's in the tracker. Store the key alongside the\n\t// instance via WeakMap so capture knows which cache slot to use\n\t// without instance pollution.\n\tgetTracker().add(new WeakRef(instance));\n\tgetKeyMap().set(instance, fullKey);\n\n\t// Restoration is HMR-cycle-scoped. Outside an active reboot, the\n\t// new instance keeps its class-field defaults; we don't want stale\n\t// state from a previous HMR to leak into normal navigations.\n\tif (getRebootFlag().value) {\n\t\trestoreFromCache(instance, fullKey);\n\t}\n};\n\n/** Snapshot every tracked instance's preservable own properties into\n * the cache and flip the reboot flag on. Called by the HMR client\n * right before `destroyAngularApp()`. Properties that aren't\n * preservable (Angular-injected services, Subjects, class instances)\n * are skipped — the new instance gets fresh ones from its new\n * injector, just like at first bootstrap. */\nexport const captureTrackedInstanceStates = (): void => {\n\tif (!isDev()) return;\n\n\tconst cache = getCache();\n\tconst tracker = getTracker();\n\tconst keyMap = getKeyMap();\n\tconst stats = getRebootStats();\n\tconst seen = new Set<string>();\n\n\tcache.clear();\n\tstats.restoredKeys.clear();\n\tstats.captured = 0;\n\n\tfor (const ref of tracker) {\n\t\tconst instance = ref.deref();\n\t\t// Skip already-GC'd refs. We don't bother bookkeeping a \"dead\"\n\t\t// list because the entire tracker is cleared after this loop.\n\t\tif (!instance) continue;\n\n\t\tconst fullKey = keyMap.get(instance) ?? buildCacheKey(instance);\n\t\tif (fullKey === null) continue;\n\n\t\t// Warn when two instances would collide on the same cache slot\n\t\t// (same className with no key, or duplicate user-supplied keys).\n\t\t// On collision the second instance's state silently overwrites\n\t\t// the first — pass an explicit `key` to differentiate.\n\t\tif (seen.has(fullKey)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[HMR] preserveAcrossHmr collision on \"${fullKey}\". Two instances would use the same cache slot — the later one will overwrite the earlier one's state on full re-bootstrap. Pass a unique \\`key\\` argument (e.g. an @Input id) to differentiate.`\n\t\t\t);\n\t\t}\n\t\tseen.add(fullKey);\n\n\t\tconst props: Record<string, unknown> = {};\n\t\tfor (const prop of Object.keys(instance)) {\n\t\t\tconst value = (instance as Record<string, unknown>)[prop];\n\t\t\tif (isPreservable(value)) props[prop] = value;\n\t\t}\n\t\tcache.set(fullKey, props);\n\t\tstats.captured++;\n\t}\n\n\t// Every instance just captured is about to die: `destroyAngularApp()`\n\t// runs immediately after this and tears down the root injector. New\n\t// instances from the next bootstrap repopulate the tracker via their\n\t// own `preserveAcrossHmr(this)` calls. If we left existing WeakRefs\n\t// in place, the JS engine often won't have GC'd the old objects yet\n\t// at the next capture — `weakRef.deref()` would return zombies that\n\t// inflate the captured count and trigger spurious collision warnings\n\t// (since both the prior generation's zombie and the new instance map\n\t// to the same `${className}:` slot).\n\ttracker.clear();\n\n\tgetRebootFlag().value = true;\n};\n\n/** Clear the active-reboot flag and emit a one-line summary so\n * developers can see at-a-glance which classes had state preserved.\n * Helps surface the existence of `preserveAcrossHmr` to anyone whose\n * state was reset because they hadn't opted in.\n * Called by the HMR client after the new app has finished\n * bootstrapping (success or failure — wrap in `try/finally`). After\n * this, `preserveAcrossHmr` calls will track but won't restore — so\n * navigating to a route after HMR doesn't resurrect stale state from\n * the last reboot. */\nexport const endHmrReboot = (): void => {\n\tif (!isDev()) return;\n\tgetRebootFlag().value = false;\n\n\tconst stats = getRebootStats();\n\tif (stats.captured > 0) {\n\t\tconst restored = Array.from(stats.restoredKeys)\n\t\t\t.map((k) => k.replace(/:$/, ''))\n\t\t\t.sort();\n\t\tconsole.info(\n\t\t\t`[HMR] Full re-bootstrap: restored state for ${restored.length}/${stats.captured} tracked instance(s)${\n\t\t\t\trestored.length > 0 ? ` — ${restored.join(', ')}` : ''\n\t\t\t}. Components without preservation reset to defaults; opt in via \\`preserveAcrossHmr(this)\\`.`\n\t\t);\n\t}\n};\n",
|
|
277
277
|
"import { inject, PendingTasks } from '@angular/core';\n\nexport const withPendingTask = async <Value>(work: () => Promise<Value>) => {\n\tconst removeTask = inject(PendingTasks).add();\n\n\ttry {\n\t\treturn await work();\n\t} finally {\n\t\tremoveTask();\n\t}\n};\n",
|
|
278
278
|
"import type {\n\tIslandRegistry,\n\tIslandRegistryInput,\n\tTypedIslandRenderProps\n} from '../../types/island';\nimport { renderIslandMarkup } from '../core/renderIslandMarkup';\n\nexport const createTypedIsland =\n\t<T extends IslandRegistryInput>(registry: IslandRegistry<T>) =>\n\t(props: TypedIslandRenderProps<T>) =>\n\t\trenderIslandMarkup(registry, props);\n",
|
|
279
279
|
"import '@angular/compiler';\nimport { Component, HostBinding, Input } from '@angular/core';\nimport type * as i0 from '@angular/core';\nimport type { RuntimeIslandRenderProps } from '../../types/island';\n\n@Component({\n\tselector: 'absolute-island',\n\tstandalone: true,\n\ttemplate: '<div></div>'\n})\nexport class Island {\n\t@Input() component = '';\n\t@Input() framework: RuntimeIslandRenderProps['framework'] = 'react';\n\t@Input() hydrate: RuntimeIslandRenderProps['hydrate'] = 'load';\n\t@Input() props: RuntimeIslandRenderProps['props'] = {};\n\n\tdeclare static ɵfac: i0.ɵɵFactoryDeclaration<Island, never>;\n\tdeclare static ɵcmp: i0.ɵɵComponentDeclaration<\n\t\tIsland,\n\t\t'absolute-island',\n\t\tnever,\n\t\t{\n\t\t\tcomponent: { alias: 'component'; required: false };\n\t\t\tframework: { alias: 'framework'; required: false };\n\t\t\thydrate: { alias: 'hydrate'; required: false };\n\t\t\tprops: { alias: 'props'; required: false };\n\t\t},\n\t\tRecord<string, string>,\n\t\tnever,\n\t\tnever,\n\t\ttrue,\n\t\tnever\n\t>;\n\n\t@HostBinding('attr.data-abs-props')\n\tget serializedProps() {\n\t\treturn JSON.stringify(this.props);\n\t}\n}\n",
|
|
@@ -826,17 +826,15 @@ const captureHmrPreservedInstanceStates = () => {
|
|
|
826
826
|
|
|
827
827
|
const cache = (scope.__ABS_HMR_INSTANCE_STATE__ ??= new Map());
|
|
828
828
|
const keyMap = scope.__ABS_HMR_INSTANCE_KEYS__;
|
|
829
|
-
const dead: WeakRef<object>[] = [];
|
|
830
829
|
const seen = new Set<string>();
|
|
831
830
|
|
|
832
831
|
cache.clear();
|
|
833
832
|
|
|
834
833
|
for (const ref of tracker) {
|
|
835
834
|
const instance = ref.deref();
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
835
|
+
// Skip already-GC'd refs. We don't bother bookkeeping a "dead"
|
|
836
|
+
// list because the entire tracker is cleared after this loop.
|
|
837
|
+
if (!instance) continue;
|
|
840
838
|
|
|
841
839
|
const className = instance.constructor?.name;
|
|
842
840
|
if (!className || className === 'Object') continue;
|
|
@@ -858,7 +856,14 @@ const captureHmrPreservedInstanceStates = () => {
|
|
|
858
856
|
stats.captured++;
|
|
859
857
|
}
|
|
860
858
|
|
|
861
|
-
|
|
859
|
+
// Every instance just captured is about to die: `destroyAngularApp()`
|
|
860
|
+
// runs immediately after this. New instances from the next bootstrap
|
|
861
|
+
// repopulate the tracker via their own `preserveAcrossHmr(this)`
|
|
862
|
+
// calls. If we left existing WeakRefs in place, the JS engine often
|
|
863
|
+
// won't have GC'd the old objects yet at the next capture — those
|
|
864
|
+
// zombies would inflate the captured count and trigger spurious
|
|
865
|
+
// collision warnings against the new generation's instances.
|
|
866
|
+
tracker.clear();
|
|
862
867
|
|
|
863
868
|
flag.value = true;
|
|
864
869
|
};
|
|
@@ -25,6 +25,7 @@ import type {} from '../../../types/globals';
|
|
|
25
25
|
type AngularComponentDefinition = {
|
|
26
26
|
providers?: unknown;
|
|
27
27
|
providersResolver?: unknown;
|
|
28
|
+
selectors?: unknown[];
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
type ComponentCtor = (abstract new (...args: never[]) => unknown) & {
|
|
@@ -217,6 +218,55 @@ const patchConstructor = (entry: RegistryEntry, newCtor: ComponentCtor) => {
|
|
|
217
218
|
entry.registeredAt = Date.now();
|
|
218
219
|
};
|
|
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
|
+
|
|
220
270
|
const applyUpdate = (id: string, newCtor: unknown) => {
|
|
221
271
|
if (!isComponentCtor(newCtor)) return false;
|
|
222
272
|
|
|
@@ -251,6 +301,13 @@ const applyUpdate = (id: string, newCtor: unknown) => {
|
|
|
251
301
|
|
|
252
302
|
try {
|
|
253
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);
|
|
254
311
|
|
|
255
312
|
return true;
|
|
256
313
|
} catch (err) {
|
|
@@ -262,6 +319,15 @@ const applyUpdate = (id: string, newCtor: unknown) => {
|
|
|
262
319
|
|
|
263
320
|
const refresh = () => {
|
|
264
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();
|
|
265
331
|
try {
|
|
266
332
|
window.__ANGULAR_APP__.tick();
|
|
267
333
|
} catch (err) {
|
package/package.json
CHANGED