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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createPagesBuildContext } from './build-context.js';
|
|
2
1
|
import { createPagesHandler } from './handler.js';
|
|
2
|
+
import { createLazyPagesContext } from './lazy-context.js';
|
|
3
3
|
import { createPagesWatcher } from './watcher.js';
|
|
4
4
|
|
|
5
5
|
class PagesRouter {
|
|
@@ -10,7 +10,7 @@ class PagesRouter {
|
|
|
10
10
|
fileWatcher;
|
|
11
11
|
handler = null;
|
|
12
12
|
watcher = null;
|
|
13
|
-
|
|
13
|
+
lazyContext = null;
|
|
14
14
|
|
|
15
15
|
constructor(config, { rootDir, log, mode, fileWatcher, appContext }) {
|
|
16
16
|
this.config = config;
|
|
@@ -25,24 +25,22 @@ class PagesRouter {
|
|
|
25
25
|
const { config, rootDir, log, mode, fileWatcher } = this;
|
|
26
26
|
const isDev = mode === 'development';
|
|
27
27
|
|
|
28
|
-
let initialClientMetafile;
|
|
29
28
|
if (isDev) {
|
|
30
|
-
// Dev mode
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
29
|
+
// Dev mode: `createLazyPagesContext()` builds only the shared bits
|
|
30
|
+
// (client bundle, CSS, error pages, HMR runtime) at startup. Per-page /
|
|
31
|
+
// layout / middleware server bundles compile on first request via
|
|
32
|
+
// `ensurePageBuilt()` threaded through the handler. The watcher
|
|
33
|
+
// translates file changes into per-entry invalidations on this context.
|
|
34
34
|
try {
|
|
35
|
-
this.
|
|
35
|
+
this.lazyContext = await createLazyPagesContext({
|
|
36
36
|
rootDir,
|
|
37
37
|
pagesDir: config.pages.dir,
|
|
38
38
|
fonts: config.pages?.fonts,
|
|
39
39
|
minify: false,
|
|
40
40
|
devMode: true,
|
|
41
41
|
});
|
|
42
|
-
const result = await this.pagesBuildContext.rebuild();
|
|
43
|
-
initialClientMetafile = result.clientMetafile;
|
|
44
42
|
} catch (err) {
|
|
45
|
-
log.error('Pages
|
|
43
|
+
log.error('Pages lazy context init failed', { error: String(err) });
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
|
|
@@ -51,23 +49,20 @@ class PagesRouter {
|
|
|
51
49
|
session: config.session,
|
|
52
50
|
appContext: this.appContext,
|
|
53
51
|
mode,
|
|
52
|
+
...(this.lazyContext ? { lazyContext: this.lazyContext } : {}),
|
|
54
53
|
});
|
|
55
54
|
|
|
56
55
|
if (!this.handler) return;
|
|
57
56
|
|
|
58
57
|
log.info('Pages: SSR enabled');
|
|
59
58
|
|
|
60
|
-
if (isDev && fileWatcher) {
|
|
59
|
+
if (isDev && fileWatcher && this.lazyContext) {
|
|
61
60
|
this.watcher = createPagesWatcher({
|
|
62
|
-
|
|
63
|
-
handler: this.handler,
|
|
61
|
+
lazyContext: this.lazyContext,
|
|
64
62
|
log,
|
|
65
|
-
fonts: config.pages?.fonts,
|
|
66
|
-
initialClientMetafile,
|
|
67
63
|
fileWatcher,
|
|
68
|
-
pagesBuildContext: this.pagesBuildContext,
|
|
69
64
|
});
|
|
70
|
-
log.info('Pages: watching for changes (
|
|
65
|
+
log.info('Pages: watching for changes (per-entry invalidate)');
|
|
71
66
|
}
|
|
72
67
|
}
|
|
73
68
|
|
|
@@ -81,9 +76,9 @@ class PagesRouter {
|
|
|
81
76
|
await this.watcher.close();
|
|
82
77
|
this.watcher = null;
|
|
83
78
|
}
|
|
84
|
-
if (this.
|
|
85
|
-
await this.
|
|
86
|
-
this.
|
|
79
|
+
if (this.lazyContext) {
|
|
80
|
+
await this.lazyContext.dispose();
|
|
81
|
+
this.lazyContext = null;
|
|
87
82
|
}
|
|
88
83
|
}
|
|
89
84
|
}
|
package/server/pages/watcher.js
CHANGED
|
@@ -1,88 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// Dev-only watcher that translates file-system events into per-entry
|
|
2
|
+
// invalidations or structural rediscovery on the lazy pages context.
|
|
3
|
+
//
|
|
4
|
+
// Change events (file edits) flip the affected manifest entries to
|
|
5
|
+
// `stale: true` and bump the version; the next request for that route
|
|
6
|
+
// drives the actual rebuild through `ensurePageBuilt()`. There is no
|
|
7
|
+
// serialization flag here -- lazy builds are per-entry and already
|
|
8
|
+
// coalesced inside the context.
|
|
9
|
+
//
|
|
10
|
+
// Structural events (`add` / `unlink` of page / layout / middleware
|
|
11
|
+
// files) trigger a single `lazyContext.rediscover()` per debounced batch.
|
|
12
|
+
// Rediscover diffs the manifest against discovery output, applies
|
|
13
|
+
// adds/removes, cascades stale flags through layout/middleware chains,
|
|
14
|
+
// and tears down + recreates the client esbuild context when the page
|
|
15
|
+
// or layout set changes.
|
|
4
16
|
|
|
5
17
|
function createPagesWatcher(options) {
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let pendingStructuralChange = false;
|
|
10
|
-
let prevClientMetafile = options.initialClientMetafile;
|
|
11
|
-
const outDir = path.resolve(rootDir, '.build/pages');
|
|
12
|
-
|
|
13
|
-
async function runBuild() {
|
|
14
|
-
if (pagesBuildContext) return pagesBuildContext.rebuild();
|
|
15
|
-
return buildPages({ rootDir, fonts, minify: false, devMode: true });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function rebuild(isStructuralChange) {
|
|
19
|
-
if (building) {
|
|
20
|
-
pendingRebuild = true;
|
|
21
|
-
if (isStructuralChange) pendingStructuralChange = true;
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
building = true;
|
|
25
|
-
try {
|
|
26
|
-
const start = Date.now();
|
|
27
|
-
const result = await runBuild();
|
|
28
|
-
handler.reload();
|
|
29
|
-
const elapsed = Date.now() - start;
|
|
30
|
-
if (handler.emitEvent && prevClientMetafile && result.clientMetafile && !isStructuralChange) {
|
|
31
|
-
const diff = diffClientMetafiles(prevClientMetafile, result.clientMetafile, outDir);
|
|
32
|
-
const hmrModules = [...diff.changedNavBundles, ...diff.changedHydrationBundles];
|
|
33
|
-
if (diff.needsFullReload) {
|
|
34
|
-
handler.emitEvent({ type: 'reload' });
|
|
35
|
-
log.info(`Pages rebuilt in ${elapsed}ms (full reload)`);
|
|
36
|
-
} else if (hmrModules.length > 0) {
|
|
37
|
-
const manifestJson = handler.getClientManifestJson?.();
|
|
38
|
-
handler.emitEvent({
|
|
39
|
-
type: 'hmr-update',
|
|
40
|
-
modules: hmrModules,
|
|
41
|
-
timestamp: Date.now(),
|
|
42
|
-
...(manifestJson ? { manifest: manifestJson } : {}),
|
|
43
|
-
});
|
|
44
|
-
log.info(
|
|
45
|
-
`Pages rebuilt in ${elapsed}ms (HMR: ${hmrModules.length} module(s))`,
|
|
46
|
-
);
|
|
47
|
-
} else if (diff.changedCssBundle) {
|
|
48
|
-
handler.emitEvent({
|
|
49
|
-
type: 'css-update',
|
|
50
|
-
cssBundle: diff.changedCssBundle,
|
|
51
|
-
timestamp: Date.now(),
|
|
52
|
-
});
|
|
53
|
-
log.info(`Pages rebuilt in ${elapsed}ms (CSS update)`);
|
|
54
|
-
} else {
|
|
55
|
-
log.info(`Pages rebuilt in ${elapsed}ms (no client changes)`);
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
if (handler.emitEvent) {
|
|
59
|
-
handler.emitEvent({ type: 'reload' });
|
|
60
|
-
}
|
|
61
|
-
log.info(`Pages rebuilt in ${elapsed}ms`);
|
|
62
|
-
}
|
|
63
|
-
prevClientMetafile = result.clientMetafile;
|
|
64
|
-
} catch (err) {
|
|
65
|
-
log.error('Pages rebuild failed', { error: String(err) });
|
|
66
|
-
} finally {
|
|
67
|
-
building = false;
|
|
68
|
-
if (pendingRebuild) {
|
|
69
|
-
pendingRebuild = false;
|
|
70
|
-
const structural = pendingStructuralChange;
|
|
71
|
-
pendingStructuralChange = false;
|
|
72
|
-
await rebuild(structural);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
18
|
+
const { lazyContext, log, fileWatcher } = options;
|
|
19
|
+
if (!lazyContext) {
|
|
20
|
+
throw new Error('createPagesWatcher requires a lazyContext');
|
|
75
21
|
}
|
|
76
22
|
|
|
77
23
|
fileWatcher.subscribe('pages', {
|
|
78
24
|
extensions: ['.tsx', '.jsx', '.ts', '.js', '.css'],
|
|
79
25
|
debounceMs: 150,
|
|
80
|
-
onChange(events) {
|
|
81
|
-
const
|
|
26
|
+
async onChange(events) {
|
|
27
|
+
const hasStructural = events.some((e) => e.event === 'add' || e.event === 'unlink');
|
|
28
|
+
if (hasStructural) {
|
|
29
|
+
try {
|
|
30
|
+
const result = await lazyContext.rediscover();
|
|
31
|
+
const summary = formatRediscover(result);
|
|
32
|
+
if (summary) log.info(`Pages: rediscovered (${summary})`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
log.error(`Pages: rediscover failed — ${String(err)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
82
37
|
for (const e of events) {
|
|
83
|
-
|
|
38
|
+
if (e.event !== 'change') continue;
|
|
39
|
+
const result = lazyContext.invalidate(e.path);
|
|
40
|
+
if (result.touched) {
|
|
41
|
+
log.info(`Pages: invalidated ${e.relativePath} (${formatAffected(result.affected)})`);
|
|
42
|
+
}
|
|
84
43
|
}
|
|
85
|
-
return rebuild(isStructural);
|
|
86
44
|
},
|
|
87
45
|
});
|
|
88
46
|
|
|
@@ -93,4 +51,29 @@ function createPagesWatcher(options) {
|
|
|
93
51
|
};
|
|
94
52
|
}
|
|
95
53
|
|
|
54
|
+
function formatAffected(affected) {
|
|
55
|
+
const parts = [];
|
|
56
|
+
if (affected.pages.length) parts.push(`${affected.pages.length} page(s)`);
|
|
57
|
+
if (affected.layouts.length) parts.push(`${affected.layouts.length} layout(s)`);
|
|
58
|
+
if (affected.middlewares.length) parts.push(`${affected.middlewares.length} middleware(s)`);
|
|
59
|
+
return parts.length ? parts.join(', ') : 'no entries';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatRediscover(result) {
|
|
63
|
+
const parts = [];
|
|
64
|
+
const { added, removed, staleByCascade, clientRebuilt } = result;
|
|
65
|
+
if (added.pages.length || removed.pages.length) {
|
|
66
|
+
parts.push(`pages +${added.pages.length}/-${removed.pages.length}`);
|
|
67
|
+
}
|
|
68
|
+
if (added.layouts.length || removed.layouts.length) {
|
|
69
|
+
parts.push(`layouts +${added.layouts.length}/-${removed.layouts.length}`);
|
|
70
|
+
}
|
|
71
|
+
if (added.middlewares.length || removed.middlewares.length) {
|
|
72
|
+
parts.push(`middlewares +${added.middlewares.length}/-${removed.middlewares.length}`);
|
|
73
|
+
}
|
|
74
|
+
if (staleByCascade.length) parts.push(`${staleByCascade.length} cascaded stale`);
|
|
75
|
+
if (clientRebuilt) parts.push('client rebuilt');
|
|
76
|
+
return parts.join(', ');
|
|
77
|
+
}
|
|
78
|
+
|
|
96
79
|
export { createPagesWatcher };
|
package/server/watcher.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { watch } from 'chokidar';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
function startWatcher(options) {
|
|
4
|
-
const dirs = options.dirs ?? [
|
|
5
|
-
|
|
4
|
+
const dirs = options.dirs ?? [
|
|
5
|
+
'api',
|
|
6
|
+
'pages',
|
|
7
|
+
'components',
|
|
8
|
+
'hooks',
|
|
9
|
+
'lib',
|
|
10
|
+
'listeners',
|
|
11
|
+
'jobs',
|
|
12
|
+
'packages',
|
|
13
|
+
'server',
|
|
14
|
+
'templates',
|
|
15
|
+
'migrations',
|
|
16
|
+
'seeds',
|
|
17
|
+
];
|
|
18
|
+
const extensions = new Set(options.extensions ?? ['.js', '.jsx', '.ts', '.tsx', '.css']);
|
|
6
19
|
const debounceMs = options.debounceMs ?? 300;
|
|
7
20
|
const watchPaths = dirs.map((dir) => path.join(options.rootDir, dir));
|
|
8
21
|
let debounceTimer = null;
|
package/server/ws/ws-router.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Server as SocketIOServer } from 'socket.io';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { registerWsServer, unregisterWsServer } from './registry.js';
|
|
4
4
|
import { parseCookies, resolveSession, flattenHeaders } from '../session/helpers.js';
|
|
5
|
+
import { getMiddlewareForRoute } from '../router/middleware.js';
|
|
5
6
|
import { toErrorMessage } from '../helpers.js';
|
|
6
7
|
|
|
7
8
|
class WsRouter {
|
|
@@ -199,9 +200,38 @@ class WsRouter {
|
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
const reqInfo = this._buildRequestInfo(client, { ...wsMsg, method: 'GET' }, params);
|
|
203
|
+
const ctx = this._apiRouter.buildCtx(reqInfo);
|
|
204
|
+
|
|
205
|
+
// Run api/_middleware.js chain BEFORE invoking route.config.ws(). Without
|
|
206
|
+
// this, ws() side effects (listener registration, resource allocation,
|
|
207
|
+
// initial emits) fire even when auth middleware would have rejected the
|
|
208
|
+
// subscribe. Matches the gating already in place for _handleMethodCall,
|
|
209
|
+
// which routes through executeRoute() (middleware-first).
|
|
210
|
+
const middlewareFns = getMiddlewareForRoute(this._apiRouter.middleware, route.pattern, 'GET');
|
|
211
|
+
for (const mw of middlewareFns) {
|
|
212
|
+
let result;
|
|
213
|
+
try {
|
|
214
|
+
result = await mw(ctx);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
this._log.error(`WS subscribe middleware error for ${msgPath}`, {
|
|
217
|
+
error: toErrorMessage(err),
|
|
218
|
+
});
|
|
219
|
+
this._send(client, {
|
|
220
|
+
path: msgPath,
|
|
221
|
+
error: { code: 'MIDDLEWARE_ERROR', message: 'Middleware error' },
|
|
222
|
+
status: 500,
|
|
223
|
+
id: wsMsg.id,
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (result !== undefined && result !== null) {
|
|
228
|
+
this._send(client, { path: msgPath, ...result, id: wsMsg.id });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
202
233
|
let cleanup;
|
|
203
234
|
try {
|
|
204
|
-
const ctx = this._apiRouter.buildCtx(reqInfo);
|
|
205
235
|
cleanup = await route.config.ws(ctx);
|
|
206
236
|
} catch (err) {
|
|
207
237
|
this._log.error(`WS subscribe error for ${msgPath}`, {
|
|
@@ -221,7 +251,20 @@ class WsRouter {
|
|
|
221
251
|
req: reqInfo,
|
|
222
252
|
});
|
|
223
253
|
|
|
224
|
-
|
|
254
|
+
// Middleware already ran above; invoke the handler directly to avoid
|
|
255
|
+
// executing the chain a second time via executeRoute().
|
|
256
|
+
let response;
|
|
257
|
+
try {
|
|
258
|
+
response = await route.config.handler(ctx);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
this._log.error(`WS subscribe handler error for GET ${route.pattern}`, {
|
|
261
|
+
error: toErrorMessage(err),
|
|
262
|
+
});
|
|
263
|
+
response = {
|
|
264
|
+
status: 500,
|
|
265
|
+
error: { code: 'HANDLER_ERROR', message: 'An internal error occurred' },
|
|
266
|
+
};
|
|
267
|
+
}
|
|
225
268
|
this._send(client, { path: msgPath, ...response, id: wsMsg.id });
|
|
226
269
|
}
|
|
227
270
|
|