arcway 0.1.20 → 0.1.22

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.
@@ -6,6 +6,7 @@ import { compilePattern, sortBySpecificity, matchPattern } from '../router/route
6
6
  import { parseCookies, resolveSession, flattenHeaders } from '../session/helpers.js';
7
7
  import {
8
8
  buildCssLinkTag,
9
+ buildRouteCssLinkTags,
9
10
  buildScriptTags,
10
11
  loadComponent,
11
12
  renderErrorPage,
@@ -13,6 +14,7 @@ import {
13
14
  syncReactInternals,
14
15
  } from './ssr.js';
15
16
  import { MIME_TYPES } from './static.js';
17
+ import { createArcwayDevEndpoint } from './arcway-endpoint.js';
16
18
  function createPagesHandler(options) {
17
19
  const rootDir = options.rootDir;
18
20
  const outDir = path.resolve(rootDir, options.outDir ?? '.build/pages');
@@ -21,7 +23,10 @@ function createPagesHandler(options) {
21
23
  const appContext = options.appContext ?? null;
22
24
  const mode = options.mode ?? 'production';
23
25
  const devMode = mode === 'development';
24
- if (!fs.existsSync(manifestPath)) {
26
+ const lazyContext = options.lazyContext ?? null;
27
+ // Dev with a lazy context drives the handler off an in-memory manifest; no
28
+ // disk `pages-manifest.json` is produced. Prod still reads from disk.
29
+ if (!lazyContext && !fs.existsSync(manifestPath)) {
25
30
  return null;
26
31
  }
27
32
  const projectRequire = createRequire(path.join(options.rootDir, 'package.json'));
@@ -31,16 +36,47 @@ function createPagesHandler(options) {
31
36
  renderToPipeableStream: projectRequire('react-dom/server').renderToPipeableStream,
32
37
  };
33
38
  syncReactInternals(projectReactModule);
34
- let manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
39
+ let manifest = lazyContext
40
+ ? lazyContext.manifest
41
+ : JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
35
42
  let routes = compileRoutes(manifest);
36
43
  let clientManifestJson = buildClientManifestJson(manifest);
44
+ let lastSeenVersion = lazyContext ? manifest.version : 0;
37
45
  const componentCache = new Map();
38
46
  let cacheVersion = 0;
39
47
  const reloadEmitter = devMode ? new EventEmitter() : null;
40
- if (manifest.entries.length === 0 && !manifest.notFoundBundle && !manifest.errorBundle) {
48
+ if (
49
+ !lazyContext &&
50
+ manifest.entries.length === 0 &&
51
+ !manifest.notFoundBundle &&
52
+ !manifest.errorBundle
53
+ ) {
41
54
  return null;
42
55
  }
56
+ // Keep `routes` / `clientManifestJson` in sync with the lazy context's
57
+ // mutable manifest. Every `built` / `invalidate` event bumps the version,
58
+ // which is our cheap change signal — recompile only when it moves.
59
+ function refreshFromLazy() {
60
+ if (!lazyContext) return;
61
+ if (manifest.version === lastSeenVersion) return;
62
+ lastSeenVersion = manifest.version;
63
+ routes = compileRoutes(manifest);
64
+ clientManifestJson = buildClientManifestJson(manifest);
65
+ componentCache.clear();
66
+ cacheVersion++;
67
+ }
68
+ if (lazyContext) {
69
+ lazyContext.on('update', (event) => {
70
+ refreshFromLazy();
71
+ if (reloadEmitter) reloadEmitter.emit('update', event);
72
+ });
73
+ }
74
+ const arcwayEndpoint = lazyContext ? createArcwayDevEndpoint(lazyContext) : null;
43
75
  function reload() {
76
+ if (lazyContext) {
77
+ refreshFromLazy();
78
+ return;
79
+ }
44
80
  if (!fs.existsSync(manifestPath)) return;
45
81
  manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
46
82
  routes = compileRoutes(manifest);
@@ -57,6 +93,9 @@ function createPagesHandler(options) {
57
93
  const url = req.url ?? '/';
58
94
  const pathname = url.split('?')[0];
59
95
  if (method !== 'GET') return false;
96
+ // Internal `/_arcway/*` endpoints (dev only) resolve before the route
97
+ // matcher so they can never be shadowed by a user-defined page.
98
+ if (arcwayEndpoint && arcwayEndpoint(req, res)) return true;
60
99
  if (reloadEmitter && pathname === '/__livereload') {
61
100
  res.writeHead(200, {
62
101
  'Content-Type': 'text/event-stream',
@@ -73,6 +112,9 @@ function createPagesHandler(options) {
73
112
  req.on('close', () => reloadEmitter.off('update', onUpdate));
74
113
  return true;
75
114
  }
115
+ // Pick up any invalidations or completed builds that landed since the
116
+ // last request before we resolve the route.
117
+ refreshFromLazy();
76
118
  const matched = matchPageRoute(routes, pathname);
77
119
  if (!matched) {
78
120
  if (manifest.notFoundBundle) {
@@ -91,6 +133,28 @@ function createPagesHandler(options) {
91
133
  }
92
134
  return false;
93
135
  }
136
+ // Lazy dev: trigger on-demand build of this page's server bundle +
137
+ // every ancestor layout/middleware before we attempt to load any
138
+ // component. ensurePageBuilt() coalesces concurrent calls internally.
139
+ if (lazyContext) {
140
+ try {
141
+ await lazyContext.ensurePageBuilt(matched.route.pattern);
142
+ } catch (err) {
143
+ await renderDevBuildError(
144
+ err,
145
+ res,
146
+ manifest,
147
+ outDir,
148
+ componentCache,
149
+ cacheVersion,
150
+ projectReact,
151
+ );
152
+ return true;
153
+ }
154
+ refreshFromLazy();
155
+ const rematched = matchPageRoute(routes, pathname);
156
+ if (rematched) matched.route = rematched.route;
157
+ }
94
158
  if (matched.route.middlewareServerBundles.length > 0) {
95
159
  const middlewareResult = await runPageMiddleware(
96
160
  matched.route,
@@ -174,6 +238,43 @@ function createPagesHandler(options) {
174
238
  : {}),
175
239
  };
176
240
  }
241
+ // Render a dev-only build-error page. Preferred path: use the project's
242
+ // `_error.jsx` bundle so devs see the same shell they'd see for runtime errors,
243
+ // with the esbuild message in the `error` prop. Fallback: plain-text 500 when
244
+ // no errorBundle is available or the response has already started.
245
+ async function renderDevBuildError(
246
+ err,
247
+ res,
248
+ manifest,
249
+ outDir,
250
+ componentCache,
251
+ cacheVersion,
252
+ projectReact,
253
+ ) {
254
+ const errorMessage = err instanceof Error ? err.message : String(err);
255
+ if (manifest.errorBundle && !res.headersSent) {
256
+ await renderErrorPage(
257
+ manifest.errorBundle,
258
+ 500,
259
+ { error: errorMessage },
260
+ outDir,
261
+ res,
262
+ componentCache,
263
+ manifest,
264
+ cacheVersion,
265
+ projectReact,
266
+ );
267
+ return;
268
+ }
269
+ if (!res.headersSent) {
270
+ const escaped = String(errorMessage)
271
+ .replace(/&/g, '&')
272
+ .replace(/</g, '&lt;')
273
+ .replace(/>/g, '&gt;');
274
+ res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
275
+ res.end(`<h1>500 - Build Error</h1><pre>${escaped}</pre>`);
276
+ }
277
+ }
177
278
  function compileRoutes(manifest) {
178
279
  const routes = manifest.entries.map((entry) => {
179
280
  const { regex, paramNames, catchAllParam } = compilePattern(entry.pattern);
@@ -186,6 +287,8 @@ function compileRoutes(manifest) {
186
287
  clientBundle: entry.clientBundle,
187
288
  layoutServerBundles: entry.layoutServerBundles ?? [],
188
289
  middlewareServerBundles: entry.middlewareServerBundles ?? [],
290
+ sharedChunks: entry.sharedChunks ?? [],
291
+ sharedCssChunks: entry.sharedCssChunks ?? [],
189
292
  };
190
293
  });
191
294
  sortBySpecificity(routes);
@@ -269,6 +372,7 @@ export {
269
372
  MIME_TYPES,
270
373
  buildClientManifestJson,
271
374
  buildCssLinkTag,
375
+ buildRouteCssLinkTags,
272
376
  buildScriptTags,
273
377
  createPagesHandler,
274
378
  matchPageRoute,