arcway 0.1.22 → 0.1.24

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.
@@ -36,6 +36,50 @@ function readClientManifest() {
36
36
  }
37
37
  }
38
38
 
39
+ function syncStylesForPath(manifest, pathname) {
40
+ if (typeof document === 'undefined' || !manifest) return;
41
+
42
+ const globalHref = manifest.cssBundle ?? null;
43
+ const matched = matchClientRoute(manifest, pathname);
44
+ const routeHrefs = matched?.route?.cssBundles ?? [];
45
+
46
+ const globalLinks = Array.from(
47
+ document.querySelectorAll('link[rel="stylesheet"][data-arcway-css="global"]'),
48
+ );
49
+ for (const link of globalLinks) {
50
+ if (!globalHref) continue;
51
+ const current = link.getAttribute('href')?.split('?')[0] ?? '';
52
+ if (current !== globalHref) link.setAttribute('href', globalHref);
53
+ }
54
+
55
+ const existingRouteLinks = Array.from(
56
+ document.querySelectorAll('link[rel="stylesheet"][data-arcway-css="route"]'),
57
+ );
58
+ const head = document.head || document.querySelector('head');
59
+ if (!head) return;
60
+
61
+ for (let i = 0; i < routeHrefs.length; i++) {
62
+ const href = routeHrefs[i];
63
+ const existing = existingRouteLinks[i];
64
+ if (existing) {
65
+ const current = existing.getAttribute('href')?.split('?')[0] ?? '';
66
+ if (current !== href) existing.setAttribute('href', href);
67
+ existing.setAttribute('data-arcway-css-index', String(i));
68
+ continue;
69
+ }
70
+ const link = document.createElement('link');
71
+ link.rel = 'stylesheet';
72
+ link.setAttribute('data-arcway-css', 'route');
73
+ link.setAttribute('data-arcway-css-index', String(i));
74
+ link.href = href;
75
+ head.appendChild(link);
76
+ }
77
+
78
+ for (let i = routeHrefs.length; i < existingRouteLinks.length; i++) {
79
+ existingRouteLinks[i].remove();
80
+ }
81
+ }
82
+
39
83
  function matchClientRoute(manifest, pathname) {
40
84
  for (const route of manifest.routes) {
41
85
  let compiled = patternCache.get(route.pattern);
@@ -109,4 +153,5 @@ export {
109
153
  matchClientRoute,
110
154
  prefetchRoute,
111
155
  readClientManifest,
156
+ syncStylesForPath,
112
157
  };
package/client/router.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  matchClientRoute,
16
16
  loadLoadingComponents,
17
17
  prefetchRoute,
18
+ syncStylesForPath,
18
19
  } from './page-loader.js';
19
20
  import { parseQuery } from './query.js';
20
21
 
@@ -72,6 +73,7 @@ function useSearchParams() {
72
73
  function Router({
73
74
  initialPath,
74
75
  initialParams,
76
+ initialPattern,
75
77
  initialComponent,
76
78
  initialLayouts,
77
79
  initialLoadings,
@@ -81,6 +83,7 @@ function Router({
81
83
  () => initialPath ?? (typeof window !== 'undefined' ? window.location.pathname : '/'),
82
84
  );
83
85
  const [pageState, setPageState] = useState({
86
+ pattern: initialPattern ?? null,
84
87
  component: initialComponent ?? null,
85
88
  layouts: initialLayouts ?? [],
86
89
  loadings: initialLoadings ?? [],
@@ -96,12 +99,24 @@ function Router({
96
99
 
97
100
  useEffect(() => {
98
101
  manifestRef.current = readClientManifest();
102
+ if (manifestRef.current) {
103
+ syncStylesForPath(manifestRef.current, pathname);
104
+ }
99
105
  const onManifestUpdate = () => {
100
106
  manifestRef.current = readClientManifest();
107
+ if (manifestRef.current) {
108
+ syncStylesForPath(manifestRef.current, window.location.pathname);
109
+ }
101
110
  };
102
111
  window.addEventListener('manifest-update', onManifestUpdate);
103
112
  return () => window.removeEventListener('manifest-update', onManifestUpdate);
104
- }, []);
113
+ }, [pathname]);
114
+
115
+ useEffect(() => {
116
+ if (manifestRef.current) {
117
+ syncStylesForPath(manifestRef.current, pathname);
118
+ }
119
+ }, [pathname]);
105
120
 
106
121
  // `bumpQuery` controls whether `queryVersion` is incremented atomically
107
122
  // with the page-state updates. It lives inside the same `startTransition`
@@ -116,6 +131,7 @@ function Router({
116
131
  startTransition(() => {
117
132
  setPathname(newPath);
118
133
  setPageState({
134
+ pattern: loaded.pattern ?? null,
119
135
  component: loaded.component,
120
136
  layouts: loaded.layouts,
121
137
  loadings: loaded.loadings,
@@ -177,6 +193,7 @@ function Router({
177
193
  setPathname(pathOnly);
178
194
  setPageState((prev) => ({
179
195
  ...prev,
196
+ pattern: matched.route.pattern,
180
197
  loadings: targetLoadings,
181
198
  params: matched.params,
182
199
  }));
@@ -217,7 +234,7 @@ function Router({
217
234
 
218
235
  if (!manifest) {
219
236
  setPathname(newPath);
220
- setPageState((prev) => ({ ...prev, params: {} }));
237
+ setPageState((prev) => ({ ...prev, pattern: null, params: {} }));
221
238
  setQueryVersion((v) => v + 1);
222
239
  return;
223
240
  }
@@ -238,6 +255,12 @@ function Router({
238
255
  return () => window.removeEventListener('popstate', onPopState);
239
256
  }, [pathname, applyLoaded]);
240
257
 
258
+ useEffect(() => {
259
+ if (typeof window !== 'undefined') {
260
+ window.__arcway_current_pattern__ = pageState.pattern ?? null;
261
+ }
262
+ }, [pageState.pattern]);
263
+
241
264
  const { component: PageComponent, layouts, loadings, params } = pageState;
242
265
 
243
266
  let content;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcway",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "A convention-based framework for building modular monoliths with strict domain boundaries.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,8 +47,10 @@
47
47
  "dependencies": {
48
48
  "@aws-sdk/client-s3": "^3.987.0",
49
49
  "@babel/core": "^7.29.0",
50
+ "@vitejs/plugin-react": "^5.1.0",
50
51
  "@base-ui/react": "^1.2.0",
51
52
  "@modelcontextprotocol/sdk": "^1.26.0",
53
+ "@tailwindcss/vite": "^4.1.18",
52
54
  "@tailwindcss/postcss": "^4.1.18",
53
55
  "arktype": "^2.1.29",
54
56
  "bcryptjs": "^3.0.3",
@@ -99,6 +101,7 @@
99
101
  "tailwind-merge": "^3.4.1",
100
102
  "tailwindcss": "^4.1.18",
101
103
  "vaul": "^1.1.2",
104
+ "vite": "^7.1.12",
102
105
  "ws": "^8.19.0",
103
106
  "zod": "^3.24.0"
104
107
  },
@@ -14,6 +14,8 @@ import { StaticRouter } from '../static/index.js';
14
14
  import { WsRouter } from '../ws/ws-router.js';
15
15
  import { createWsBackplane } from '../ws/backplane.js';
16
16
  import { runHook } from './hooks.js';
17
+ import { createViteDevRouter } from '../pages/vite-dev.js';
18
+ import { resolvePagesOutDir } from '../pages/out-dir.js';
17
19
  import path from 'node:path';
18
20
 
19
21
  async function boot(options) {
@@ -85,6 +87,11 @@ async function boot(options) {
85
87
 
86
88
  const pagesRouter = new PagesRouter(config, { rootDir, log, mode, fileWatcher, appContext });
87
89
  await pagesRouter.init();
90
+ const pagesOutDir = resolvePagesOutDir(config, rootDir, mode);
91
+ const viteRouter =
92
+ mode === 'development' && config.pages?.vite?.enabled === true
93
+ ? await createViteDevRouter({ rootDir, log, config })
94
+ : null;
88
95
 
89
96
  const healthDeps = {
90
97
  db,
@@ -93,7 +100,7 @@ async function boot(options) {
93
100
  const systemRouter = new SystemRouter({ healthDeps });
94
101
 
95
102
  const staticRouter = new StaticRouter({
96
- outDir: config.pages?.outDir ?? path.resolve(rootDir, '.build/pages'),
103
+ outDir: pagesOutDir,
97
104
  publicDir: path.join(rootDir, 'public'),
98
105
  });
99
106
 
@@ -113,6 +120,7 @@ async function boot(options) {
113
120
  webServer.use(systemRouter, '/_system');
114
121
  webServer.use(apiRouter, apiRouter.prefix);
115
122
  webServer.use(wsRouter, config.websocket.path);
123
+ if (viteRouter) webServer.use(viteRouter);
116
124
  webServer.use(staticRouter);
117
125
  webServer.use(pagesRouter);
118
126
 
@@ -130,6 +138,7 @@ async function boot(options) {
130
138
  if (workerPool) await workerPool.shutdown();
131
139
  await pagesRouter.close();
132
140
  await apiRouter.close();
141
+ if (viteRouter) await viteRouter.close();
133
142
  await webServer.close();
134
143
  if (fileWatcher) await fileWatcher.close();
135
144
  await destroyInfrastructure(infrastructure);
package/server/build.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { makeConfig } from './config/loader.js';
2
2
  import { buildPages } from './pages/build.js';
3
+ import { resolvePagesOutDir } from './pages/out-dir.js';
3
4
 
4
5
  async function build(options) {
5
6
  const rootDir = options?.rootDir ?? process.cwd();
@@ -11,6 +12,7 @@ async function build(options) {
11
12
  if (pagesEnabled) {
12
13
  pages = await buildPages({
13
14
  rootDir,
15
+ outDir: resolvePagesOutDir(config, rootDir, 'production'),
14
16
  serverTarget: buildTarget,
15
17
  minify: true,
16
18
  fonts: config?.pages?.fonts,
@@ -4,16 +4,31 @@ const DEFAULTS = {
4
4
  enabled: true,
5
5
  dir: 'pages',
6
6
  outDir: '.build/pages',
7
+ devOutDir: '.build-dev/pages',
8
+ vite: {
9
+ enabled: false,
10
+ },
7
11
  };
8
12
 
9
13
  function resolve(config, { rootDir } = {}) {
10
- const pages = { ...DEFAULTS, ...config.pages };
14
+ const rawPages = config.pages ?? {};
15
+ const pages = {
16
+ ...DEFAULTS,
17
+ ...rawPages,
18
+ vite: {
19
+ ...DEFAULTS.vite,
20
+ ...(rawPages.vite ?? {}),
21
+ },
22
+ };
11
23
  if (pages.dir && !path.isAbsolute(pages.dir)) {
12
24
  pages.dir = path.resolve(rootDir, pages.dir);
13
25
  }
14
26
  if (pages.outDir && !path.isAbsolute(pages.outDir)) {
15
27
  pages.outDir = path.resolve(rootDir, pages.outDir);
16
28
  }
29
+ if (pages.devOutDir && !path.isAbsolute(pages.devOutDir)) {
30
+ pages.devOutDir = path.resolve(rootDir, pages.devOutDir);
31
+ }
17
32
  return { ...config, pages };
18
33
  }
19
34
 
@@ -263,7 +263,7 @@ async function restoreMultiFileCache({ cacheDir, outputs, destDir }) {
263
263
  const src = path.join(cacheDir, rel);
264
264
  const dst = path.join(destDir, rel);
265
265
  await fs.mkdir(path.dirname(dst), { recursive: true });
266
- await fs.cp(src, dst);
266
+ await cpAtomic(src, dst);
267
267
  }),
268
268
  );
269
269
  }
@@ -35,7 +35,7 @@ function planHydrationEntries(pages, layouts, loadings, tempDir) {
35
35
  const outName = patternToFileName(page.pattern);
36
36
  const layoutChain = resolveLayoutChain(page.pattern, layouts);
37
37
  const loadingChain = resolveLoadingChain(page.pattern, loadings);
38
- const content = generateHydrationEntry(page.filePath, layoutChain, loadingChain);
38
+ const content = generateHydrationEntry(page.filePath, layoutChain, loadingChain, page.pattern);
39
39
  const entryPath = path.join(tempDir, `${outName}.tsx`);
40
40
  files.push({ entryPath, content });
41
41
  entryPoints[outName] = entryPath;
@@ -284,6 +284,7 @@ async function collectStaleOutputs(metafile, clientDir) {
284
284
  for (const entry of entries) {
285
285
  if (!entry.isFile()) continue;
286
286
  const abs = path.join(dir, entry.name);
287
+ if (shouldPreserveClientAsset(abs, clientDir)) continue;
287
288
  if (!currentOutputs.has(abs)) stale.add(abs);
288
289
  }
289
290
  }
@@ -295,6 +296,14 @@ async function collectStaleOutputs(metafile, clientDir) {
295
296
  return stale;
296
297
  }
297
298
 
299
+ function shouldPreserveClientAsset(absPath, clientDir) {
300
+ const rel = path.relative(clientDir, absPath).replace(/\\/g, '/');
301
+ if (!rel || rel.startsWith('..')) return false;
302
+ if (rel.startsWith('hmr/')) return true;
303
+ if (/^styles-[a-f0-9]+\.css$/i.test(rel)) return true;
304
+ return false;
305
+ }
306
+
298
307
  async function tryRestoreClientFromCache({ rootDir, clientDir, coarseKey }) {
299
308
  const lookup = await lookupMultiFileCache({ rootDir, kind: 'client', coarseKey });
300
309
  if (!lookup.hit) return { hit: false, bucket: lookup.bucket };
@@ -459,7 +468,7 @@ function extractMetadata(result, plan, outDir) {
459
468
  return { bundles, navBundles, layoutNavBundles, loadingNavBundles, entryChunks, entryCssChunks };
460
469
  }
461
470
 
462
- function generateHydrationEntry(componentPath, layouts = [], loadings = []) {
471
+ function generateHydrationEntry(componentPath, layouts = [], loadings = [], pattern = '/') {
463
472
  const importPath = componentPath.replace(/\\/g, '/');
464
473
  const imports = [
465
474
  `import { hydrateRoot } from 'react-dom/client';`,
@@ -485,8 +494,19 @@ function generateHydrationEntry(componentPath, layouts = [], loadings = []) {
485
494
  const container = document.getElementById('__app');
486
495
  const propsEl = document.getElementById('__app_props');
487
496
  const props = propsEl ? JSON.parse(propsEl.textContent || '{}') : {};
497
+ const ROOT_KEY = '__arcway_root__';
498
+ const rootOwner = window;
499
+ const element = <ApiProvider><Router initialPath={window.location.pathname} initialParams={props} initialPattern={${JSON.stringify(pattern)}} initialComponent={Component} initialLayouts={${layoutsArray}} initialLoadings={${loadingsArray}} /></ApiProvider>;
488
500
 
489
- hydrateRoot(container!, <ApiProvider><Router initialPath={window.location.pathname} initialParams={props} initialComponent={Component} initialLayouts={${layoutsArray}} initialLoadings={${loadingsArray}} /></ApiProvider>);
501
+ if (!container) {
502
+ throw new Error('Arcway hydrate entry could not find #__app');
503
+ }
504
+
505
+ if (!rootOwner[ROOT_KEY]) {
506
+ rootOwner[ROOT_KEY] = hydrateRoot(container, element);
507
+ } else {
508
+ rootOwner[ROOT_KEY].render(element);
509
+ }
490
510
  `;
491
511
  }
492
512
  export { buildClientBundles, createClientBuildContext, generateHydrationEntry };
@@ -15,6 +15,7 @@ import {
15
15
  } from './ssr.js';
16
16
  import { MIME_TYPES } from './static.js';
17
17
  import { createArcwayDevEndpoint } from './arcway-endpoint.js';
18
+ import { buildViteClientManifestJson, buildViteRoute } from './vite-dev.js';
18
19
  function createPagesHandler(options) {
19
20
  const rootDir = options.rootDir;
20
21
  const outDir = path.resolve(rootDir, options.outDir ?? '.build/pages');
@@ -23,6 +24,7 @@ function createPagesHandler(options) {
23
24
  const appContext = options.appContext ?? null;
24
25
  const mode = options.mode ?? 'production';
25
26
  const devMode = mode === 'development';
27
+ const viteDev = options.viteDev === true;
26
28
  const lazyContext = options.lazyContext ?? null;
27
29
  // Dev with a lazy context drives the handler off an in-memory manifest; no
28
30
  // disk `pages-manifest.json` is produced. Prod still reads from disk.
@@ -39,8 +41,12 @@ function createPagesHandler(options) {
39
41
  let manifest = lazyContext
40
42
  ? lazyContext.manifest
41
43
  : JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
42
- let routes = compileRoutes(manifest);
43
- let clientManifestJson = buildClientManifestJson(manifest);
44
+ let routes = compileRoutes(manifest, { rootDir, outDir, viteDev });
45
+ let clientManifestJson = buildClientManifestJson(manifest, {
46
+ rootDir,
47
+ outDir,
48
+ viteDev,
49
+ });
44
50
  let lastSeenVersion = lazyContext ? manifest.version : 0;
45
51
  const componentCache = new Map();
46
52
  let cacheVersion = 0;
@@ -60,8 +66,12 @@ function createPagesHandler(options) {
60
66
  if (!lazyContext) return;
61
67
  if (manifest.version === lastSeenVersion) return;
62
68
  lastSeenVersion = manifest.version;
63
- routes = compileRoutes(manifest);
64
- clientManifestJson = buildClientManifestJson(manifest);
69
+ routes = compileRoutes(manifest, { rootDir, outDir, viteDev });
70
+ clientManifestJson = buildClientManifestJson(manifest, {
71
+ rootDir,
72
+ outDir,
73
+ viteDev,
74
+ });
65
75
  componentCache.clear();
66
76
  cacheVersion++;
67
77
  }
@@ -79,8 +89,12 @@ function createPagesHandler(options) {
79
89
  }
80
90
  if (!fs.existsSync(manifestPath)) return;
81
91
  manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
82
- routes = compileRoutes(manifest);
83
- clientManifestJson = buildClientManifestJson(manifest);
92
+ routes = compileRoutes(manifest, { rootDir, outDir, viteDev });
93
+ clientManifestJson = buildClientManifestJson(manifest, {
94
+ rootDir,
95
+ outDir,
96
+ viteDev,
97
+ });
84
98
  componentCache.clear();
85
99
  cacheVersion++;
86
100
  }
@@ -104,7 +118,11 @@ function createPagesHandler(options) {
104
118
  });
105
119
  res.write('data: connected\n\n');
106
120
  const onUpdate = (event) => {
107
- res.write(`data: ${JSON.stringify(event)}
121
+ let payload = event;
122
+ if (event?.type === 'hmr-update' || event?.type === 'css-update') {
123
+ payload = { ...event, manifest: clientManifestJson };
124
+ }
125
+ res.write(`data: ${JSON.stringify(payload)}
108
126
 
109
127
  `);
110
128
  };
@@ -201,6 +219,7 @@ function createPagesHandler(options) {
201
219
  cacheVersion,
202
220
  projectReact,
203
221
  devMode,
222
+ viteDev,
204
223
  );
205
224
  } catch (err) {
206
225
  if (manifest.errorBundle && !res.headersSent) {
@@ -275,10 +294,10 @@ async function renderDevBuildError(
275
294
  res.end(`<h1>500 - Build Error</h1><pre>${escaped}</pre>`);
276
295
  }
277
296
  }
278
- function compileRoutes(manifest) {
297
+ function compileRoutes(manifest, { rootDir, outDir, viteDev } = {}) {
279
298
  const routes = manifest.entries.map((entry) => {
280
299
  const { regex, paramNames, catchAllParam } = compilePattern(entry.pattern);
281
- return {
300
+ let route = {
282
301
  pattern: entry.pattern,
283
302
  regex,
284
303
  paramNames,
@@ -290,6 +309,10 @@ function compileRoutes(manifest) {
290
309
  sharedChunks: entry.sharedChunks ?? [],
291
310
  sharedCssChunks: entry.sharedCssChunks ?? [],
292
311
  };
312
+ if (viteDev) {
313
+ route = buildViteRoute(route, { rootDir, outDir });
314
+ }
315
+ return route;
293
316
  });
294
317
  sortBySpecificity(routes);
295
318
  return routes;
@@ -350,8 +373,12 @@ async function runPageMiddleware(
350
373
  }
351
374
  return null;
352
375
  }
353
- function buildClientManifestJson(manifest) {
376
+ function buildClientManifestJson(manifest, { rootDir, outDir, viteDev } = {}) {
377
+ if (viteDev) {
378
+ return buildViteClientManifestJson(manifest, rootDir);
379
+ }
354
380
  const clientManifest = {
381
+ cssBundle: manifest.cssBundle ? `/static/${manifest.cssBundle}` : null,
355
382
  routes: manifest.entries.map((entry) => {
356
383
  const route = {
357
384
  pattern: entry.pattern,
@@ -359,6 +386,7 @@ function buildClientManifestJson(manifest) {
359
386
  clientBundle: `/static/${entry.navBundle}`,
360
387
  layoutBundles: (entry.layoutClientBundles ?? []).map((b) => `/static/${b}`),
361
388
  loadingBundles: (entry.loadingClientBundles ?? []).map((b) => `/static/${b}`),
389
+ cssBundles: (entry.sharedCssChunks ?? []).map((b) => `/static/${b}`),
362
390
  };
363
391
  if (entry.catchAllParam) {
364
392
  route.catchAllParam = entry.catchAllParam;
@@ -79,38 +79,65 @@ window.$RefreshReg$ = function() {};
79
79
  window.$RefreshSig$ = function() { return function(type) { return type; }; };
80
80
  window.__REACT_REFRESH__ = RefreshRuntime;
81
81
 
82
- var es = new EventSource('/__livereload');
83
- es.onmessage = function(e) {
84
- if (e.data === 'connected') return;
85
- var msg;
86
- try { msg = JSON.parse(e.data); } catch { window.location.reload(); return; }
87
- if (msg.type === 'reload') {
88
- window.location.reload();
89
- } else if (msg.type === 'hmr-update') {
90
- var imports = msg.modules.map(function(m) {
91
- return import('/static/' + m + '?t=' + msg.timestamp);
92
- });
93
- Promise.all(imports).then(function() {
94
- RefreshRuntime.performReactRefresh();
95
- if (msg.manifest) {
96
- var el = document.getElementById('__app_manifest');
97
- if (el) el.textContent = msg.manifest;
98
- window.dispatchEvent(new CustomEvent('manifest-update'));
99
- }
100
- }).catch(function(err) {
101
- console.warn('[HMR] Fast Refresh failed, doing full reload', err);
102
- window.location.reload();
103
- });
104
- } else if (msg.type === 'css-update') {
105
- var links = document.querySelectorAll('link[rel="stylesheet"][href*="/static/"]');
106
- links.forEach(function(link) {
107
- link.href = '/static/' + msg.cssBundle + '?t=' + msg.timestamp;
108
- });
82
+ var es = null;
83
+ var reconnectTimer = null;
84
+
85
+ function applyManifest(manifestJson) {
86
+ if (!manifestJson) return;
87
+ var el = document.getElementById('__app_manifest');
88
+ if (el) el.textContent = manifestJson;
89
+ window.dispatchEvent(new CustomEvent('manifest-update'));
90
+ }
91
+
92
+ function pickCurrentHmrModules(msg) {
93
+ var currentPattern = window.__arcway_current_pattern__;
94
+ if (currentPattern && msg.routeBundles && msg.routeBundles[currentPattern]) {
95
+ return ['/static/' + msg.routeBundles[currentPattern] + '?t=' + msg.timestamp];
109
96
  }
110
- };
111
- es.onerror = function() {
112
- setTimeout(function() { es.close(); }, 5000);
113
- };
97
+ return (msg.modules || []).map(function(m) {
98
+ return '/static/' + m + '?t=' + msg.timestamp;
99
+ });
100
+ }
101
+
102
+ function connect() {
103
+ if (es) es.close();
104
+ es = new EventSource('/__livereload');
105
+ es.onmessage = function(e) {
106
+ if (e.data === 'connected') return;
107
+ var msg;
108
+ try { msg = JSON.parse(e.data); } catch { window.location.reload(); return; }
109
+ if (msg.type === 'reload') {
110
+ window.location.reload();
111
+ } else if (msg.type === 'hmr-update') {
112
+ var imports = pickCurrentHmrModules(msg).map(function(specifier) {
113
+ return import(specifier);
114
+ });
115
+ Promise.all(imports).then(function() {
116
+ RefreshRuntime.performReactRefresh();
117
+ applyManifest(msg.manifest);
118
+ }).catch(function(err) {
119
+ console.warn('[HMR] Fast Refresh failed, doing full reload', err);
120
+ window.location.reload();
121
+ });
122
+ } else if (msg.type === 'css-update') {
123
+ var links = document.querySelectorAll('link[rel="stylesheet"][data-arcway-css="global"]');
124
+ links.forEach(function(link) {
125
+ link.href = '/static/' + msg.cssBundle + '?t=' + msg.timestamp;
126
+ });
127
+ applyManifest(msg.manifest);
128
+ }
129
+ };
130
+ es.onerror = function() {
131
+ if (reconnectTimer) return;
132
+ try { es.close(); } catch {}
133
+ reconnectTimer = setTimeout(function() {
134
+ reconnectTimer = null;
135
+ connect();
136
+ }, 1000);
137
+ };
138
+ }
139
+
140
+ connect();
114
141
  </script>`;
115
142
  }
116
143
  function diffClientMetafiles(oldMeta, newMeta, outDir) {