@absolutejs/absolute 0.20.0-beta.0 → 0.20.0-beta.2

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.
Files changed (91) hide show
  1. package/dist/angular/components/core/streamingSlotRegistrar.js +1 -1
  2. package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
  3. package/dist/angular/index.js +14 -1
  4. package/dist/angular/index.js.map +3 -3
  5. package/dist/angular/server.js +14 -1
  6. package/dist/angular/server.js.map +3 -3
  7. package/dist/build.js +206 -47
  8. package/dist/build.js.map +20 -19
  9. package/dist/cli/config/server.js +2 -2
  10. package/dist/cli/index.js +16506 -11326
  11. package/dist/dev/client/cssUtils.ts +98 -41
  12. package/dist/dev/client/handlers/html.ts +75 -15
  13. package/dist/dev/client/handlers/htmx.ts +43 -8
  14. package/dist/dev/client/handlers/react.ts +114 -32
  15. package/dist/dev/client/handlers/rebuild.ts +28 -8
  16. package/dist/dev/client/handlers/svelte.ts +33 -50
  17. package/dist/dev/client/handlers/vue.ts +36 -45
  18. package/dist/dev/client/hmrClient.ts +38 -6
  19. package/dist/dev/client/hmrTiming.ts +141 -0
  20. package/dist/dev/serverBootstrap.js +20 -11
  21. package/dist/index.js +953 -423
  22. package/dist/index.js.map +27 -23
  23. package/dist/mobile/browser.js.map +1 -1
  24. package/dist/mobile/index.js +2667 -260
  25. package/dist/mobile/index.js.map +27 -7
  26. package/dist/react/index.js +16 -3
  27. package/dist/react/index.js.map +4 -4
  28. package/dist/react/jsxDevRuntimeCompat.js +22 -2
  29. package/dist/react/jsxDevRuntimeCompat.js.map +5 -4
  30. package/dist/react/jsxRuntimeCompat.js +90 -0
  31. package/dist/react/jsxRuntimeCompat.js.map +10 -0
  32. package/dist/react/server.js +16 -3
  33. package/dist/react/server.js.map +4 -4
  34. package/dist/src/build/compileVue.d.ts +1 -1
  35. package/dist/src/build/resolveBuildMode.d.ts +7 -0
  36. package/dist/src/cli/scripts/dev.d.ts +4 -1
  37. package/dist/src/cli/scripts/mobile.d.ts +1 -0
  38. package/dist/src/cli/utils.d.ts +2 -2
  39. package/dist/src/core/build.d.ts +1 -1
  40. package/dist/src/dev/clientManager.d.ts +7 -0
  41. package/dist/src/dev/configResolver.d.ts +2 -0
  42. package/dist/src/dev/moduleMapper.d.ts +1 -1
  43. package/dist/src/dev/moduleServer.d.ts +2 -1
  44. package/dist/src/dev/pathUtils.d.ts +1 -1
  45. package/dist/src/dev/reactComponentClassifier.d.ts +1 -1
  46. package/dist/src/mobile/androidConformance.d.ts +42 -0
  47. package/dist/src/mobile/androidEmulatorController.d.ts +100 -0
  48. package/dist/src/mobile/androidNativeWatcher.d.ts +19 -0
  49. package/dist/src/mobile/androidRelease.d.ts +51 -0
  50. package/dist/src/mobile/androidWebView.d.ts +46 -0
  51. package/dist/src/mobile/associationFiles.d.ts +49 -0
  52. package/dist/src/mobile/buildMetadata.d.ts +10 -0
  53. package/dist/src/mobile/buildPipeline.d.ts +8 -0
  54. package/dist/src/mobile/buildRelease.d.ts +24 -0
  55. package/dist/src/mobile/capacitorBundle.d.ts +28 -0
  56. package/dist/src/mobile/capacitorProject.d.ts +9 -0
  57. package/dist/src/mobile/client.d.ts +1 -1
  58. package/dist/src/mobile/config.d.ts +17 -0
  59. package/dist/src/mobile/emulatorDoctor.d.ts +25 -0
  60. package/dist/src/mobile/emulatorInstaller.d.ts +41 -0
  61. package/dist/src/mobile/index.d.ts +14 -0
  62. package/dist/src/mobile/iosRelease.d.ts +61 -0
  63. package/dist/src/mobile/materializedBundle.d.ts +14 -0
  64. package/dist/src/mobile/nativeDeepLinks.d.ts +5 -0
  65. package/dist/src/mobile/releaseArtifact.d.ts +1 -0
  66. package/dist/src/mobile/releaseDoctor.d.ts +13 -0
  67. package/dist/src/mobile/releasePublisher.d.ts +130 -0
  68. package/dist/src/mobile/routeMatcher.d.ts +3 -0
  69. package/dist/src/mobile/routeMetadataTransform.d.ts +13 -0
  70. package/dist/src/mobile/shellBootstrap.d.ts +1 -0
  71. package/dist/src/mobile/transport.d.ts +31 -0
  72. package/dist/src/react/hooks/useMediaQuery.d.ts +1 -1
  73. package/dist/src/react/jsxDevRuntimeCompat.d.ts +1 -1
  74. package/dist/src/react/jsxRuntimeCompat.d.ts +7 -0
  75. package/dist/src/react/pageHandler.d.ts +3 -0
  76. package/dist/src/utils/loadConfig.d.ts +1 -0
  77. package/dist/src/utils/logger.d.ts +2 -0
  78. package/dist/src/utils/userAgentFunctions.d.ts +1 -1
  79. package/dist/svelte/index.js +14 -1
  80. package/dist/svelte/index.js.map +3 -3
  81. package/dist/svelte/server.js +14 -1
  82. package/dist/svelte/server.js.map +3 -3
  83. package/dist/types/build.d.ts +45 -0
  84. package/dist/types/cli.d.ts +2 -0
  85. package/dist/types/globals.d.ts +14 -0
  86. package/dist/types/messages.d.ts +12 -0
  87. package/dist/vue/index.js +14 -1
  88. package/dist/vue/index.js.map +3 -3
  89. package/dist/vue/server.js +14 -1
  90. package/dist/vue/server.js.map +3 -3
  91. package/package.json +35 -7
@@ -149,34 +149,90 @@ const findManifestHref = (
149
149
  return null;
150
150
  };
151
151
 
152
+ const replaceStylesheetLink = (
153
+ existingLink: HTMLLinkElement,
154
+ newHref: string
155
+ ) => {
156
+ const replacement = existingLink.cloneNode(true);
157
+ if (!(replacement instanceof HTMLLinkElement)) {
158
+ return Promise.resolve(false);
159
+ }
160
+ const { promise, resolve } = Promise.withResolvers<boolean>();
161
+ let settled = false;
162
+ const finish = (applied: boolean) => {
163
+ if (settled) return;
164
+ settled = true;
165
+ if (applied) existingLink.remove();
166
+ else replacement.remove();
167
+ resolve(applied);
168
+ };
169
+ replacement.onload = () => finish(true);
170
+ replacement.onerror = () => finish(false);
171
+ replacement.href = newHref;
172
+ existingLink.after(replacement);
173
+ setTimeout(() => {
174
+ if (replacement.sheet) finish(true);
175
+ }, CSS_SHEET_READY_TIMEOUT_MS);
176
+ setTimeout(
177
+ () => finish(Boolean(replacement.sheet)),
178
+ CSS_MAX_PARSE_TIMEOUT_MS
179
+ );
180
+
181
+ return promise;
182
+ };
183
+
152
184
  const updateStylesheetLink = (
153
185
  link: Element,
154
186
  manifest: Record<string, string>
155
187
  ) => {
156
- if (!(link instanceof HTMLLinkElement)) return;
188
+ if (!(link instanceof HTMLLinkElement)) return Promise.resolve(true);
157
189
  const href = link.getAttribute('href');
158
- if (!href || href.includes('htmx.min.js')) return;
190
+ if (!href || href.includes('htmx.min.js')) return Promise.resolve(true);
159
191
 
160
192
  let newHref: string | null = null;
161
193
  if (manifest) {
162
194
  const baseName = getCSSBaseName(href);
163
195
  newHref = findManifestHref(manifest, baseName);
164
196
  }
197
+ const currentUrl = new URL(href, window.location.href);
198
+ if (!newHref && currentUrl.origin !== window.location.origin) {
199
+ return Promise.resolve(true);
200
+ }
165
201
 
202
+ let replacementHref: string;
166
203
  if (newHref && newHref !== href) {
167
- link.href = `${newHref}?t=${Date.now()}`;
204
+ replacementHref = `${newHref}?t=${Date.now()}`;
168
205
  } else {
169
- const url = new URL(href, window.location.origin);
170
- url.searchParams.set('t', Date.now().toString());
171
- link.href = url.toString();
206
+ currentUrl.searchParams.set('t', Date.now().toString());
207
+ replacementHref = currentUrl.toString();
172
208
  }
209
+
210
+ return replaceStylesheetLink(link, replacementHref);
173
211
  };
174
212
 
175
213
  export const reloadCSSStylesheets = (manifest: Record<string, string>) => {
176
- const stylesheets = document.querySelectorAll('link[rel="stylesheet"]');
177
- stylesheets.forEach((link) => {
178
- updateStylesheetLink(link, manifest);
179
- });
214
+ const stylesheets = Array.from(
215
+ document.querySelectorAll('link[rel="stylesheet"]')
216
+ );
217
+
218
+ return Promise.all(
219
+ stylesheets.map((link) => updateStylesheetLink(link, manifest))
220
+ ).then((results) => results.every(Boolean));
221
+ };
222
+
223
+ export const swapCSSStylesheet = (
224
+ cssUrl: string,
225
+ matches: (href: string) => boolean
226
+ ) => {
227
+ const existingLink = Array.from(
228
+ document.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]')
229
+ ).find((link) => matches(link.getAttribute('href') ?? ''));
230
+ if (!existingLink) return Promise.resolve(false);
231
+
232
+ return replaceStylesheetLink(
233
+ existingLink,
234
+ `${cssUrl}${cssUrl.includes('?') ? '&' : '?'}t=${Date.now()}`
235
+ );
180
236
  };
181
237
 
182
238
  const createCSSLoadPromise = (
@@ -268,44 +324,45 @@ const chainRAF = (depth: number, callback: () => void) => {
268
324
  activates new CSS, removes old CSS. Handles first-update delay. */
269
325
  export const waitForCSSAndUpdate = (
270
326
  cssResult: CSSUpdateResult,
271
- updateBody: () => void
327
+ updateBody: () => void | Promise<void>
272
328
  ) => {
273
329
  const { linksToActivate, linksToRemove, linksToWaitFor } = cssResult;
274
-
275
- if (linksToWaitFor.length > 0) {
276
- void Promise.all(linksToWaitFor).then(() => {
277
- setTimeout(() => {
278
- chainRAF(RAF_BATCH_COUNT, () => {
279
- updateBody();
280
- activateLinks(linksToActivate);
330
+ const { promise, reject, resolve } = Promise.withResolvers<void>();
331
+ const doUpdate = () => {
332
+ chainRAF(RAF_BATCH_COUNT, () => {
333
+ let updateResult: void | Promise<void>;
334
+ try {
335
+ updateResult = updateBody();
336
+ activateLinks(linksToActivate);
337
+ } catch (error) {
338
+ reject(error);
339
+
340
+ return;
341
+ }
342
+ void Promise.resolve(updateResult).then(
343
+ () => {
281
344
  requestAnimationFrame(() => {
282
345
  removeLinks(linksToRemove);
283
- if (hmrState.isFirstHMRUpdate) {
284
- hmrState.isFirstHMRUpdate = false;
285
- }
346
+ hmrState.isFirstHMRUpdate = false;
347
+ resolve();
286
348
  });
287
- });
288
- }, DOM_UPDATE_DELAY_MS);
289
-
290
- return undefined;
291
- });
292
-
293
- return;
294
- }
295
-
296
- const doUpdate = function () {
297
- chainRAF(RAF_BATCH_COUNT, () => {
298
- updateBody();
299
- requestAnimationFrame(() => {
300
- removeLinks(linksToRemove);
301
- });
349
+ },
350
+ (error: unknown) => reject(error)
351
+ );
302
352
  });
303
353
  };
304
-
305
- if (hmrState.isFirstHMRUpdate) {
306
- hmrState.isFirstHMRUpdate = false;
307
- setTimeout(doUpdate, DOM_UPDATE_DELAY_MS);
354
+ const scheduleUpdate = () => {
355
+ if (hmrState.isFirstHMRUpdate || linksToWaitFor.length > 0) {
356
+ setTimeout(doUpdate, DOM_UPDATE_DELAY_MS);
357
+ } else {
358
+ doUpdate();
359
+ }
360
+ };
361
+ if (linksToWaitFor.length > 0) {
362
+ void Promise.all(linksToWaitFor).then(scheduleUpdate, reject);
308
363
  } else {
309
- doUpdate();
364
+ scheduleUpdate();
310
365
  }
366
+
367
+ return promise;
311
368
  };
@@ -17,6 +17,7 @@ import { detectCurrentFramework } from '../frameworkDetect';
17
17
  import type { ScriptInfo } from '../../../types/client';
18
18
  import { restoreDOMChanges, snapshotDOMChanges } from '../domTracker';
19
19
  import { hmrState } from '../hmrState';
20
+ import { sendAbsoluteHmrTiming } from '../hmrTiming';
20
21
 
21
22
  const parseHTMLMessage = (
22
23
  html: string | { body?: string; head?: string } | null | undefined
@@ -57,11 +58,10 @@ const handleHTMLBodyWithHead = (
57
58
 
58
59
  const cssResult = processCSSLinks(htmlHead);
59
60
 
60
- const updateBodyAfterCSS = () => {
61
+ const updateBodyAfterCSS = () =>
61
62
  updateHTMLBody(htmlBody, htmlDomState, document.body);
62
- };
63
63
 
64
- waitForCSSAndUpdate(cssResult, updateBodyAfterCSS);
64
+ return waitForCSSAndUpdate(cssResult, updateBodyAfterCSS);
65
65
  };
66
66
 
67
67
  const handleHTMLBodyWithoutHead = (
@@ -72,17 +72,21 @@ const handleHTMLBodyWithoutHead = (
72
72
  if (!container) {
73
73
  sessionStorage.removeItem('__HMR_ACTIVE__');
74
74
 
75
- return;
75
+ return Promise.resolve();
76
76
  }
77
77
 
78
- updateHTMLBodyDirect(htmlBody, htmlDomState, container);
78
+ const updated = updateHTMLBodyDirect(htmlBody, htmlDomState, container);
79
79
  restoreDOMState(container, htmlDomState);
80
+
81
+ return updated;
80
82
  };
81
83
 
82
84
  export const handleHTMLUpdate = (message: {
83
85
  data: {
84
86
  html?: string | { body?: string; head?: string } | null;
87
+ serverDuration?: number;
85
88
  };
89
+ timestamp?: number;
86
90
  }) => {
87
91
  const htmlFrameworkCheck = detectCurrentFramework();
88
92
  if (htmlFrameworkCheck !== 'html') {
@@ -94,6 +98,7 @@ export const handleHTMLUpdate = (message: {
94
98
  }
95
99
 
96
100
  sessionStorage.setItem('__HMR_ACTIVE__', 'true');
101
+ const clientStart = performance.now();
97
102
 
98
103
  const htmlDomState = saveDOMState(document.body);
99
104
  const { body: htmlBody, head: htmlHead } = parseHTMLMessage(
@@ -102,18 +107,48 @@ export const handleHTMLUpdate = (message: {
102
107
 
103
108
  if (!htmlBody) {
104
109
  sessionStorage.removeItem('__HMR_ACTIVE__');
110
+ sendAbsoluteHmrTiming({
111
+ clientStart,
112
+ kind: 'html',
113
+ outcome: 'failed',
114
+ serverMs: message.data.serverDuration,
115
+ updateId: message.timestamp
116
+ });
105
117
 
106
118
  return;
107
119
  }
108
120
 
109
- if (htmlHead) {
110
- handleHTMLBodyWithHead(htmlBody, htmlHead, htmlDomState);
111
- } else {
112
- handleHTMLBodyWithoutHead(htmlBody, htmlDomState);
113
- }
121
+ const update = htmlHead
122
+ ? handleHTMLBodyWithHead(htmlBody, htmlHead, htmlDomState)
123
+ : handleHTMLBodyWithoutHead(htmlBody, htmlDomState);
124
+ void update.then(
125
+ () => {
126
+ sendAbsoluteHmrTiming({
127
+ clientStart,
128
+ kind: 'html',
129
+ serverMs: message.data.serverDuration,
130
+ updateId: message.timestamp
131
+ });
132
+ },
133
+ () => {
134
+ sendAbsoluteHmrTiming({
135
+ clientStart,
136
+ kind: 'html',
137
+ outcome: 'reloaded',
138
+ serverMs: message.data.serverDuration,
139
+ updateId: message.timestamp
140
+ });
141
+ window.location.reload();
142
+ }
143
+ );
114
144
  };
115
145
  export const handleScriptUpdate = (message: {
116
- data: { framework?: string; scriptPath?: string };
146
+ data: {
147
+ framework?: string;
148
+ scriptPath?: string;
149
+ serverDuration?: number;
150
+ };
151
+ timestamp?: number;
117
152
  }) => {
118
153
  const scriptFramework = message.data.framework;
119
154
  const currentFw = detectCurrentFramework();
@@ -139,13 +174,30 @@ export const handleScriptUpdate = (message: {
139
174
  });
140
175
 
141
176
  const cacheBustedPath = `${scriptPath}?t=${Date.now()}`;
177
+ const clientStart = performance.now();
142
178
  import(cacheBustedPath)
143
- .then(() => true)
179
+ .then(() => {
180
+ sendAbsoluteHmrTiming({
181
+ clientStart,
182
+ kind: 'script',
183
+ serverMs: message.data.serverDuration,
184
+ updateId: message.timestamp
185
+ });
186
+
187
+ return true;
188
+ })
144
189
  .catch((err: unknown) => {
145
190
  console.error(
146
191
  '[HMR] Script hot-reload failed, falling back to page reload:',
147
192
  err
148
193
  );
194
+ sendAbsoluteHmrTiming({
195
+ clientStart,
196
+ kind: 'script',
197
+ outcome: 'reloaded',
198
+ serverMs: message.data.serverDuration,
199
+ updateId: message.timestamp
200
+ });
149
201
  window.location.reload();
150
202
  });
151
203
  };
@@ -172,8 +224,9 @@ const updateHTMLBody = (
172
224
  container: HTMLElement
173
225
  ) => {
174
226
  if (!container) {
175
- return;
227
+ return Promise.resolve();
176
228
  }
229
+ const { promise, resolve } = Promise.withResolvers<void>();
177
230
 
178
231
  const savedState = saveHTMLState();
179
232
  const domSnapshot = snapshotDOMChanges(container);
@@ -222,8 +275,11 @@ const updateHTMLBody = (
222
275
  }
223
276
  reExecuteScripts(container, newScripts, oldInlineScripts);
224
277
  }
278
+ sessionStorage.removeItem('__HMR_ACTIVE__');
279
+ resolve();
225
280
  });
226
- sessionStorage.removeItem('__HMR_ACTIVE__');
281
+
282
+ return promise;
227
283
  };
228
284
 
229
285
  const replaceInlineScript = (script: Element) => {
@@ -245,6 +301,7 @@ const updateHTMLBodyDirect = (
245
301
  htmlDomState: ReturnType<typeof saveDOMState>,
246
302
  container: HTMLElement
247
303
  ) => {
304
+ const { promise, resolve } = Promise.withResolvers<void>();
248
305
  const savedState = saveHTMLState();
249
306
  const domSnapshot = snapshotDOMChanges(container);
250
307
 
@@ -282,8 +339,11 @@ const updateHTMLBodyDirect = (
282
339
  });
283
340
 
284
341
  reExecuteInlineScripts(container, oldInlineScripts);
342
+ sessionStorage.removeItem('__HMR_ACTIVE__');
343
+ resolve();
285
344
  });
286
- sessionStorage.removeItem('__HMR_ACTIVE__');
345
+
346
+ return promise;
287
347
  };
288
348
 
289
349
  /* Shared helpers for HTML body updates */
@@ -16,6 +16,7 @@ import { patchHeadInPlace } from '../headPatch';
16
16
  import { detectCurrentFramework } from '../frameworkDetect';
17
17
  import { type HTMXSavedState, type ScriptInfo } from '../../../types/client';
18
18
  import { hmrState } from '../hmrState';
19
+ import { sendAbsoluteHmrTiming } from '../hmrTiming';
19
20
 
20
21
  const parseHTMXMessage = (
21
22
  html: string | { body?: string; head?: string } | null | undefined
@@ -52,23 +53,26 @@ const handleHTMXBodyUpdate = (
52
53
  htmxHead: string | null,
53
54
  htmxDomState: ReturnType<typeof saveDOMState>
54
55
  ) => {
55
- const updateHTMXBodyAfterCSS = () => {
56
+ const updateHTMXBodyAfterCSS = () =>
56
57
  updateHTMXBody(htmxBody, htmxDomState, document.body);
57
- };
58
58
 
59
59
  if (htmxHead) {
60
60
  applyHeadPatch(htmxHead);
61
+
61
62
  const cssResult = processCSSLinks(htmxHead);
62
- waitForCSSAndUpdate(cssResult, updateHTMXBodyAfterCSS);
63
- } else {
64
- updateHTMXBodyAfterCSS();
63
+
64
+ return waitForCSSAndUpdate(cssResult, updateHTMXBodyAfterCSS);
65
65
  }
66
+
67
+ return updateHTMXBodyAfterCSS();
66
68
  };
67
69
 
68
70
  export const handleHTMXUpdate = (message: {
69
71
  data: {
70
72
  html?: string | { body?: string; head?: string } | null;
73
+ serverDuration?: number;
71
74
  };
75
+ timestamp?: number;
72
76
  }) => {
73
77
  const htmxFrameworkCheck = detectCurrentFramework();
74
78
  if (htmxFrameworkCheck !== 'htmx') return;
@@ -78,6 +82,7 @@ export const handleHTMXUpdate = (message: {
78
82
  }
79
83
 
80
84
  sessionStorage.setItem('__HMR_ACTIVE__', 'true');
85
+ const clientStart = performance.now();
81
86
 
82
87
  const htmxDomState = saveDOMState(document.body);
83
88
  const { body: htmxBody, head: htmxHead } = parseHTMXMessage(
@@ -86,11 +91,37 @@ export const handleHTMXUpdate = (message: {
86
91
 
87
92
  if (!htmxBody) {
88
93
  sessionStorage.removeItem('__HMR_ACTIVE__');
94
+ sendAbsoluteHmrTiming({
95
+ clientStart,
96
+ kind: 'htmx',
97
+ outcome: 'failed',
98
+ serverMs: message.data.serverDuration,
99
+ updateId: message.timestamp
100
+ });
89
101
 
90
102
  return;
91
103
  }
92
104
 
93
- handleHTMXBodyUpdate(htmxBody, htmxHead, htmxDomState);
105
+ void handleHTMXBodyUpdate(htmxBody, htmxHead, htmxDomState).then(
106
+ () => {
107
+ sendAbsoluteHmrTiming({
108
+ clientStart,
109
+ kind: 'htmx',
110
+ serverMs: message.data.serverDuration,
111
+ updateId: message.timestamp
112
+ });
113
+ },
114
+ () => {
115
+ sendAbsoluteHmrTiming({
116
+ clientStart,
117
+ kind: 'htmx',
118
+ outcome: 'reloaded',
119
+ serverMs: message.data.serverDuration,
120
+ updateId: message.timestamp
121
+ });
122
+ window.location.reload();
123
+ }
124
+ );
94
125
  };
95
126
 
96
127
  const cloneHmrListenerElements = (container: HTMLElement) => {
@@ -169,7 +200,8 @@ const updateHTMXBody = (
169
200
  htmxDomState: ReturnType<typeof saveDOMState>,
170
201
  container: HTMLElement
171
202
  ) => {
172
- if (!container) return;
203
+ if (!container) return Promise.resolve();
204
+ const { promise, resolve } = Promise.withResolvers<void>();
173
205
 
174
206
  const countSpan = container.querySelector('#count');
175
207
  const countValue = countSpan
@@ -218,8 +250,11 @@ const updateHTMXBody = (
218
250
  if (window.htmx) {
219
251
  window.htmx.process(container);
220
252
  }
253
+ sessionStorage.removeItem('__HMR_ACTIVE__');
254
+ resolve();
221
255
  });
222
- sessionStorage.removeItem('__HMR_ACTIVE__');
256
+
257
+ return promise;
223
258
  };
224
259
 
225
260
  /* Shared helpers */
@@ -6,9 +6,18 @@ import type {} from '../../../types/globals';
6
6
 
7
7
  import { hideErrorOverlay } from '../errorOverlay';
8
8
  import { detectCurrentFramework } from '../frameworkDetect';
9
+ import { sendAbsoluteHmrTiming } from '../hmrTiming';
10
+ import { swapCSSStylesheet } from '../cssUtils';
11
+
12
+ const reloadReactPage = () => {
13
+ const url = new URL(window.location.href);
14
+ url.searchParams.set('__absolute_hmr', Date.now().toString());
15
+ window.location.replace(url.href);
16
+ };
9
17
 
10
18
  export const handleReactUpdate = (message: {
11
19
  data: {
20
+ fastRefreshSupported?: boolean;
12
21
  hasCSSChanges?: boolean;
13
22
  hasComponentChanges?: boolean;
14
23
  manifest?: Record<string, string>;
@@ -16,6 +25,7 @@ export const handleReactUpdate = (message: {
16
25
  primarySource?: string;
17
26
  serverDuration?: number;
18
27
  };
28
+ timestamp?: number;
19
29
  }) => {
20
30
  const currentFramework = detectCurrentFramework();
21
31
  if (currentFramework !== 'react') return;
@@ -24,52 +34,99 @@ export const handleReactUpdate = (message: {
24
34
  const hasCSSChanges = message.data.hasCSSChanges === true;
25
35
  const cssPath =
26
36
  message.data.manifest && message.data.manifest.ReactExampleCSS;
27
-
28
37
  if (!hasComponentChanges && hasCSSChanges && cssPath) {
29
- reloadReactCSS(cssPath);
38
+ const clientStart = performance.now();
39
+ void reloadReactCSS(cssPath).then((applied) => {
40
+ sendAbsoluteHmrTiming({
41
+ clientStart,
42
+ kind: 'css',
43
+ outcome: applied ? 'applied' : 'failed',
44
+ serverMs: message.data.serverDuration,
45
+ updateId: message.timestamp
46
+ });
47
+ });
30
48
 
31
49
  return;
32
50
  }
51
+ if (message.data.fastRefreshSupported === false) {
52
+ const { pageModuleUrl } = message.data;
53
+ const remount = window.__ABS_REACT_REMOUNT__;
54
+ if (pageModuleUrl && remount) {
55
+ applyRemountImport(
56
+ pageModuleUrl,
57
+ remount,
58
+ message.data.serverDuration,
59
+ message.timestamp
60
+ );
61
+ } else {
62
+ const clientStart = performance.now();
63
+ sendAbsoluteHmrTiming({
64
+ clientStart,
65
+ kind: 'component',
66
+ outcome: 'reloaded',
67
+ serverMs: message.data.serverDuration,
68
+ updateId: message.timestamp
69
+ });
70
+ reloadReactPage();
71
+ }
33
72
 
73
+ return;
74
+ }
34
75
  const refreshRuntime = window.$RefreshRuntime$;
35
76
  const { serverDuration } = message.data;
36
77
  const { pageModuleUrl } = message.data;
37
78
 
38
79
  if (pageModuleUrl && refreshRuntime) {
39
- applyRefreshImport(pageModuleUrl, refreshRuntime, serverDuration);
80
+ applyRefreshImport(
81
+ pageModuleUrl,
82
+ refreshRuntime,
83
+ serverDuration,
84
+ message.timestamp
85
+ );
40
86
 
41
87
  return;
42
88
  }
43
89
 
44
90
  // No module URL — shouldn't happen, but reload as safety fallback
91
+ const clientStart = performance.now();
92
+ sendAbsoluteHmrTiming({
93
+ clientStart,
94
+ kind: 'component',
95
+ outcome: 'reloaded',
96
+ serverMs: message.data.serverDuration,
97
+ updateId: message.timestamp
98
+ });
45
99
  window.location.reload();
46
100
  };
47
101
 
48
- const sendTiming = (clientStart: number, serverDuration?: number) => {
49
- if (window.__HMR_WS__) {
50
- const clientMs = Math.round(performance.now() - clientStart);
51
- const total = (serverDuration ?? 0) + clientMs;
52
- window.__HMR_WS__.send(
53
- JSON.stringify({ duration: total, type: 'hmr-timing' })
54
- );
55
- }
102
+ const finishUpdate = (
103
+ clientStart: number,
104
+ serverDuration?: number,
105
+ updateId?: number
106
+ ) => {
107
+ sendAbsoluteHmrTiming({
108
+ clientStart,
109
+ kind: 'component',
110
+ serverMs: serverDuration,
111
+ updateId
112
+ });
56
113
  if (window.__ERROR_BOUNDARY__) {
57
114
  window.__ERROR_BOUNDARY__.reset();
58
- } else {
59
- hideErrorOverlay();
60
115
  }
116
+ hideErrorOverlay();
61
117
  };
62
118
 
63
119
  const applyRefreshImport = (
64
120
  moduleUrl: string,
65
121
  refreshRuntime: { performReactRefresh: () => unknown },
66
- serverDuration?: number
122
+ serverDuration?: number,
123
+ updateId?: number
67
124
  ) => {
68
125
  const clientStart = performance.now();
69
126
  import(`${moduleUrl}?t=${Date.now()}`)
70
127
  .then(() => {
71
128
  refreshRuntime.performReactRefresh();
72
- sendTiming(clientStart, serverDuration);
129
+ finishUpdate(clientStart, serverDuration, updateId);
73
130
 
74
131
  return undefined;
75
132
  })
@@ -78,31 +135,56 @@ const applyRefreshImport = (
78
135
  '[HMR] React Fast Refresh failed, falling back to reload:',
79
136
  err
80
137
  );
138
+ sendAbsoluteHmrTiming({
139
+ clientStart,
140
+ kind: 'component',
141
+ outcome: 'reloaded',
142
+ serverMs: serverDuration,
143
+ updateId
144
+ });
81
145
  window.location.reload();
82
146
  });
83
147
  };
84
148
 
85
- const reloadReactCSS = (cssPath: string) => {
86
- const existingCSSLinks = document.head.querySelectorAll<HTMLLinkElement>(
87
- 'link[rel="stylesheet"]'
88
- );
89
- existingCSSLinks.forEach((link) => {
90
- const href = link.getAttribute('href');
91
- if (!href) {
92
- return;
93
- }
149
+ const applyRemountImport = (
150
+ moduleUrl: string,
151
+ remount: (module: Record<string, unknown>) => void,
152
+ serverDuration?: number,
153
+ updateId?: number
154
+ ) => {
155
+ const clientStart = performance.now();
156
+ import(`${moduleUrl}?t=${Date.now()}`)
157
+ .then((module) => {
158
+ remount(module);
159
+ finishUpdate(clientStart, serverDuration, updateId);
160
+
161
+ return undefined;
162
+ })
163
+ .catch((err) => {
164
+ console.warn(
165
+ '[HMR] React remount failed, falling back to reload:',
166
+ err
167
+ );
168
+ sendAbsoluteHmrTiming({
169
+ clientStart,
170
+ kind: 'component',
171
+ outcome: 'reloaded',
172
+ serverMs: serverDuration,
173
+ updateId
174
+ });
175
+ reloadReactPage();
176
+ });
177
+ };
178
+
179
+ const reloadReactCSS = (cssPath: string) =>
180
+ swapCSSStylesheet(cssPath, (href) => {
94
181
  const hrefBase = (href.split('?')[0] ?? '').split('/').pop() ?? '';
95
182
  const cssPathBase =
96
183
  (cssPath.split('?')[0] ?? '').split('/').pop() ?? '';
97
- if (
184
+
185
+ return (
98
186
  hrefBase === cssPathBase ||
99
187
  href.includes('react-example') ||
100
188
  cssPathBase.includes(hrefBase)
101
- ) {
102
- const newHref = `${
103
- cssPath + (cssPath.includes('?') ? '&' : '?')
104
- }t=${Date.now()}`;
105
- link.href = newHref;
106
- }
189
+ );
107
190
  });
108
- };