arcway 0.4.13 → 0.4.15
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/router.js +23 -9
- package/package.json +1 -1
- package/server/config/modules/pages.js +16 -0
- package/server/pages/build-client.js +22 -14
- package/server/pages/build-manifest.js +1 -0
- package/server/pages/discovery.js +6 -6
- package/server/pages/handler.js +293 -60
- package/server/pages/lazy-context.js +41 -4
- package/server/pages/pages-router.js +1 -0
- package/server/pages/server-loader-transform.js +191 -0
- package/server/pages/ssr.js +78 -18
- package/server/pages/vite-dev.js +26 -6
package/client/router.js
CHANGED
|
@@ -23,9 +23,9 @@ import { parseQuery, stringifyQuery } from './query.js';
|
|
|
23
23
|
const ROUTER_CTX_KEY = '__router_context__';
|
|
24
24
|
const RouterContext = (globalThis[ROUTER_CTX_KEY] ??= createContext(null));
|
|
25
25
|
|
|
26
|
-
function wrapInLayouts(element, layouts) {
|
|
26
|
+
function wrapInLayouts(element, layouts, props = {}) {
|
|
27
27
|
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
28
|
-
element = createElement(layouts[i],
|
|
28
|
+
element = createElement(layouts[i], props, element);
|
|
29
29
|
}
|
|
30
30
|
return element;
|
|
31
31
|
}
|
|
@@ -56,8 +56,10 @@ function useRouter() {
|
|
|
56
56
|
pathname: ctx.pathname,
|
|
57
57
|
params: ctx.params,
|
|
58
58
|
query: typeof window !== 'undefined' ? parseQuery(window.location.search) : {},
|
|
59
|
-
push: (to, options) =>
|
|
60
|
-
|
|
59
|
+
push: (to, options) =>
|
|
60
|
+
ctx.navigate(normalizeNavigateTarget(to), { ...options, replace: false }),
|
|
61
|
+
replace: (to, options) =>
|
|
62
|
+
ctx.navigate(normalizeNavigateTarget(to), { ...options, replace: true }),
|
|
61
63
|
back: () => {
|
|
62
64
|
if (typeof window !== 'undefined') window.history.back();
|
|
63
65
|
},
|
|
@@ -77,6 +79,7 @@ function useRouter() {
|
|
|
77
79
|
function Router({
|
|
78
80
|
initialPath,
|
|
79
81
|
initialParams,
|
|
82
|
+
initialPageProps,
|
|
80
83
|
initialPattern,
|
|
81
84
|
initialComponent,
|
|
82
85
|
initialLayouts,
|
|
@@ -92,6 +95,7 @@ function Router({
|
|
|
92
95
|
layouts: initialLayouts ?? [],
|
|
93
96
|
loadings: initialLoadings ?? [],
|
|
94
97
|
params: initialParams ?? {},
|
|
98
|
+
props: initialPageProps ?? initialParams ?? {},
|
|
95
99
|
});
|
|
96
100
|
const [isNavigating, setIsNavigating] = useState(false);
|
|
97
101
|
const [isPending, startTransition] = useTransition();
|
|
@@ -140,6 +144,7 @@ function Router({
|
|
|
140
144
|
layouts: loaded.layouts,
|
|
141
145
|
loadings: loaded.loadings,
|
|
142
146
|
params: loaded.params,
|
|
147
|
+
props: loaded.params,
|
|
143
148
|
});
|
|
144
149
|
setIsNavigating(false);
|
|
145
150
|
if (bumpQuery) setQueryVersion((v) => v + 1);
|
|
@@ -158,8 +163,7 @@ function Router({
|
|
|
158
163
|
const qIdx = to.indexOf('?');
|
|
159
164
|
const pathOnly = qIdx === -1 ? to : to.slice(0, qIdx);
|
|
160
165
|
const search = qIdx === -1 ? '' : to.slice(qIdx);
|
|
161
|
-
const currentSearch =
|
|
162
|
-
typeof window !== 'undefined' ? window.location.search : '';
|
|
166
|
+
const currentSearch = typeof window !== 'undefined' ? window.location.search : '';
|
|
163
167
|
|
|
164
168
|
if (pathOnly === pathname && search === currentSearch) return;
|
|
165
169
|
|
|
@@ -184,6 +188,10 @@ function Router({
|
|
|
184
188
|
window.location.href = to;
|
|
185
189
|
return;
|
|
186
190
|
}
|
|
191
|
+
if (matched.route.hasLoader) {
|
|
192
|
+
window.location.href = to;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
187
195
|
|
|
188
196
|
setIsNavigating(true);
|
|
189
197
|
if (replace) {
|
|
@@ -200,6 +208,7 @@ function Router({
|
|
|
200
208
|
pattern: matched.route.pattern,
|
|
201
209
|
loadings: targetLoadings,
|
|
202
210
|
params: matched.params,
|
|
211
|
+
props: matched.params,
|
|
203
212
|
}));
|
|
204
213
|
}
|
|
205
214
|
|
|
@@ -244,6 +253,11 @@ function Router({
|
|
|
244
253
|
}
|
|
245
254
|
|
|
246
255
|
try {
|
|
256
|
+
const matched = matchClientRoute(manifest, newPath);
|
|
257
|
+
if (matched?.route.hasLoader) {
|
|
258
|
+
window.location.reload();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
247
261
|
const loaded = await loadPage(manifest, newPath);
|
|
248
262
|
if (loaded) {
|
|
249
263
|
applyLoaded(loaded, newPath, true);
|
|
@@ -265,15 +279,15 @@ function Router({
|
|
|
265
279
|
}
|
|
266
280
|
}, [pageState.pattern]);
|
|
267
281
|
|
|
268
|
-
const { component: PageComponent, layouts, loadings, params } = pageState;
|
|
282
|
+
const { component: PageComponent, layouts, loadings, params, props } = pageState;
|
|
269
283
|
|
|
270
284
|
let content;
|
|
271
285
|
if (PageComponent) {
|
|
272
286
|
const inner =
|
|
273
287
|
isNavigating && loadings.length > 0
|
|
274
288
|
? createElement(loadings.at(-1))
|
|
275
|
-
: createElement(PageComponent,
|
|
276
|
-
content = wrapInLayouts(inner, layouts);
|
|
289
|
+
: createElement(PageComponent, props);
|
|
290
|
+
content = wrapInLayouts(inner, layouts, props);
|
|
277
291
|
} else {
|
|
278
292
|
content = children;
|
|
279
293
|
}
|
package/package.json
CHANGED
|
@@ -72,6 +72,22 @@ function resolve(config, { rootDir } = {}) {
|
|
|
72
72
|
...(hmr ? { hmr } : {}),
|
|
73
73
|
},
|
|
74
74
|
};
|
|
75
|
+
if (pages.rewrite != null) {
|
|
76
|
+
if (
|
|
77
|
+
typeof pages.rewrite !== 'object' ||
|
|
78
|
+
Array.isArray(pages.rewrite) ||
|
|
79
|
+
typeof pages.rewrite.handler !== 'function' ||
|
|
80
|
+
typeof pages.rewrite.reservedPrefix !== 'string' ||
|
|
81
|
+
pages.rewrite.reservedPrefix === '/' ||
|
|
82
|
+
!pages.rewrite.reservedPrefix.startsWith('/') ||
|
|
83
|
+
pages.rewrite.reservedPrefix.endsWith('/') ||
|
|
84
|
+
/[?#\s]/.test(pages.rewrite.reservedPrefix)
|
|
85
|
+
) {
|
|
86
|
+
throw new TypeError(
|
|
87
|
+
'Invalid config: pages.rewrite requires a handler function and a reservedPrefix such as "/_sites"',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
75
91
|
if (pages.dir && !path.isAbsolute(pages.dir)) {
|
|
76
92
|
pages.dir = path.resolve(rootDir, pages.dir);
|
|
77
93
|
}
|
|
@@ -6,6 +6,7 @@ import { resolveLayoutChain, resolveLoadingChain } from './discovery.js';
|
|
|
6
6
|
import { clientIsolationPlugin } from './build-plugins.js';
|
|
7
7
|
import { patternToFileName, layoutDirToFileName } from './build-server.js';
|
|
8
8
|
import { reactRefreshPlugin } from './hmr.js';
|
|
9
|
+
import { serverLoaderClientPlugin } from './server-loader-transform.js';
|
|
9
10
|
import {
|
|
10
11
|
sha256,
|
|
11
12
|
ESBUILD_VERSION,
|
|
@@ -68,7 +69,14 @@ function planHydrationEntries(pages, layouts, loadings, tempDir) {
|
|
|
68
69
|
entryPoints[loadingName] = navPath;
|
|
69
70
|
entryToLoadingDir.set(navPath, loading.dirPath);
|
|
70
71
|
}
|
|
71
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
files,
|
|
74
|
+
entryPoints,
|
|
75
|
+
entryToPattern,
|
|
76
|
+
entryToNavPattern,
|
|
77
|
+
entryToLayoutDir,
|
|
78
|
+
entryToLoadingDir,
|
|
79
|
+
};
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
function resolveReactAlias(rootDir) {
|
|
@@ -144,6 +152,7 @@ function buildClientEsbuildOptions({
|
|
|
144
152
|
nodeEnv,
|
|
145
153
|
reactAlias,
|
|
146
154
|
rootDir,
|
|
155
|
+
pagePaths,
|
|
147
156
|
}) {
|
|
148
157
|
return {
|
|
149
158
|
entryPoints,
|
|
@@ -162,7 +171,11 @@ function buildClientEsbuildOptions({
|
|
|
162
171
|
metafile: true,
|
|
163
172
|
alias: reactAlias,
|
|
164
173
|
define: { 'process.env.NODE_ENV': JSON.stringify(nodeEnv) },
|
|
165
|
-
plugins: [
|
|
174
|
+
plugins: [
|
|
175
|
+
...(devMode ? [reactRefreshPlugin(rootDir)] : []),
|
|
176
|
+
serverLoaderClientPlugin(pagePaths),
|
|
177
|
+
clientIsolationPlugin(),
|
|
178
|
+
],
|
|
166
179
|
};
|
|
167
180
|
}
|
|
168
181
|
|
|
@@ -210,6 +223,7 @@ async function createClientBuildContext(
|
|
|
210
223
|
nodeEnv,
|
|
211
224
|
reactAlias,
|
|
212
225
|
rootDir,
|
|
226
|
+
pagePaths: pages.map((page) => page.filePath),
|
|
213
227
|
}),
|
|
214
228
|
);
|
|
215
229
|
|
|
@@ -262,9 +276,7 @@ async function unlinkAll(absPaths) {
|
|
|
262
276
|
// an atomic rename, so the served clientDir always contains exactly the
|
|
263
277
|
// current build's outputs.
|
|
264
278
|
async function collectStaleOutputs(metafile, clientDir) {
|
|
265
|
-
const currentOutputs = new Set(
|
|
266
|
-
Object.keys(metafile.outputs ?? {}).map((p) => path.resolve(p)),
|
|
267
|
-
);
|
|
279
|
+
const currentOutputs = new Set(Object.keys(metafile.outputs ?? {}).map((p) => path.resolve(p)));
|
|
268
280
|
// Scope the scan to directories esbuild actually writes to. This keeps
|
|
269
281
|
// sibling artifacts owned by other build steps (e.g. the HMR runtime under
|
|
270
282
|
// `client/hmr/`) out of the GC set, since they will never appear in the
|
|
@@ -315,14 +327,7 @@ async function tryRestoreClientFromCache({ rootDir, clientDir, coarseKey }) {
|
|
|
315
327
|
return { hit: true, bucket: lookup.bucket, metadata: deserializeMetadata(lookup.meta.metadata) };
|
|
316
328
|
}
|
|
317
329
|
|
|
318
|
-
async function storeClientInCache({
|
|
319
|
-
bucket,
|
|
320
|
-
coarseKey,
|
|
321
|
-
clientDir,
|
|
322
|
-
tempDir,
|
|
323
|
-
result,
|
|
324
|
-
metadata,
|
|
325
|
-
}) {
|
|
330
|
+
async function storeClientInCache({ bucket, coarseKey, clientDir, tempDir, result, metadata }) {
|
|
326
331
|
const outputs = Object.keys(result.metafile.outputs)
|
|
327
332
|
.map((p) => path.relative(clientDir, path.resolve(p)).replace(/\\/g, '/'))
|
|
328
333
|
.filter((rel) => !rel.startsWith('..'));
|
|
@@ -403,6 +408,7 @@ async function buildClientBundles(
|
|
|
403
408
|
nodeEnv,
|
|
404
409
|
reactAlias,
|
|
405
410
|
rootDir,
|
|
411
|
+
pagePaths: pages.map((page) => page.filePath),
|
|
406
412
|
});
|
|
407
413
|
const result = await limit(() => runClientEsbuild(plan, esbuildOptions, tempDir));
|
|
408
414
|
const metadata = extractMetadata(result, plan, outDir);
|
|
@@ -494,9 +500,11 @@ function generateHydrationEntry(componentPath, layouts = [], loadings = [], patt
|
|
|
494
500
|
const container = document.getElementById('__app');
|
|
495
501
|
const propsEl = document.getElementById('__app_props');
|
|
496
502
|
const props = propsEl ? JSON.parse(propsEl.textContent || '{}') : {};
|
|
503
|
+
const routeParamsEl = document.getElementById('__app_route_params');
|
|
504
|
+
const routeParams = routeParamsEl ? JSON.parse(routeParamsEl.textContent || '{}') : props;
|
|
497
505
|
const ROOT_KEY = '__arcway_root__';
|
|
498
506
|
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>;
|
|
507
|
+
const element = <ApiProvider><Router initialPath={window.location.pathname} initialParams={routeParams} initialPageProps={props} initialPattern={${JSON.stringify(pattern)}} initialComponent={Component} initialLayouts={${layoutsArray}} initialLoadings={${loadingsArray}} /></ApiProvider>;
|
|
500
508
|
|
|
501
509
|
if (!container) {
|
|
502
510
|
throw new Error('Arcway hydrate entry could not find #__app');
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
sortBySpecificity,
|
|
8
8
|
matchPattern,
|
|
9
9
|
} from '../router/routes.js';
|
|
10
|
+
import { hasServerLoaderExport } from './server-loader-transform.js';
|
|
10
11
|
function dirDepth(dirPath) {
|
|
11
12
|
return dirPath === '' ? 0 : dirPath.split(path.sep).length;
|
|
12
13
|
}
|
|
@@ -49,6 +50,7 @@ async function discoverPages(pagesDir) {
|
|
|
49
50
|
const fileName = path.basename(filePath);
|
|
50
51
|
if (fileName.startsWith('_')) continue;
|
|
51
52
|
const relPath = path.relative(pagesDir, filePath);
|
|
53
|
+
const source = await fs.readFile(filePath, 'utf8');
|
|
52
54
|
const urlPattern = filePathToPattern(relPath);
|
|
53
55
|
const { regex, paramNames, catchAllParam } = compilePattern(urlPattern);
|
|
54
56
|
pages.push({
|
|
@@ -57,6 +59,7 @@ async function discoverPages(pagesDir) {
|
|
|
57
59
|
paramNames,
|
|
58
60
|
...(catchAllParam ? { catchAllParam } : {}),
|
|
59
61
|
filePath,
|
|
62
|
+
hasLoader: await hasServerLoaderExport(source, filePath),
|
|
60
63
|
});
|
|
61
64
|
}
|
|
62
65
|
sortBySpecificity(pages);
|
|
@@ -125,9 +128,7 @@ function mapFileToAffected(filePath, manifest) {
|
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
const layoutEntries = asDirPathList(manifest.layouts);
|
|
128
|
-
const hitLayout = layoutEntries.find(
|
|
129
|
-
(l) => l.srcPath && path.resolve(l.srcPath) === target,
|
|
130
|
-
);
|
|
131
|
+
const hitLayout = layoutEntries.find((l) => l.srcPath && path.resolve(l.srcPath) === target);
|
|
131
132
|
if (hitLayout) {
|
|
132
133
|
result.layouts.push(hitLayout.dirPath);
|
|
133
134
|
for (const entry of entries) {
|
|
@@ -139,9 +140,7 @@ function mapFileToAffected(filePath, manifest) {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
const middlewareEntries = asDirPathList(manifest.middlewares);
|
|
142
|
-
const hitMw = middlewareEntries.find(
|
|
143
|
-
(m) => m.srcPath && path.resolve(m.srcPath) === target,
|
|
144
|
-
);
|
|
143
|
+
const hitMw = middlewareEntries.find((m) => m.srcPath && path.resolve(m.srcPath) === target);
|
|
145
144
|
if (hitMw) {
|
|
146
145
|
result.middlewares.push(hitMw.dirPath);
|
|
147
146
|
for (const entry of entries) {
|
|
@@ -179,6 +178,7 @@ function buildPageManifestEntry(page, { layouts, middlewares, loadings }) {
|
|
|
179
178
|
paramNames: page.paramNames,
|
|
180
179
|
...(page.catchAllParam ? { catchAllParam: page.catchAllParam } : {}),
|
|
181
180
|
srcPath: path.resolve(page.filePath),
|
|
181
|
+
...(page.hasLoader ? { hasLoader: true } : {}),
|
|
182
182
|
layoutDirs: resolveLayoutChain(page.pattern, layouts).map((l) => l.dirPath),
|
|
183
183
|
middlewareDirs: resolvePageMiddlewareChain(page.pattern, middlewares).map((m) => m.dirPath),
|
|
184
184
|
loadingDirs: resolveLoadingChain(page.pattern, loadings).map((l) => l.dirPath),
|
package/server/pages/handler.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import { domainToASCII } from 'node:url';
|
|
3
4
|
import { createRequire } from 'node:module';
|
|
4
5
|
import { EventEmitter } from 'node:events';
|
|
5
6
|
import { compilePattern, sortBySpecificity, matchPattern } from '../router/routes.js';
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
buildRouteCssLinkTags,
|
|
10
11
|
buildScriptTags,
|
|
11
12
|
loadComponent,
|
|
13
|
+
loadPageModule,
|
|
12
14
|
renderErrorPage,
|
|
13
15
|
renderPage,
|
|
14
16
|
syncReactInternals,
|
|
@@ -33,6 +35,7 @@ function createPagesHandler(options) {
|
|
|
33
35
|
let manifestPath = path.join(outDir, 'pages-manifest.json');
|
|
34
36
|
const sessionConfig = options.session;
|
|
35
37
|
const appContext = options.appContext ?? null;
|
|
38
|
+
const rewrite = options.rewrite ?? null;
|
|
36
39
|
const mode = options.mode ?? 'production';
|
|
37
40
|
const devMode = mode === 'development';
|
|
38
41
|
const viteDev = options.viteDev === true;
|
|
@@ -57,6 +60,7 @@ function createPagesHandler(options) {
|
|
|
57
60
|
rootDir,
|
|
58
61
|
outDir,
|
|
59
62
|
viteDev,
|
|
63
|
+
reservedPrefix: rewrite?.reservedPrefix,
|
|
60
64
|
});
|
|
61
65
|
let lastSeenVersion = lazyContext ? manifest.version : 0;
|
|
62
66
|
const componentCache = new Map();
|
|
@@ -82,6 +86,7 @@ function createPagesHandler(options) {
|
|
|
82
86
|
rootDir,
|
|
83
87
|
outDir,
|
|
84
88
|
viteDev,
|
|
89
|
+
reservedPrefix: rewrite?.reservedPrefix,
|
|
85
90
|
});
|
|
86
91
|
componentCache.clear();
|
|
87
92
|
cacheVersion++;
|
|
@@ -110,6 +115,7 @@ function createPagesHandler(options) {
|
|
|
110
115
|
rootDir,
|
|
111
116
|
outDir,
|
|
112
117
|
viteDev,
|
|
118
|
+
reservedPrefix: rewrite?.reservedPrefix,
|
|
113
119
|
});
|
|
114
120
|
componentCache.clear();
|
|
115
121
|
cacheVersion++;
|
|
@@ -118,10 +124,34 @@ function createPagesHandler(options) {
|
|
|
118
124
|
if (!reloadEmitter) return;
|
|
119
125
|
reloadEmitter.emit('update', event);
|
|
120
126
|
}
|
|
127
|
+
async function renderRuntimeError(err, res) {
|
|
128
|
+
if (manifest.errorBundle && !res.headersSent) {
|
|
129
|
+
const errorMessage = devMode
|
|
130
|
+
? err instanceof Error
|
|
131
|
+
? err.stack || err.message
|
|
132
|
+
: String(err)
|
|
133
|
+
: 'An unexpected error occurred';
|
|
134
|
+
await renderErrorPage(
|
|
135
|
+
manifest.errorBundle,
|
|
136
|
+
500,
|
|
137
|
+
{ error: errorMessage },
|
|
138
|
+
outDir,
|
|
139
|
+
res,
|
|
140
|
+
componentCache,
|
|
141
|
+
manifest,
|
|
142
|
+
cacheVersion,
|
|
143
|
+
projectReact,
|
|
144
|
+
);
|
|
145
|
+
} else if (!res.headersSent) {
|
|
146
|
+
res.writeHead(500, getHtmlHeaders());
|
|
147
|
+
res.end('<h1>500 - Internal Server Error</h1>');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
121
150
|
async function handle(req, res) {
|
|
122
151
|
const method = req.method ?? 'GET';
|
|
123
152
|
const url = req.url ?? '/';
|
|
124
|
-
const
|
|
153
|
+
const requestUrl = new URL(url, 'http://arcway.internal');
|
|
154
|
+
const pathname = requestUrl.pathname;
|
|
125
155
|
if (method !== 'GET') return false;
|
|
126
156
|
// Internal `/_arcway/*` endpoints (dev only) resolve before the route
|
|
127
157
|
// matcher so they can never be shadowed by a user-defined page.
|
|
@@ -150,20 +180,32 @@ function createPagesHandler(options) {
|
|
|
150
180
|
// last request before we resolve the route.
|
|
151
181
|
if (!lazyContext) reload();
|
|
152
182
|
refreshFromLazy();
|
|
153
|
-
|
|
183
|
+
let requestContext;
|
|
184
|
+
let rewritten;
|
|
185
|
+
try {
|
|
186
|
+
requestContext = await createPageRequestContext({
|
|
187
|
+
req,
|
|
188
|
+
pathname,
|
|
189
|
+
internalPathname: pathname,
|
|
190
|
+
params: {},
|
|
191
|
+
requestUrl,
|
|
192
|
+
sessionConfig,
|
|
193
|
+
appContext,
|
|
194
|
+
});
|
|
195
|
+
rewritten = await resolvePageRewrite(rewrite, requestContext);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
await renderRuntimeError(err, res);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (rewritten.notFound) {
|
|
201
|
+
await renderNotFound(pathname);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
const internalPathname = rewritten.pathname;
|
|
205
|
+
let matched = matchPageRoute(routes, internalPathname);
|
|
154
206
|
if (!matched) {
|
|
155
|
-
if (manifest.notFoundBundle) {
|
|
156
|
-
await
|
|
157
|
-
manifest.notFoundBundle,
|
|
158
|
-
404,
|
|
159
|
-
{ pathname },
|
|
160
|
-
outDir,
|
|
161
|
-
res,
|
|
162
|
-
componentCache,
|
|
163
|
-
manifest,
|
|
164
|
-
cacheVersion,
|
|
165
|
-
projectReact,
|
|
166
|
-
);
|
|
207
|
+
if (rewrite || manifest.notFoundBundle) {
|
|
208
|
+
await renderNotFound(pathname);
|
|
167
209
|
return true;
|
|
168
210
|
}
|
|
169
211
|
return false;
|
|
@@ -187,20 +229,19 @@ function createPagesHandler(options) {
|
|
|
187
229
|
return true;
|
|
188
230
|
}
|
|
189
231
|
refreshFromLazy();
|
|
190
|
-
const rematched = matchPageRoute(routes,
|
|
232
|
+
const rematched = matchPageRoute(routes, internalPathname);
|
|
191
233
|
if (rematched) matched.route = rematched.route;
|
|
192
234
|
}
|
|
235
|
+
requestContext.page.internalPathname = internalPathname;
|
|
236
|
+
requestContext.page.params = matched.params;
|
|
237
|
+
requestContext.page.query = matched.params;
|
|
193
238
|
if (matched.route.middlewareServerBundles.length > 0) {
|
|
194
239
|
const middlewareResult = await runPageMiddleware(
|
|
195
240
|
matched.route,
|
|
196
|
-
matched.params,
|
|
197
|
-
pathname,
|
|
198
|
-
req,
|
|
199
241
|
outDir,
|
|
200
242
|
componentCache,
|
|
201
243
|
cacheVersion,
|
|
202
|
-
|
|
203
|
-
appContext,
|
|
244
|
+
requestContext,
|
|
204
245
|
);
|
|
205
246
|
if (middlewareResult) {
|
|
206
247
|
if (middlewareResult.redirect) {
|
|
@@ -223,6 +264,37 @@ function createPagesHandler(options) {
|
|
|
223
264
|
return true;
|
|
224
265
|
}
|
|
225
266
|
}
|
|
267
|
+
let loaderResult;
|
|
268
|
+
try {
|
|
269
|
+
loaderResult = await runPageLoader(
|
|
270
|
+
matched.route,
|
|
271
|
+
matched.params,
|
|
272
|
+
outDir,
|
|
273
|
+
componentCache,
|
|
274
|
+
cacheVersion,
|
|
275
|
+
requestContext,
|
|
276
|
+
);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
await renderRuntimeError(err, res);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (loaderResult?.redirect) {
|
|
282
|
+
res.writeHead(loaderResult.status ?? 302, {
|
|
283
|
+
Location: loaderResult.redirect,
|
|
284
|
+
...loaderResult.headers,
|
|
285
|
+
});
|
|
286
|
+
res.end();
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
if (loaderResult?.notFound) {
|
|
290
|
+
await renderNotFound(pathname);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
const pageProps = {
|
|
294
|
+
...matched.params,
|
|
295
|
+
...(loaderResult?.props ?? {}),
|
|
296
|
+
...(loaderResult?.metadata === undefined ? {} : { metadata: loaderResult.metadata }),
|
|
297
|
+
};
|
|
226
298
|
try {
|
|
227
299
|
await renderPage(
|
|
228
300
|
matched.route,
|
|
@@ -237,18 +309,24 @@ function createPagesHandler(options) {
|
|
|
237
309
|
projectReact,
|
|
238
310
|
devMode,
|
|
239
311
|
viteDev,
|
|
312
|
+
(err) => renderRuntimeError(err, res),
|
|
313
|
+
pageProps,
|
|
314
|
+
{
|
|
315
|
+
status: loaderResult?.status,
|
|
316
|
+
headers: loaderResult?.headers,
|
|
317
|
+
},
|
|
240
318
|
);
|
|
241
319
|
} catch (err) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
320
|
+
await renderRuntimeError(err, res);
|
|
321
|
+
}
|
|
322
|
+
return true;
|
|
323
|
+
|
|
324
|
+
async function renderNotFound(originalPathname) {
|
|
325
|
+
if (manifest.notFoundBundle) {
|
|
248
326
|
await renderErrorPage(
|
|
249
|
-
manifest.
|
|
250
|
-
|
|
251
|
-
{
|
|
327
|
+
manifest.notFoundBundle,
|
|
328
|
+
404,
|
|
329
|
+
{ pathname: originalPathname },
|
|
252
330
|
outDir,
|
|
253
331
|
res,
|
|
254
332
|
componentCache,
|
|
@@ -256,12 +334,11 @@ function createPagesHandler(options) {
|
|
|
256
334
|
cacheVersion,
|
|
257
335
|
projectReact,
|
|
258
336
|
);
|
|
259
|
-
} else
|
|
260
|
-
res.writeHead(
|
|
261
|
-
res.end('<h1>
|
|
337
|
+
} else {
|
|
338
|
+
res.writeHead(404, getHtmlHeaders());
|
|
339
|
+
res.end('<h1>404 - Not Found</h1>');
|
|
262
340
|
}
|
|
263
341
|
}
|
|
264
|
-
return true;
|
|
265
342
|
}
|
|
266
343
|
return {
|
|
267
344
|
handle,
|
|
@@ -325,6 +402,7 @@ function compileRoutes(manifest, { rootDir, outDir, viteDev } = {}) {
|
|
|
325
402
|
middlewareServerBundles: entry.middlewareServerBundles ?? [],
|
|
326
403
|
sharedChunks: entry.sharedChunks ?? [],
|
|
327
404
|
sharedCssChunks: entry.sharedCssChunks ?? [],
|
|
405
|
+
hasLoader: entry.hasLoader === true,
|
|
328
406
|
};
|
|
329
407
|
if (viteDev) {
|
|
330
408
|
route = buildViteRoute(route, { rootDir, outDir });
|
|
@@ -339,28 +417,51 @@ function matchPageRoute(routes, pathname) {
|
|
|
339
417
|
if (!result) return null;
|
|
340
418
|
return { route: result.match, params: result.params };
|
|
341
419
|
}
|
|
342
|
-
async function runPageMiddleware(
|
|
343
|
-
route
|
|
344
|
-
|
|
345
|
-
|
|
420
|
+
async function runPageMiddleware(route, outDir, componentCache, cacheVersion, ctx) {
|
|
421
|
+
for (const bundlePath of route.middlewareServerBundles) {
|
|
422
|
+
const fullPath = path.join(outDir, bundlePath);
|
|
423
|
+
const middlewareFn = await loadComponent(
|
|
424
|
+
fullPath,
|
|
425
|
+
`middleware:${bundlePath}`,
|
|
426
|
+
componentCache,
|
|
427
|
+
cacheVersion,
|
|
428
|
+
);
|
|
429
|
+
if (!middlewareFn || typeof middlewareFn !== 'function') {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const result = await middlewareFn(ctx);
|
|
433
|
+
if (result) {
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function createPageRequestContext({
|
|
346
441
|
req,
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
442
|
+
pathname,
|
|
443
|
+
internalPathname,
|
|
444
|
+
params,
|
|
445
|
+
requestUrl,
|
|
350
446
|
sessionConfig,
|
|
351
447
|
appContext,
|
|
352
|
-
) {
|
|
448
|
+
}) {
|
|
353
449
|
const headers = flattenHeaders(req.headers);
|
|
354
450
|
const cookies = parseCookies(req);
|
|
355
451
|
const session = await resolveSession(cookies, sessionConfig);
|
|
356
452
|
const page = {
|
|
453
|
+
host: normalizeHost(headers.host),
|
|
357
454
|
pathname,
|
|
455
|
+
internalPathname,
|
|
456
|
+
params,
|
|
358
457
|
query: params,
|
|
458
|
+
search: requestUrl.search,
|
|
459
|
+
searchParams: requestUrl.searchParams,
|
|
359
460
|
headers,
|
|
360
461
|
cookies,
|
|
361
462
|
session,
|
|
362
463
|
};
|
|
363
|
-
|
|
464
|
+
return appContext
|
|
364
465
|
? {
|
|
365
466
|
page,
|
|
366
467
|
db: appContext.db,
|
|
@@ -373,34 +474,166 @@ async function runPageMiddleware(
|
|
|
373
474
|
meta: appContext.meta,
|
|
374
475
|
}
|
|
375
476
|
: { page };
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function normalizeHost(value) {
|
|
480
|
+
if (typeof value !== 'string' || !value || /[\s,/@?#]/.test(value)) return null;
|
|
481
|
+
const match = value.match(/^([^:]+?)(?::([0-9]+))?$/);
|
|
482
|
+
if (!match) return null;
|
|
483
|
+
const port = match[2] ? Number(match[2]) : null;
|
|
484
|
+
if (port !== null && (port < 1 || port > 65535)) return null;
|
|
485
|
+
const source = match[1].endsWith('.') ? match[1].slice(0, -1) : match[1];
|
|
486
|
+
const host = domainToASCII(source.toLowerCase());
|
|
487
|
+
if (
|
|
488
|
+
!host ||
|
|
489
|
+
host.length > 253 ||
|
|
490
|
+
!host
|
|
491
|
+
.split('.')
|
|
492
|
+
.every(
|
|
493
|
+
(label) =>
|
|
494
|
+
label.length > 0 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label),
|
|
495
|
+
)
|
|
496
|
+
) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
return host;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function resolvePageRewrite(rewrite, ctx) {
|
|
503
|
+
if (!rewrite) return { pathname: ctx.page.pathname };
|
|
504
|
+
const { reservedPrefix, handler } = rewrite;
|
|
505
|
+
if (
|
|
506
|
+
!ctx.page.host ||
|
|
507
|
+
ctx.page.pathname === reservedPrefix ||
|
|
508
|
+
ctx.page.pathname.startsWith(`${reservedPrefix}/`)
|
|
509
|
+
) {
|
|
510
|
+
return { notFound: true };
|
|
511
|
+
}
|
|
512
|
+
const result = await handler(ctx);
|
|
513
|
+
if (!result) return { pathname: ctx.page.pathname };
|
|
514
|
+
if (result.notFound === true) return { notFound: true };
|
|
515
|
+
if (
|
|
516
|
+
typeof result.pathname !== 'string' ||
|
|
517
|
+
(result.pathname !== reservedPrefix && !result.pathname.startsWith(`${reservedPrefix}/`)) ||
|
|
518
|
+
/[?#]/.test(result.pathname)
|
|
519
|
+
) {
|
|
520
|
+
throw new TypeError('Page rewrite must target its configured reservedPrefix');
|
|
521
|
+
}
|
|
522
|
+
return { pathname: result.pathname };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function runPageLoader(route, params, outDir, componentCache, cacheVersion, ctx) {
|
|
526
|
+
const bundlePath = path.join(outDir, route.serverBundle);
|
|
527
|
+
const pageModule = await loadPageModule(
|
|
528
|
+
bundlePath,
|
|
529
|
+
`page-module:${route.pattern}`,
|
|
530
|
+
componentCache,
|
|
531
|
+
cacheVersion,
|
|
532
|
+
);
|
|
533
|
+
if (pageModule.loader === undefined) return null;
|
|
534
|
+
if (typeof pageModule.loader !== 'function') {
|
|
535
|
+
throw new TypeError(`Page loader for ${route.pattern} must be a function`);
|
|
536
|
+
}
|
|
537
|
+
const result = await pageModule.loader(ctx);
|
|
538
|
+
if (result == null) return { props: {} };
|
|
539
|
+
if (typeof result !== 'object' || Array.isArray(result)) {
|
|
540
|
+
throw new TypeError(`Page loader for ${route.pattern} must return an object`);
|
|
541
|
+
}
|
|
542
|
+
if (result.redirect !== undefined) {
|
|
543
|
+
if (typeof result.redirect !== 'string' || !result.redirect) {
|
|
544
|
+
throw new TypeError('Page loader redirect must be a non-empty string');
|
|
386
545
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
546
|
+
if (
|
|
547
|
+
result.status !== undefined &&
|
|
548
|
+
(!Number.isInteger(result.status) || result.status < 300 || result.status > 399)
|
|
549
|
+
) {
|
|
550
|
+
throw new TypeError('Page loader redirect status must be an integer from 300 to 399');
|
|
390
551
|
}
|
|
552
|
+
validateLoaderHeaders(result.headers);
|
|
553
|
+
return result;
|
|
391
554
|
}
|
|
392
|
-
return
|
|
555
|
+
if (result.notFound === true) return result;
|
|
556
|
+
assertJsonSerializable(result.props ?? {}, 'loader props');
|
|
557
|
+
if (result.metadata !== undefined) {
|
|
558
|
+
assertJsonSerializable(result.metadata, 'loader metadata');
|
|
559
|
+
}
|
|
560
|
+
if (
|
|
561
|
+
result.status !== undefined &&
|
|
562
|
+
(!Number.isInteger(result.status) || result.status < 200 || result.status > 599)
|
|
563
|
+
) {
|
|
564
|
+
throw new TypeError('Page loader status must be an integer from 200 to 599');
|
|
565
|
+
}
|
|
566
|
+
validateLoaderHeaders(result.headers);
|
|
567
|
+
return result;
|
|
393
568
|
}
|
|
394
|
-
|
|
569
|
+
|
|
570
|
+
function validateLoaderHeaders(headers) {
|
|
571
|
+
if (headers === undefined) return;
|
|
572
|
+
if (!headers || typeof headers !== 'object' || Array.isArray(headers)) {
|
|
573
|
+
throw new TypeError('Page loader headers must be an object');
|
|
574
|
+
}
|
|
575
|
+
const forbidden = new Set([
|
|
576
|
+
'connection',
|
|
577
|
+
'content-length',
|
|
578
|
+
'keep-alive',
|
|
579
|
+
'proxy-authenticate',
|
|
580
|
+
'proxy-authorization',
|
|
581
|
+
'set-cookie',
|
|
582
|
+
'te',
|
|
583
|
+
'trailer',
|
|
584
|
+
'transfer-encoding',
|
|
585
|
+
'upgrade',
|
|
586
|
+
]);
|
|
587
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
588
|
+
if (forbidden.has(name.toLowerCase())) {
|
|
589
|
+
throw new TypeError(`Page loader may not set the ${name} header`);
|
|
590
|
+
}
|
|
591
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
592
|
+
throw new TypeError('Page loader header values must be strings or numbers');
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function assertJsonSerializable(value, label, seen = new Set()) {
|
|
598
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
|
|
599
|
+
if (typeof value === 'number' && Number.isFinite(value)) return;
|
|
600
|
+
if (typeof value !== 'object') {
|
|
601
|
+
throw new TypeError(`${label} must contain only JSON-serializable values`);
|
|
602
|
+
}
|
|
603
|
+
if (
|
|
604
|
+
seen.has(value) ||
|
|
605
|
+
(!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype)
|
|
606
|
+
) {
|
|
607
|
+
throw new TypeError(`${label} must contain only JSON-serializable values`);
|
|
608
|
+
}
|
|
609
|
+
seen.add(value);
|
|
610
|
+
for (const entry of Array.isArray(value) ? value : Object.values(value)) {
|
|
611
|
+
assertJsonSerializable(entry, label, seen);
|
|
612
|
+
}
|
|
613
|
+
seen.delete(value);
|
|
614
|
+
}
|
|
615
|
+
function buildClientManifestJson(manifest, { rootDir, viteDev, reservedPrefix } = {}) {
|
|
616
|
+
const clientVisibleManifest = reservedPrefix
|
|
617
|
+
? {
|
|
618
|
+
...manifest,
|
|
619
|
+
entries: manifest.entries.filter(
|
|
620
|
+
(entry) =>
|
|
621
|
+
entry.pattern !== reservedPrefix && !entry.pattern.startsWith(`${reservedPrefix}/`),
|
|
622
|
+
),
|
|
623
|
+
}
|
|
624
|
+
: manifest;
|
|
395
625
|
if (viteDev) {
|
|
396
|
-
return buildViteClientManifestJson(
|
|
626
|
+
return buildViteClientManifestJson(clientVisibleManifest, rootDir);
|
|
397
627
|
}
|
|
398
628
|
const clientManifest = {
|
|
399
|
-
cssBundle:
|
|
400
|
-
|
|
629
|
+
cssBundle: clientVisibleManifest.cssBundle
|
|
630
|
+
? `/static/${clientVisibleManifest.cssBundle}`
|
|
631
|
+
: null,
|
|
632
|
+
routes: clientVisibleManifest.entries.map((entry) => {
|
|
401
633
|
const route = {
|
|
402
634
|
pattern: entry.pattern,
|
|
403
635
|
paramNames: entry.paramNames,
|
|
636
|
+
...(entry.hasLoader ? { hasLoader: true } : {}),
|
|
404
637
|
clientBundle: `/static/${entry.navBundle}`,
|
|
405
638
|
layoutBundles: (entry.layoutClientBundles ?? []).map((b) => `/static/${b}`),
|
|
406
639
|
loadingBundles: (entry.loadingClientBundles ?? []).map((b) => `/static/${b}`),
|
|
@@ -381,6 +381,26 @@ async function createLazyPagesContext(options) {
|
|
|
381
381
|
return /^(components|hooks|client|packages)\//.test(rel);
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
function isSpecialPageSource(filePath) {
|
|
385
|
+
const absolutePath = path.resolve(filePath);
|
|
386
|
+
return [errorPages.errorPage, errorPages.notFoundPage]
|
|
387
|
+
.filter(Boolean)
|
|
388
|
+
.some((pagePath) => path.resolve(pagePath) === absolutePath);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function rebuildErrorPages() {
|
|
392
|
+
const errorBundles = await buildErrorPageBundles(errorPages, outDir, serverTarget, {
|
|
393
|
+
rootDir,
|
|
394
|
+
devMode,
|
|
395
|
+
minify,
|
|
396
|
+
limit,
|
|
397
|
+
});
|
|
398
|
+
if (errorBundles.error) manifest.errorBundle = errorBundles.error;
|
|
399
|
+
else delete manifest.errorBundle;
|
|
400
|
+
if (errorBundles.notFound) manifest.notFoundBundle = errorBundles.notFound;
|
|
401
|
+
else delete manifest.notFoundBundle;
|
|
402
|
+
}
|
|
403
|
+
|
|
384
404
|
function markAffectedStale(filePath) {
|
|
385
405
|
const affected = mapFileToAffected(filePath, manifest);
|
|
386
406
|
if (shouldInvalidateWholeTree(filePath, affected)) {
|
|
@@ -433,12 +453,21 @@ async function createLazyPagesContext(options) {
|
|
|
433
453
|
|
|
434
454
|
async function handleSourceChange(filePath) {
|
|
435
455
|
if (disposed) throw new Error('Lazy pages context is disposed');
|
|
456
|
+
const specialPageSource = isSpecialPageSource(filePath);
|
|
457
|
+
const rebuildSpecialPages =
|
|
458
|
+
specialPageSource ||
|
|
459
|
+
shouldInvalidateWholeTree(filePath, { pages: [], layouts: [], middlewares: [] });
|
|
436
460
|
const { touched, affected } = markAffectedStale(filePath);
|
|
437
|
-
if (!touched) return { touched: false, affected, hmr: null };
|
|
461
|
+
if (!touched && !rebuildSpecialPages) return { touched: false, affected, hmr: null };
|
|
438
462
|
if (viteDev) {
|
|
463
|
+
if (rebuildSpecialPages) await rebuildErrorPages();
|
|
439
464
|
bumpVersion();
|
|
440
465
|
emit('update', { type: 'invalidate', filePath, affected, version: manifest.version });
|
|
441
|
-
|
|
466
|
+
const hmr = specialPageSource ? { type: 'reload', timestamp: Date.now() } : null;
|
|
467
|
+
if (hmr) {
|
|
468
|
+
emit('update', { ...hmr, filePath, reason: 'special-page', version: manifest.version });
|
|
469
|
+
}
|
|
470
|
+
return { touched: true, affected, hmr };
|
|
442
471
|
}
|
|
443
472
|
if (inFlightClientRefresh) return inFlightClientRefresh;
|
|
444
473
|
|
|
@@ -449,6 +478,7 @@ async function createLazyPagesContext(options) {
|
|
|
449
478
|
const [clientResult, cssBundle] = await Promise.all([
|
|
450
479
|
clientCtx?.rebuild() ?? Promise.resolve(null),
|
|
451
480
|
buildCssBundle(pagesDir, outDir, minify, stylesPath, fontFaceCss, rootDir),
|
|
481
|
+
rebuildSpecialPages ? rebuildErrorPages() : Promise.resolve(),
|
|
452
482
|
]);
|
|
453
483
|
|
|
454
484
|
if (clientResult) {
|
|
@@ -459,8 +489,15 @@ async function createLazyPagesContext(options) {
|
|
|
459
489
|
bumpVersion();
|
|
460
490
|
emit('update', { type: 'invalidate', filePath, affected, version: manifest.version });
|
|
461
491
|
|
|
462
|
-
let hmr = null;
|
|
463
|
-
if (
|
|
492
|
+
let hmr = specialPageSource ? { type: 'reload', timestamp } : null;
|
|
493
|
+
if (hmr) {
|
|
494
|
+
emit('update', {
|
|
495
|
+
...hmr,
|
|
496
|
+
filePath,
|
|
497
|
+
reason: 'special-page',
|
|
498
|
+
version: manifest.version,
|
|
499
|
+
});
|
|
500
|
+
} else if (clientResult && previousMetafile) {
|
|
464
501
|
const diff = diffClientMetafiles(previousMetafile, clientResult.metafile, outDir);
|
|
465
502
|
const modules = [...diff.changedHydrationBundles, ...diff.changedNavBundles];
|
|
466
503
|
const routeBundles = {};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { parseAsync, transformAsync } from '@babel/core';
|
|
4
|
+
|
|
5
|
+
function exportedName(specifier) {
|
|
6
|
+
if (specifier?.exported?.type === 'Identifier') return specifier.exported.name;
|
|
7
|
+
if (specifier?.exported?.type === 'StringLiteral') return specifier.exported.value;
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function hasServerLoaderExport(code, filename) {
|
|
12
|
+
if (!code.includes('loader') && !/\bexport\s*\*/.test(code)) return false;
|
|
13
|
+
const ast = await parseAsync(code, {
|
|
14
|
+
filename,
|
|
15
|
+
babelrc: false,
|
|
16
|
+
configFile: false,
|
|
17
|
+
parserOpts: { plugins: ['jsx', 'typescript'] },
|
|
18
|
+
});
|
|
19
|
+
return ast.program.body.some((node) => {
|
|
20
|
+
// A star re-export can acquire a loader through any depth of barrel module.
|
|
21
|
+
// Treat it conservatively so client navigation can never bypass a loader
|
|
22
|
+
// that is only visible after module resolution.
|
|
23
|
+
if (node.type === 'ExportAllDeclaration') return true;
|
|
24
|
+
if (node.type !== 'ExportNamedDeclaration') return false;
|
|
25
|
+
if (node.declaration?.type === 'FunctionDeclaration') {
|
|
26
|
+
return node.declaration.id?.name === 'loader';
|
|
27
|
+
}
|
|
28
|
+
if (node.declaration?.type === 'VariableDeclaration') {
|
|
29
|
+
return node.declaration.declarations.some(
|
|
30
|
+
(item) => item.id.type === 'Identifier' && item.id.name === 'loader',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return node.specifiers.some((specifier) => exportedName(specifier) === 'loader');
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rememberDependencies(targetPath, state) {
|
|
38
|
+
const dependencies = state.file.get('arcwayLoaderDependencies') ?? new Set();
|
|
39
|
+
targetPath.traverse({
|
|
40
|
+
ReferencedIdentifier(identifierPath) {
|
|
41
|
+
const binding = identifierPath.scope.getBinding(identifierPath.node.name);
|
|
42
|
+
if (binding?.scope.path.isProgram()) dependencies.add(identifierPath.node.name);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
state.file.set('arcwayLoaderDependencies', dependencies);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function removeBinding(binding, state) {
|
|
49
|
+
if (!binding) return;
|
|
50
|
+
const bindingPath = binding.path;
|
|
51
|
+
rememberDependencies(bindingPath, state);
|
|
52
|
+
if (bindingPath.isFunctionDeclaration() || bindingPath.isClassDeclaration()) {
|
|
53
|
+
bindingPath.remove();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (bindingPath.isVariableDeclarator()) {
|
|
57
|
+
const declarationPath = bindingPath.parentPath;
|
|
58
|
+
bindingPath.remove();
|
|
59
|
+
if (declarationPath.node?.declarations.length === 0) declarationPath.remove();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function stripLoaderExportPlugin() {
|
|
64
|
+
return {
|
|
65
|
+
visitor: {
|
|
66
|
+
ExportAllDeclaration(exportPath) {
|
|
67
|
+
// Page named exports have no browser runtime contract. Removing a star
|
|
68
|
+
// export is the only safe client transform because its export names can
|
|
69
|
+
// change through transitive barrels without changing this page file.
|
|
70
|
+
exportPath.remove();
|
|
71
|
+
},
|
|
72
|
+
ExportNamedDeclaration(exportPath, state) {
|
|
73
|
+
const declaration = exportPath.node.declaration;
|
|
74
|
+
if (declaration?.type === 'FunctionDeclaration' && declaration.id?.name === 'loader') {
|
|
75
|
+
rememberDependencies(exportPath.get('declaration'), state);
|
|
76
|
+
exportPath.remove();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (declaration?.type === 'VariableDeclaration') {
|
|
80
|
+
for (const declaratorPath of exportPath.get('declaration.declarations')) {
|
|
81
|
+
if (
|
|
82
|
+
declaratorPath.node.id.type === 'Identifier' &&
|
|
83
|
+
declaratorPath.node.id.name === 'loader'
|
|
84
|
+
) {
|
|
85
|
+
rememberDependencies(declaratorPath, state);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
declaration.declarations = declaration.declarations.filter(
|
|
89
|
+
(item) => item.id.type !== 'Identifier' || item.id.name !== 'loader',
|
|
90
|
+
);
|
|
91
|
+
if (declaration.declarations.length === 0) exportPath.remove();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
exportPath.node.specifiers = exportPath.node.specifiers.filter((specifier) => {
|
|
95
|
+
if (exportedName(specifier) !== 'loader') return true;
|
|
96
|
+
if (!exportPath.node.source && specifier.local?.name) {
|
|
97
|
+
const names = state.file.get('arcwayLoaderBindings') ?? [];
|
|
98
|
+
names.push(specifier.local.name);
|
|
99
|
+
state.file.set('arcwayLoaderBindings', names);
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
});
|
|
103
|
+
if (exportPath.node.specifiers.length === 0 && !declaration) exportPath.remove();
|
|
104
|
+
},
|
|
105
|
+
Program: {
|
|
106
|
+
exit(programPath, state) {
|
|
107
|
+
for (const name of state.file.get('arcwayLoaderBindings') ?? []) {
|
|
108
|
+
const binding = programPath.scope.getBinding(name);
|
|
109
|
+
removeBinding(binding, state);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pending = state.file.get('arcwayLoaderDependencies') ?? new Set();
|
|
113
|
+
const inspected = new Set();
|
|
114
|
+
while (pending.size > 0) {
|
|
115
|
+
programPath.scope.crawl();
|
|
116
|
+
const name = pending.values().next().value;
|
|
117
|
+
pending.delete(name);
|
|
118
|
+
if (inspected.has(name)) continue;
|
|
119
|
+
inspected.add(name);
|
|
120
|
+
const binding = programPath.scope.getBinding(name);
|
|
121
|
+
if (binding && !binding.referenced) removeBinding(binding, state);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
programPath.scope.crawl();
|
|
125
|
+
for (const importPath of programPath.get('body')) {
|
|
126
|
+
if (!importPath.isImportDeclaration()) continue;
|
|
127
|
+
const hadSpecifiers = importPath.node.specifiers.length > 0;
|
|
128
|
+
for (const specifierPath of importPath.get('specifiers')) {
|
|
129
|
+
const binding = programPath.scope.getBinding(specifierPath.node.local.name);
|
|
130
|
+
if (!binding?.referenced) specifierPath.remove();
|
|
131
|
+
}
|
|
132
|
+
if (hadSpecifiers && importPath.node.specifiers.length === 0) importPath.remove();
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function stripServerLoaderExport(code, filename) {
|
|
141
|
+
if (!code.includes('loader') && !/\bexport\s*\*/.test(code)) return code;
|
|
142
|
+
const result = await transformAsync(code, {
|
|
143
|
+
filename,
|
|
144
|
+
babelrc: false,
|
|
145
|
+
configFile: false,
|
|
146
|
+
sourceMaps: false,
|
|
147
|
+
parserOpts: {
|
|
148
|
+
plugins: ['jsx', 'typescript'],
|
|
149
|
+
},
|
|
150
|
+
plugins: [stripLoaderExportPlugin],
|
|
151
|
+
});
|
|
152
|
+
return result?.code ?? code;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function serverLoaderClientPlugin(pagePaths) {
|
|
156
|
+
const pages = new Set(pagePaths.map((filePath) => path.resolve(filePath)));
|
|
157
|
+
return {
|
|
158
|
+
name: 'arcway-server-page-loader',
|
|
159
|
+
setup(build) {
|
|
160
|
+
build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async (args) => {
|
|
161
|
+
if (!pages.has(path.resolve(args.path))) return null;
|
|
162
|
+
const source = await fs.readFile(args.path, 'utf8');
|
|
163
|
+
const contents = await stripServerLoaderExport(source, args.path);
|
|
164
|
+
const extension = path.extname(args.path).toLowerCase();
|
|
165
|
+
const loader = extension === '.ts' ? 'ts' : extension === '.tsx' ? 'tsx' : 'jsx';
|
|
166
|
+
return { contents, loader };
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function serverLoaderVitePlugin(pagesRoot) {
|
|
173
|
+
const pagesDir = `${path.resolve(pagesRoot)}${path.sep}`;
|
|
174
|
+
return {
|
|
175
|
+
name: 'arcway-server-page-loader',
|
|
176
|
+
enforce: 'pre',
|
|
177
|
+
async transform(code, id) {
|
|
178
|
+
const filename = id.split('?')[0];
|
|
179
|
+
if (!filename.startsWith(pagesDir) || !/\.[cm]?[jt]sx?$/.test(filename)) return null;
|
|
180
|
+
const transformed = await stripServerLoaderExport(code, filename);
|
|
181
|
+
return transformed === code ? null : { code: transformed, map: null };
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export {
|
|
187
|
+
hasServerLoaderExport,
|
|
188
|
+
serverLoaderClientPlugin,
|
|
189
|
+
serverLoaderVitePlugin,
|
|
190
|
+
stripServerLoaderExport,
|
|
191
|
+
};
|
package/server/pages/ssr.js
CHANGED
|
@@ -19,10 +19,7 @@ function syncReactInternals(projectReactModule) {
|
|
|
19
19
|
const projectInternals = projectReactModule[REACT_INTERNALS_KEY];
|
|
20
20
|
const fwInternals = fwReact[REACT_INTERNALS_KEY];
|
|
21
21
|
if (projectInternals && fwInternals && projectInternals !== fwInternals) {
|
|
22
|
-
const keys = new Set([
|
|
23
|
-
...Object.keys(projectInternals),
|
|
24
|
-
...Object.keys(fwInternals),
|
|
25
|
-
]);
|
|
22
|
+
const keys = new Set([...Object.keys(projectInternals), ...Object.keys(fwInternals)]);
|
|
26
23
|
for (const key of keys) {
|
|
27
24
|
Object.defineProperty(fwInternals, key, {
|
|
28
25
|
get() {
|
|
@@ -51,11 +48,24 @@ async function loadComponent(bundlePath, cacheKey, componentCache, cacheVersion)
|
|
|
51
48
|
componentCache.set(cacheKey, Component);
|
|
52
49
|
return Component;
|
|
53
50
|
} catch (error) {
|
|
54
|
-
console.error(
|
|
51
|
+
console.error(
|
|
52
|
+
`[arcway] failed to load component ${cacheKey} from ${bundlePath}:`,
|
|
53
|
+
error && error.stack ? error.stack : error,
|
|
54
|
+
);
|
|
55
55
|
// Component failed to load (missing file, syntax error) — caller handles null
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
async function loadPageModule(bundlePath, cacheKey, componentCache, cacheVersion) {
|
|
60
|
+
if (componentCache.has(cacheKey)) {
|
|
61
|
+
return componentCache.get(cacheKey);
|
|
62
|
+
}
|
|
63
|
+
const fileUrl = pathToFileURL(bundlePath).href;
|
|
64
|
+
const specifier = cacheVersion > 0 ? `${fileUrl}?v=${cacheVersion}` : fileUrl;
|
|
65
|
+
const mod = await import(specifier);
|
|
66
|
+
componentCache.set(cacheKey, mod);
|
|
67
|
+
return mod;
|
|
68
|
+
}
|
|
59
69
|
const ROUTER_CTX_KEY = '__router_context__';
|
|
60
70
|
const PROVIDER_CTX_KEY = '__provider_context__';
|
|
61
71
|
function wrapWithProviders(createElement, element, pathname, params) {
|
|
@@ -152,15 +162,23 @@ async function renderPage(
|
|
|
152
162
|
react,
|
|
153
163
|
devMode,
|
|
154
164
|
viteDev = false,
|
|
165
|
+
onRenderFailure = null,
|
|
166
|
+
pageProps = params,
|
|
167
|
+
response = {},
|
|
155
168
|
) {
|
|
156
169
|
const { createElement, renderToPipeableStream } = react;
|
|
157
170
|
const bundlePath = path.join(outDir, route.serverBundle);
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
const legacyCachedComponent = componentCache.get(`page:${route.pattern}`);
|
|
172
|
+
const Component = legacyCachedComponent
|
|
173
|
+
? legacyCachedComponent
|
|
174
|
+
: (
|
|
175
|
+
await loadPageModule(
|
|
176
|
+
bundlePath,
|
|
177
|
+
`page-module:${route.pattern}`,
|
|
178
|
+
componentCache,
|
|
179
|
+
cacheVersion,
|
|
180
|
+
)
|
|
181
|
+
).default;
|
|
164
182
|
if (!Component || typeof Component !== 'function') {
|
|
165
183
|
res.writeHead(500, getHtmlHeaders());
|
|
166
184
|
res.end('<h1>500 - Page component is not a valid React component</h1>');
|
|
@@ -187,21 +205,32 @@ async function renderPage(
|
|
|
187
205
|
const envScriptTag = buildEnvScriptTag(collectPublicEnv());
|
|
188
206
|
const hmrTag = devMode && !viteDev ? buildHmrScript() : '';
|
|
189
207
|
const liveReloadTag = devMode && !viteDev && !hmrTag ? buildLiveReloadScript() : '';
|
|
190
|
-
const propsJson = JSON.stringify(
|
|
208
|
+
const propsJson = JSON.stringify(pageProps).replace(/</g, '\\u003c');
|
|
209
|
+
const routeParamsJson = JSON.stringify(params).replace(/</g, '\\u003c');
|
|
191
210
|
const headData = { meta: [], links: [] };
|
|
192
211
|
setSSRHeadData(headData);
|
|
193
|
-
let element = createElement(Component,
|
|
212
|
+
let element = createElement(Component, pageProps);
|
|
194
213
|
for (let i = layoutComponents.length - 1; i >= 0; i--) {
|
|
195
|
-
element = createElement(layoutComponents[i],
|
|
214
|
+
element = createElement(layoutComponents[i], pageProps, element);
|
|
196
215
|
}
|
|
197
216
|
element = wrapWithProviders(createElement, element, pathname, params);
|
|
198
217
|
const { PassThrough } = await import('node:stream');
|
|
199
218
|
let pass = null;
|
|
200
219
|
let closed = false;
|
|
220
|
+
let renderFailureHandled = false;
|
|
221
|
+
let resolveRender;
|
|
222
|
+
const renderComplete = new Promise((resolve) => {
|
|
223
|
+
resolveRender = resolve;
|
|
224
|
+
});
|
|
201
225
|
const { pipe, abort } = renderToPipeableStream(element, {
|
|
202
226
|
onAllReady() {
|
|
203
|
-
|
|
204
|
-
|
|
227
|
+
// React may call onAllReady after onShellError but before the recovery
|
|
228
|
+
// promise starts. Never let the failed stream win that race and commit 200.
|
|
229
|
+
if (res.headersSent || closed || renderFailureHandled) return;
|
|
230
|
+
res.writeHead(response.status ?? 200, {
|
|
231
|
+
...getHtmlHeaders(),
|
|
232
|
+
...response.headers,
|
|
233
|
+
});
|
|
205
234
|
const headHtml = renderHeadToString(headData);
|
|
206
235
|
const shell = buildHtmlShell({
|
|
207
236
|
headHtml,
|
|
@@ -209,6 +238,7 @@ async function renderPage(
|
|
|
209
238
|
cssLinkTag,
|
|
210
239
|
bodysuffix: `${envScriptTag}
|
|
211
240
|
<script id="__app_props" type="application/json">${propsJson}</script>
|
|
241
|
+
<script id="__app_route_params" type="application/json">${routeParamsJson}</script>
|
|
212
242
|
<script id="__app_manifest" type="application/json">${clientManifestJson}</script>
|
|
213
243
|
${hmrTag}
|
|
214
244
|
${scriptTags}
|
|
@@ -225,19 +255,38 @@ ${liveReloadTag}`,
|
|
|
225
255
|
res.end();
|
|
226
256
|
}
|
|
227
257
|
clearSSRHeadData();
|
|
258
|
+
resolveRender();
|
|
228
259
|
});
|
|
229
260
|
pipe(pass);
|
|
230
261
|
},
|
|
231
262
|
onError(err) {
|
|
232
|
-
clearSSRHeadData();
|
|
233
263
|
// A client disconnect aborts the render and surfaces here; that's a normal
|
|
234
264
|
// peer disconnect, not a server fault — don't log it or write to a dead res.
|
|
235
265
|
if (closed) return;
|
|
236
266
|
console.error('SSR render error:', err);
|
|
237
|
-
|
|
267
|
+
},
|
|
268
|
+
onShellError(err) {
|
|
269
|
+
clearSSRHeadData();
|
|
270
|
+
if (closed || res.headersSent || renderFailureHandled) return;
|
|
271
|
+
renderFailureHandled = true;
|
|
272
|
+
|
|
273
|
+
if (!onRenderFailure) {
|
|
238
274
|
res.writeHead(500, getHtmlHeaders());
|
|
239
275
|
res.end('<h1>500 - Render Error</h1>');
|
|
276
|
+
resolveRender();
|
|
277
|
+
return;
|
|
240
278
|
}
|
|
279
|
+
|
|
280
|
+
Promise.resolve()
|
|
281
|
+
.then(() => onRenderFailure(err))
|
|
282
|
+
.catch((renderError) => {
|
|
283
|
+
console.error('SSR error-page render error:', renderError);
|
|
284
|
+
if (!closed && !res.headersSent) {
|
|
285
|
+
res.writeHead(500, getHtmlHeaders());
|
|
286
|
+
res.end('<h1>500 - Internal Server Error</h1>');
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
.finally(resolveRender);
|
|
241
290
|
},
|
|
242
291
|
});
|
|
243
292
|
// Client gone (mid-stream, or before the shell was ready): stop React from
|
|
@@ -247,7 +296,9 @@ ${liveReloadTag}`,
|
|
|
247
296
|
closed = true;
|
|
248
297
|
abort();
|
|
249
298
|
if (pass) pass.destroy();
|
|
299
|
+
resolveRender();
|
|
250
300
|
});
|
|
301
|
+
await renderComplete;
|
|
251
302
|
}
|
|
252
303
|
async function renderErrorPage(
|
|
253
304
|
bundlePath,
|
|
@@ -289,6 +340,10 @@ async function renderErrorPage(
|
|
|
289
340
|
const { PassThrough } = await import('node:stream');
|
|
290
341
|
let pass = null;
|
|
291
342
|
let closed = false;
|
|
343
|
+
let resolveRender;
|
|
344
|
+
const renderComplete = new Promise((resolve) => {
|
|
345
|
+
resolveRender = resolve;
|
|
346
|
+
});
|
|
292
347
|
const { pipe, abort } = renderToPipeableStream(element, {
|
|
293
348
|
onAllReady() {
|
|
294
349
|
if (res.headersSent || closed) return;
|
|
@@ -306,6 +361,7 @@ async function renderErrorPage(
|
|
|
306
361
|
res.end();
|
|
307
362
|
}
|
|
308
363
|
clearSSRHeadData();
|
|
364
|
+
resolveRender();
|
|
309
365
|
});
|
|
310
366
|
pipe(pass);
|
|
311
367
|
},
|
|
@@ -319,6 +375,7 @@ async function renderErrorPage(
|
|
|
319
375
|
`<h1>${statusCode} - ${statusCode === 404 ? 'Not Found' : 'Internal Server Error'}</h1>`,
|
|
320
376
|
);
|
|
321
377
|
}
|
|
378
|
+
resolveRender();
|
|
322
379
|
},
|
|
323
380
|
});
|
|
324
381
|
res.on('close', () => {
|
|
@@ -326,7 +383,9 @@ async function renderErrorPage(
|
|
|
326
383
|
closed = true;
|
|
327
384
|
abort();
|
|
328
385
|
if (pass) pass.destroy();
|
|
386
|
+
resolveRender();
|
|
329
387
|
});
|
|
388
|
+
await renderComplete;
|
|
330
389
|
}
|
|
331
390
|
export {
|
|
332
391
|
buildCssLinkTag,
|
|
@@ -335,6 +394,7 @@ export {
|
|
|
335
394
|
buildRouteCssLinkTags,
|
|
336
395
|
buildScriptTags,
|
|
337
396
|
loadComponent,
|
|
397
|
+
loadPageModule,
|
|
338
398
|
renderErrorPage,
|
|
339
399
|
renderPage,
|
|
340
400
|
syncReactInternals,
|
package/server/pages/vite-dev.js
CHANGED
|
@@ -6,6 +6,7 @@ import tailwindcss from '@tailwindcss/vite';
|
|
|
6
6
|
import { createServer as createViteServer } from 'vite';
|
|
7
7
|
import { patternToFileName } from './build-server.js';
|
|
8
8
|
import { closeServer, createHttpServer, listen } from '../server.js';
|
|
9
|
+
import { serverLoaderVitePlugin } from './server-loader-transform.js';
|
|
9
10
|
|
|
10
11
|
function toPosixPath(value) {
|
|
11
12
|
return value.replace(/\\/g, '/');
|
|
@@ -72,9 +73,11 @@ function buildViteHydrationEntry({
|
|
|
72
73
|
const container = document.getElementById('__app');
|
|
73
74
|
const propsEl = document.getElementById('__app_props');
|
|
74
75
|
const props = propsEl ? JSON.parse(propsEl.textContent || '{}') : {};
|
|
76
|
+
const routeParamsEl = document.getElementById('__app_route_params');
|
|
77
|
+
const routeParams = routeParamsEl ? JSON.parse(routeParamsEl.textContent || '{}') : props;
|
|
75
78
|
const ROOT_KEY = '__arcway_root__';
|
|
76
79
|
const rootOwner = window;
|
|
77
|
-
const element = <ApiProvider><Router initialPath={window.location.pathname} initialParams={props} initialPattern={${JSON.stringify(pattern)}} initialComponent={Component} initialLayouts={${layoutsArray}} initialLoadings={${loadingsArray}} /></ApiProvider>;
|
|
80
|
+
const element = <ApiProvider><Router initialPath={window.location.pathname} initialParams={routeParams} initialPageProps={props} initialPattern={${JSON.stringify(pattern)}} initialComponent={Component} initialLayouts={${layoutsArray}} initialLoadings={${loadingsArray}} /></ApiProvider>;
|
|
78
81
|
|
|
79
82
|
if (!container) {
|
|
80
83
|
throw new Error('Arcway Vite hydrate entry could not find #__app');
|
|
@@ -153,6 +156,7 @@ function buildViteClientManifestJson(manifest, rootDir) {
|
|
|
153
156
|
const route = {
|
|
154
157
|
pattern: entry.pattern,
|
|
155
158
|
paramNames: entry.paramNames,
|
|
159
|
+
...(entry.hasLoader ? { hasLoader: true } : {}),
|
|
156
160
|
clientBundle: toViteModuleUrl(rootDir, entry.srcPath),
|
|
157
161
|
layoutBundles: entry.layoutDirs
|
|
158
162
|
.map((dirPath) => manifest.layouts.get(dirPath)?.srcPath)
|
|
@@ -187,14 +191,26 @@ function resolveAppAliases(rootDir) {
|
|
|
187
191
|
try {
|
|
188
192
|
const reactRoot = path.dirname(appRequire.resolve('react/package.json'));
|
|
189
193
|
aliases.push({ find: /^react$/, replacement: toPosixPath(path.join(reactRoot, 'index.js')) });
|
|
190
|
-
aliases.push({
|
|
191
|
-
|
|
194
|
+
aliases.push({
|
|
195
|
+
find: /^react\/jsx-runtime$/,
|
|
196
|
+
replacement: toPosixPath(path.join(reactRoot, 'jsx-runtime.js')),
|
|
197
|
+
});
|
|
198
|
+
aliases.push({
|
|
199
|
+
find: /^react\/jsx-dev-runtime$/,
|
|
200
|
+
replacement: toPosixPath(path.join(reactRoot, 'jsx-dev-runtime.js')),
|
|
201
|
+
});
|
|
192
202
|
} catch {}
|
|
193
203
|
|
|
194
204
|
try {
|
|
195
205
|
const reactDomRoot = path.dirname(appRequire.resolve('react-dom/package.json'));
|
|
196
|
-
aliases.push({
|
|
197
|
-
|
|
206
|
+
aliases.push({
|
|
207
|
+
find: /^react-dom$/,
|
|
208
|
+
replacement: toPosixPath(path.join(reactDomRoot, 'index.js')),
|
|
209
|
+
});
|
|
210
|
+
aliases.push({
|
|
211
|
+
find: /^react-dom\/client$/,
|
|
212
|
+
replacement: toPosixPath(path.join(reactDomRoot, 'client.js')),
|
|
213
|
+
});
|
|
198
214
|
} catch {}
|
|
199
215
|
|
|
200
216
|
try {
|
|
@@ -251,7 +267,11 @@ async function createViteDevRouter({ rootDir, log, config }) {
|
|
|
251
267
|
'@radix-ui/react-use-is-hydrated',
|
|
252
268
|
],
|
|
253
269
|
},
|
|
254
|
-
plugins: [
|
|
270
|
+
plugins: [
|
|
271
|
+
serverLoaderVitePlugin(config?.pages?.dir ?? path.resolve(rootDir, 'pages')),
|
|
272
|
+
reactPlugin(),
|
|
273
|
+
tailwindcss(),
|
|
274
|
+
],
|
|
255
275
|
clearScreen: false,
|
|
256
276
|
});
|
|
257
277
|
} catch (error) {
|