@absolutejs/absolute 0.19.0-beta.706 → 0.19.0-beta.708
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/angular/browser.js +1 -19
- package/dist/angular/browser.js.map +3 -3
- package/dist/angular/components/constants.js +78 -0
- package/dist/angular/components/core/streamingSlotRegistrar.js +58 -0
- package/dist/angular/components/core/streamingSlotRegistry.js +114 -0
- package/dist/angular/components/defer-slot-payload.js +6 -0
- package/dist/angular/components/defer-slot-templates.directive.js +44 -0
- package/dist/angular/components/defer-slot.component.js +149 -0
- package/dist/angular/components/image.component.js +202 -0
- package/dist/angular/components/index.js +4 -0
- package/dist/angular/components/stream-slot.component.js +103 -0
- package/dist/angular/index.js +91 -36
- package/dist/angular/index.js.map +6 -6
- package/dist/angular/server.js +91 -36
- package/dist/angular/server.js.map +6 -6
- package/dist/build.js +242 -162
- package/dist/build.js.map +12 -12
- package/dist/cli/index.js +214 -142
- package/dist/client/index.js +86 -31
- package/dist/client/index.js.map +4 -4
- package/dist/core/streamingSlotRegistrar.js +1 -19
- package/dist/core/streamingSlotRegistrar.js.map +2 -2
- package/dist/core/streamingSlotRegistry.js +1 -19
- package/dist/core/streamingSlotRegistry.js.map +2 -2
- package/dist/dev/client/constants.ts +26 -0
- package/dist/dev/client/cssUtils.ts +307 -0
- package/dist/dev/client/domDiff.ts +226 -0
- package/dist/dev/client/domState.ts +421 -0
- package/dist/dev/client/domTracker.ts +61 -0
- package/dist/dev/client/errorOverlay.ts +184 -0
- package/dist/dev/client/frameworkDetect.ts +63 -0
- package/dist/dev/client/handlers/angular.ts +578 -0
- package/dist/dev/client/handlers/angularRuntime.ts +231 -0
- package/dist/dev/client/handlers/html.ts +364 -0
- package/dist/dev/client/handlers/htmx.ts +278 -0
- package/dist/dev/client/handlers/react.ts +108 -0
- package/dist/dev/client/handlers/rebuild.ts +153 -0
- package/dist/dev/client/handlers/svelte.ts +334 -0
- package/dist/dev/client/handlers/vue.ts +292 -0
- package/dist/dev/client/headPatch.ts +233 -0
- package/dist/dev/client/hmrClient.ts +273 -0
- package/dist/dev/client/hmrState.ts +14 -0
- package/dist/dev/client/moduleVersions.ts +62 -0
- package/dist/dev/client/reactRefreshSetup.ts +31 -0
- package/dist/index.js +282 -187
- package/dist/index.js.map +15 -15
- package/dist/islands/browser.js +1 -19
- package/dist/islands/browser.js.map +2 -2
- package/dist/islands/index.js +80 -26
- package/dist/islands/index.js.map +5 -5
- package/dist/react/browser.js +7 -25
- package/dist/react/browser.js.map +2 -2
- package/dist/react/components/browser/index.js +101 -101
- package/dist/react/components/index.js +104 -122
- package/dist/react/components/index.js.map +3 -3
- package/dist/react/hooks/index.js +1 -19
- package/dist/react/hooks/index.js.map +2 -2
- package/dist/react/index.js +101 -46
- package/dist/react/index.js.map +6 -6
- package/dist/react/jsxDevRuntimeCompat.js +1 -19
- package/dist/react/jsxDevRuntimeCompat.js.map +2 -2
- package/dist/react/server.js +13 -30
- package/dist/react/server.js.map +4 -4
- package/dist/src/angular/components/constants.d.ts +75 -0
- package/dist/src/angular/components/defer-slot-templates.directive.d.ts +7 -0
- package/dist/src/angular/components/defer-slot.component.d.ts +5 -2
- package/dist/src/angular/components/image.component.d.ts +5 -2
- package/dist/src/angular/components/index.d.ts +4 -4
- package/dist/src/angular/components/stream-slot.component.d.ts +3 -0
- package/dist/src/client/streamSwap.d.ts +0 -10
- package/dist/src/constants.d.ts +1 -0
- package/dist/src/dev/rebuildTrigger.d.ts +1 -1
- package/dist/src/svelte/renderToPipeableStream.d.ts +2 -2
- package/dist/src/svelte/renderToReadableStream.d.ts +2 -2
- package/dist/src/svelte/renderToString.d.ts +2 -2
- package/dist/src/vue/components/Image.d.ts +3 -3
- package/dist/svelte/browser.js +1 -19
- package/dist/svelte/browser.js.map +2 -2
- package/dist/svelte/components/AwaitSlot.svelte +39 -0
- package/dist/svelte/components/AwaitSlot.svelte.d.ts +2 -0
- package/dist/svelte/components/Head.svelte +144 -0
- package/dist/svelte/components/Head.svelte.d.ts +2 -0
- package/dist/svelte/components/Image.svelte +164 -0
- package/dist/svelte/components/Image.svelte.d.ts +5 -0
- package/dist/svelte/components/Island.svelte +71 -0
- package/dist/svelte/components/Island.svelte.d.ts +5 -0
- package/dist/svelte/components/JsonLd.svelte +21 -0
- package/dist/svelte/components/JsonLd.svelte.d.ts +2 -0
- package/dist/svelte/components/StreamSlot.svelte +41 -0
- package/dist/svelte/components/StreamSlot.svelte.d.ts +2 -0
- package/dist/svelte/index.js +93 -37
- package/dist/svelte/index.js.map +7 -7
- package/dist/svelte/server.js +16 -32
- package/dist/svelte/server.js.map +5 -5
- package/dist/types/globals.d.ts +130 -0
- package/dist/vue/browser.js +1 -19
- package/dist/vue/browser.js.map +2 -2
- package/dist/vue/components/Image.js +1 -19
- package/dist/vue/components/Image.js.map +3 -3
- package/dist/vue/components/index.js +1 -19
- package/dist/vue/components/index.js.map +3 -3
- package/dist/vue/index.js +91 -36
- package/dist/vue/index.js.map +7 -7
- package/dist/vue/server.js +13 -30
- package/dist/vue/server.js.map +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {} from '../../types/globals';
|
|
2
|
+
/* Framework detection and manifest lookup utilities */
|
|
3
|
+
|
|
4
|
+
export const detectCurrentFramework = () => {
|
|
5
|
+
if (window.__HMR_FRAMEWORK__) return window.__HMR_FRAMEWORK__;
|
|
6
|
+
if (window.__REACT_ROOT__) return 'react';
|
|
7
|
+
const path = window.location.pathname;
|
|
8
|
+
if (path === '/vue' || path.startsWith('/vue/')) return 'vue';
|
|
9
|
+
if (path === '/svelte' || path.startsWith('/svelte/')) return 'svelte';
|
|
10
|
+
if (path === '/angular' || path.startsWith('/angular/')) return 'angular';
|
|
11
|
+
if (path === '/htmx' || path.startsWith('/htmx/')) return 'htmx';
|
|
12
|
+
if (path === '/html' || path.startsWith('/html/')) return 'html';
|
|
13
|
+
if (path === '/react' || path.startsWith('/react/')) return 'react';
|
|
14
|
+
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
17
|
+
export const findIndexPath = (
|
|
18
|
+
manifest: Record<string, string> | undefined,
|
|
19
|
+
sourceFile: string | undefined,
|
|
20
|
+
framework: string
|
|
21
|
+
) => {
|
|
22
|
+
if (!manifest) return null;
|
|
23
|
+
|
|
24
|
+
if (sourceFile) {
|
|
25
|
+
const componentName = getComponentNameFromPath(sourceFile);
|
|
26
|
+
const indexKey = componentName ? `${componentName}Index` : null;
|
|
27
|
+
if (indexKey && manifest[indexKey]) {
|
|
28
|
+
return manifest[indexKey];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const frameworkPatterns: Record<string, RegExp> = {
|
|
33
|
+
angular: /angular/i,
|
|
34
|
+
react: /react/i,
|
|
35
|
+
svelte: /svelte/i,
|
|
36
|
+
vue: /vue/i
|
|
37
|
+
};
|
|
38
|
+
const pattern = frameworkPatterns[framework];
|
|
39
|
+
|
|
40
|
+
for (const key in manifest) {
|
|
41
|
+
const value = manifest[key];
|
|
42
|
+
if (
|
|
43
|
+
key.endsWith('Index') &&
|
|
44
|
+
value &&
|
|
45
|
+
(!pattern || pattern.test(key) || value.includes(`/${framework}/`))
|
|
46
|
+
) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
export const getComponentNameFromPath = (filePath: string) => {
|
|
54
|
+
if (!filePath) return null;
|
|
55
|
+
const parts = filePath.replace(/\\/g, '/').split('/');
|
|
56
|
+
const fileName = parts[parts.length - 1] || '';
|
|
57
|
+
const baseName = fileName.replace(/\.(tsx?|jsx?|vue|svelte|html)$/, '');
|
|
58
|
+
|
|
59
|
+
return baseName
|
|
60
|
+
.split(/[-_]/)
|
|
61
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
62
|
+
.join('');
|
|
63
|
+
};
|
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import type {} from '../../../types/globals';
|
|
2
|
+
/* Angular HMR — Re-Bootstrap with View Transitions API (Zero Flicker)
|
|
3
|
+
DEV MODE ONLY — never active in production.
|
|
4
|
+
|
|
5
|
+
Strategy:
|
|
6
|
+
1. Capture component state (ng.getComponent) + DOM state
|
|
7
|
+
2. Use document.startViewTransition() — browser captures a screenshot
|
|
8
|
+
3. Destroy old app, recreate root element, import new module
|
|
9
|
+
4. bootstrapApplication() renders new content (behind the screenshot)
|
|
10
|
+
5. After bootstrap: restore state via ng.getComponent + ng.applyChanges
|
|
11
|
+
6. View transition resolves — browser smoothly crossfades to new content
|
|
12
|
+
|
|
13
|
+
document.startViewTransition() is the native browser API for page
|
|
14
|
+
transitions. It captures a screenshot before the callback, runs the
|
|
15
|
+
callback (which can be async), and crossfades when the callback finishes.
|
|
16
|
+
The user never sees empty/default state — only the before and after. */
|
|
17
|
+
|
|
18
|
+
import { ANGULAR_INIT_TIMEOUT_MS } from '../constants';
|
|
19
|
+
import {
|
|
20
|
+
saveFormState,
|
|
21
|
+
restoreFormState,
|
|
22
|
+
saveScrollState,
|
|
23
|
+
restoreScrollState
|
|
24
|
+
} from '../domState';
|
|
25
|
+
import { detectCurrentFramework, findIndexPath } from '../frameworkDetect';
|
|
26
|
+
|
|
27
|
+
type HMRMessage = {
|
|
28
|
+
data: {
|
|
29
|
+
cssBaseName?: string;
|
|
30
|
+
cssUrl?: string;
|
|
31
|
+
html?: string;
|
|
32
|
+
manifest?: Record<string, string>;
|
|
33
|
+
pageModuleUrl?: string;
|
|
34
|
+
serverDuration?: number;
|
|
35
|
+
sourceFile?: string;
|
|
36
|
+
updateType?: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type NgApi = {
|
|
41
|
+
applyChanges?: (component: unknown) => void;
|
|
42
|
+
getComponent?: (element: Element) => unknown;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type AngularClientWindow = Window & {
|
|
46
|
+
ng?: NgApi;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type AngularHmrApi = {
|
|
50
|
+
applyUpdate: (id: string, newCtor: unknown) => boolean;
|
|
51
|
+
getRegistry?: () => Map<string, unknown>;
|
|
52
|
+
refresh: () => void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type ViewTransitionDocument = Document & {
|
|
56
|
+
startViewTransition?: (updateCallback: () => Promise<void>) => {
|
|
57
|
+
finished: Promise<void>;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type AngularComponentExport = ((...args: unknown[]) => unknown) & {
|
|
62
|
+
ɵcmp?: unknown;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isAngularComponentExport = (
|
|
66
|
+
value: unknown
|
|
67
|
+
): value is AngularComponentExport => {
|
|
68
|
+
if (typeof value !== 'function') {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return 'ɵcmp' in value && Boolean(value.ɵcmp);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
|
76
|
+
Boolean(value) && typeof value === 'object';
|
|
77
|
+
|
|
78
|
+
const swapStylesheet = (
|
|
79
|
+
cssUrl: string,
|
|
80
|
+
cssBaseName: string,
|
|
81
|
+
framework: string
|
|
82
|
+
) => {
|
|
83
|
+
let existingLink: HTMLLinkElement | null = null;
|
|
84
|
+
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
|
85
|
+
const linkEl = link instanceof HTMLLinkElement ? link : null;
|
|
86
|
+
const href = linkEl?.getAttribute('href') ?? '';
|
|
87
|
+
if (href.includes(cssBaseName) || href.includes(framework)) {
|
|
88
|
+
existingLink = linkEl;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (!existingLink) return;
|
|
92
|
+
|
|
93
|
+
const capturedExisting: HTMLLinkElement = existingLink;
|
|
94
|
+
const newLink = document.createElement('link');
|
|
95
|
+
newLink.rel = 'stylesheet';
|
|
96
|
+
newLink.href = `${cssUrl}?t=${Date.now()}`;
|
|
97
|
+
newLink.onload = function () {
|
|
98
|
+
if (capturedExisting && capturedExisting.parentNode)
|
|
99
|
+
capturedExisting.remove();
|
|
100
|
+
};
|
|
101
|
+
document.head.appendChild(newLink);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// ─── State Capture/Restore via ng.getComponent ──────────────
|
|
105
|
+
|
|
106
|
+
type StateSnapshot = {
|
|
107
|
+
selector: string;
|
|
108
|
+
index: number;
|
|
109
|
+
properties: Record<string, unknown>;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const readDomCounter = (
|
|
113
|
+
element: Element,
|
|
114
|
+
properties: Record<string, unknown>
|
|
115
|
+
) => {
|
|
116
|
+
element
|
|
117
|
+
.querySelectorAll('[class*="value"], [class*="count"]')
|
|
118
|
+
.forEach((stateEl) => {
|
|
119
|
+
const text = stateEl.textContent;
|
|
120
|
+
if (text === null || text.trim() === '') return;
|
|
121
|
+
const num = parseInt(text.trim(), 10);
|
|
122
|
+
if (!isNaN(num)) properties['__dom_counter'] = num;
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const copyInstanceProperty = (
|
|
127
|
+
instance: Record<string, unknown>,
|
|
128
|
+
key: string,
|
|
129
|
+
properties: Record<string, unknown>
|
|
130
|
+
) => {
|
|
131
|
+
if (key.startsWith('ɵ') || key.startsWith('__')) return;
|
|
132
|
+
const val = instance[key];
|
|
133
|
+
if (typeof val === 'function') return;
|
|
134
|
+
properties[key] = val;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const captureInstanceProperties = (
|
|
138
|
+
ngApi: NgApi | undefined,
|
|
139
|
+
element: Element,
|
|
140
|
+
properties: Record<string, unknown>
|
|
141
|
+
) => {
|
|
142
|
+
if (!ngApi || typeof ngApi.getComponent !== 'function') return;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const instance = ngApi.getComponent(element);
|
|
146
|
+
if (!isObjectRecord(instance)) return;
|
|
147
|
+
|
|
148
|
+
Object.keys(instance).forEach((key) => {
|
|
149
|
+
copyInstanceProperty(instance, key, properties);
|
|
150
|
+
});
|
|
151
|
+
} catch {
|
|
152
|
+
/* ignored */
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const captureComponentState = () => {
|
|
157
|
+
const snapshots: StateSnapshot[] = [];
|
|
158
|
+
const selectorCounts = new Map<string, number>();
|
|
159
|
+
const angularWindow: AngularClientWindow = window;
|
|
160
|
+
const ngApi = angularWindow.ng;
|
|
161
|
+
|
|
162
|
+
document.querySelectorAll('*').forEach((elem) => {
|
|
163
|
+
const tagName = elem.tagName.toLowerCase();
|
|
164
|
+
if (!tagName.includes('-')) return;
|
|
165
|
+
|
|
166
|
+
const count = selectorCounts.get(tagName) || 0;
|
|
167
|
+
selectorCounts.set(tagName, count + 1);
|
|
168
|
+
|
|
169
|
+
const properties: Record<string, unknown> = {};
|
|
170
|
+
readDomCounter(elem, properties);
|
|
171
|
+
captureInstanceProperties(ngApi, elem, properties);
|
|
172
|
+
|
|
173
|
+
if (Object.keys(properties).length > 0) {
|
|
174
|
+
snapshots.push({ index: count, properties, selector: tagName });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return snapshots;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const safeSetProperty = (
|
|
182
|
+
instance: Record<string, unknown>,
|
|
183
|
+
key: string,
|
|
184
|
+
value: unknown
|
|
185
|
+
) => {
|
|
186
|
+
try {
|
|
187
|
+
instance[key] = value;
|
|
188
|
+
} catch {
|
|
189
|
+
/* ignored */
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const restoreInstanceProperties = (
|
|
194
|
+
instance: Record<string, unknown>,
|
|
195
|
+
snap: StateSnapshot
|
|
196
|
+
) => {
|
|
197
|
+
const domCounter = snap.properties['__dom_counter'];
|
|
198
|
+
Object.entries(snap.properties).forEach(([key, value]) => {
|
|
199
|
+
if (key === '__dom_counter') return;
|
|
200
|
+
safeSetProperty(instance, key, value);
|
|
201
|
+
});
|
|
202
|
+
if (
|
|
203
|
+
domCounter !== undefined &&
|
|
204
|
+
typeof domCounter === 'number' &&
|
|
205
|
+
'count' in instance
|
|
206
|
+
) {
|
|
207
|
+
instance['count'] = domCounter;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const restoreViaInstance = (
|
|
212
|
+
ngApi: NgApi | undefined,
|
|
213
|
+
element: Element,
|
|
214
|
+
snap: StateSnapshot
|
|
215
|
+
) => {
|
|
216
|
+
if (!ngApi || typeof ngApi.getComponent !== 'function') return false;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const instance = ngApi.getComponent(element);
|
|
220
|
+
if (!isObjectRecord(instance)) return false;
|
|
221
|
+
|
|
222
|
+
restoreInstanceProperties(instance, snap);
|
|
223
|
+
if (typeof ngApi.applyChanges === 'function')
|
|
224
|
+
ngApi.applyChanges(element);
|
|
225
|
+
|
|
226
|
+
return true;
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const restoreDomFallback = (element: Element, snap: StateSnapshot) => {
|
|
233
|
+
const domCounter = snap.properties['__dom_counter'];
|
|
234
|
+
if (domCounter === undefined) return;
|
|
235
|
+
|
|
236
|
+
element
|
|
237
|
+
.querySelectorAll('[class*="value"], [class*="count"]')
|
|
238
|
+
.forEach((counterEl) => {
|
|
239
|
+
counterEl.textContent = String(domCounter);
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const restoreComponentState = (snapshots: StateSnapshot[]) => {
|
|
244
|
+
const angularWindow: AngularClientWindow = window;
|
|
245
|
+
const ngApi = angularWindow.ng;
|
|
246
|
+
if (snapshots.length === 0) return;
|
|
247
|
+
|
|
248
|
+
const bySelector = new Map<string, StateSnapshot[]>();
|
|
249
|
+
for (const snap of snapshots) {
|
|
250
|
+
const list = bySelector.get(snap.selector) || [];
|
|
251
|
+
list.push(snap);
|
|
252
|
+
bySelector.set(snap.selector, list);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
bySelector.forEach((snaps, selector) => {
|
|
256
|
+
const elements = document.querySelectorAll(selector);
|
|
257
|
+
snaps.forEach((snap) => {
|
|
258
|
+
const element = elements[snap.index];
|
|
259
|
+
if (!element) return;
|
|
260
|
+
|
|
261
|
+
const restored = restoreViaInstance(ngApi, element, snap);
|
|
262
|
+
if (!restored) restoreDomFallback(element, snap);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// ─── Wait for Angular bootstrap (event-based, no polling) ───
|
|
268
|
+
// Installs a property setter trap on window.__ANGULAR_APP__ that
|
|
269
|
+
// resolves the promise the instant the bootstrap code writes to it.
|
|
270
|
+
// Falls back to a short timeout in case the setter is bypassed.
|
|
271
|
+
|
|
272
|
+
const waitForAngularApp = () => {
|
|
273
|
+
if (window.__ANGULAR_APP__) return Promise.resolve();
|
|
274
|
+
|
|
275
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
276
|
+
const timeout = setTimeout(resolve, ANGULAR_INIT_TIMEOUT_MS);
|
|
277
|
+
|
|
278
|
+
let stored = window.__ANGULAR_APP__;
|
|
279
|
+
|
|
280
|
+
Object.defineProperty(window, '__ANGULAR_APP__', {
|
|
281
|
+
configurable: true,
|
|
282
|
+
enumerable: true,
|
|
283
|
+
get() {
|
|
284
|
+
return stored;
|
|
285
|
+
},
|
|
286
|
+
set(val) {
|
|
287
|
+
stored = val;
|
|
288
|
+
Object.defineProperty(window, '__ANGULAR_APP__', {
|
|
289
|
+
configurable: true,
|
|
290
|
+
enumerable: true,
|
|
291
|
+
value: val,
|
|
292
|
+
writable: true
|
|
293
|
+
});
|
|
294
|
+
clearTimeout(timeout);
|
|
295
|
+
resolve();
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return promise;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// ============================================================
|
|
303
|
+
// FAST UPDATE — Runtime patching without destroy/re-bootstrap
|
|
304
|
+
// ============================================================
|
|
305
|
+
|
|
306
|
+
const suppressNg0912 = () => {
|
|
307
|
+
const origWarn = console.warn;
|
|
308
|
+
console.warn = function (...args: unknown[]) {
|
|
309
|
+
if (typeof args[0] === 'string' && args[0].includes('NG0912')) return;
|
|
310
|
+
origWarn.apply(console, args);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
return origWarn;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const tryPatchExport = (
|
|
317
|
+
exportName: string,
|
|
318
|
+
newModule: Record<string, unknown>,
|
|
319
|
+
registry: Map<string, unknown>,
|
|
320
|
+
hmr: AngularHmrApi,
|
|
321
|
+
sourceFile: string
|
|
322
|
+
) => {
|
|
323
|
+
const exported = newModule[exportName];
|
|
324
|
+
if (!isAngularComponentExport(exported)) return 'skip';
|
|
325
|
+
|
|
326
|
+
const registryId = `${sourceFile}#${exportName}`;
|
|
327
|
+
if (!registry.has(registryId)) return 'skip';
|
|
328
|
+
|
|
329
|
+
const success = hmr.applyUpdate(registryId, exported);
|
|
330
|
+
if (!success) return 'fail';
|
|
331
|
+
|
|
332
|
+
return 'patched';
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const patchRegisteredComponents = (
|
|
336
|
+
newModule: Record<string, unknown>,
|
|
337
|
+
registry: Map<string, unknown>,
|
|
338
|
+
hmr: AngularHmrApi,
|
|
339
|
+
sourceFile: string
|
|
340
|
+
) => {
|
|
341
|
+
let patchedAny = false;
|
|
342
|
+
const allPatched = Object.keys(newModule).every((exportName) => {
|
|
343
|
+
const result = tryPatchExport(
|
|
344
|
+
exportName,
|
|
345
|
+
newModule,
|
|
346
|
+
registry,
|
|
347
|
+
hmr,
|
|
348
|
+
sourceFile
|
|
349
|
+
);
|
|
350
|
+
if (result === 'skip') {
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
if (result === 'fail') {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
patchedAny = true;
|
|
357
|
+
|
|
358
|
+
return true;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return { allPatched, patchedAny };
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const attemptFastPatch = async (
|
|
365
|
+
indexPath: string,
|
|
366
|
+
registry: Map<string, unknown>,
|
|
367
|
+
hmr: AngularHmrApi,
|
|
368
|
+
sourceFile: string,
|
|
369
|
+
origWarn: typeof console.warn
|
|
370
|
+
) => {
|
|
371
|
+
try {
|
|
372
|
+
const newModule = await import(`${indexPath}?t=${Date.now()}`);
|
|
373
|
+
|
|
374
|
+
console.warn = origWarn;
|
|
375
|
+
|
|
376
|
+
const { allPatched, patchedAny } = patchRegisteredComponents(
|
|
377
|
+
newModule,
|
|
378
|
+
registry,
|
|
379
|
+
hmr,
|
|
380
|
+
sourceFile
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
if (!patchedAny) return false;
|
|
384
|
+
if (!allPatched) return false;
|
|
385
|
+
|
|
386
|
+
hmr.refresh();
|
|
387
|
+
|
|
388
|
+
return true;
|
|
389
|
+
} catch (err) {
|
|
390
|
+
console.warn = origWarn;
|
|
391
|
+
console.warn('[HMR] Angular fast update failed, falling back:', err);
|
|
392
|
+
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// handleFastUpdate is kept for future use when the fast path is re-enabled.
|
|
398
|
+
const _handleFastUpdate = async (message: HMRMessage) => {
|
|
399
|
+
const hmr = window.__ANGULAR_HMR__;
|
|
400
|
+
if (!hmr || !hmr.getRegistry) return false;
|
|
401
|
+
|
|
402
|
+
const registry = hmr.getRegistry();
|
|
403
|
+
if (registry.size === 0) return false;
|
|
404
|
+
|
|
405
|
+
const indexPath = findIndexPath(
|
|
406
|
+
message.data.manifest,
|
|
407
|
+
message.data.sourceFile,
|
|
408
|
+
'angular'
|
|
409
|
+
);
|
|
410
|
+
if (!indexPath) return false;
|
|
411
|
+
|
|
412
|
+
const origWarn = suppressNg0912();
|
|
413
|
+
|
|
414
|
+
const patched = await attemptFastPatch(
|
|
415
|
+
indexPath,
|
|
416
|
+
registry,
|
|
417
|
+
hmr,
|
|
418
|
+
message.data.sourceFile || '',
|
|
419
|
+
origWarn
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
if (patched && message.data.cssUrl) {
|
|
423
|
+
swapStylesheet(
|
|
424
|
+
message.data.cssUrl,
|
|
425
|
+
message.data.cssBaseName || '',
|
|
426
|
+
'angular'
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return patched;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// ============================================================
|
|
434
|
+
// MAIN ENTRY POINT
|
|
435
|
+
// ============================================================
|
|
436
|
+
|
|
437
|
+
export const handleAngularUpdate = (message: HMRMessage) => {
|
|
438
|
+
if (detectCurrentFramework() !== 'angular') return;
|
|
439
|
+
|
|
440
|
+
const updateType = message.data.updateType || 'logic';
|
|
441
|
+
|
|
442
|
+
if (
|
|
443
|
+
(updateType === 'style' || updateType === 'css-only') &&
|
|
444
|
+
message.data.cssUrl
|
|
445
|
+
) {
|
|
446
|
+
swapStylesheet(
|
|
447
|
+
message.data.cssUrl,
|
|
448
|
+
message.data.cssBaseName || '',
|
|
449
|
+
'angular'
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
handleFullUpdate(message);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// ============================================================
|
|
459
|
+
// RE-BOOTSTRAP WITH VIEW TRANSITIONS API
|
|
460
|
+
// ============================================================
|
|
461
|
+
|
|
462
|
+
const findRootSelector = (container: Element) => {
|
|
463
|
+
const candidates = container.querySelectorAll('*');
|
|
464
|
+
for (let idx = 0; idx < candidates.length; idx++) {
|
|
465
|
+
const candidate = candidates[idx];
|
|
466
|
+
if (!candidate) continue;
|
|
467
|
+
const tag = candidate.tagName.toLowerCase();
|
|
468
|
+
if (tag.includes('-')) return tag;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return null;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const destroyAngularApp = () => {
|
|
475
|
+
if (!window.__ANGULAR_APP__) return;
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
window.__ANGULAR_APP__.destroy();
|
|
479
|
+
} catch {
|
|
480
|
+
/* ignored */
|
|
481
|
+
}
|
|
482
|
+
window.__ANGULAR_APP__ = null;
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const bootstrapAngularModule = async (
|
|
486
|
+
indexPath: string,
|
|
487
|
+
rootSelector: string | null,
|
|
488
|
+
rootContainer: Element
|
|
489
|
+
) => {
|
|
490
|
+
if (rootSelector && !rootContainer.querySelector(rootSelector)) {
|
|
491
|
+
rootContainer.appendChild(document.createElement(rootSelector));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
window.__HMR_SKIP_HYDRATION__ = true;
|
|
495
|
+
|
|
496
|
+
const origWarn = suppressNg0912();
|
|
497
|
+
|
|
498
|
+
await import(`${indexPath}?t=${Date.now()}`);
|
|
499
|
+
await waitForAngularApp();
|
|
500
|
+
|
|
501
|
+
console.warn = origWarn;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const tickAngularApp = () => {
|
|
505
|
+
if (!window.__ANGULAR_APP__) return;
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
window.__ANGULAR_APP__.tick();
|
|
509
|
+
} catch {
|
|
510
|
+
/* ignored */
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const runWithViewTransition = (updateFn: () => Promise<void>) => {
|
|
515
|
+
const doc: ViewTransitionDocument = document;
|
|
516
|
+
if (typeof doc.startViewTransition !== 'function') {
|
|
517
|
+
updateFn().catch((err: unknown) => {
|
|
518
|
+
console.warn('[HMR] Angular update failed (non-fatal):', err);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let styleEl: HTMLStyleElement | null = null;
|
|
525
|
+
try {
|
|
526
|
+
styleEl = document.createElement('style');
|
|
527
|
+
styleEl.textContent =
|
|
528
|
+
'::view-transition-old(root),::view-transition-new(root){animation:none!important}';
|
|
529
|
+
document.head.appendChild(styleEl);
|
|
530
|
+
} catch {
|
|
531
|
+
/* ignored */
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const removeStyle = () => {
|
|
535
|
+
if (styleEl && styleEl.parentNode) styleEl.remove();
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
doc.startViewTransition(async () => {
|
|
539
|
+
await updateFn();
|
|
540
|
+
})
|
|
541
|
+
.finished.then(removeStyle)
|
|
542
|
+
.catch(removeStyle);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const handleFullUpdate = (message: HMRMessage) => {
|
|
546
|
+
const componentState = captureComponentState();
|
|
547
|
+
const scrollState = saveScrollState();
|
|
548
|
+
const formState = saveFormState();
|
|
549
|
+
|
|
550
|
+
if (message.data.cssUrl) {
|
|
551
|
+
swapStylesheet(
|
|
552
|
+
message.data.cssUrl,
|
|
553
|
+
message.data.cssBaseName || '',
|
|
554
|
+
'angular'
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const rootContainer = document.getElementById('root') || document.body;
|
|
559
|
+
const rootSelector = findRootSelector(rootContainer);
|
|
560
|
+
|
|
561
|
+
const indexPath = findIndexPath(
|
|
562
|
+
message.data.manifest,
|
|
563
|
+
message.data.sourceFile,
|
|
564
|
+
'angular'
|
|
565
|
+
);
|
|
566
|
+
if (!indexPath) return;
|
|
567
|
+
|
|
568
|
+
const doUpdate = async () => {
|
|
569
|
+
destroyAngularApp();
|
|
570
|
+
await bootstrapAngularModule(indexPath, rootSelector, rootContainer);
|
|
571
|
+
restoreComponentState(componentState);
|
|
572
|
+
tickAngularApp();
|
|
573
|
+
restoreFormState(formState);
|
|
574
|
+
restoreScrollState(scrollState);
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
runWithViewTransition(doUpdate);
|
|
578
|
+
};
|