arcway 0.1.14 → 0.1.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/package.json +1 -1
- package/server/boot/index.js +5 -4
- package/server/pages/build-cache.js +71 -0
- package/server/pages/build-client.js +280 -84
- package/server/pages/build-context.js +172 -0
- package/server/pages/build-css.js +85 -9
- package/server/pages/build.js +15 -4
- package/server/pages/handler.js +7 -7
- package/server/pages/pages-router.js +7 -5
- package/server/router/api-router.js +5 -3
- package/server/session/index.js +2 -1
package/package.json
CHANGED
package/server/boot/index.js
CHANGED
|
@@ -59,8 +59,8 @@ async function boot(options) {
|
|
|
59
59
|
});
|
|
60
60
|
await jobRunner.init();
|
|
61
61
|
|
|
62
|
-
const fileWatcher = new FileWatcher(rootDir, { log });
|
|
63
|
-
await fileWatcher.start();
|
|
62
|
+
const fileWatcher = mode === 'development' ? new FileWatcher(rootDir, { log }) : null;
|
|
63
|
+
if (fileWatcher) await fileWatcher.start();
|
|
64
64
|
|
|
65
65
|
const appContext = { db, redis, events, queue, cache, files, mail, log, fileWatcher };
|
|
66
66
|
|
|
@@ -68,13 +68,14 @@ async function boot(options) {
|
|
|
68
68
|
|
|
69
69
|
const apiRouter = new ApiRouter(config.api, {
|
|
70
70
|
log,
|
|
71
|
+
mode,
|
|
71
72
|
fileWatcher,
|
|
72
73
|
appContext,
|
|
73
74
|
sessionConfig: config.session,
|
|
74
75
|
});
|
|
75
76
|
await apiRouter.init();
|
|
76
77
|
|
|
77
|
-
const pagesRouter = new PagesRouter(config, { rootDir, log, fileWatcher, appContext });
|
|
78
|
+
const pagesRouter = new PagesRouter(config, { rootDir, log, mode, fileWatcher, appContext });
|
|
78
79
|
await pagesRouter.init();
|
|
79
80
|
|
|
80
81
|
const healthDeps = {
|
|
@@ -117,7 +118,7 @@ async function boot(options) {
|
|
|
117
118
|
await pagesRouter.close();
|
|
118
119
|
await apiRouter.close();
|
|
119
120
|
await webServer.close();
|
|
120
|
-
await fileWatcher.close();
|
|
121
|
+
if (fileWatcher) await fileWatcher.close();
|
|
121
122
|
await destroyInfrastructure(infrastructure);
|
|
122
123
|
await mcpRouter.cleanup(rootDir);
|
|
123
124
|
};
|
|
@@ -194,10 +194,81 @@ async function buildWithCache({
|
|
|
194
194
|
return { cacheHit: false, metafile: result.metafile };
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// Multi-file cache for bundles with several output files (client build with
|
|
198
|
+
// code-splitting). Each cache entry lives under <bucket>/<coarseKey>/ and the
|
|
199
|
+
// index is a sidecar <bucket>/<coarseKey>.meta.json.
|
|
200
|
+
async function lookupMultiFileCache({ rootDir, kind, coarseKey }) {
|
|
201
|
+
const bucket = bucketDir(rootDir, kind);
|
|
202
|
+
const metaPath = path.join(bucket, `${coarseKey}.meta.json`);
|
|
203
|
+
const cacheDir = path.join(bucket, coarseKey);
|
|
204
|
+
const meta = await readJson(metaPath);
|
|
205
|
+
if (!meta) return { hit: false, bucket, cacheDir, metaPath };
|
|
206
|
+
const currentHash = await hashInputs(meta.inputs);
|
|
207
|
+
if (currentHash === null || currentHash !== meta.inputsHash) {
|
|
208
|
+
return { hit: false, bucket, cacheDir, metaPath };
|
|
209
|
+
}
|
|
210
|
+
// Sanity-check every recorded output file still exists in the cache dir.
|
|
211
|
+
for (const rel of meta.outputs) {
|
|
212
|
+
try {
|
|
213
|
+
await fs.access(path.join(cacheDir, rel));
|
|
214
|
+
} catch {
|
|
215
|
+
return { hit: false, bucket, cacheDir, metaPath };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { hit: true, bucket, cacheDir, metaPath, meta };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function restoreMultiFileCache({ cacheDir, outputs, destDir }) {
|
|
222
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
223
|
+
await Promise.all(
|
|
224
|
+
outputs.map(async (rel) => {
|
|
225
|
+
const src = path.join(cacheDir, rel);
|
|
226
|
+
const dst = path.join(destDir, rel);
|
|
227
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
228
|
+
await fs.cp(src, dst);
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function storeMultiFileCache({
|
|
234
|
+
bucket,
|
|
235
|
+
coarseKey,
|
|
236
|
+
destDir,
|
|
237
|
+
outputs,
|
|
238
|
+
inputs,
|
|
239
|
+
metadata,
|
|
240
|
+
}) {
|
|
241
|
+
const cacheDir = path.join(bucket, coarseKey);
|
|
242
|
+
await fs.rm(cacheDir, { recursive: true, force: true });
|
|
243
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
244
|
+
await Promise.all(
|
|
245
|
+
outputs.map(async (rel) => {
|
|
246
|
+
const src = path.join(destDir, rel);
|
|
247
|
+
const dst = path.join(cacheDir, rel);
|
|
248
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
249
|
+
await cpAtomic(src, dst);
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
const inputsHash = await hashInputs(inputs);
|
|
253
|
+
const metaPath = path.join(bucket, `${coarseKey}.meta.json`);
|
|
254
|
+
await writeJsonAtomic(metaPath, {
|
|
255
|
+
inputs,
|
|
256
|
+
inputsHash,
|
|
257
|
+
outputs,
|
|
258
|
+
metadata,
|
|
259
|
+
mtime: Date.now(),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
197
263
|
export {
|
|
198
264
|
buildWithCache,
|
|
199
265
|
computeConfigHash,
|
|
200
266
|
cacheRoot,
|
|
201
267
|
entryKey,
|
|
202
268
|
ESBUILD_VERSION,
|
|
269
|
+
sha256,
|
|
270
|
+
hashInputs,
|
|
271
|
+
lookupMultiFileCache,
|
|
272
|
+
restoreMultiFileCache,
|
|
273
|
+
storeMultiFileCache,
|
|
203
274
|
};
|
|
@@ -6,73 +6,137 @@ 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const navBundles = new Map();
|
|
23
|
-
const layoutNavBundles = new Map();
|
|
24
|
-
const loadingNavBundles = new Map();
|
|
9
|
+
import {
|
|
10
|
+
sha256,
|
|
11
|
+
ESBUILD_VERSION,
|
|
12
|
+
lookupMultiFileCache,
|
|
13
|
+
restoreMultiFileCache,
|
|
14
|
+
storeMultiFileCache,
|
|
15
|
+
} from './build-cache.js';
|
|
16
|
+
|
|
17
|
+
// Build a deterministic plan of the hydration entries that will be written
|
|
18
|
+
// into the staging dir. Computed up-front so we can derive a cache key
|
|
19
|
+
// before running esbuild.
|
|
20
|
+
function planHydrationEntries(pages, layouts, loadings, tempDir) {
|
|
21
|
+
const files = [];
|
|
25
22
|
const entryPoints = {};
|
|
26
|
-
const
|
|
27
|
-
|
|
23
|
+
const entryToPattern = new Map();
|
|
24
|
+
const entryToNavPattern = new Map();
|
|
25
|
+
const entryToLayoutDir = new Map();
|
|
26
|
+
const entryToLoadingDir = new Map();
|
|
27
|
+
|
|
28
28
|
for (const page of pages) {
|
|
29
29
|
const outName = patternToFileName(page.pattern);
|
|
30
30
|
const layoutChain = resolveLayoutChain(page.pattern, layouts);
|
|
31
31
|
const loadingChain = resolveLoadingChain(page.pattern, loadings);
|
|
32
|
-
const
|
|
32
|
+
const content = generateHydrationEntry(page.filePath, layoutChain, loadingChain);
|
|
33
33
|
const entryPath = path.join(tempDir, `${outName}.tsx`);
|
|
34
|
-
|
|
35
|
-
await fs.writeFile(entryPath, entryContent);
|
|
34
|
+
files.push({ entryPath, content });
|
|
36
35
|
entryPoints[outName] = entryPath;
|
|
36
|
+
entryToPattern.set(entryPath, page.pattern);
|
|
37
37
|
}
|
|
38
38
|
for (const page of pages) {
|
|
39
39
|
const navName = `nav_${patternToFileName(page.pattern)}`;
|
|
40
40
|
const importPath = page.filePath.replace(/\\/g, '/');
|
|
41
|
-
const
|
|
42
|
-
`;
|
|
41
|
+
const content = `export { default } from '${importPath}';\n`;
|
|
43
42
|
const navPath = path.join(tempDir, `${navName}.tsx`);
|
|
44
|
-
|
|
45
|
-
await fs.writeFile(navPath, navContent);
|
|
43
|
+
files.push({ entryPath: navPath, content });
|
|
46
44
|
entryPoints[navName] = navPath;
|
|
45
|
+
entryToNavPattern.set(navPath, page.pattern);
|
|
47
46
|
}
|
|
48
47
|
for (const layout of layouts) {
|
|
49
48
|
const layoutName = `nav_layout_${layoutDirToFileName(layout.dirPath)}`;
|
|
50
49
|
const importPath = layout.filePath.replace(/\\/g, '/');
|
|
51
|
-
const
|
|
52
|
-
`;
|
|
50
|
+
const content = `export { default } from '${importPath}';\n`;
|
|
53
51
|
const navPath = path.join(tempDir, `${layoutName}.tsx`);
|
|
54
|
-
|
|
55
|
-
await fs.writeFile(navPath, navContent);
|
|
52
|
+
files.push({ entryPath: navPath, content });
|
|
56
53
|
entryPoints[layoutName] = navPath;
|
|
54
|
+
entryToLayoutDir.set(navPath, layout.dirPath);
|
|
57
55
|
}
|
|
58
56
|
for (const loading of loadings) {
|
|
59
57
|
const loadingName = `nav_loading_${layoutDirToFileName(loading.dirPath)}`;
|
|
60
58
|
const importPath = loading.filePath.replace(/\\/g, '/');
|
|
61
|
-
const
|
|
62
|
-
`;
|
|
59
|
+
const content = `export { default } from '${importPath}';\n`;
|
|
63
60
|
const navPath = path.join(tempDir, `${loadingName}.tsx`);
|
|
64
|
-
|
|
65
|
-
await fs.writeFile(navPath, navContent);
|
|
61
|
+
files.push({ entryPath: navPath, content });
|
|
66
62
|
entryPoints[loadingName] = navPath;
|
|
63
|
+
entryToLoadingDir.set(navPath, loading.dirPath);
|
|
67
64
|
}
|
|
65
|
+
return { files, entryPoints, entryToPattern, entryToNavPattern, entryToLayoutDir, entryToLoadingDir };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveReactAlias(rootDir) {
|
|
68
69
|
const projectRequire = createRequire(path.join(rootDir, 'package.json'));
|
|
69
|
-
const
|
|
70
|
+
const alias = {};
|
|
70
71
|
for (const pkg of ['react', 'react-dom']) {
|
|
71
72
|
try {
|
|
72
|
-
|
|
73
|
+
alias[pkg] = path.dirname(projectRequire.resolve(`${pkg}/package.json`));
|
|
73
74
|
} catch {}
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
+
return alias;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Cache key is derived from everything that could change the client bundle
|
|
80
|
+
// output set except for the actual source-file contents (those are tracked
|
|
81
|
+
// as the invalidation inputs list and re-hashed on lookup).
|
|
82
|
+
function computeClientCoarseKey({
|
|
83
|
+
target,
|
|
84
|
+
minify,
|
|
85
|
+
devMode,
|
|
86
|
+
nodeEnv,
|
|
87
|
+
reactAlias,
|
|
88
|
+
pages,
|
|
89
|
+
layouts,
|
|
90
|
+
loadings,
|
|
91
|
+
hydrationContents,
|
|
92
|
+
}) {
|
|
93
|
+
return sha256(
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
esbuildVersion: ESBUILD_VERSION,
|
|
96
|
+
target: target ?? '',
|
|
97
|
+
minify: !!minify,
|
|
98
|
+
devMode: !!devMode,
|
|
99
|
+
nodeEnv: nodeEnv ?? '',
|
|
100
|
+
reactAlias,
|
|
101
|
+
pages: pages.map((p) => ({ pattern: p.pattern, filePath: path.resolve(p.filePath) })),
|
|
102
|
+
layouts: layouts.map((l) => ({ dirPath: l.dirPath, filePath: path.resolve(l.filePath) })),
|
|
103
|
+
loadings: loadings.map((l) => ({ dirPath: l.dirPath, filePath: path.resolve(l.filePath) })),
|
|
104
|
+
hydrationContents,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function serializeMetadata(meta) {
|
|
110
|
+
return {
|
|
111
|
+
bundles: [...meta.bundles.entries()],
|
|
112
|
+
navBundles: [...meta.navBundles.entries()],
|
|
113
|
+
layoutNavBundles: [...meta.layoutNavBundles.entries()],
|
|
114
|
+
loadingNavBundles: [...meta.loadingNavBundles.entries()],
|
|
115
|
+
sharedChunks: meta.sharedChunks,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function deserializeMetadata(raw) {
|
|
120
|
+
return {
|
|
121
|
+
bundles: new Map(raw.bundles),
|
|
122
|
+
navBundles: new Map(raw.navBundles),
|
|
123
|
+
layoutNavBundles: new Map(raw.layoutNavBundles),
|
|
124
|
+
loadingNavBundles: new Map(raw.loadingNavBundles),
|
|
125
|
+
sharedChunks: raw.sharedChunks,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildClientEsbuildOptions({
|
|
130
|
+
entryPoints,
|
|
131
|
+
clientDir,
|
|
132
|
+
target,
|
|
133
|
+
minify,
|
|
134
|
+
devMode,
|
|
135
|
+
nodeEnv,
|
|
136
|
+
reactAlias,
|
|
137
|
+
rootDir,
|
|
138
|
+
}) {
|
|
139
|
+
return {
|
|
76
140
|
entryPoints,
|
|
77
141
|
outdir: clientDir,
|
|
78
142
|
format: 'esm',
|
|
@@ -90,25 +154,174 @@ async function buildClientBundles(
|
|
|
90
154
|
alias: reactAlias,
|
|
91
155
|
define: { 'process.env.NODE_ENV': JSON.stringify(nodeEnv) },
|
|
92
156
|
plugins: [...(devMode ? [reactRefreshPlugin(rootDir)] : []), clientIsolationPlugin()],
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function writeHydrationEntries(plan) {
|
|
161
|
+
await Promise.all(
|
|
162
|
+
plan.files.map(async (f) => {
|
|
163
|
+
await fs.mkdir(path.dirname(f.entryPath), { recursive: true });
|
|
164
|
+
await fs.writeFile(f.entryPath, f.content);
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Long-lived esbuild context for the client bundle. Used by the dev watcher so
|
|
170
|
+
// file-change rebuilds call `ctx.rebuild()` (incremental) instead of spinning up
|
|
171
|
+
// a fresh esbuild per change. The context is bound to the specific
|
|
172
|
+
// pages/layouts/loadings set; structural changes (add/remove page) require
|
|
173
|
+
// disposing and recreating.
|
|
174
|
+
async function createClientBuildContext(
|
|
175
|
+
pages,
|
|
176
|
+
layouts,
|
|
177
|
+
outDir,
|
|
178
|
+
target,
|
|
179
|
+
minify,
|
|
180
|
+
rootDir,
|
|
181
|
+
loadings = [],
|
|
182
|
+
nodeEnv = 'production',
|
|
183
|
+
devMode = false,
|
|
184
|
+
) {
|
|
185
|
+
const clientDir = path.join(outDir, 'client');
|
|
186
|
+
const tempDir = path.join(outDir, '.hydration-entries');
|
|
187
|
+
const plan = planHydrationEntries(pages, layouts, loadings, tempDir);
|
|
188
|
+
const reactAlias = resolveReactAlias(rootDir);
|
|
189
|
+
|
|
190
|
+
await fs.mkdir(clientDir, { recursive: true });
|
|
191
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
192
|
+
await writeHydrationEntries(plan);
|
|
193
|
+
|
|
194
|
+
const ctx = await esbuild.context(
|
|
195
|
+
buildClientEsbuildOptions({
|
|
196
|
+
entryPoints: plan.entryPoints,
|
|
197
|
+
clientDir,
|
|
198
|
+
target,
|
|
199
|
+
minify,
|
|
200
|
+
devMode,
|
|
201
|
+
nodeEnv,
|
|
202
|
+
reactAlias,
|
|
203
|
+
rootDir,
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
let disposed = false;
|
|
208
|
+
|
|
209
|
+
async function rebuild() {
|
|
210
|
+
const result = await ctx.rebuild();
|
|
211
|
+
const metadata = extractMetadata(result, plan, outDir);
|
|
212
|
+
return { ...metadata, metafile: result.metafile };
|
|
102
213
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
214
|
+
|
|
215
|
+
async function dispose() {
|
|
216
|
+
if (disposed) return;
|
|
217
|
+
disposed = true;
|
|
218
|
+
await ctx.dispose();
|
|
219
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
106
220
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
221
|
+
|
|
222
|
+
return { rebuild, dispose };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function buildClientBundles(
|
|
226
|
+
pages,
|
|
227
|
+
layouts,
|
|
228
|
+
outDir,
|
|
229
|
+
target,
|
|
230
|
+
minify,
|
|
231
|
+
rootDir,
|
|
232
|
+
loadings = [],
|
|
233
|
+
nodeEnv = 'production',
|
|
234
|
+
devMode = false,
|
|
235
|
+
) {
|
|
236
|
+
const clientDir = path.join(outDir, 'client');
|
|
237
|
+
const tempDir = path.join(outDir, '.hydration-entries');
|
|
238
|
+
const plan = planHydrationEntries(pages, layouts, loadings, tempDir);
|
|
239
|
+
const reactAlias = resolveReactAlias(rootDir);
|
|
240
|
+
|
|
241
|
+
// Cache is keyed on the full build intent (including exact hydration-entry
|
|
242
|
+
// contents). devMode skips the cache because the watcher will eventually own
|
|
243
|
+
// incremental rebuilds via esbuild.context() (Phase 4) and we also return
|
|
244
|
+
// the metafile directly in dev mode for HMR diffing.
|
|
245
|
+
const useCache = !devMode;
|
|
246
|
+
let cacheBucket;
|
|
247
|
+
let coarseKey;
|
|
248
|
+
if (useCache) {
|
|
249
|
+
coarseKey = computeClientCoarseKey({
|
|
250
|
+
target,
|
|
251
|
+
minify,
|
|
252
|
+
devMode,
|
|
253
|
+
nodeEnv,
|
|
254
|
+
reactAlias,
|
|
255
|
+
pages,
|
|
256
|
+
layouts,
|
|
257
|
+
loadings,
|
|
258
|
+
hydrationContents: plan.files.map((f) => ({
|
|
259
|
+
name: path.basename(f.entryPath),
|
|
260
|
+
content: f.content,
|
|
261
|
+
})),
|
|
262
|
+
});
|
|
263
|
+
const lookup = await lookupMultiFileCache({ rootDir, kind: 'client', coarseKey });
|
|
264
|
+
cacheBucket = lookup.bucket;
|
|
265
|
+
if (lookup.hit) {
|
|
266
|
+
await restoreMultiFileCache({
|
|
267
|
+
cacheDir: lookup.cacheDir,
|
|
268
|
+
outputs: lookup.meta.outputs,
|
|
269
|
+
destDir: clientDir,
|
|
270
|
+
});
|
|
271
|
+
return deserializeMetadata(lookup.meta.metadata);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
276
|
+
await writeHydrationEntries(plan);
|
|
277
|
+
|
|
278
|
+
const result = await esbuild.build(
|
|
279
|
+
buildClientEsbuildOptions({
|
|
280
|
+
entryPoints: plan.entryPoints,
|
|
281
|
+
clientDir,
|
|
282
|
+
target,
|
|
283
|
+
minify,
|
|
284
|
+
devMode,
|
|
285
|
+
nodeEnv,
|
|
286
|
+
reactAlias,
|
|
287
|
+
rootDir,
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
291
|
+
|
|
292
|
+
const metadata = extractMetadata(result, plan, outDir);
|
|
293
|
+
|
|
294
|
+
if (useCache) {
|
|
295
|
+
const outputs = Object.keys(result.metafile.outputs)
|
|
296
|
+
.map((p) => path.relative(clientDir, path.resolve(p)).replace(/\\/g, '/'))
|
|
297
|
+
.filter((rel) => !rel.startsWith('..'));
|
|
298
|
+
// Track only inputs on the real filesystem — the ephemeral hydration entries
|
|
299
|
+
// inside `tempDir` are re-generated each build from a stable content plan,
|
|
300
|
+
// so their paths change every run and can't stably invalidate the cache.
|
|
301
|
+
const inputs = Object.keys(result.metafile.inputs)
|
|
302
|
+
.map((p) => (path.isAbsolute(p) ? p : path.resolve(p)))
|
|
303
|
+
.filter((p) => !p.startsWith(tempDir));
|
|
304
|
+
await storeMultiFileCache({
|
|
305
|
+
bucket: cacheBucket,
|
|
306
|
+
coarseKey,
|
|
307
|
+
destDir: clientDir,
|
|
308
|
+
outputs,
|
|
309
|
+
inputs,
|
|
310
|
+
metadata: serializeMetadata(metadata),
|
|
311
|
+
});
|
|
111
312
|
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
...metadata,
|
|
316
|
+
...(devMode ? { metafile: result.metafile } : {}),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function extractMetadata(result, plan, outDir) {
|
|
321
|
+
const bundles = new Map();
|
|
322
|
+
const navBundles = new Map();
|
|
323
|
+
const layoutNavBundles = new Map();
|
|
324
|
+
const loadingNavBundles = new Map();
|
|
112
325
|
const sharedChunks = [];
|
|
113
326
|
for (const [outputPath, meta] of Object.entries(result.metafile.outputs)) {
|
|
114
327
|
const relPath = path.relative(outDir, outputPath).replace(/\\/g, '/');
|
|
@@ -119,41 +332,24 @@ async function buildClientBundles(
|
|
|
119
332
|
}
|
|
120
333
|
if (meta.entryPoint) {
|
|
121
334
|
const absEntry = path.resolve(meta.entryPoint);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
for (const [entryPath, dirPath] of entryToLayoutDir) {
|
|
135
|
-
if (absEntry === entryPath) {
|
|
136
|
-
layoutNavBundles.set(dirPath, relPath);
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
for (const [entryPath, dirPath] of entryToLoadingDir) {
|
|
141
|
-
if (absEntry === entryPath) {
|
|
142
|
-
loadingNavBundles.set(dirPath, relPath);
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
335
|
+
const pattern =
|
|
336
|
+
plan.entryToPattern.get(absEntry) ?? plan.entryToNavPattern.get(absEntry);
|
|
337
|
+
if (plan.entryToPattern.has(absEntry)) {
|
|
338
|
+
bundles.set(plan.entryToPattern.get(absEntry), relPath);
|
|
339
|
+
} else if (plan.entryToNavPattern.has(absEntry)) {
|
|
340
|
+
navBundles.set(plan.entryToNavPattern.get(absEntry), relPath);
|
|
341
|
+
} else if (plan.entryToLayoutDir.has(absEntry)) {
|
|
342
|
+
layoutNavBundles.set(plan.entryToLayoutDir.get(absEntry), relPath);
|
|
343
|
+
} else if (plan.entryToLoadingDir.has(absEntry)) {
|
|
344
|
+
loadingNavBundles.set(plan.entryToLoadingDir.get(absEntry), relPath);
|
|
145
345
|
}
|
|
346
|
+
// pattern variable intentionally unused when none of the maps match
|
|
347
|
+
void pattern;
|
|
146
348
|
}
|
|
147
349
|
}
|
|
148
|
-
return {
|
|
149
|
-
bundles,
|
|
150
|
-
navBundles,
|
|
151
|
-
layoutNavBundles,
|
|
152
|
-
loadingNavBundles,
|
|
153
|
-
sharedChunks,
|
|
154
|
-
...(devMode ? { metafile: result.metafile } : {}),
|
|
155
|
-
};
|
|
350
|
+
return { bundles, navBundles, layoutNavBundles, loadingNavBundles, sharedChunks };
|
|
156
351
|
}
|
|
352
|
+
|
|
157
353
|
function generateHydrationEntry(componentPath, layouts = [], loadings = []) {
|
|
158
354
|
const importPath = componentPath.replace(/\\/g, '/');
|
|
159
355
|
const imports = [
|
|
@@ -184,4 +380,4 @@ const props = propsEl ? JSON.parse(propsEl.textContent || '{}') : {};
|
|
|
184
380
|
hydrateRoot(container!, <ApiProvider><Router initialPath={window.location.pathname} initialParams={props} initialComponent={Component} initialLayouts={${layoutsArray}} initialLoadings={${loadingsArray}} /></ApiProvider>);
|
|
185
381
|
`;
|
|
186
382
|
}
|
|
187
|
-
export { buildClientBundles, generateHydrationEntry };
|
|
383
|
+
export { buildClientBundles, createClientBuildContext, generateHydrationEntry };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import {
|
|
4
|
+
discoverPages,
|
|
5
|
+
discoverLayouts,
|
|
6
|
+
discoverLoadings,
|
|
7
|
+
discoverPageMiddleware,
|
|
8
|
+
discoverErrorPages,
|
|
9
|
+
discoverStyles,
|
|
10
|
+
} from './discovery.js';
|
|
11
|
+
import {
|
|
12
|
+
buildServerBundles,
|
|
13
|
+
buildLayoutServerBundles,
|
|
14
|
+
buildMiddlewareServerBundles,
|
|
15
|
+
buildErrorPageBundles,
|
|
16
|
+
} from './build-server.js';
|
|
17
|
+
import { createClientBuildContext } from './build-client.js';
|
|
18
|
+
import { buildCssBundle } from './build-css.js';
|
|
19
|
+
import { generateManifest } from './build-manifest.js';
|
|
20
|
+
import { resolveFonts } from './fonts.js';
|
|
21
|
+
import { buildHmrRuntimeBundle } from './hmr.js';
|
|
22
|
+
|
|
23
|
+
// Long-lived build context for dev-watch mode. Unlike `buildPages()`, which
|
|
24
|
+
// rebuilds every esbuild graph from scratch, this keeps an incremental
|
|
25
|
+
// `esbuild.context()` alive for the client bundle so file-change rebuilds are
|
|
26
|
+
// near-instant. Server, layout, middleware, CSS, and error-page bundles reuse
|
|
27
|
+
// the persistent on-disk cache (phase 2/3), which already handles per-entry
|
|
28
|
+
// invalidation cheaply. The dev outDir is written in place — the atomic staging
|
|
29
|
+
// swap is a prod-safety feature that conflicts with esbuild's requirement for a
|
|
30
|
+
// stable outdir across rebuilds.
|
|
31
|
+
async function createPagesBuildContext(options) {
|
|
32
|
+
const { rootDir } = options;
|
|
33
|
+
const pagesDir = options.pagesDir ?? path.join(rootDir, 'pages');
|
|
34
|
+
const outDir = path.resolve(rootDir, options.outDir ?? '.build/pages');
|
|
35
|
+
const serverTarget = options.serverTarget ?? 'node22';
|
|
36
|
+
const clientTarget = options.clientTarget ?? 'es2022';
|
|
37
|
+
const minify = options.minify ?? false;
|
|
38
|
+
const devMode = options.devMode ?? true;
|
|
39
|
+
|
|
40
|
+
let fontFaceCss;
|
|
41
|
+
let fontPreloadHtml;
|
|
42
|
+
if (options.fonts && options.fonts.length > 0) {
|
|
43
|
+
const publicDir = path.join(rootDir, 'public');
|
|
44
|
+
const fontResult = await resolveFonts(options.fonts, publicDir);
|
|
45
|
+
if (fontResult.fontFaceCss) fontFaceCss = fontResult.fontFaceCss;
|
|
46
|
+
if (fontResult.preloadHtml) fontPreloadHtml = fontResult.preloadHtml;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let clientCtx = null;
|
|
50
|
+
let lastStructuralKey = null;
|
|
51
|
+
let lastResult = null;
|
|
52
|
+
let disposed = false;
|
|
53
|
+
|
|
54
|
+
async function ensureClientContext(pages, layouts, loadings) {
|
|
55
|
+
const structuralKey = computeStructuralKey(pages, layouts, loadings);
|
|
56
|
+
if (clientCtx !== null && structuralKey === lastStructuralKey) return;
|
|
57
|
+
if (clientCtx) {
|
|
58
|
+
await clientCtx.dispose();
|
|
59
|
+
clientCtx = null;
|
|
60
|
+
}
|
|
61
|
+
clientCtx = await createClientBuildContext(
|
|
62
|
+
pages,
|
|
63
|
+
layouts,
|
|
64
|
+
outDir,
|
|
65
|
+
clientTarget,
|
|
66
|
+
minify,
|
|
67
|
+
rootDir,
|
|
68
|
+
loadings,
|
|
69
|
+
minify ? 'production' : 'development',
|
|
70
|
+
devMode,
|
|
71
|
+
);
|
|
72
|
+
lastStructuralKey = structuralKey;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function rebuild() {
|
|
76
|
+
if (disposed) throw new Error('createPagesBuildContext: context has been disposed');
|
|
77
|
+
|
|
78
|
+
const [pages, layouts, loadings, middlewares, errorPages, stylesPath] = await Promise.all([
|
|
79
|
+
discoverPages(pagesDir),
|
|
80
|
+
discoverLayouts(pagesDir),
|
|
81
|
+
discoverLoadings(pagesDir),
|
|
82
|
+
discoverPageMiddleware(pagesDir),
|
|
83
|
+
discoverErrorPages(pagesDir),
|
|
84
|
+
discoverStyles(pagesDir),
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
if (pages.length === 0) {
|
|
88
|
+
lastResult = { pageCount: 0, outDir, manifest: { entries: [], sharedChunks: [] } };
|
|
89
|
+
return lastResult;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await fs.mkdir(path.join(outDir, 'server'), { recursive: true });
|
|
93
|
+
await fs.mkdir(path.join(outDir, 'client'), { recursive: true });
|
|
94
|
+
|
|
95
|
+
await ensureClientContext(pages, layouts, loadings);
|
|
96
|
+
|
|
97
|
+
const serverCacheOpts = { rootDir, devMode, minify };
|
|
98
|
+
const builds = [
|
|
99
|
+
buildServerBundles(pages, outDir, serverTarget, serverCacheOpts),
|
|
100
|
+
buildLayoutServerBundles(layouts, outDir, serverTarget, serverCacheOpts),
|
|
101
|
+
buildMiddlewareServerBundles(middlewares, outDir, serverTarget, serverCacheOpts),
|
|
102
|
+
clientCtx.rebuild(),
|
|
103
|
+
buildErrorPageBundles(errorPages, outDir, serverTarget, serverCacheOpts),
|
|
104
|
+
buildCssBundle(pagesDir, outDir, minify, stylesPath, fontFaceCss, rootDir),
|
|
105
|
+
];
|
|
106
|
+
if (devMode) builds.push(buildHmrRuntimeBundle(outDir));
|
|
107
|
+
|
|
108
|
+
const [
|
|
109
|
+
serverResult,
|
|
110
|
+
layoutServerResult,
|
|
111
|
+
middlewareServerResult,
|
|
112
|
+
clientResult,
|
|
113
|
+
errorBundles,
|
|
114
|
+
cssBundle,
|
|
115
|
+
] = await Promise.all(builds);
|
|
116
|
+
|
|
117
|
+
const manifest = generateManifest(
|
|
118
|
+
pages,
|
|
119
|
+
layouts,
|
|
120
|
+
loadings,
|
|
121
|
+
middlewares,
|
|
122
|
+
serverResult,
|
|
123
|
+
layoutServerResult,
|
|
124
|
+
middlewareServerResult,
|
|
125
|
+
clientResult,
|
|
126
|
+
);
|
|
127
|
+
if (errorBundles.error) manifest.errorBundle = errorBundles.error;
|
|
128
|
+
if (errorBundles.notFound) manifest.notFoundBundle = errorBundles.notFound;
|
|
129
|
+
if (cssBundle) manifest.cssBundle = cssBundle;
|
|
130
|
+
if (fontPreloadHtml) manifest.fontPreloadHtml = fontPreloadHtml;
|
|
131
|
+
|
|
132
|
+
await fs.writeFile(
|
|
133
|
+
path.join(outDir, 'pages-manifest.json'),
|
|
134
|
+
JSON.stringify(manifest, null, 2),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
lastResult = {
|
|
138
|
+
pageCount: pages.length,
|
|
139
|
+
outDir,
|
|
140
|
+
manifest,
|
|
141
|
+
...(clientResult.metafile ? { clientMetafile: clientResult.metafile } : {}),
|
|
142
|
+
};
|
|
143
|
+
return lastResult;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function dispose() {
|
|
147
|
+
if (disposed) return;
|
|
148
|
+
disposed = true;
|
|
149
|
+
if (clientCtx) {
|
|
150
|
+
await clientCtx.dispose();
|
|
151
|
+
clientCtx = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
rebuild,
|
|
157
|
+
dispose,
|
|
158
|
+
get currentResult() {
|
|
159
|
+
return lastResult;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function computeStructuralKey(pages, layouts, loadings) {
|
|
165
|
+
return JSON.stringify({
|
|
166
|
+
p: pages.map((p) => p.pattern).sort(),
|
|
167
|
+
l: layouts.map((l) => l.dirPath).sort(),
|
|
168
|
+
ld: loadings.map((l) => l.dirPath).sort(),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { createPagesBuildContext };
|
|
@@ -5,13 +5,80 @@ import { createRequire } from 'node:module';
|
|
|
5
5
|
import esbuild from 'esbuild';
|
|
6
6
|
import postcss from 'postcss';
|
|
7
7
|
import tailwindPostcss from '@tailwindcss/postcss';
|
|
8
|
-
|
|
8
|
+
import {
|
|
9
|
+
sha256,
|
|
10
|
+
lookupMultiFileCache,
|
|
11
|
+
restoreMultiFileCache,
|
|
12
|
+
storeMultiFileCache,
|
|
13
|
+
} from './build-cache.js';
|
|
14
|
+
|
|
15
|
+
// Shallow-walk the pages dir and return every source-shaped file. Used as the
|
|
16
|
+
// cache-invalidation input set when tailwind will scan it via @source.
|
|
17
|
+
async function collectTailwindSources(pagesDir) {
|
|
18
|
+
const out = [];
|
|
19
|
+
async function walk(dir) {
|
|
20
|
+
let entries;
|
|
21
|
+
try {
|
|
22
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
23
|
+
} catch {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
for (const e of entries) {
|
|
27
|
+
const full = path.join(dir, e.name);
|
|
28
|
+
if (e.isDirectory()) {
|
|
29
|
+
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
30
|
+
await walk(full);
|
|
31
|
+
} else if (e.isFile()) {
|
|
32
|
+
if (/\.(jsx?|tsx?|mjs|cjs|css)$/i.test(e.name)) {
|
|
33
|
+
out.push(full);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
await walk(pagesDir);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function computeCssCoarseKey({ minify, fontFaceCss, stylesPath }) {
|
|
43
|
+
return sha256(
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
minify: !!minify,
|
|
46
|
+
fontFaceCss: fontFaceCss ?? '',
|
|
47
|
+
stylesPath: stylesPath ? path.resolve(stylesPath) : null,
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function buildCssBundle(pagesDir, outDir, minify, stylesPath, fontFaceCss, rootDir) {
|
|
9
53
|
try {
|
|
10
54
|
await fs.access(pagesDir);
|
|
11
55
|
} catch {
|
|
12
|
-
// Pages directory doesn't exist — no CSS to build
|
|
13
56
|
return void 0;
|
|
14
57
|
}
|
|
58
|
+
|
|
59
|
+
const clientDir = path.join(outDir, 'client');
|
|
60
|
+
rootDir = rootDir ?? path.dirname(outDir);
|
|
61
|
+
const coarseKey = computeCssCoarseKey({ minify, fontFaceCss, stylesPath });
|
|
62
|
+
|
|
63
|
+
// Inputs that should invalidate the cache on content change.
|
|
64
|
+
const inputs = [];
|
|
65
|
+
if (stylesPath) {
|
|
66
|
+
inputs.push(path.resolve(stylesPath));
|
|
67
|
+
} else {
|
|
68
|
+
const scanned = await collectTailwindSources(pagesDir);
|
|
69
|
+
for (const f of scanned) inputs.push(f);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const lookup = await lookupMultiFileCache({ rootDir, kind: 'css', coarseKey });
|
|
73
|
+
if (lookup.hit) {
|
|
74
|
+
await restoreMultiFileCache({
|
|
75
|
+
cacheDir: lookup.cacheDir,
|
|
76
|
+
outputs: lookup.meta.outputs,
|
|
77
|
+
destDir: clientDir,
|
|
78
|
+
});
|
|
79
|
+
return lookup.meta.metadata.cssBundleRel;
|
|
80
|
+
}
|
|
81
|
+
|
|
15
82
|
let cssSource;
|
|
16
83
|
if (stylesPath) {
|
|
17
84
|
cssSource = `@import ${JSON.stringify(stylesPath)};`;
|
|
@@ -29,19 +96,28 @@ async function buildCssBundle(pagesDir, outDir, minify, stylesPath, fontFaceCss)
|
|
|
29
96
|
from: stylesPath ?? path.join(pagesDir, '__virtual_entry.css'),
|
|
30
97
|
to: path.join(outDir, 'client', 'styles.css'),
|
|
31
98
|
});
|
|
32
|
-
let css = fontFaceCss
|
|
33
|
-
? `${fontFaceCss}
|
|
34
|
-
|
|
35
|
-
${result.css}`
|
|
36
|
-
: result.css;
|
|
99
|
+
let css = fontFaceCss ? `${fontFaceCss}\n\n${result.css}` : result.css;
|
|
37
100
|
if (minify) {
|
|
38
101
|
const minified = await esbuild.transform(css, { loader: 'css', minify: true });
|
|
39
102
|
css = minified.code;
|
|
40
103
|
}
|
|
41
104
|
const hash = crypto.createHash('sha256').update(css).digest('hex').slice(0, 8);
|
|
42
105
|
const cssFileName = `styles-${hash}.css`;
|
|
43
|
-
const outFile = path.join(
|
|
106
|
+
const outFile = path.join(clientDir, cssFileName);
|
|
107
|
+
await fs.mkdir(clientDir, { recursive: true });
|
|
44
108
|
await fs.writeFile(outFile, css);
|
|
45
|
-
|
|
109
|
+
const cssBundleRel = `client/${cssFileName}`;
|
|
110
|
+
|
|
111
|
+
await storeMultiFileCache({
|
|
112
|
+
bucket: lookup.bucket,
|
|
113
|
+
coarseKey,
|
|
114
|
+
destDir: clientDir,
|
|
115
|
+
outputs: [cssFileName],
|
|
116
|
+
inputs,
|
|
117
|
+
metadata: { cssBundleRel },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return cssBundleRel;
|
|
46
121
|
}
|
|
122
|
+
|
|
47
123
|
export { buildCssBundle };
|
package/server/pages/build.js
CHANGED
|
@@ -74,7 +74,7 @@ async function buildPages(options) {
|
|
|
74
74
|
devMode,
|
|
75
75
|
),
|
|
76
76
|
buildErrorPageBundles(errorPages, buildDir, serverTarget, serverCacheOpts),
|
|
77
|
-
buildCssBundle(pagesDir, buildDir, minify, stylesPath, fontFaceCss),
|
|
77
|
+
buildCssBundle(pagesDir, buildDir, minify, stylesPath, fontFaceCss, rootDir),
|
|
78
78
|
);
|
|
79
79
|
if (devMode) builds.push(buildHmrRuntimeBundle(buildDir));
|
|
80
80
|
// Fail fast: `Promise.all` rejects the moment any build throws, so a broken
|
|
@@ -123,13 +123,24 @@ async function buildPages(options) {
|
|
|
123
123
|
// before removing the directory — otherwise `fs.rm` races the writes and
|
|
124
124
|
// fails with ENOTEMPTY. The cleanup is fire-and-forget so the caller sees
|
|
125
125
|
// the original error immediately; the unique staging-dir suffix means any
|
|
126
|
-
// lingering files can't pollute a subsequent `buildPages` call.
|
|
127
|
-
|
|
126
|
+
// lingering files can't pollute a subsequent `buildPages` call. Pending
|
|
127
|
+
// cleanups are tracked so tests can await quiescence.
|
|
128
|
+
const cleanup = Promise.allSettled(builds).then(() =>
|
|
128
129
|
fs.rm(buildDir, { recursive: true, force: true }).catch(() => {}),
|
|
129
130
|
);
|
|
131
|
+
pendingCleanups.add(cleanup);
|
|
132
|
+
cleanup.finally(() => pendingCleanups.delete(cleanup));
|
|
130
133
|
throw err;
|
|
131
134
|
}
|
|
132
135
|
}
|
|
136
|
+
// Tracks fire-and-forget cleanup promises from failed builds so tests can
|
|
137
|
+
// await `buildPagesIdle()` and see a clean `.build` dir.
|
|
138
|
+
const pendingCleanups = new Set();
|
|
139
|
+
async function buildPagesIdle() {
|
|
140
|
+
while (pendingCleanups.size > 0) {
|
|
141
|
+
await Promise.allSettled([...pendingCleanups]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
133
144
|
// Serialize concurrent swaps targeting the same outDir. Without this, two
|
|
134
145
|
// parallel `buildPages` calls against the same outDir race between the
|
|
135
146
|
// `fs.rm(outDir)` and `fs.rename(buildDir, outDir)` steps: both rm's succeed,
|
|
@@ -161,4 +172,4 @@ function rewriteMetafilePaths(metafile, fromDir, toDir) {
|
|
|
161
172
|
return { ...metafile, outputs };
|
|
162
173
|
}
|
|
163
174
|
import { patternToFileName } from './build-server.js';
|
|
164
|
-
export { buildPages, patternToFileName };
|
|
175
|
+
export { buildPages, patternToFileName, buildPagesIdle };
|
package/server/pages/handler.js
CHANGED
|
@@ -19,7 +19,8 @@ function createPagesHandler(options) {
|
|
|
19
19
|
const manifestPath = path.join(outDir, 'pages-manifest.json');
|
|
20
20
|
const sessionConfig = options.session;
|
|
21
21
|
const appContext = options.appContext ?? null;
|
|
22
|
-
const
|
|
22
|
+
const mode = options.mode ?? 'production';
|
|
23
|
+
const devMode = mode === 'development';
|
|
23
24
|
if (!fs.existsSync(manifestPath)) {
|
|
24
25
|
return null;
|
|
25
26
|
}
|
|
@@ -139,12 +140,11 @@ function createPagesHandler(options) {
|
|
|
139
140
|
);
|
|
140
141
|
} catch (err) {
|
|
141
142
|
if (manifest.errorBundle && !res.headersSent) {
|
|
142
|
-
const errorMessage =
|
|
143
|
-
|
|
144
|
-
?
|
|
145
|
-
: err
|
|
146
|
-
|
|
147
|
-
: String(err);
|
|
143
|
+
const errorMessage = devMode
|
|
144
|
+
? err instanceof Error
|
|
145
|
+
? err.message
|
|
146
|
+
: String(err)
|
|
147
|
+
: 'An unexpected error occurred';
|
|
148
148
|
await renderErrorPage(
|
|
149
149
|
manifest.errorBundle,
|
|
150
150
|
500,
|
|
@@ -6,21 +6,23 @@ class PagesRouter {
|
|
|
6
6
|
config;
|
|
7
7
|
rootDir;
|
|
8
8
|
log;
|
|
9
|
+
mode;
|
|
9
10
|
fileWatcher;
|
|
10
11
|
handler = null;
|
|
11
12
|
watcher = null;
|
|
12
13
|
|
|
13
|
-
constructor(config, { rootDir, log, fileWatcher, appContext }) {
|
|
14
|
+
constructor(config, { rootDir, log, mode, fileWatcher, appContext }) {
|
|
14
15
|
this.config = config;
|
|
15
16
|
this.rootDir = rootDir;
|
|
16
17
|
this.log = log;
|
|
18
|
+
this.mode = mode ?? 'production';
|
|
17
19
|
this.fileWatcher = fileWatcher ?? null;
|
|
18
20
|
this.appContext = appContext ?? null;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
async init() {
|
|
22
|
-
const { config, rootDir, log, fileWatcher } = this;
|
|
23
|
-
const isDev =
|
|
24
|
+
const { config, rootDir, log, mode, fileWatcher } = this;
|
|
25
|
+
const isDev = mode === 'development';
|
|
24
26
|
|
|
25
27
|
let initialClientMetafile;
|
|
26
28
|
if (isDev) {
|
|
@@ -42,14 +44,14 @@ class PagesRouter {
|
|
|
42
44
|
rootDir,
|
|
43
45
|
session: config.session,
|
|
44
46
|
appContext: this.appContext,
|
|
45
|
-
|
|
47
|
+
mode,
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
if (!this.handler) return;
|
|
49
51
|
|
|
50
52
|
log.info('Pages: SSR enabled');
|
|
51
53
|
|
|
52
|
-
if (isDev) {
|
|
54
|
+
if (isDev && fileWatcher) {
|
|
53
55
|
this.watcher = createPagesWatcher({
|
|
54
56
|
rootDir,
|
|
55
57
|
handler: this.handler,
|
|
@@ -82,6 +82,7 @@ async function serializeResponse(res, response, responseHeaders, statusCode) {
|
|
|
82
82
|
class ApiRouter {
|
|
83
83
|
_config;
|
|
84
84
|
_log;
|
|
85
|
+
_mode;
|
|
85
86
|
_fileWatcher;
|
|
86
87
|
_appContext;
|
|
87
88
|
_sessionConfig;
|
|
@@ -91,9 +92,10 @@ class ApiRouter {
|
|
|
91
92
|
_reloading = false;
|
|
92
93
|
_pendingReload = false;
|
|
93
94
|
|
|
94
|
-
constructor(config, { log, fileWatcher, appContext, sessionConfig } = {}) {
|
|
95
|
+
constructor(config, { log, mode, fileWatcher, appContext, sessionConfig } = {}) {
|
|
95
96
|
this._config = config;
|
|
96
97
|
this._log = log;
|
|
98
|
+
this._mode = mode ?? 'production';
|
|
97
99
|
this._fileWatcher = fileWatcher ?? null;
|
|
98
100
|
this._appContext = appContext ?? null;
|
|
99
101
|
this._sessionConfig = sessionConfig ?? null;
|
|
@@ -176,7 +178,7 @@ class ApiRouter {
|
|
|
176
178
|
this._prefix = normalizePrefix(this._config.pathPrefix);
|
|
177
179
|
this._log?.info('Discovering routes...');
|
|
178
180
|
await this._discover();
|
|
179
|
-
if (this._fileWatcher) {
|
|
181
|
+
if (this._mode === 'development' && this._fileWatcher) {
|
|
180
182
|
this._fileWatcher.subscribe('routes', {
|
|
181
183
|
dirs: ['api'],
|
|
182
184
|
extensions: ['.js'],
|
|
@@ -303,7 +305,7 @@ class ApiRouter {
|
|
|
303
305
|
}
|
|
304
306
|
|
|
305
307
|
async close() {
|
|
306
|
-
if (this._fileWatcher) {
|
|
308
|
+
if (this._mode === 'development' && this._fileWatcher) {
|
|
307
309
|
this._fileWatcher.unsubscribe('routes');
|
|
308
310
|
}
|
|
309
311
|
}
|
package/server/session/index.js
CHANGED
|
@@ -28,6 +28,7 @@ function resolveSessionConfig(config, mode) {
|
|
|
28
28
|
password: config.password,
|
|
29
29
|
cookieName: config.cookieName ?? 'arcway.session',
|
|
30
30
|
ttl: config.ttl ?? 14 * 24 * 3600,
|
|
31
|
+
mode: mode ?? 'production',
|
|
31
32
|
cookie: {
|
|
32
33
|
httpOnly: config.cookie?.httpOnly ?? true,
|
|
33
34
|
secure: config.cookie?.secure ?? mode === 'production',
|
|
@@ -46,7 +47,7 @@ async function unsealSession(cookieValue, config) {
|
|
|
46
47
|
});
|
|
47
48
|
return data && typeof data === 'object' ? data : {};
|
|
48
49
|
} catch (err) {
|
|
49
|
-
if (
|
|
50
|
+
if (config.mode === 'development') {
|
|
50
51
|
const reason = toErrorMessage(err);
|
|
51
52
|
console.warn(`[arcway] Failed to unseal session cookie: ${reason}`);
|
|
52
53
|
}
|