arcway 0.1.21 → 0.1.23
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/README.md +69 -8
- package/package.json +1 -1
- package/server/boot/hooks.js +19 -0
- package/server/boot/index.js +18 -8
- package/server/context.js +10 -0
- package/server/db/index.js +9 -0
- package/server/events/handler.js +0 -1
- package/server/pages/arcway-endpoint.js +58 -0
- package/server/pages/build-client.js +2 -1
- package/server/pages/build-css.js +23 -9
- package/server/pages/build-server.js +80 -63
- package/server/pages/build.js +32 -3
- package/server/pages/discovery.js +99 -0
- package/server/pages/handler.js +103 -3
- package/server/pages/lazy-context.js +640 -0
- package/server/pages/pages-router.js +16 -21
- package/server/pages/watcher.js +59 -76
- package/server/watcher.js +15 -2
- package/server/ws/ws-router.js +45 -2
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { EventEmitter } from 'node:events';
|
|
5
|
+
import esbuild from 'esbuild';
|
|
6
|
+
import pLimit from 'p-limit';
|
|
7
|
+
import {
|
|
8
|
+
buildPageManifestEntry,
|
|
9
|
+
discoverPages,
|
|
10
|
+
discoverLayouts,
|
|
11
|
+
discoverLoadings,
|
|
12
|
+
discoverPageMiddleware,
|
|
13
|
+
discoverErrorPages,
|
|
14
|
+
discoverStyles,
|
|
15
|
+
resolveLayoutChain,
|
|
16
|
+
resolveLoadingChain,
|
|
17
|
+
resolvePageMiddlewareChain,
|
|
18
|
+
mapFileToAffected,
|
|
19
|
+
} from './discovery.js';
|
|
20
|
+
import { buildWithCache } from './build-cache.js';
|
|
21
|
+
import {
|
|
22
|
+
jsxEsbuildOptions,
|
|
23
|
+
plainEsbuildOptions,
|
|
24
|
+
patternToFileName,
|
|
25
|
+
layoutDirToFileName,
|
|
26
|
+
buildErrorPageBundles,
|
|
27
|
+
} from './build-server.js';
|
|
28
|
+
import { createClientBuildContext } from './build-client.js';
|
|
29
|
+
import { buildCssBundle } from './build-css.js';
|
|
30
|
+
import { resolveFonts } from './fonts.js';
|
|
31
|
+
import { buildHmrRuntimeBundle } from './hmr.js';
|
|
32
|
+
|
|
33
|
+
// Dev-mode build context that defers per-page/layout/middleware server-bundle
|
|
34
|
+
// compilation to first request. Startup is cheap: discover routes, build the
|
|
35
|
+
// client bundle + CSS + error pages eagerly (unavoidable — they're shared
|
|
36
|
+
// across all routes), and emit an in-memory manifest where every server
|
|
37
|
+
// entry is flagged `built: false`. Handler calls `ensurePageBuilt(pattern)`
|
|
38
|
+
// before rendering; concurrent calls for the same pattern coalesce to one
|
|
39
|
+
// build. A single shared `p-limit(cpuCount)` gates every buildWithCache call
|
|
40
|
+
// so a request burst can't saturate CPU.
|
|
41
|
+
//
|
|
42
|
+
// Prod is untouched — `buildPages()` and `createPagesBuildContext()` still
|
|
43
|
+
// fully build every server bundle upfront and atomically swap into place.
|
|
44
|
+
|
|
45
|
+
async function createLazyPagesContext(options) {
|
|
46
|
+
const { rootDir } = options;
|
|
47
|
+
const pagesDir = options.pagesDir ?? path.join(rootDir, 'pages');
|
|
48
|
+
const outDir = path.resolve(rootDir, options.outDir ?? '.build/pages');
|
|
49
|
+
const serverTarget = options.serverTarget ?? 'node22';
|
|
50
|
+
const clientTarget = options.clientTarget ?? 'es2022';
|
|
51
|
+
const minify = options.minify ?? false;
|
|
52
|
+
const devMode = options.devMode ?? true;
|
|
53
|
+
const esbuildImpl = options.esbuild ?? esbuild;
|
|
54
|
+
// Cap per-context server-bundle concurrency so a request burst after a
|
|
55
|
+
// cold start can't spawn N pages × M esbuild Go workers simultaneously.
|
|
56
|
+
// Defaults to logical core count for parity with today's eager fan-out on
|
|
57
|
+
// a single-user dev box; callers override in tests to assert coalescing.
|
|
58
|
+
const maxConcurrency = options.concurrency ?? os.cpus().length;
|
|
59
|
+
const limit = pLimit(Math.max(1, maxConcurrency));
|
|
60
|
+
|
|
61
|
+
const events = new EventEmitter();
|
|
62
|
+
events.setMaxListeners(0);
|
|
63
|
+
|
|
64
|
+
let disposed = false;
|
|
65
|
+
let clientCtx = null;
|
|
66
|
+
let fontFaceCss;
|
|
67
|
+
let fontPreloadHtml;
|
|
68
|
+
|
|
69
|
+
if (options.fonts && options.fonts.length > 0) {
|
|
70
|
+
const publicDir = path.join(rootDir, 'public');
|
|
71
|
+
const fontResult = await resolveFonts(options.fonts, publicDir);
|
|
72
|
+
if (fontResult.fontFaceCss) fontFaceCss = fontResult.fontFaceCss;
|
|
73
|
+
if (fontResult.preloadHtml) fontPreloadHtml = fontResult.preloadHtml;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Discover routes up front. Structural changes (new/deleted files) are
|
|
77
|
+
// picked up later via the watcher's re-discovery path (phase 5).
|
|
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
|
+
await fs.mkdir(path.join(outDir, 'server'), { recursive: true });
|
|
88
|
+
await fs.mkdir(path.join(outDir, 'client'), { recursive: true });
|
|
89
|
+
|
|
90
|
+
const manifest = {
|
|
91
|
+
entries: pages.map((page) => buildPageManifestEntry(page, { layouts, middlewares, loadings })),
|
|
92
|
+
layouts: new Map(
|
|
93
|
+
layouts.map((l) => [
|
|
94
|
+
l.dirPath,
|
|
95
|
+
{ srcPath: path.resolve(l.filePath), built: false, stale: false },
|
|
96
|
+
]),
|
|
97
|
+
),
|
|
98
|
+
middlewares: new Map(
|
|
99
|
+
middlewares.map((m) => [
|
|
100
|
+
m.dirPath,
|
|
101
|
+
{ srcPath: path.resolve(m.filePath), built: false, stale: false },
|
|
102
|
+
]),
|
|
103
|
+
),
|
|
104
|
+
version: 0,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (fontPreloadHtml) manifest.fontPreloadHtml = fontPreloadHtml;
|
|
108
|
+
|
|
109
|
+
// In-flight coalescing: when a burst of concurrent requests hits an unbuilt
|
|
110
|
+
// page, only the first triggers a build; the rest await the same promise.
|
|
111
|
+
const inFlightPages = new Map();
|
|
112
|
+
const inFlightLayouts = new Map();
|
|
113
|
+
const inFlightMiddlewares = new Map();
|
|
114
|
+
|
|
115
|
+
function bumpVersion() {
|
|
116
|
+
manifest.version += 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function emit(event, payload) {
|
|
120
|
+
events.emit(event, payload);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Client bundle + CSS + error pages + HMR runtime are built eagerly at
|
|
124
|
+
// startup. They're shared across every request and their compilation cost
|
|
125
|
+
// is bounded (a single esbuild.context for the client, one esbuild.transform
|
|
126
|
+
// for CSS), so there's no win in deferring them.
|
|
127
|
+
async function eagerSharedBuilds() {
|
|
128
|
+
clientCtx = await createClientBuildContext(
|
|
129
|
+
pages,
|
|
130
|
+
layouts,
|
|
131
|
+
outDir,
|
|
132
|
+
clientTarget,
|
|
133
|
+
minify,
|
|
134
|
+
rootDir,
|
|
135
|
+
loadings,
|
|
136
|
+
minify ? 'production' : 'development',
|
|
137
|
+
devMode,
|
|
138
|
+
);
|
|
139
|
+
const [clientResult, cssBundle, errorBundles] = await Promise.all([
|
|
140
|
+
clientCtx.rebuild(),
|
|
141
|
+
buildCssBundle(pagesDir, outDir, minify, stylesPath, fontFaceCss, rootDir),
|
|
142
|
+
buildErrorPageBundles(errorPages, outDir, serverTarget, {
|
|
143
|
+
rootDir,
|
|
144
|
+
devMode,
|
|
145
|
+
minify,
|
|
146
|
+
limit,
|
|
147
|
+
}),
|
|
148
|
+
devMode ? buildHmrRuntimeBundle(outDir) : Promise.resolve(),
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
// Fold client bundle output into the manifest so renderPage() can find
|
|
152
|
+
// navBundle / layoutClientBundles / shared chunks immediately, the same
|
|
153
|
+
// as it would from a disk-backed prod manifest.
|
|
154
|
+
for (const entry of manifest.entries) {
|
|
155
|
+
const clientBundle = clientResult.bundles.get(entry.pattern);
|
|
156
|
+
const navBundle = clientResult.navBundles.get(entry.pattern);
|
|
157
|
+
if (clientBundle) entry.clientBundle = clientBundle;
|
|
158
|
+
if (navBundle) entry.navBundle = navBundle;
|
|
159
|
+
entry.layoutClientBundles = entry.layoutDirs
|
|
160
|
+
.map((d) => clientResult.layoutNavBundles.get(d))
|
|
161
|
+
.filter((v) => v !== undefined);
|
|
162
|
+
entry.loadingClientBundles = entry.loadingDirs
|
|
163
|
+
.map((d) => clientResult.loadingNavBundles.get(d))
|
|
164
|
+
.filter((v) => v !== undefined);
|
|
165
|
+
entry.sharedChunks = clientResult.entryChunks?.get(entry.pattern) ?? [];
|
|
166
|
+
entry.sharedCssChunks = clientResult.entryCssChunks?.get(entry.pattern) ?? [];
|
|
167
|
+
}
|
|
168
|
+
if (cssBundle) manifest.cssBundle = cssBundle;
|
|
169
|
+
if (errorBundles.error) manifest.errorBundle = errorBundles.error;
|
|
170
|
+
if (errorBundles.notFound) manifest.notFoundBundle = errorBundles.notFound;
|
|
171
|
+
bumpVersion();
|
|
172
|
+
emit('update', { type: 'startup', version: manifest.version });
|
|
173
|
+
return { clientMetafile: clientResult.metafile };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const startup = await eagerSharedBuilds();
|
|
177
|
+
|
|
178
|
+
function findEntry(pattern) {
|
|
179
|
+
return manifest.entries.find((e) => e.pattern === pattern) ?? null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function buildPageServer(entry) {
|
|
183
|
+
const outName = patternToFileName(entry.pattern);
|
|
184
|
+
const outFile = path.join(outDir, 'server', `${outName}.js`);
|
|
185
|
+
await buildWithCache({
|
|
186
|
+
rootDir,
|
|
187
|
+
kind: 'server',
|
|
188
|
+
entryPath: entry.srcPath,
|
|
189
|
+
outFile,
|
|
190
|
+
esbuildOptions: jsxEsbuildOptions(entry.srcPath, outFile, serverTarget),
|
|
191
|
+
esbuild: esbuildImpl,
|
|
192
|
+
devMode,
|
|
193
|
+
minify,
|
|
194
|
+
});
|
|
195
|
+
entry.serverBundle = path.relative(outDir, outFile).replace(/\\/g, '/');
|
|
196
|
+
entry.built = true;
|
|
197
|
+
entry.stale = false;
|
|
198
|
+
entry.lastBuiltAt = Date.now();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function buildLayoutServer(dirPath) {
|
|
202
|
+
const layout = manifest.layouts.get(dirPath);
|
|
203
|
+
if (!layout) return;
|
|
204
|
+
const outName = layoutDirToFileName(dirPath);
|
|
205
|
+
const outFile = path.join(outDir, 'server', `_layout_${outName}.js`);
|
|
206
|
+
await buildWithCache({
|
|
207
|
+
rootDir,
|
|
208
|
+
kind: 'layout',
|
|
209
|
+
entryPath: layout.srcPath,
|
|
210
|
+
outFile,
|
|
211
|
+
esbuildOptions: jsxEsbuildOptions(layout.srcPath, outFile, serverTarget),
|
|
212
|
+
esbuild: esbuildImpl,
|
|
213
|
+
devMode,
|
|
214
|
+
minify,
|
|
215
|
+
});
|
|
216
|
+
layout.serverBundle = path.relative(outDir, outFile).replace(/\\/g, '/');
|
|
217
|
+
layout.built = true;
|
|
218
|
+
layout.stale = false;
|
|
219
|
+
layout.lastBuiltAt = Date.now();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function buildMiddlewareServer(dirPath) {
|
|
223
|
+
const mw = manifest.middlewares.get(dirPath);
|
|
224
|
+
if (!mw) return;
|
|
225
|
+
const outName = layoutDirToFileName(dirPath);
|
|
226
|
+
const outFile = path.join(outDir, 'server', `_middleware_${outName}.js`);
|
|
227
|
+
await buildWithCache({
|
|
228
|
+
rootDir,
|
|
229
|
+
kind: 'middleware',
|
|
230
|
+
entryPath: mw.srcPath,
|
|
231
|
+
outFile,
|
|
232
|
+
esbuildOptions: plainEsbuildOptions(mw.srcPath, outFile, serverTarget),
|
|
233
|
+
esbuild: esbuildImpl,
|
|
234
|
+
devMode,
|
|
235
|
+
minify,
|
|
236
|
+
});
|
|
237
|
+
mw.serverBundle = path.relative(outDir, outFile).replace(/\\/g, '/');
|
|
238
|
+
mw.built = true;
|
|
239
|
+
mw.stale = false;
|
|
240
|
+
mw.lastBuiltAt = Date.now();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function ensureLayoutBuilt(dirPath) {
|
|
244
|
+
const layout = manifest.layouts.get(dirPath);
|
|
245
|
+
if (!layout) return Promise.reject(new Error(`Unknown layout dirPath: ${dirPath}`));
|
|
246
|
+
if (layout.built && !layout.stale) return Promise.resolve();
|
|
247
|
+
const existing = inFlightLayouts.get(dirPath);
|
|
248
|
+
if (existing) return existing;
|
|
249
|
+
const task = limit(() => buildLayoutServer(dirPath))
|
|
250
|
+
.then(() => {
|
|
251
|
+
emit('update', { type: 'built', kind: 'layout', dirPath, version: ++manifest.version });
|
|
252
|
+
})
|
|
253
|
+
.finally(() => inFlightLayouts.delete(dirPath));
|
|
254
|
+
inFlightLayouts.set(dirPath, task);
|
|
255
|
+
return task;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function ensureMiddlewareBuilt(dirPath) {
|
|
259
|
+
const mw = manifest.middlewares.get(dirPath);
|
|
260
|
+
if (!mw) return Promise.reject(new Error(`Unknown middleware dirPath: ${dirPath}`));
|
|
261
|
+
if (mw.built && !mw.stale) return Promise.resolve();
|
|
262
|
+
const existing = inFlightMiddlewares.get(dirPath);
|
|
263
|
+
if (existing) return existing;
|
|
264
|
+
const task = limit(() => buildMiddlewareServer(dirPath))
|
|
265
|
+
.then(() => {
|
|
266
|
+
emit('update', { type: 'built', kind: 'middleware', dirPath, version: ++manifest.version });
|
|
267
|
+
})
|
|
268
|
+
.finally(() => inFlightMiddlewares.delete(dirPath));
|
|
269
|
+
inFlightMiddlewares.set(dirPath, task);
|
|
270
|
+
return task;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function ensurePageBuilt(pattern) {
|
|
274
|
+
const entry = findEntry(pattern);
|
|
275
|
+
if (!entry) return Promise.reject(new Error(`Unknown page pattern: ${pattern}`));
|
|
276
|
+
if (entry.built && !entry.stale) {
|
|
277
|
+
// Still need to ensure layouts/middlewares stay current; they can be
|
|
278
|
+
// invalidated independently of the page itself.
|
|
279
|
+
return ensureChainBuilt(entry);
|
|
280
|
+
}
|
|
281
|
+
const existing = inFlightPages.get(pattern);
|
|
282
|
+
if (existing) return existing;
|
|
283
|
+
const task = Promise.all([
|
|
284
|
+
limit(() => buildPageServer(entry)),
|
|
285
|
+
ensureChainBuilt(entry),
|
|
286
|
+
])
|
|
287
|
+
.then(() => {
|
|
288
|
+
entry.layoutServerBundles = entry.layoutDirs
|
|
289
|
+
.map((d) => manifest.layouts.get(d)?.serverBundle)
|
|
290
|
+
.filter((v) => v !== undefined);
|
|
291
|
+
entry.middlewareServerBundles = entry.middlewareDirs
|
|
292
|
+
.map((d) => manifest.middlewares.get(d)?.serverBundle)
|
|
293
|
+
.filter((v) => v !== undefined);
|
|
294
|
+
emit('update', { type: 'built', kind: 'page', pattern, version: ++manifest.version });
|
|
295
|
+
})
|
|
296
|
+
.finally(() => inFlightPages.delete(pattern));
|
|
297
|
+
inFlightPages.set(pattern, task);
|
|
298
|
+
return task;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function ensureChainBuilt(entry) {
|
|
302
|
+
const jobs = [];
|
|
303
|
+
for (const dirPath of entry.layoutDirs) jobs.push(ensureLayoutBuilt(dirPath));
|
|
304
|
+
for (const dirPath of entry.middlewareDirs) jobs.push(ensureMiddlewareBuilt(dirPath));
|
|
305
|
+
return Promise.all(jobs).then(() => {
|
|
306
|
+
// Re-stitch the page's bundle list after the chain settles so any
|
|
307
|
+
// layout that just finished building is reflected on the entry.
|
|
308
|
+
entry.layoutServerBundles = entry.layoutDirs
|
|
309
|
+
.map((d) => manifest.layouts.get(d)?.serverBundle)
|
|
310
|
+
.filter((v) => v !== undefined);
|
|
311
|
+
entry.middlewareServerBundles = entry.middlewareDirs
|
|
312
|
+
.map((d) => manifest.middlewares.get(d)?.serverBundle)
|
|
313
|
+
.filter((v) => v !== undefined);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Mark every manifest entry affected by a source change as stale. Does not
|
|
318
|
+
// trigger rebuilds directly — the next `ensurePageBuilt()` call (driven by
|
|
319
|
+
// an incoming request) handles the actual work. Structural changes
|
|
320
|
+
// (add/remove files) are not handled here; the watcher will re-init the
|
|
321
|
+
// context or extend the manifest in phase 5.
|
|
322
|
+
function invalidate(filePath) {
|
|
323
|
+
const affected = mapFileToAffected(filePath, manifest);
|
|
324
|
+
let touched = false;
|
|
325
|
+
for (const pattern of affected.pages) {
|
|
326
|
+
const entry = findEntry(pattern);
|
|
327
|
+
if (entry) {
|
|
328
|
+
entry.stale = true;
|
|
329
|
+
entry.built = false;
|
|
330
|
+
touched = true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for (const dirPath of affected.layouts) {
|
|
334
|
+
const layout = manifest.layouts.get(dirPath);
|
|
335
|
+
if (layout) {
|
|
336
|
+
layout.stale = true;
|
|
337
|
+
layout.built = false;
|
|
338
|
+
touched = true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
for (const dirPath of affected.middlewares) {
|
|
342
|
+
const mw = manifest.middlewares.get(dirPath);
|
|
343
|
+
if (mw) {
|
|
344
|
+
mw.stale = true;
|
|
345
|
+
mw.built = false;
|
|
346
|
+
touched = true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (touched) {
|
|
350
|
+
bumpVersion();
|
|
351
|
+
emit('update', { type: 'invalidate', filePath, affected, version: manifest.version });
|
|
352
|
+
}
|
|
353
|
+
return { touched, affected };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Structural rediscovery ─────────────────────────────────────────────
|
|
357
|
+
// `invalidate()` handles content edits to known files; `rediscover()`
|
|
358
|
+
// handles add/delete of pages, layouts, and middlewares. Re-runs the same
|
|
359
|
+
// discovery helpers used at startup, diffs the result against the in-memory
|
|
360
|
+
// manifest, and applies adds/removes plus a cascade re-resolution of every
|
|
361
|
+
// surviving entry's chain (added/removed layouts and middlewares mutate
|
|
362
|
+
// descendants' chains). The client esbuild context is torn down and
|
|
363
|
+
// recreated when the page or layout set changes — middleware-only changes
|
|
364
|
+
// don't touch the client bundle. Concurrent calls coalesce to a single
|
|
365
|
+
// in-flight discovery so a burst (e.g. `git checkout` swap) triggers one
|
|
366
|
+
// pass, not N.
|
|
367
|
+
let inFlightRediscover = null;
|
|
368
|
+
|
|
369
|
+
async function rediscover() {
|
|
370
|
+
if (disposed) throw new Error('Lazy pages context is disposed');
|
|
371
|
+
if (inFlightRediscover) return inFlightRediscover;
|
|
372
|
+
const task = (async () => {
|
|
373
|
+
const [
|
|
374
|
+
nextPages,
|
|
375
|
+
nextLayouts,
|
|
376
|
+
nextLoadings,
|
|
377
|
+
nextMiddlewares,
|
|
378
|
+
] = await Promise.all([
|
|
379
|
+
discoverPages(pagesDir),
|
|
380
|
+
discoverLayouts(pagesDir),
|
|
381
|
+
discoverLoadings(pagesDir),
|
|
382
|
+
discoverPageMiddleware(pagesDir),
|
|
383
|
+
]);
|
|
384
|
+
|
|
385
|
+
const result = {
|
|
386
|
+
added: { pages: [], layouts: [], middlewares: [] },
|
|
387
|
+
removed: { pages: [], layouts: [], middlewares: [] },
|
|
388
|
+
staleByCascade: [],
|
|
389
|
+
clientRebuilt: false,
|
|
390
|
+
version: manifest.version,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// ── Diff ─────────────────────────────────────────────────────────
|
|
394
|
+
const currentPagePatterns = new Set(manifest.entries.map((e) => e.pattern));
|
|
395
|
+
const nextPagePatterns = new Set(nextPages.map((p) => p.pattern));
|
|
396
|
+
const addedPages = nextPages.filter((p) => !currentPagePatterns.has(p.pattern));
|
|
397
|
+
const removedPages = manifest.entries.filter((e) => !nextPagePatterns.has(e.pattern));
|
|
398
|
+
|
|
399
|
+
const currentLayoutDirs = new Set(manifest.layouts.keys());
|
|
400
|
+
const nextLayoutDirs = new Set(nextLayouts.map((l) => l.dirPath));
|
|
401
|
+
const addedLayouts = nextLayouts.filter((l) => !currentLayoutDirs.has(l.dirPath));
|
|
402
|
+
const removedLayouts = [...currentLayoutDirs].filter((d) => !nextLayoutDirs.has(d));
|
|
403
|
+
|
|
404
|
+
const currentMwDirs = new Set(manifest.middlewares.keys());
|
|
405
|
+
const nextMwDirs = new Set(nextMiddlewares.map((m) => m.dirPath));
|
|
406
|
+
const addedMiddlewares = nextMiddlewares.filter((m) => !currentMwDirs.has(m.dirPath));
|
|
407
|
+
const removedMiddlewares = [...currentMwDirs].filter((d) => !nextMwDirs.has(d));
|
|
408
|
+
|
|
409
|
+
const noOp =
|
|
410
|
+
addedPages.length === 0 &&
|
|
411
|
+
removedPages.length === 0 &&
|
|
412
|
+
addedLayouts.length === 0 &&
|
|
413
|
+
removedLayouts.length === 0 &&
|
|
414
|
+
addedMiddlewares.length === 0 &&
|
|
415
|
+
removedMiddlewares.length === 0;
|
|
416
|
+
if (noOp) return result;
|
|
417
|
+
|
|
418
|
+
// ── Apply layout add/remove ──────────────────────────────────────
|
|
419
|
+
for (const layout of addedLayouts) {
|
|
420
|
+
manifest.layouts.set(layout.dirPath, {
|
|
421
|
+
srcPath: path.resolve(layout.filePath),
|
|
422
|
+
built: false,
|
|
423
|
+
stale: false,
|
|
424
|
+
});
|
|
425
|
+
result.added.layouts.push(layout.dirPath);
|
|
426
|
+
}
|
|
427
|
+
for (const dirPath of removedLayouts) {
|
|
428
|
+
manifest.layouts.delete(dirPath);
|
|
429
|
+
inFlightLayouts.delete(dirPath);
|
|
430
|
+
result.removed.layouts.push(dirPath);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── Apply middleware add/remove ──────────────────────────────────
|
|
434
|
+
for (const mw of addedMiddlewares) {
|
|
435
|
+
manifest.middlewares.set(mw.dirPath, {
|
|
436
|
+
srcPath: path.resolve(mw.filePath),
|
|
437
|
+
built: false,
|
|
438
|
+
stale: false,
|
|
439
|
+
});
|
|
440
|
+
result.added.middlewares.push(mw.dirPath);
|
|
441
|
+
}
|
|
442
|
+
for (const dirPath of removedMiddlewares) {
|
|
443
|
+
manifest.middlewares.delete(dirPath);
|
|
444
|
+
inFlightMiddlewares.delete(dirPath);
|
|
445
|
+
result.removed.middlewares.push(dirPath);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ── Apply page add/remove ────────────────────────────────────────
|
|
449
|
+
const addedPagePatterns = new Set();
|
|
450
|
+
for (const page of addedPages) {
|
|
451
|
+
manifest.entries.push(
|
|
452
|
+
buildPageManifestEntry(page, {
|
|
453
|
+
layouts: nextLayouts,
|
|
454
|
+
middlewares: nextMiddlewares,
|
|
455
|
+
loadings: nextLoadings,
|
|
456
|
+
}),
|
|
457
|
+
);
|
|
458
|
+
result.added.pages.push(page.pattern);
|
|
459
|
+
addedPagePatterns.add(page.pattern);
|
|
460
|
+
}
|
|
461
|
+
for (const removed of removedPages) {
|
|
462
|
+
const idx = manifest.entries.findIndex((e) => e.pattern === removed.pattern);
|
|
463
|
+
if (idx >= 0) manifest.entries.splice(idx, 1);
|
|
464
|
+
inFlightPages.delete(removed.pattern);
|
|
465
|
+
result.removed.pages.push(removed.pattern);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Cascade: re-resolve chains for surviving entries; mark stale on diff
|
|
469
|
+
for (const entry of manifest.entries) {
|
|
470
|
+
if (addedPagePatterns.has(entry.pattern)) continue; // freshly built, already current
|
|
471
|
+
const newLayoutDirs = resolveLayoutChain(entry.pattern, nextLayouts).map((l) => l.dirPath);
|
|
472
|
+
const newMwDirs = resolvePageMiddlewareChain(entry.pattern, nextMiddlewares).map(
|
|
473
|
+
(m) => m.dirPath,
|
|
474
|
+
);
|
|
475
|
+
const newLoadingDirs = resolveLoadingChain(entry.pattern, nextLoadings).map(
|
|
476
|
+
(l) => l.dirPath,
|
|
477
|
+
);
|
|
478
|
+
const changed =
|
|
479
|
+
!arraysEqual(entry.layoutDirs, newLayoutDirs) ||
|
|
480
|
+
!arraysEqual(entry.middlewareDirs, newMwDirs) ||
|
|
481
|
+
!arraysEqual(entry.loadingDirs, newLoadingDirs);
|
|
482
|
+
if (changed) {
|
|
483
|
+
entry.layoutDirs = newLayoutDirs;
|
|
484
|
+
entry.middlewareDirs = newMwDirs;
|
|
485
|
+
entry.loadingDirs = newLoadingDirs;
|
|
486
|
+
entry.stale = true;
|
|
487
|
+
entry.built = false;
|
|
488
|
+
result.staleByCascade.push(entry.pattern);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ── Client bundle rebuild on page or layout structural changes ───
|
|
493
|
+
const needClientRebuild =
|
|
494
|
+
result.added.pages.length > 0 ||
|
|
495
|
+
result.removed.pages.length > 0 ||
|
|
496
|
+
result.added.layouts.length > 0 ||
|
|
497
|
+
result.removed.layouts.length > 0;
|
|
498
|
+
if (needClientRebuild) {
|
|
499
|
+
if (clientCtx) {
|
|
500
|
+
try {
|
|
501
|
+
await clientCtx.dispose();
|
|
502
|
+
} catch {
|
|
503
|
+
/* ignore — we're replacing it anyway */
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
clientCtx = await createClientBuildContext(
|
|
507
|
+
nextPages,
|
|
508
|
+
nextLayouts,
|
|
509
|
+
outDir,
|
|
510
|
+
clientTarget,
|
|
511
|
+
minify,
|
|
512
|
+
rootDir,
|
|
513
|
+
nextLoadings,
|
|
514
|
+
minify ? 'production' : 'development',
|
|
515
|
+
devMode,
|
|
516
|
+
);
|
|
517
|
+
const clientResult = await clientCtx.rebuild();
|
|
518
|
+
for (const entry of manifest.entries) {
|
|
519
|
+
const clientBundle = clientResult.bundles.get(entry.pattern);
|
|
520
|
+
const navBundle = clientResult.navBundles.get(entry.pattern);
|
|
521
|
+
if (clientBundle) entry.clientBundle = clientBundle;
|
|
522
|
+
if (navBundle) entry.navBundle = navBundle;
|
|
523
|
+
entry.layoutClientBundles = entry.layoutDirs
|
|
524
|
+
.map((d) => clientResult.layoutNavBundles.get(d))
|
|
525
|
+
.filter((v) => v !== undefined);
|
|
526
|
+
entry.loadingClientBundles = entry.loadingDirs
|
|
527
|
+
.map((d) => clientResult.loadingNavBundles.get(d))
|
|
528
|
+
.filter((v) => v !== undefined);
|
|
529
|
+
entry.sharedChunks = clientResult.entryChunks?.get(entry.pattern) ?? [];
|
|
530
|
+
entry.sharedCssChunks = clientResult.entryCssChunks?.get(entry.pattern) ?? [];
|
|
531
|
+
}
|
|
532
|
+
result.clientRebuilt = true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ── Version bump + event emission ────────────────────────────────
|
|
536
|
+
bumpVersion();
|
|
537
|
+
result.version = manifest.version;
|
|
538
|
+
for (const pattern of result.added.pages) {
|
|
539
|
+
emit('update', { type: 'add-page', pattern, version: manifest.version });
|
|
540
|
+
}
|
|
541
|
+
for (const pattern of result.removed.pages) {
|
|
542
|
+
emit('update', { type: 'remove-page', pattern, version: manifest.version });
|
|
543
|
+
}
|
|
544
|
+
for (const dirPath of result.added.layouts) {
|
|
545
|
+
emit('update', { type: 'add-layout', dirPath, version: manifest.version });
|
|
546
|
+
}
|
|
547
|
+
for (const dirPath of result.removed.layouts) {
|
|
548
|
+
emit('update', { type: 'remove-layout', dirPath, version: manifest.version });
|
|
549
|
+
}
|
|
550
|
+
for (const dirPath of result.added.middlewares) {
|
|
551
|
+
emit('update', { type: 'add-middleware', dirPath, version: manifest.version });
|
|
552
|
+
}
|
|
553
|
+
for (const dirPath of result.removed.middlewares) {
|
|
554
|
+
emit('update', { type: 'remove-middleware', dirPath, version: manifest.version });
|
|
555
|
+
}
|
|
556
|
+
if (result.clientRebuilt) {
|
|
557
|
+
emit('update', { type: 'client-rebuilt', version: manifest.version });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return result;
|
|
561
|
+
})().finally(() => {
|
|
562
|
+
inFlightRediscover = null;
|
|
563
|
+
});
|
|
564
|
+
inFlightRediscover = task;
|
|
565
|
+
return task;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function arraysEqual(a, b) {
|
|
569
|
+
if (a === b) return true;
|
|
570
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
571
|
+
for (let i = 0; i < a.length; i++) {
|
|
572
|
+
if (a[i] !== b[i]) return false;
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function on(event, cb) {
|
|
578
|
+
events.on(event, cb);
|
|
579
|
+
return () => events.off(event, cb);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Public, serializable snapshot for the `/_arcway/manifest.json` endpoint
|
|
583
|
+
// (phase 4). Strips absolute source paths and internal bookkeeping so a
|
|
584
|
+
// dev overlay can't infer disk layout.
|
|
585
|
+
function getManifestJson() {
|
|
586
|
+
return {
|
|
587
|
+
version: manifest.version,
|
|
588
|
+
entries: manifest.entries.map((entry) => ({
|
|
589
|
+
pattern: entry.pattern,
|
|
590
|
+
built: entry.built,
|
|
591
|
+
stale: entry.stale,
|
|
592
|
+
...(entry.lastBuiltAt ? { lastBuiltAt: entry.lastBuiltAt } : {}),
|
|
593
|
+
})),
|
|
594
|
+
layouts: Array.from(manifest.layouts.entries()).map(([dirPath, l]) => ({
|
|
595
|
+
dirPath,
|
|
596
|
+
built: l.built,
|
|
597
|
+
stale: l.stale,
|
|
598
|
+
...(l.lastBuiltAt ? { lastBuiltAt: l.lastBuiltAt } : {}),
|
|
599
|
+
})),
|
|
600
|
+
middlewares: Array.from(manifest.middlewares.entries()).map(([dirPath, m]) => ({
|
|
601
|
+
dirPath,
|
|
602
|
+
built: m.built,
|
|
603
|
+
stale: m.stale,
|
|
604
|
+
...(m.lastBuiltAt ? { lastBuiltAt: m.lastBuiltAt } : {}),
|
|
605
|
+
})),
|
|
606
|
+
...(manifest.errorBundle ? { errorBundle: manifest.errorBundle } : {}),
|
|
607
|
+
...(manifest.notFoundBundle ? { notFoundBundle: manifest.notFoundBundle } : {}),
|
|
608
|
+
...(manifest.cssBundle ? { cssBundle: manifest.cssBundle } : {}),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function dispose() {
|
|
613
|
+
if (disposed) return;
|
|
614
|
+
disposed = true;
|
|
615
|
+
if (clientCtx) {
|
|
616
|
+
await clientCtx.dispose();
|
|
617
|
+
clientCtx = null;
|
|
618
|
+
}
|
|
619
|
+
events.removeAllListeners();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
manifest,
|
|
624
|
+
outDir,
|
|
625
|
+
rootDir,
|
|
626
|
+
ensurePageBuilt,
|
|
627
|
+
ensureLayoutBuilt,
|
|
628
|
+
ensureMiddlewareBuilt,
|
|
629
|
+
invalidate,
|
|
630
|
+
rediscover,
|
|
631
|
+
dispose,
|
|
632
|
+
on,
|
|
633
|
+
getManifestJson,
|
|
634
|
+
get clientMetafile() {
|
|
635
|
+
return startup.clientMetafile;
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export { createLazyPagesContext };
|