arcway 0.1.23 → 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.
- package/client/page-loader.js +45 -0
- package/client/router.js +25 -2
- package/package.json +4 -1
- package/server/boot/index.js +10 -1
- package/server/build.js +2 -0
- package/server/config/modules/pages.js +16 -1
- package/server/pages/build-cache.js +1 -1
- package/server/pages/build-client.js +23 -3
- package/server/pages/handler.js +38 -10
- package/server/pages/hmr.js +58 -31
- package/server/pages/lazy-context.js +227 -39
- package/server/pages/out-dir.js +11 -0
- package/server/pages/pages-router.js +7 -0
- package/server/pages/ssr.js +23 -7
- package/server/pages/vite-dev.js +246 -0
- package/server/pages/watcher.js +3 -1
- package/server/watcher.js +0 -4
package/client/page-loader.js
CHANGED
|
@@ -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.
|
|
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
|
},
|
package/server/boot/index.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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 };
|
package/server/pages/handler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
package/server/pages/hmr.js
CHANGED
|
@@ -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 =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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) {
|
|
@@ -28,7 +28,8 @@ import {
|
|
|
28
28
|
import { createClientBuildContext } from './build-client.js';
|
|
29
29
|
import { buildCssBundle } from './build-css.js';
|
|
30
30
|
import { resolveFonts } from './fonts.js';
|
|
31
|
-
import { buildHmrRuntimeBundle } from './hmr.js';
|
|
31
|
+
import { buildHmrRuntimeBundle, diffClientMetafiles } from './hmr.js';
|
|
32
|
+
import { syncViteHydrationEntries } from './vite-dev.js';
|
|
32
33
|
|
|
33
34
|
// Dev-mode build context that defers per-page/layout/middleware server-bundle
|
|
34
35
|
// compilation to first request. Startup is cheap: discover routes, build the
|
|
@@ -50,6 +51,7 @@ async function createLazyPagesContext(options) {
|
|
|
50
51
|
const clientTarget = options.clientTarget ?? 'es2022';
|
|
51
52
|
const minify = options.minify ?? false;
|
|
52
53
|
const devMode = options.devMode ?? true;
|
|
54
|
+
const viteDev = options.viteDev ?? false;
|
|
53
55
|
const esbuildImpl = options.esbuild ?? esbuild;
|
|
54
56
|
// Cap per-context server-bundle concurrency so a request burst after a
|
|
55
57
|
// cold start can't spawn N pages × M esbuild Go workers simultaneously.
|
|
@@ -63,6 +65,7 @@ async function createLazyPagesContext(options) {
|
|
|
63
65
|
|
|
64
66
|
let disposed = false;
|
|
65
67
|
let clientCtx = null;
|
|
68
|
+
let currentClientMetafile = null;
|
|
66
69
|
let fontFaceCss;
|
|
67
70
|
let fontPreloadHtml;
|
|
68
71
|
|
|
@@ -101,6 +104,13 @@ async function createLazyPagesContext(options) {
|
|
|
101
104
|
{ srcPath: path.resolve(m.filePath), built: false, stale: false },
|
|
102
105
|
]),
|
|
103
106
|
),
|
|
107
|
+
loadings: new Map(
|
|
108
|
+
loadings.map((l) => [
|
|
109
|
+
l.dirPath,
|
|
110
|
+
{ srcPath: path.resolve(l.filePath) },
|
|
111
|
+
]),
|
|
112
|
+
),
|
|
113
|
+
stylesPath: stylesPath ? path.resolve(stylesPath) : null,
|
|
104
114
|
version: 0,
|
|
105
115
|
};
|
|
106
116
|
|
|
@@ -120,11 +130,46 @@ async function createLazyPagesContext(options) {
|
|
|
120
130
|
events.emit(event, payload);
|
|
121
131
|
}
|
|
122
132
|
|
|
133
|
+
function applyClientResult(clientResult) {
|
|
134
|
+
for (const entry of manifest.entries) {
|
|
135
|
+
const clientBundle = clientResult.bundles.get(entry.pattern);
|
|
136
|
+
const navBundle = clientResult.navBundles.get(entry.pattern);
|
|
137
|
+
if (clientBundle) entry.clientBundle = clientBundle;
|
|
138
|
+
if (navBundle) entry.navBundle = navBundle;
|
|
139
|
+
entry.layoutClientBundles = entry.layoutDirs
|
|
140
|
+
.map((d) => clientResult.layoutNavBundles.get(d))
|
|
141
|
+
.filter((v) => v !== undefined);
|
|
142
|
+
entry.loadingClientBundles = entry.loadingDirs
|
|
143
|
+
.map((d) => clientResult.loadingNavBundles.get(d))
|
|
144
|
+
.filter((v) => v !== undefined);
|
|
145
|
+
entry.sharedChunks = clientResult.entryChunks?.get(entry.pattern) ?? [];
|
|
146
|
+
entry.sharedCssChunks = clientResult.entryCssChunks?.get(entry.pattern) ?? [];
|
|
147
|
+
}
|
|
148
|
+
currentClientMetafile = clientResult.metafile ?? currentClientMetafile;
|
|
149
|
+
}
|
|
150
|
+
|
|
123
151
|
// Client bundle + CSS + error pages + HMR runtime are built eagerly at
|
|
124
152
|
// startup. They're shared across every request and their compilation cost
|
|
125
153
|
// is bounded (a single esbuild.context for the client, one esbuild.transform
|
|
126
154
|
// for CSS), so there's no win in deferring them.
|
|
127
155
|
async function eagerSharedBuilds() {
|
|
156
|
+
if (viteDev) {
|
|
157
|
+
const [errorBundles] = await Promise.all([
|
|
158
|
+
buildErrorPageBundles(errorPages, outDir, serverTarget, {
|
|
159
|
+
rootDir,
|
|
160
|
+
devMode,
|
|
161
|
+
minify,
|
|
162
|
+
limit,
|
|
163
|
+
}),
|
|
164
|
+
syncViteHydrationEntries({ manifest, outDir }),
|
|
165
|
+
]);
|
|
166
|
+
if (errorBundles.error) manifest.errorBundle = errorBundles.error;
|
|
167
|
+
if (errorBundles.notFound) manifest.notFoundBundle = errorBundles.notFound;
|
|
168
|
+
bumpVersion();
|
|
169
|
+
emit('update', { type: 'startup', version: manifest.version });
|
|
170
|
+
return { clientMetafile: null };
|
|
171
|
+
}
|
|
172
|
+
|
|
128
173
|
clientCtx = await createClientBuildContext(
|
|
129
174
|
pages,
|
|
130
175
|
layouts,
|
|
@@ -151,20 +196,7 @@ async function createLazyPagesContext(options) {
|
|
|
151
196
|
// Fold client bundle output into the manifest so renderPage() can find
|
|
152
197
|
// navBundle / layoutClientBundles / shared chunks immediately, the same
|
|
153
198
|
// as it would from a disk-backed prod manifest.
|
|
154
|
-
|
|
155
|
-
const clientBundle = clientResult.bundles.get(entry.pattern);
|
|
156
|
-
const navBundle = clientResult.navBundles.get(entry.pattern);
|
|
157
|
-
if (clientBundle) entry.clientBundle = clientBundle;
|
|
158
|
-
if (navBundle) entry.navBundle = navBundle;
|
|
159
|
-
entry.layoutClientBundles = entry.layoutDirs
|
|
160
|
-
.map((d) => clientResult.layoutNavBundles.get(d))
|
|
161
|
-
.filter((v) => v !== undefined);
|
|
162
|
-
entry.loadingClientBundles = entry.loadingDirs
|
|
163
|
-
.map((d) => clientResult.loadingNavBundles.get(d))
|
|
164
|
-
.filter((v) => v !== undefined);
|
|
165
|
-
entry.sharedChunks = clientResult.entryChunks?.get(entry.pattern) ?? [];
|
|
166
|
-
entry.sharedCssChunks = clientResult.entryCssChunks?.get(entry.pattern) ?? [];
|
|
167
|
-
}
|
|
199
|
+
applyClientResult(clientResult);
|
|
168
200
|
if (cssBundle) manifest.cssBundle = cssBundle;
|
|
169
201
|
if (errorBundles.error) manifest.errorBundle = errorBundles.error;
|
|
170
202
|
if (errorBundles.notFound) manifest.notFoundBundle = errorBundles.notFound;
|
|
@@ -314,13 +346,55 @@ async function createLazyPagesContext(options) {
|
|
|
314
346
|
});
|
|
315
347
|
}
|
|
316
348
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
349
|
+
function markAllEntriesStale() {
|
|
350
|
+
let touched = false;
|
|
351
|
+
const affected = { pages: [], layouts: [], middlewares: [] };
|
|
352
|
+
for (const entry of manifest.entries) {
|
|
353
|
+
if (!entry.stale || entry.built) {
|
|
354
|
+
entry.stale = true;
|
|
355
|
+
entry.built = false;
|
|
356
|
+
touched = true;
|
|
357
|
+
}
|
|
358
|
+
affected.pages.push(entry.pattern);
|
|
359
|
+
}
|
|
360
|
+
for (const [dirPath, layout] of manifest.layouts.entries()) {
|
|
361
|
+
if (!layout.stale || layout.built) {
|
|
362
|
+
layout.stale = true;
|
|
363
|
+
layout.built = false;
|
|
364
|
+
touched = true;
|
|
365
|
+
}
|
|
366
|
+
affected.layouts.push(dirPath);
|
|
367
|
+
}
|
|
368
|
+
for (const [dirPath, mw] of manifest.middlewares.entries()) {
|
|
369
|
+
if (!mw.stale || mw.built) {
|
|
370
|
+
mw.stale = true;
|
|
371
|
+
mw.built = false;
|
|
372
|
+
touched = true;
|
|
373
|
+
}
|
|
374
|
+
affected.middlewares.push(dirPath);
|
|
375
|
+
}
|
|
376
|
+
return { touched, affected };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function shouldInvalidateWholeTree(filePath, affected) {
|
|
380
|
+
if (
|
|
381
|
+
affected.pages.length > 0 ||
|
|
382
|
+
affected.layouts.length > 0 ||
|
|
383
|
+
affected.middlewares.length > 0
|
|
384
|
+
) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
const rel = path.relative(rootDir, filePath).replace(/\\/g, '/');
|
|
388
|
+
if (!rel || rel.startsWith('..')) return false;
|
|
389
|
+
return /^(components|hooks|client|packages)\//.test(rel);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function markAffectedStale(filePath) {
|
|
323
393
|
const affected = mapFileToAffected(filePath, manifest);
|
|
394
|
+
if (shouldInvalidateWholeTree(filePath, affected)) {
|
|
395
|
+
return markAllEntriesStale();
|
|
396
|
+
}
|
|
397
|
+
|
|
324
398
|
let touched = false;
|
|
325
399
|
for (const pattern of affected.pages) {
|
|
326
400
|
const entry = findEntry(pattern);
|
|
@@ -346,6 +420,16 @@ async function createLazyPagesContext(options) {
|
|
|
346
420
|
touched = true;
|
|
347
421
|
}
|
|
348
422
|
}
|
|
423
|
+
return { touched, affected };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Mark every manifest entry affected by a source change as stale. Does not
|
|
427
|
+
// trigger rebuilds directly — the next `ensurePageBuilt()` call (driven by
|
|
428
|
+
// an incoming request) handles the actual work. Structural changes
|
|
429
|
+
// (add/remove files) are not handled here; the watcher will re-init the
|
|
430
|
+
// context or extend the manifest in phase 5.
|
|
431
|
+
function invalidate(filePath) {
|
|
432
|
+
const { touched, affected } = markAffectedStale(filePath);
|
|
349
433
|
if (touched) {
|
|
350
434
|
bumpVersion();
|
|
351
435
|
emit('update', { type: 'invalidate', filePath, affected, version: manifest.version });
|
|
@@ -353,6 +437,87 @@ async function createLazyPagesContext(options) {
|
|
|
353
437
|
return { touched, affected };
|
|
354
438
|
}
|
|
355
439
|
|
|
440
|
+
let inFlightClientRefresh = null;
|
|
441
|
+
|
|
442
|
+
async function handleSourceChange(filePath) {
|
|
443
|
+
if (disposed) throw new Error('Lazy pages context is disposed');
|
|
444
|
+
const { touched, affected } = markAffectedStale(filePath);
|
|
445
|
+
if (!touched) return { touched: false, affected, hmr: null };
|
|
446
|
+
if (viteDev) {
|
|
447
|
+
bumpVersion();
|
|
448
|
+
emit('update', { type: 'invalidate', filePath, affected, version: manifest.version });
|
|
449
|
+
return { touched: true, affected, hmr: null };
|
|
450
|
+
}
|
|
451
|
+
if (inFlightClientRefresh) return inFlightClientRefresh;
|
|
452
|
+
|
|
453
|
+
const task = (async () => {
|
|
454
|
+
const previousMetafile = currentClientMetafile;
|
|
455
|
+
const previousCssBundle = manifest.cssBundle ?? null;
|
|
456
|
+
const timestamp = Date.now();
|
|
457
|
+
|
|
458
|
+
const [clientResult, cssBundle] = await Promise.all([
|
|
459
|
+
clientCtx?.rebuild() ?? Promise.resolve(null),
|
|
460
|
+
buildCssBundle(pagesDir, outDir, minify, stylesPath, fontFaceCss, rootDir),
|
|
461
|
+
]);
|
|
462
|
+
|
|
463
|
+
if (clientResult) {
|
|
464
|
+
applyClientResult(clientResult);
|
|
465
|
+
}
|
|
466
|
+
if (cssBundle) manifest.cssBundle = cssBundle;
|
|
467
|
+
|
|
468
|
+
bumpVersion();
|
|
469
|
+
emit('update', { type: 'invalidate', filePath, affected, version: manifest.version });
|
|
470
|
+
|
|
471
|
+
let hmr = null;
|
|
472
|
+
if (clientResult && previousMetafile) {
|
|
473
|
+
const diff = diffClientMetafiles(previousMetafile, clientResult.metafile, outDir);
|
|
474
|
+
const modules = [...diff.changedHydrationBundles, ...diff.changedNavBundles];
|
|
475
|
+
const routeBundles = {};
|
|
476
|
+
for (const pattern of affected.pages) {
|
|
477
|
+
const entry = findEntry(pattern);
|
|
478
|
+
if (entry?.clientBundle) routeBundles[pattern] = entry.clientBundle;
|
|
479
|
+
}
|
|
480
|
+
if (diff.needsFullReload) {
|
|
481
|
+
hmr = { type: 'reload', timestamp };
|
|
482
|
+
emit('update', {
|
|
483
|
+
type: 'reload',
|
|
484
|
+
filePath,
|
|
485
|
+
reason: 'client-bundle-drift',
|
|
486
|
+
timestamp,
|
|
487
|
+
version: manifest.version,
|
|
488
|
+
});
|
|
489
|
+
} else if (modules.length > 0) {
|
|
490
|
+
hmr = { type: 'hmr-update', modules, routeBundles, timestamp };
|
|
491
|
+
emit('update', {
|
|
492
|
+
type: 'hmr-update',
|
|
493
|
+
filePath,
|
|
494
|
+
modules,
|
|
495
|
+
routeBundles,
|
|
496
|
+
timestamp,
|
|
497
|
+
version: manifest.version,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (manifest.cssBundle) {
|
|
503
|
+
emit('update', {
|
|
504
|
+
type: 'css-update',
|
|
505
|
+
filePath,
|
|
506
|
+
cssBundle: manifest.cssBundle,
|
|
507
|
+
timestamp,
|
|
508
|
+
version: manifest.version,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return { touched: true, affected, hmr };
|
|
513
|
+
})().finally(() => {
|
|
514
|
+
inFlightClientRefresh = null;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
inFlightClientRefresh = task;
|
|
518
|
+
return task;
|
|
519
|
+
}
|
|
520
|
+
|
|
356
521
|
// ── Structural rediscovery ─────────────────────────────────────────────
|
|
357
522
|
// `invalidate()` handles content edits to known files; `rediscover()`
|
|
358
523
|
// handles add/delete of pages, layouts, and middlewares. Re-runs the same
|
|
@@ -385,6 +550,8 @@ async function createLazyPagesContext(options) {
|
|
|
385
550
|
const result = {
|
|
386
551
|
added: { pages: [], layouts: [], middlewares: [] },
|
|
387
552
|
removed: { pages: [], layouts: [], middlewares: [] },
|
|
553
|
+
addedLoadings: [],
|
|
554
|
+
removedLoadings: [],
|
|
388
555
|
staleByCascade: [],
|
|
389
556
|
clientRebuilt: false,
|
|
390
557
|
version: manifest.version,
|
|
@@ -406,13 +573,20 @@ async function createLazyPagesContext(options) {
|
|
|
406
573
|
const addedMiddlewares = nextMiddlewares.filter((m) => !currentMwDirs.has(m.dirPath));
|
|
407
574
|
const removedMiddlewares = [...currentMwDirs].filter((d) => !nextMwDirs.has(d));
|
|
408
575
|
|
|
576
|
+
const currentLoadingDirs = new Set(manifest.loadings.keys());
|
|
577
|
+
const nextLoadingDirs = new Set(nextLoadings.map((l) => l.dirPath));
|
|
578
|
+
const addedLoadings = nextLoadings.filter((l) => !currentLoadingDirs.has(l.dirPath));
|
|
579
|
+
const removedLoadings = [...currentLoadingDirs].filter((d) => !nextLoadingDirs.has(d));
|
|
580
|
+
|
|
409
581
|
const noOp =
|
|
410
582
|
addedPages.length === 0 &&
|
|
411
583
|
removedPages.length === 0 &&
|
|
412
584
|
addedLayouts.length === 0 &&
|
|
413
585
|
removedLayouts.length === 0 &&
|
|
414
586
|
addedMiddlewares.length === 0 &&
|
|
415
|
-
removedMiddlewares.length === 0
|
|
587
|
+
removedMiddlewares.length === 0 &&
|
|
588
|
+
addedLoadings.length === 0 &&
|
|
589
|
+
removedLoadings.length === 0;
|
|
416
590
|
if (noOp) return result;
|
|
417
591
|
|
|
418
592
|
// ── Apply layout add/remove ──────────────────────────────────────
|
|
@@ -445,6 +619,17 @@ async function createLazyPagesContext(options) {
|
|
|
445
619
|
result.removed.middlewares.push(dirPath);
|
|
446
620
|
}
|
|
447
621
|
|
|
622
|
+
for (const loading of addedLoadings) {
|
|
623
|
+
manifest.loadings.set(loading.dirPath, {
|
|
624
|
+
srcPath: path.resolve(loading.filePath),
|
|
625
|
+
});
|
|
626
|
+
result.addedLoadings.push(loading.dirPath);
|
|
627
|
+
}
|
|
628
|
+
for (const dirPath of removedLoadings) {
|
|
629
|
+
manifest.loadings.delete(dirPath);
|
|
630
|
+
result.removedLoadings.push(dirPath);
|
|
631
|
+
}
|
|
632
|
+
|
|
448
633
|
// ── Apply page add/remove ────────────────────────────────────────
|
|
449
634
|
const addedPagePatterns = new Set();
|
|
450
635
|
for (const page of addedPages) {
|
|
@@ -494,8 +679,10 @@ async function createLazyPagesContext(options) {
|
|
|
494
679
|
result.added.pages.length > 0 ||
|
|
495
680
|
result.removed.pages.length > 0 ||
|
|
496
681
|
result.added.layouts.length > 0 ||
|
|
497
|
-
result.removed.layouts.length > 0
|
|
498
|
-
|
|
682
|
+
result.removed.layouts.length > 0 ||
|
|
683
|
+
result.addedLoadings.length > 0 ||
|
|
684
|
+
result.removedLoadings.length > 0;
|
|
685
|
+
if (needClientRebuild && !viteDev) {
|
|
499
686
|
if (clientCtx) {
|
|
500
687
|
try {
|
|
501
688
|
await clientCtx.dispose();
|
|
@@ -515,22 +702,14 @@ async function createLazyPagesContext(options) {
|
|
|
515
702
|
devMode,
|
|
516
703
|
);
|
|
517
704
|
const clientResult = await clientCtx.rebuild();
|
|
518
|
-
|
|
519
|
-
const clientBundle = clientResult.bundles.get(entry.pattern);
|
|
520
|
-
const navBundle = clientResult.navBundles.get(entry.pattern);
|
|
521
|
-
if (clientBundle) entry.clientBundle = clientBundle;
|
|
522
|
-
if (navBundle) entry.navBundle = navBundle;
|
|
523
|
-
entry.layoutClientBundles = entry.layoutDirs
|
|
524
|
-
.map((d) => clientResult.layoutNavBundles.get(d))
|
|
525
|
-
.filter((v) => v !== undefined);
|
|
526
|
-
entry.loadingClientBundles = entry.loadingDirs
|
|
527
|
-
.map((d) => clientResult.loadingNavBundles.get(d))
|
|
528
|
-
.filter((v) => v !== undefined);
|
|
529
|
-
entry.sharedChunks = clientResult.entryChunks?.get(entry.pattern) ?? [];
|
|
530
|
-
entry.sharedCssChunks = clientResult.entryCssChunks?.get(entry.pattern) ?? [];
|
|
531
|
-
}
|
|
705
|
+
applyClientResult(clientResult);
|
|
532
706
|
result.clientRebuilt = true;
|
|
533
707
|
}
|
|
708
|
+
if (viteDev) {
|
|
709
|
+
const nextStylesPath = await discoverStyles(pagesDir);
|
|
710
|
+
manifest.stylesPath = nextStylesPath ? path.resolve(nextStylesPath) : null;
|
|
711
|
+
await syncViteHydrationEntries({ manifest, outDir });
|
|
712
|
+
}
|
|
534
713
|
|
|
535
714
|
// ── Version bump + event emission ────────────────────────────────
|
|
536
715
|
bumpVersion();
|
|
@@ -553,6 +732,12 @@ async function createLazyPagesContext(options) {
|
|
|
553
732
|
for (const dirPath of result.removed.middlewares) {
|
|
554
733
|
emit('update', { type: 'remove-middleware', dirPath, version: manifest.version });
|
|
555
734
|
}
|
|
735
|
+
for (const dirPath of result.addedLoadings) {
|
|
736
|
+
emit('update', { type: 'add-loading', dirPath, version: manifest.version });
|
|
737
|
+
}
|
|
738
|
+
for (const dirPath of result.removedLoadings) {
|
|
739
|
+
emit('update', { type: 'remove-loading', dirPath, version: manifest.version });
|
|
740
|
+
}
|
|
556
741
|
if (result.clientRebuilt) {
|
|
557
742
|
emit('update', { type: 'client-rebuilt', version: manifest.version });
|
|
558
743
|
}
|
|
@@ -603,9 +788,11 @@ async function createLazyPagesContext(options) {
|
|
|
603
788
|
stale: m.stale,
|
|
604
789
|
...(m.lastBuiltAt ? { lastBuiltAt: m.lastBuiltAt } : {}),
|
|
605
790
|
})),
|
|
791
|
+
loadings: Array.from(manifest.loadings.keys()).map((dirPath) => ({ dirPath })),
|
|
606
792
|
...(manifest.errorBundle ? { errorBundle: manifest.errorBundle } : {}),
|
|
607
793
|
...(manifest.notFoundBundle ? { notFoundBundle: manifest.notFoundBundle } : {}),
|
|
608
794
|
...(manifest.cssBundle ? { cssBundle: manifest.cssBundle } : {}),
|
|
795
|
+
...(manifest.stylesPath ? { stylesPath: manifest.stylesPath } : {}),
|
|
609
796
|
};
|
|
610
797
|
}
|
|
611
798
|
|
|
@@ -627,12 +814,13 @@ async function createLazyPagesContext(options) {
|
|
|
627
814
|
ensureLayoutBuilt,
|
|
628
815
|
ensureMiddlewareBuilt,
|
|
629
816
|
invalidate,
|
|
817
|
+
handleSourceChange,
|
|
630
818
|
rediscover,
|
|
631
819
|
dispose,
|
|
632
820
|
on,
|
|
633
821
|
getManifestJson,
|
|
634
822
|
get clientMetafile() {
|
|
635
|
-
return startup.clientMetafile;
|
|
823
|
+
return currentClientMetafile ?? startup.clientMetafile;
|
|
636
824
|
},
|
|
637
825
|
};
|
|
638
826
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
function resolvePagesOutDir(config, rootDir, mode = 'production') {
|
|
4
|
+
const pages = config?.pages ?? {};
|
|
5
|
+
const isDev = mode === 'development';
|
|
6
|
+
const prodOutDir = pages.outDir ?? path.resolve(rootDir, '.build/pages');
|
|
7
|
+
const devOutDir = pages.devOutDir ?? path.resolve(rootDir, '.build-dev/pages');
|
|
8
|
+
return isDev ? devOutDir : prodOutDir;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { resolvePagesOutDir };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createPagesHandler } from './handler.js';
|
|
2
2
|
import { createLazyPagesContext } from './lazy-context.js';
|
|
3
3
|
import { createPagesWatcher } from './watcher.js';
|
|
4
|
+
import { resolvePagesOutDir } from './out-dir.js';
|
|
4
5
|
|
|
5
6
|
class PagesRouter {
|
|
6
7
|
config;
|
|
@@ -24,6 +25,8 @@ class PagesRouter {
|
|
|
24
25
|
async init() {
|
|
25
26
|
const { config, rootDir, log, mode, fileWatcher } = this;
|
|
26
27
|
const isDev = mode === 'development';
|
|
28
|
+
const viteDev = isDev && config.pages?.vite?.enabled === true;
|
|
29
|
+
const outDir = resolvePagesOutDir(config, rootDir, mode);
|
|
27
30
|
|
|
28
31
|
if (isDev) {
|
|
29
32
|
// Dev mode: `createLazyPagesContext()` builds only the shared bits
|
|
@@ -35,9 +38,11 @@ class PagesRouter {
|
|
|
35
38
|
this.lazyContext = await createLazyPagesContext({
|
|
36
39
|
rootDir,
|
|
37
40
|
pagesDir: config.pages.dir,
|
|
41
|
+
outDir,
|
|
38
42
|
fonts: config.pages?.fonts,
|
|
39
43
|
minify: false,
|
|
40
44
|
devMode: true,
|
|
45
|
+
viteDev,
|
|
41
46
|
});
|
|
42
47
|
} catch (err) {
|
|
43
48
|
log.error('Pages lazy context init failed', { error: String(err) });
|
|
@@ -46,9 +51,11 @@ class PagesRouter {
|
|
|
46
51
|
|
|
47
52
|
this.handler = createPagesHandler({
|
|
48
53
|
rootDir,
|
|
54
|
+
outDir,
|
|
49
55
|
session: config.session,
|
|
50
56
|
appContext: this.appContext,
|
|
51
57
|
mode,
|
|
58
|
+
viteDev,
|
|
52
59
|
...(this.lazyContext ? { lazyContext: this.lazyContext } : {}),
|
|
53
60
|
});
|
|
54
61
|
|
package/server/pages/ssr.js
CHANGED
|
@@ -70,12 +70,14 @@ function wrapWithProviders(createElement, element, pathname, params) {
|
|
|
70
70
|
}
|
|
71
71
|
function buildCssLinkTag(manifest) {
|
|
72
72
|
if (!manifest.cssBundle) return '';
|
|
73
|
-
return `<link rel="stylesheet" href="/static/${manifest.cssBundle}" />`;
|
|
73
|
+
return `<link rel="stylesheet" data-arcway-css="global" href="/static/${manifest.cssBundle}" />`;
|
|
74
74
|
}
|
|
75
75
|
function buildRouteCssLinkTags(route) {
|
|
76
76
|
const chunks = route?.sharedCssChunks ?? [];
|
|
77
77
|
if (chunks.length === 0) return '';
|
|
78
|
-
return chunks
|
|
78
|
+
return chunks
|
|
79
|
+
.map((c) => `<link rel="stylesheet" data-arcway-css="route" href="/static/${c}" />`)
|
|
80
|
+
.join('\n');
|
|
79
81
|
}
|
|
80
82
|
function buildFontPreloadTags(manifest) {
|
|
81
83
|
return manifest.fontPreloadHtml ?? '';
|
|
@@ -97,6 +99,19 @@ function buildScriptTags(route) {
|
|
|
97
99
|
tags.push(`<script type="module" src="/static/${route.clientBundle}"></script>`);
|
|
98
100
|
return tags.join('\n');
|
|
99
101
|
}
|
|
102
|
+
function buildViteScriptTags(route) {
|
|
103
|
+
return [
|
|
104
|
+
`<script type="module">
|
|
105
|
+
import RefreshRuntime from "/@react-refresh";
|
|
106
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
107
|
+
window.$RefreshReg$ = () => {};
|
|
108
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
109
|
+
window.__vite_plugin_react_preamble_installed__ = true;
|
|
110
|
+
</script>`,
|
|
111
|
+
`<script type="module" src="/@vite/client"></script>`,
|
|
112
|
+
`<script type="module" src="${route.clientBundle}"></script>`,
|
|
113
|
+
].join('\n');
|
|
114
|
+
}
|
|
100
115
|
function buildHtmlShell({ headHtml, fontPreloadTags, cssLinkTag, bodysuffix }) {
|
|
101
116
|
return {
|
|
102
117
|
head: `<!DOCTYPE html>
|
|
@@ -128,6 +143,7 @@ async function renderPage(
|
|
|
128
143
|
cacheVersion,
|
|
129
144
|
react,
|
|
130
145
|
devMode,
|
|
146
|
+
viteDev = false,
|
|
131
147
|
) {
|
|
132
148
|
const { createElement, renderToPipeableStream } = react;
|
|
133
149
|
const bundlePath = path.join(outDir, route.serverBundle);
|
|
@@ -155,14 +171,14 @@ async function renderPage(
|
|
|
155
171
|
layoutComponents.push(Layout);
|
|
156
172
|
}
|
|
157
173
|
}
|
|
158
|
-
const scriptTags = buildScriptTags(route);
|
|
159
|
-
const manifestCssTag = buildCssLinkTag(manifest);
|
|
160
|
-
const routeCssTags = buildRouteCssLinkTags(route);
|
|
174
|
+
const scriptTags = viteDev ? buildViteScriptTags(route) : buildScriptTags(route);
|
|
175
|
+
const manifestCssTag = viteDev ? '' : buildCssLinkTag(manifest);
|
|
176
|
+
const routeCssTags = viteDev ? '' : buildRouteCssLinkTags(route);
|
|
161
177
|
const cssLinkTag = [manifestCssTag, routeCssTags].filter(Boolean).join('\n');
|
|
162
178
|
const fontPreloadTags = buildFontPreloadTags(manifest);
|
|
163
179
|
const envScriptTag = buildEnvScriptTag(collectPublicEnv());
|
|
164
|
-
const hmrTag = devMode ? buildHmrScript() : '';
|
|
165
|
-
const liveReloadTag = devMode && !hmrTag ? buildLiveReloadScript() : '';
|
|
180
|
+
const hmrTag = devMode && !viteDev ? buildHmrScript() : '';
|
|
181
|
+
const liveReloadTag = devMode && !viteDev && !hmrTag ? buildLiveReloadScript() : '';
|
|
166
182
|
const propsJson = JSON.stringify(params).replace(/</g, '\\u003c');
|
|
167
183
|
const headData = { meta: [], links: [] };
|
|
168
184
|
setSSRHeadData(headData);
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import reactPlugin from '@vitejs/plugin-react';
|
|
5
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
6
|
+
import { createServer as createViteServer } from 'vite';
|
|
7
|
+
import { patternToFileName } from './build-server.js';
|
|
8
|
+
|
|
9
|
+
function toPosixPath(value) {
|
|
10
|
+
return value.replace(/\\/g, '/');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toViteModuleUrl(rootDir, filePath) {
|
|
14
|
+
if (!filePath) return null;
|
|
15
|
+
const abs = path.resolve(filePath);
|
|
16
|
+
const rel = path.relative(rootDir, abs);
|
|
17
|
+
if (!rel.startsWith('..') && !path.isAbsolute(rel)) {
|
|
18
|
+
return `/${toPosixPath(rel)}`;
|
|
19
|
+
}
|
|
20
|
+
return `/@fs/${toPosixPath(abs)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getViteEntryPath(outDir, pattern) {
|
|
24
|
+
return path.join(outDir, '.vite-entries', `${patternToFileName(pattern)}.tsx`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildViteHydrationEntry({
|
|
28
|
+
componentPath,
|
|
29
|
+
layouts = [],
|
|
30
|
+
loadings = [],
|
|
31
|
+
pattern = '/',
|
|
32
|
+
globalStylesPath,
|
|
33
|
+
}) {
|
|
34
|
+
const imports = [
|
|
35
|
+
`import { hydrateRoot } from 'react-dom/client';`,
|
|
36
|
+
`import { ApiProvider, Router } from 'arcway/lib/client';`,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
if (globalStylesPath) {
|
|
40
|
+
imports.push(`import '${toPosixPath(path.resolve(globalStylesPath))}';`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
imports.push(`import Component from '${toPosixPath(path.resolve(componentPath))}';`);
|
|
44
|
+
|
|
45
|
+
const layoutNames = [];
|
|
46
|
+
for (let i = 0; i < layouts.length; i++) {
|
|
47
|
+
imports.push(`import Layout${i} from '${toPosixPath(path.resolve(layouts[i]))}';`);
|
|
48
|
+
layoutNames.push(`Layout${i}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const loadingNames = [];
|
|
52
|
+
for (let i = 0; i < loadings.length; i++) {
|
|
53
|
+
imports.push(`import Loading${i} from '${toPosixPath(path.resolve(loadings[i]))}';`);
|
|
54
|
+
loadingNames.push(`Loading${i}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const layoutsArray = layoutNames.length > 0 ? `[${layoutNames.join(', ')}]` : '[]';
|
|
58
|
+
const loadingsArray = loadingNames.length > 0 ? `[${loadingNames.join(', ')}]` : '[]';
|
|
59
|
+
|
|
60
|
+
return `${imports.join('\n')}
|
|
61
|
+
|
|
62
|
+
const container = document.getElementById('__app');
|
|
63
|
+
const propsEl = document.getElementById('__app_props');
|
|
64
|
+
const props = propsEl ? JSON.parse(propsEl.textContent || '{}') : {};
|
|
65
|
+
const ROOT_KEY = '__arcway_root__';
|
|
66
|
+
const rootOwner = window;
|
|
67
|
+
const element = <ApiProvider><Router initialPath={window.location.pathname} initialParams={props} initialPattern={${JSON.stringify(pattern)}} initialComponent={Component} initialLayouts={${layoutsArray}} initialLoadings={${loadingsArray}} /></ApiProvider>;
|
|
68
|
+
|
|
69
|
+
if (!container) {
|
|
70
|
+
throw new Error('Arcway Vite hydrate entry could not find #__app');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!rootOwner[ROOT_KEY]) {
|
|
74
|
+
rootOwner[ROOT_KEY] = hydrateRoot(container, element);
|
|
75
|
+
} else {
|
|
76
|
+
rootOwner[ROOT_KEY].render(element);
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function syncViteHydrationEntries({ manifest, outDir }) {
|
|
82
|
+
const viteDir = path.join(outDir, '.vite-entries');
|
|
83
|
+
await fs.mkdir(viteDir, { recursive: true });
|
|
84
|
+
|
|
85
|
+
const expected = new Set();
|
|
86
|
+
|
|
87
|
+
for (const entry of manifest.entries) {
|
|
88
|
+
const entryPath = getViteEntryPath(outDir, entry.pattern);
|
|
89
|
+
const content = buildViteHydrationEntry({
|
|
90
|
+
componentPath: entry.srcPath,
|
|
91
|
+
layouts: entry.layoutDirs
|
|
92
|
+
.map((dirPath) => manifest.layouts.get(dirPath)?.srcPath)
|
|
93
|
+
.filter((value) => value !== undefined),
|
|
94
|
+
loadings: entry.loadingDirs
|
|
95
|
+
.map((dirPath) => manifest.loadings.get(dirPath)?.srcPath)
|
|
96
|
+
.filter((value) => value !== undefined),
|
|
97
|
+
pattern: entry.pattern,
|
|
98
|
+
globalStylesPath: manifest.stylesPath,
|
|
99
|
+
});
|
|
100
|
+
await fs.mkdir(path.dirname(entryPath), { recursive: true });
|
|
101
|
+
await fs.writeFile(entryPath, content);
|
|
102
|
+
expected.add(toPosixPath(path.relative(viteDir, entryPath)));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const existing = await listFiles(viteDir);
|
|
106
|
+
await Promise.all(
|
|
107
|
+
existing
|
|
108
|
+
.filter((name) => !expected.has(name))
|
|
109
|
+
.map((name) => fs.rm(path.join(viteDir, name), { force: true })),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function listFiles(rootDir, currentDir = rootDir) {
|
|
114
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true }).catch(() => []);
|
|
115
|
+
const files = [];
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const abs = path.join(currentDir, entry.name);
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
const nested = await listFiles(rootDir, abs);
|
|
120
|
+
files.push(...nested);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
files.push(toPosixPath(path.relative(rootDir, abs)));
|
|
124
|
+
}
|
|
125
|
+
return files;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildViteClientManifestJson(manifest, rootDir) {
|
|
129
|
+
const clientManifest = {
|
|
130
|
+
cssBundle: null,
|
|
131
|
+
routes: manifest.entries.map((entry) => {
|
|
132
|
+
const route = {
|
|
133
|
+
pattern: entry.pattern,
|
|
134
|
+
paramNames: entry.paramNames,
|
|
135
|
+
clientBundle: toViteModuleUrl(rootDir, entry.srcPath),
|
|
136
|
+
layoutBundles: entry.layoutDirs
|
|
137
|
+
.map((dirPath) => manifest.layouts.get(dirPath)?.srcPath)
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.map((filePath) => toViteModuleUrl(rootDir, filePath)),
|
|
140
|
+
loadingBundles: entry.loadingDirs
|
|
141
|
+
.map((dirPath) => manifest.loadings.get(dirPath)?.srcPath)
|
|
142
|
+
.filter(Boolean)
|
|
143
|
+
.map((filePath) => toViteModuleUrl(rootDir, filePath)),
|
|
144
|
+
cssBundles: [],
|
|
145
|
+
};
|
|
146
|
+
if (entry.catchAllParam) route.catchAllParam = entry.catchAllParam;
|
|
147
|
+
return route;
|
|
148
|
+
}),
|
|
149
|
+
};
|
|
150
|
+
return JSON.stringify(clientManifest).replace(/</g, '\\u003c');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildViteRoute(route, { rootDir, outDir }) {
|
|
154
|
+
return {
|
|
155
|
+
...route,
|
|
156
|
+
clientBundle: toViteModuleUrl(rootDir, getViteEntryPath(outDir, route.pattern)),
|
|
157
|
+
sharedChunks: ['/@vite/client'],
|
|
158
|
+
sharedCssChunks: [],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveAppAliases(rootDir) {
|
|
163
|
+
const appRequire = createRequire(path.join(rootDir, 'package.json'));
|
|
164
|
+
const aliases = [];
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const reactRoot = path.dirname(appRequire.resolve('react/package.json'));
|
|
168
|
+
aliases.push({ find: /^react$/, replacement: toPosixPath(path.join(reactRoot, 'index.js')) });
|
|
169
|
+
aliases.push({ find: /^react\/jsx-runtime$/, replacement: toPosixPath(path.join(reactRoot, 'jsx-runtime.js')) });
|
|
170
|
+
aliases.push({ find: /^react\/jsx-dev-runtime$/, replacement: toPosixPath(path.join(reactRoot, 'jsx-dev-runtime.js')) });
|
|
171
|
+
} catch {}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const reactDomRoot = path.dirname(appRequire.resolve('react-dom/package.json'));
|
|
175
|
+
aliases.push({ find: /^react-dom$/, replacement: toPosixPath(path.join(reactDomRoot, 'index.js')) });
|
|
176
|
+
aliases.push({ find: /^react-dom\/client$/, replacement: toPosixPath(path.join(reactDomRoot, 'client.js')) });
|
|
177
|
+
} catch {}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
aliases.push({ find: /^swr$/, replacement: toPosixPath(appRequire.resolve('swr')) });
|
|
181
|
+
} catch {}
|
|
182
|
+
|
|
183
|
+
return aliases;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function createViteDevRouter({ rootDir, log, config }) {
|
|
187
|
+
const appAliases = resolveAppAliases(rootDir);
|
|
188
|
+
const vite = await createViteServer({
|
|
189
|
+
root: rootDir,
|
|
190
|
+
appType: 'custom',
|
|
191
|
+
server: {
|
|
192
|
+
middlewareMode: true,
|
|
193
|
+
},
|
|
194
|
+
resolve: {
|
|
195
|
+
alias: appAliases,
|
|
196
|
+
dedupe: ['react', 'react-dom', 'swr'],
|
|
197
|
+
},
|
|
198
|
+
optimizeDeps: {
|
|
199
|
+
include: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'swr'],
|
|
200
|
+
},
|
|
201
|
+
plugins: [reactPlugin(), tailwindcss()],
|
|
202
|
+
clearScreen: false,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const internalPrefixes = ['/@vite/', '/@id/', '/@react-refresh', '/@fs/', '/node_modules/.vite/'];
|
|
206
|
+
const sourceExt = /\.(?:[cm]?[jt]sx?|css|pcss|postcss|json|svg)$/i;
|
|
207
|
+
|
|
208
|
+
async function handle(req, res) {
|
|
209
|
+
const url = req.url ?? '/';
|
|
210
|
+
const pathname = url.split('?')[0];
|
|
211
|
+
const shouldDelegate =
|
|
212
|
+
internalPrefixes.some((prefix) => pathname.startsWith(prefix)) ||
|
|
213
|
+
sourceExt.test(pathname) ||
|
|
214
|
+
pathname.startsWith('/.build/pages/.vite-entries/');
|
|
215
|
+
|
|
216
|
+
if (!shouldDelegate) return false;
|
|
217
|
+
|
|
218
|
+
return new Promise((resolve, reject) => {
|
|
219
|
+
vite.middlewares(req, res, (err) => {
|
|
220
|
+
if (err) {
|
|
221
|
+
reject(err);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
resolve(res.writableEnded || res.headersSent);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
vite,
|
|
231
|
+
handle,
|
|
232
|
+
async close() {
|
|
233
|
+
await vite.close();
|
|
234
|
+
log?.info?.('Vite dev server closed');
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export {
|
|
240
|
+
buildViteClientManifestJson,
|
|
241
|
+
buildViteRoute,
|
|
242
|
+
createViteDevRouter,
|
|
243
|
+
getViteEntryPath,
|
|
244
|
+
syncViteHydrationEntries,
|
|
245
|
+
toViteModuleUrl,
|
|
246
|
+
};
|
package/server/pages/watcher.js
CHANGED
|
@@ -36,7 +36,9 @@ function createPagesWatcher(options) {
|
|
|
36
36
|
}
|
|
37
37
|
for (const e of events) {
|
|
38
38
|
if (e.event !== 'change') continue;
|
|
39
|
-
const result = lazyContext.
|
|
39
|
+
const result = lazyContext.handleSourceChange
|
|
40
|
+
? await lazyContext.handleSourceChange(e.path)
|
|
41
|
+
: lazyContext.invalidate(e.path);
|
|
40
42
|
if (result.touched) {
|
|
41
43
|
log.info(`Pages: invalidated ${e.relativePath} (${formatAffected(result.affected)})`);
|
|
42
44
|
}
|
package/server/watcher.js
CHANGED