@antelopejs/dms-frontend 0.5.5 → 0.6.0

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.
Files changed (75) hide show
  1. package/README.md +377 -19
  2. package/dist/app-configs.d.ts +9 -0
  3. package/dist/app-configs.js +52 -0
  4. package/dist/asset-roots.d.ts +7 -0
  5. package/dist/asset-roots.js +34 -0
  6. package/dist/commands/build-action.js +9 -0
  7. package/dist/commands/dev-action.js +88 -19
  8. package/dist/commands/start-action.js +2 -0
  9. package/dist/common.d.ts +1 -0
  10. package/dist/common.js +1 -0
  11. package/dist/config.d.ts +2 -0
  12. package/dist/config.js +9 -3
  13. package/dist/derived-outputs.d.ts +2 -0
  14. package/dist/derived-outputs.js +38 -31
  15. package/dist/help.js +10 -3
  16. package/dist/layer-watch.js +123 -131
  17. package/dist/layers.js +2 -0
  18. package/dist/manifest-secrets.js +5 -5
  19. package/dist/manifest.js +7 -1
  20. package/dist/materialize.js +21 -1
  21. package/dist/module-set.d.ts +16 -0
  22. package/dist/module-set.js +80 -0
  23. package/dist/server-process.d.ts +1 -0
  24. package/dist/server-process.js +28 -2
  25. package/dist/verify-source-build.d.ts +11 -0
  26. package/dist/verify-source-build.js +113 -0
  27. package/dist/verify-source-runner.js +14 -57
  28. package/dist/workspace.d.ts +9 -0
  29. package/package.json +2 -1
  30. package/templates/vue/app-runtime.ts +58 -16
  31. package/templates/vue/client-server-guard.mjs +73 -0
  32. package/templates/vue/email-renderer.ts +17 -4
  33. package/templates/vue/email-runtime.ts +1 -1
  34. package/templates/vue/frontend-module.ts +1817 -259
  35. package/templates/vue/globals.d.ts +1 -1
  36. package/templates/vue/icon-hydration.ts +106 -1
  37. package/templates/vue/index.html +0 -1
  38. package/templates/vue/layer-app-config.ts +92 -0
  39. package/templates/vue/main.ts +10 -2
  40. package/templates/vue/package.json +5 -1
  41. package/templates/vue/prepare-types.mjs +34 -0
  42. package/templates/vue/server/auth/backend.mjs +13 -19
  43. package/templates/vue/server/auth/client-ip.mjs +27 -0
  44. package/templates/vue/server/auth/establish-endpoints.mjs +70 -0
  45. package/templates/vue/server/auth/session.mjs +6 -97
  46. package/templates/vue/server/auth-hooks.mjs +84 -0
  47. package/templates/vue/server/backend-proxies.mjs +110 -0
  48. package/templates/vue/server/dev-private-files.mjs +99 -0
  49. package/templates/vue/server/email.mjs +11 -33
  50. package/templates/vue/server/favicon.mjs +20 -0
  51. package/templates/vue/server/inertia.mjs +48 -3
  52. package/templates/vue/server/listen-host.mjs +19 -0
  53. package/templates/vue/server/module-event.mjs +265 -0
  54. package/templates/vue/server/module-routes.mjs +513 -0
  55. package/templates/vue/server/module-server.mjs +225 -0
  56. package/templates/vue/server/module-testing.mjs +65 -0
  57. package/templates/vue/server/page-props.mjs +151 -0
  58. package/templates/vue/server/path-patterns.mjs +38 -0
  59. package/templates/vue/server/proxy-headers.mjs +25 -0
  60. package/templates/vue/server/public-endpoints.mjs +69 -11
  61. package/templates/vue/server/request-routing.mjs +47 -0
  62. package/templates/vue/server/route-rules.mjs +129 -0
  63. package/templates/vue/server-module.ts +294 -0
  64. package/templates/vue/server.mjs +178 -169
  65. package/templates/vue/ssr-renderer.ts +60 -19
  66. package/templates/vue/tsconfig.json +2 -1
  67. package/templates/vue/ui-app-config.ts +18 -6
  68. package/templates/vue/ui-inertia-page.mjs +37 -0
  69. package/templates/vue/vite.config.ts +31 -10
  70. package/templates/vue/vite.email.config.ts +1 -1
  71. package/templates/vue/vite.server.config.ts +57 -0
  72. package/templates/vue/server/auth/oauth.mjs +0 -233
  73. package/templates/vue/server/auth/routes.mjs +0 -444
  74. package/templates/vue/server/render-token.mjs +0 -81
  75. package/templates/vue/server/tester.mjs +0 -227
@@ -10,12 +10,8 @@ import {
10
10
  RequestBodyError,
11
11
  UpstreamError,
12
12
  } from "./server/auth/backend.mjs";
13
+ import { forwardedHeaders } from "./server/auth/client-ip.mjs";
13
14
  import { proxyAccess, refuseCrossOrigin } from "./server/public-endpoints.mjs";
14
- import {
15
- handleAuth,
16
- publicSession,
17
- refreshSession,
18
- } from "./server/auth/routes.mjs";
19
15
  import { readSession } from "./server/auth/session.mjs";
20
16
  import {
21
17
  BackendResponseError,
@@ -37,13 +33,24 @@ import {
37
33
  writeViteError,
38
34
  } from "./server/vite-errors.mjs";
39
35
  import { documentStyleTags } from "./server/dev-styles.mjs";
40
- import { handleEmailRender, watchEmailBundle } from "./server/email.mjs";
36
+ import { watchEmailBundle } from "./server/email.mjs";
37
+ import { withFaviconPlaceholder } from "./server/favicon.mjs";
38
+ import { DEV_FS_DENY, denyPrivateFiles } from "./server/dev-private-files.mjs";
39
+ import { ownerLabel, requestOwner } from "./server/request-routing.mjs";
40
+ import { routingReport } from "./server/backend-proxies.mjs";
41
+ import { routeRuleFor, ruleHeaders } from "./server/route-rules.mjs";
42
+ import {
43
+ handleModuleRoute,
44
+ loadServerModules,
45
+ } from "./server/module-server.mjs";
41
46
  import { HOMEPAGE } from "./server/homepage.mjs";
47
+ import { loadPageProps } from "./server/page-props.mjs";
42
48
  import { htmlTag } from "./server/html-tag.mjs";
49
+ import { listenHost } from "./server/listen-host.mjs";
43
50
  import * as icons from "./server/icons.mjs";
44
51
  import * as requestFailure from "./server/request-failure.mjs";
52
+ import { writeProxyHeaders } from "./server/proxy-headers.mjs";
45
53
  import { backendUrl, refuseForeignTarget } from "./server/request-target.mjs";
46
- import { handleTester } from "./server/tester.mjs";
47
54
  import {
48
55
  createInertiaPage,
49
56
  htmlHeaders,
@@ -63,27 +70,8 @@ const MIME_TYPES = {
63
70
  ".js": "text/javascript",
64
71
  ".svg": "image/svg+xml",
65
72
  };
66
- const PROXY_PREFIXES = ["/api/", "/dms/"];
67
73
  const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
68
- const AUTH_SERVER_ROUTES = [
69
- ["GET", /^\/api\/_auth\/session\/?$/],
70
- ["POST", /^\/api\/_auth\/session\/?$/],
71
- ["DELETE", /^\/api\/_auth\/session\/?$/],
72
- [
73
- "POST",
74
- /^\/auth\/(?:login|signup|verify-2fa|establish|request-2fa-email|switch-account|switch-tenant|validate-account|remove-account)\/?$/,
75
- ],
76
- ["POST", /^\/auth\/oauth\/handoff\/?$/],
77
- ["GET", /^\/auth\/oauth\/[^/]+\/(?:start|callback)\/?$/],
78
- ];
79
- const HTML_RENDER_ROUTE = "/api/html/render";
80
- const TESTER_ROUTE = /^\/api\/_dms\/tester\/?$/;
81
- const LOCAL_ROUTES = [
82
- [(pathname) => TESTER_ROUTE.test(pathname), handleTester],
83
- [icons.isIconRequest, icons.handleIcons],
84
- ];
85
- const AUTH_PAGE = "/auth";
86
- const ONBOARDING_PAGE = "/onboarding";
74
+ const LOCAL_ROUTES = [[icons.isIconRequest, icons.handleIcons]];
87
75
  const SOURCE_TEMPLATE_PATH = join(PROJECT_ROOT, "index.html");
88
76
  const BUILT_SSR_RENDERER_PATH = join(PROJECT_ROOT, "dist/ssr/ssr-renderer.js");
89
77
  // The SSR bundle keeps vue-i18n external, so Node loads its esm-bundler build,
@@ -101,13 +89,17 @@ async function developmentServer() {
101
89
  if (vite) return vite;
102
90
  vitePromise ??= import("vite")
103
91
  .then(({ createServer: createViteServer }) => {
104
- const serverOptions = { middlewareMode: true };
92
+ const serverOptions = {
93
+ middlewareMode: true,
94
+ // The generated secrets and the modules' server code, never served.
95
+ fs: { deny: DEV_FS_DENY },
96
+ };
105
97
  if (frontendHttpServer)
106
98
  serverOptions.hmr = { server: frontendHttpServer };
107
99
  return createViteServer({
108
100
  server: serverOptions,
109
101
  appType: "custom",
110
- plugins: [captureViteErrors()],
102
+ plugins: [denyPrivateFiles(), captureViteErrors()],
111
103
  });
112
104
  })
113
105
  .then((server) => {
@@ -121,17 +113,28 @@ async function developmentServer() {
121
113
  return vitePromise;
122
114
  }
123
115
 
124
- function backendHeaders(request, publicEndpoint) {
125
- const headers = relayedHeaders(request, [
126
- "accept",
127
- "content-type",
128
- "if-match",
129
- "user-agent",
130
- "x-content-language",
131
- "x-realtime-session",
132
- ...(publicEndpoint?.headers ?? []),
133
- ]);
134
- const session = publicEndpoint ? undefined : readSession(request);
116
+ /**
117
+ * The headers of a backend request: the relayed client headers, the client's
118
+ * Origin and Referer as received, the `X-Forwarded-*` headers (set last, so no
119
+ * caller writes them), and the session's access token unless the request goes
120
+ * anonymously.
121
+ */
122
+ function backendHeaders(request, access = { anonymous: false, headers: [] }) {
123
+ const headers = {
124
+ ...relayedHeaders(request, [
125
+ "accept",
126
+ "content-type",
127
+ "if-match",
128
+ "origin",
129
+ "referer",
130
+ "user-agent",
131
+ "x-content-language",
132
+ "x-realtime-session",
133
+ ...access.headers,
134
+ ]),
135
+ ...forwardedHeaders(request),
136
+ };
137
+ const session = access.anonymous ? undefined : readSession(request);
135
138
  if (session?.accessToken)
136
139
  headers.authorization = `Bearer ${session.accessToken}`;
137
140
  return headers;
@@ -161,45 +164,23 @@ async function backendJson(path, request) {
161
164
  return response.json();
162
165
  }
163
166
 
164
- async function fetchPage(request, pathname) {
165
- const query = new URLSearchParams({ path: pathname });
166
- return backendJson(`/dms/page?${query}`, request);
167
- }
168
-
169
- async function pageProps(request) {
170
- const pathname = new URL(request.url, "http://frontend.local").pathname;
171
- const session = publicSession(readSession(request));
172
- try {
173
- const page = await fetchPage(request, pathname);
174
- return { path: pathname, page, ...session };
175
- } catch (error) {
176
- if (!(error instanceof BackendResponseError) || error.status !== 404)
177
- throw error;
178
- const renderer = await ssrRenderer(await developmentServer());
179
- if (!renderer.isDmsFrontendPage(pathname)) throw error;
180
- return {
181
- path: pathname,
182
- ...session,
183
- page: {
184
- componentName: pathname,
185
- isFrontendOnly: true,
186
- route: {
187
- fullId: pathname.slice(1).replaceAll("/", "."),
188
- fullSlug: pathname,
189
- hasAccess: pathname.startsWith("/auth/"),
190
- },
191
- layout: { layout: { componentName: "DefaultLayout" } },
192
- },
193
- };
194
- }
167
+ /** The session logic of the module that owns sign-in. */
168
+ async function currentAuth() {
169
+ return (await loadServerModules(await developmentServer())).serverAuth;
195
170
  }
196
171
 
197
- function writeProxyHeaders(response, upstream) {
198
- const headers = Object.fromEntries(upstream.headers);
199
- delete headers["set-cookie"];
200
- const cookies = upstream.headers.getSetCookie?.() ?? [];
201
- if (cookies.length) headers["set-cookie"] = cookies;
202
- response.writeHead(upstream.status, headers);
172
+ function pageProps(request, auth) {
173
+ return loadPageProps(
174
+ request,
175
+ {
176
+ fetchJson: (path) => backendJson(path, request),
177
+ isFrontendPage: async (pathname) =>
178
+ (await ssrRenderer(await developmentServer())).isDmsFrontendPage(
179
+ pathname,
180
+ ),
181
+ },
182
+ auth,
183
+ );
203
184
  }
204
185
 
205
186
  function redirectToHomepage(request, response, pathname) {
@@ -208,7 +189,7 @@ function redirectToHomepage(request, response, pathname) {
208
189
  return true;
209
190
  }
210
191
 
211
- function redirectSignedOutPrivatePage(request, response, props) {
192
+ function redirectSignedOutPrivatePage(request, response, props, auth) {
212
193
  const route = props.page?.route;
213
194
  if (
214
195
  route?.publicAccess === true ||
@@ -216,27 +197,14 @@ function redirectSignedOutPrivatePage(request, response, props) {
216
197
  readSession(request)
217
198
  )
218
199
  return false;
219
- const location = `${AUTH_PAGE}?redirect=${encodeURIComponent(request.url)}`;
220
- redirectFrontendVisit(request, response, location);
200
+ redirectFrontendVisit(request, response, auth.signInLocation(request.url));
221
201
  return true;
222
202
  }
223
203
 
224
- export function requestOwnership(method, pathname) {
225
- if (PROXY_PREFIXES.some((prefix) => pathname.startsWith(prefix)))
226
- return "server";
227
- if (
228
- AUTH_SERVER_ROUTES.some(
229
- ([routeMethod, pattern]) =>
230
- routeMethod === method && pattern.test(pathname),
231
- )
232
- )
233
- return "server";
234
- return method === "GET" ? "frontend" : "server";
235
- }
236
-
237
- async function proxy(request, response, fallbackOnNotFound = false) {
238
- const { publicEndpoint, isRefused } = proxyAccess(request, SAFE_METHODS);
239
- if (isRefused) {
204
+ /** Sends a request to the backend through the proxy it reached. */
205
+ async function proxy(request, response, backendProxy) {
206
+ const access = proxyAccess(request, SAFE_METHODS, backendProxy);
207
+ if (access.isRefused) {
240
208
  refuseCrossOrigin(response);
241
209
  return true;
242
210
  }
@@ -247,18 +215,13 @@ async function proxy(request, response, fallbackOnNotFound = false) {
247
215
  const upstream = await fetch(backendUrl(request.url), {
248
216
  signal: abandoned.signal,
249
217
  method: request.method,
250
- headers: backendHeaders(request, publicEndpoint),
218
+ headers: backendHeaders(request, access),
251
219
  body: ["GET", "HEAD"].includes(request.method) ? undefined : request,
252
220
  duplex: "half",
253
221
  });
254
- if (fallbackOnNotFound && upstream.status === 404) {
255
- await upstream.body?.cancel();
256
- return false;
257
- }
258
222
  writeProxyHeaders(response, upstream);
259
223
  if (!upstream.body || request.method === "HEAD") response.end();
260
224
  else await pipeline(Readable.fromWeb(upstream.body), response);
261
- return true;
262
225
  }
263
226
 
264
227
  function acceptedAsset(pathname, request) {
@@ -305,11 +268,37 @@ async function ssrRenderer(devServer) {
305
268
  return renderer;
306
269
  }
307
270
 
271
+ function clientRenderedShell(page) {
272
+ return {
273
+ body: inertiaAppHtml(page),
274
+ head: { headTags: "", htmlAttrs: "", bodyAttrs: "" },
275
+ overlays: "",
276
+ };
277
+ }
278
+
279
+ async function serverRendered(page, serverFetch, requestCookies, devServer) {
280
+ try {
281
+ const renderer = await ssrRenderer(devServer);
282
+ return await renderer.renderDmsPage(page, serverFetch, requestCookies);
283
+ } catch (error) {
284
+ console.error(
285
+ "DMS SSR render failed; falling back to client rendering",
286
+ error,
287
+ );
288
+ return { ...clientRenderedShell(page), error: { statusCode: 500 } };
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Renders a page document. With `{ ssr: false }` (a route rule), the client
294
+ * renders the page from the shell instead.
295
+ */
308
296
  export async function renderHtml(
309
297
  page,
310
298
  requestUrl,
311
299
  serverFetch,
312
300
  requestCookies,
301
+ options = {},
313
302
  ) {
314
303
  const devServer = await developmentServer();
315
304
  const rawTemplate = devServer
@@ -318,27 +307,17 @@ export async function renderHtml(
318
307
  const template = devServer
319
308
  ? await devServer.transformIndexHtml(requestUrl, rawTemplate)
320
309
  : rawTemplate;
321
- let rendered;
322
- try {
323
- const renderer = await ssrRenderer(devServer);
324
- rendered = await renderer.renderDmsPage(page, serverFetch, requestCookies);
325
- } catch (error) {
326
- console.error(
327
- "DMS SSR render failed; falling back to client rendering",
328
- error,
329
- );
330
- rendered = {
331
- body: inertiaAppHtml(page),
332
- head: { headTags: "", htmlAttrs: "", bodyAttrs: "" },
333
- overlays: "",
334
- error: { statusCode: 500 },
335
- };
336
- }
310
+ const rendered =
311
+ options.ssr === false
312
+ ? clientRenderedShell(page)
313
+ : await serverRendered(page, serverFetch, requestCookies, devServer);
337
314
  if (rendered.redirect)
338
315
  return { html: "", status: 200, redirect: rendered.redirect };
339
316
  const preloads = await documentStyleTags(devServer, page, template);
340
317
  const html = template
341
- .replace("<title>Antelope DMS</title>", rendered.head.headTags)
318
+ .replace("<title>Antelope DMS</title>", () =>
319
+ withFaviconPlaceholder(rendered.head.headTags),
320
+ )
342
321
  .replace(/<html([^>]*)>/, (_, attributes) =>
343
322
  htmlTag(attributes, rendered.head.htmlAttrs),
344
323
  )
@@ -352,23 +331,24 @@ export async function renderHtml(
352
331
  return { html, status: rendered.error?.statusCode ?? 200 };
353
332
  }
354
333
 
355
- function renderRequestHtml(page, request) {
334
+ function renderRequestHtml(page, request, rule = {}) {
356
335
  const fetch = serverComponentFetch(request);
357
- return renderHtml(page, request.url, fetch, request.headers.cookie);
336
+ return renderHtml(page, request.url, fetch, request.headers.cookie, {
337
+ ssr: rule.ssr,
338
+ });
358
339
  }
359
340
 
360
341
  async function writeBackendError(error, request, response) {
342
+ const auth = await currentAuth();
361
343
  const status = Number.isInteger(error?.status) ? error.status : 502;
362
344
  const pathname = new URL(request.url, "http://frontend.local").pathname;
363
345
  if (
364
346
  error instanceof BackendResponseError &&
365
347
  [401, 403].includes(status) &&
366
348
  !readSession(request) &&
367
- !pathname.startsWith(AUTH_PAGE) &&
368
- pathname !== ONBOARDING_PAGE
349
+ !auth.isSignInExempt(pathname)
369
350
  ) {
370
- const location = `${AUTH_PAGE}?redirect=${encodeURIComponent(request.url)}`;
371
- redirectFrontendVisit(request, response, location);
351
+ redirectFrontendVisit(request, response, auth.signInLocation(request.url));
372
352
  return;
373
353
  }
374
354
  const loadRenderer = async () => ssrRenderer(await developmentServer());
@@ -379,7 +359,7 @@ async function writeBackendError(error, request, response) {
379
359
  const props = {
380
360
  path: pathname,
381
361
  page: {},
382
- ...publicSession(readSession(request)),
362
+ ...auth.view(readSession(request)),
383
363
  error: { statusCode: status },
384
364
  };
385
365
  const page = createInertiaPage(request.url, props);
@@ -410,19 +390,45 @@ async function answeredByVite(devServer, request, response) {
410
390
  }
411
391
 
412
392
  /**
413
- * Vite's own URLs (`/@vite/client`, `/@id/…`, `/@fs/…`) and paths with an
414
- * extension name files: modules, stylesheets, fonts, images, built assets.
415
- * The backend's routes outside `/api` and `/dms` are mostly the paths of the
416
- * pages they serve data for, which have none (`/<page>/pagelayout`).
393
+ * Whether the renderer has a file at `pathname`: a development source Vite
394
+ * serves (under the workspace or its `public/`), or a built asset. Only a
395
+ * path with an extension names one.
417
396
  */
418
- function isFilePath(pathname) {
419
- return pathname.startsWith("/@") || extname(pathname) !== "";
397
+ function hasRendererFile(devServer, requested) {
398
+ let pathname;
399
+ try {
400
+ // As Vite and the asset server read it: `%2E` is a dot.
401
+ pathname = decodeURIComponent(requested);
402
+ } catch {
403
+ return false;
404
+ }
405
+ if (extname(pathname) === "") return false;
406
+ const roots = devServer
407
+ ? [devServer.config.root, devServer.config.publicDir].filter(Boolean)
408
+ : [join(PROJECT_ROOT, "dist/client")];
409
+ return roots.some((root) => {
410
+ const file = resolve(root, `.${pathname}`);
411
+ return (
412
+ file.startsWith(`${resolve(root)}${sep}`) &&
413
+ existsSync(file) &&
414
+ statSync(file).isFile()
415
+ );
416
+ });
420
417
  }
421
418
 
422
- /** Vite's files in development, the build's in production. */
423
- async function answeredWithFile(devServer, request, response, pathname) {
424
- if (!devServer) return serveAsset(pathname, request, response);
425
- return pathname !== "/" && answeredByVite(devServer, request, response);
419
+ /** Vite's files in development, the build's in production; else a 404. */
420
+ async function serveRendererFile(devServer, request, response, pathname) {
421
+ const answered = devServer
422
+ ? await answeredByVite(devServer, request, response)
423
+ : serveAsset(pathname, request, response);
424
+ if (answered) return;
425
+ response.writeHead(404, { "content-type": "text/plain" });
426
+ response.end("Not found");
427
+ }
428
+
429
+ function writeNotFound(response) {
430
+ response.writeHead(404, { "content-type": JSON_TYPE });
431
+ response.end(JSON.stringify({ message: "Not found" }));
426
432
  }
427
433
 
428
434
  export async function handleRequest(request, response) {
@@ -430,48 +436,39 @@ export async function handleRequest(request, response) {
430
436
  const pathname = url.pathname;
431
437
  const localRoute = LOCAL_ROUTES.find(([matches]) => matches(pathname));
432
438
  if (localRoute) return localRoute[1](request, response);
433
- if (pathname === "/api/_auth/session")
434
- return handleAuth(request, response, url);
435
- if (request.method === "POST" && pathname === HTML_RENDER_ROUTE)
436
- return handleEmailRender(request, response);
437
- if (
438
- AUTH_SERVER_ROUTES.some(
439
- ([method, pattern]) =>
440
- method === request.method && pattern.test(pathname),
441
- )
442
- )
443
- return handleAuth(request, response, url);
444
- if (requestOwnership(request.method, pathname) === "server")
445
- return proxy(request, response);
439
+ const devServer = await developmentServer();
440
+ const modules = await loadServerModules(devServer);
441
+ const owner = requestOwner(request.method, pathname, modules, (path) =>
442
+ hasRendererFile(devServer, path),
443
+ );
444
+ if (devServer) response.setHeader("x-dms-route-owner", ownerLabel(owner));
445
+ if (owner.kind === "renderer")
446
+ return serveRendererFile(devServer, request, response, pathname);
447
+ if (owner.kind === "module")
448
+ return handleModuleRoute(request, response, pathname, modules);
449
+ if (owner.kind === "backend") return proxy(request, response, owner.proxy);
450
+ if (owner.kind === "not-found") return writeNotFound(response);
446
451
  if (requestFailure.hasMalformedPath(request.url))
447
452
  return requestFailure.writeBadRequest(response);
448
- const devServer = await developmentServer();
449
- const visit = isFrontendVisit(request);
450
- // A file is looked for among the frontend's own before the backend is asked
451
- // for it: asking first cost a backend round trip for each of the hundreds of
452
- // modules a development page loads, and for each built asset. A file the
453
- // frontend does not have, a media file, is still the backend's.
454
- const fileFirst = !visit && isFilePath(pathname);
455
- const serveFile = () =>
456
- answeredWithFile(devServer, request, response, pathname);
457
- if (fileFirst && (await serveFile())) return;
458
- if (!visit && (await proxy(request, response, true))) return;
459
- if (!fileFirst && (await serveFile())) return;
460
453
  if (handleAssetVersionMismatch(request, response)) return;
461
454
  if (redirectToHomepage(request, response, pathname)) return;
462
- const props = await pageProps(request);
463
- if (redirectSignedOutPrivatePage(request, response, props)) return;
464
- const page = createInertiaPage(request.url, props);
455
+ const { serverAuth: auth, rules } = modules;
456
+ const { props, onceProps } = await pageProps(request, auth);
457
+ if (redirectSignedOutPrivatePage(request, response, props, auth)) return;
458
+ const page = createInertiaPage(request.url, props, undefined, onceProps);
459
+ const rule = routeRuleFor(rules, pathname);
460
+ const headersFor = (status) =>
461
+ ruleHeaders(rule, request, props.page?.route, status);
465
462
  if (request.headers[INERTIA_HEADER]) {
466
463
  return writeContent(
467
464
  request,
468
465
  response,
469
466
  200,
470
- inertiaHeaders(page.version),
467
+ { ...inertiaHeaders(page.version), ...headersFor(200) },
471
468
  JSON.stringify(page),
472
469
  );
473
470
  }
474
- const rendered = await renderRequestHtml(page, request);
471
+ const rendered = await renderRequestHtml(page, request, rule);
475
472
  if (rendered.redirect) {
476
473
  redirectFrontendVisit(request, response, rendered.redirect);
477
474
  return;
@@ -480,7 +477,7 @@ export async function handleRequest(request, response) {
480
477
  request,
481
478
  response,
482
479
  rendered.status,
483
- htmlHeaders(),
480
+ { ...htmlHeaders(), ...headersFor(rendered.status) },
484
481
  rendered.html,
485
482
  );
486
483
  }
@@ -495,7 +492,7 @@ async function writeRequestFailure(error, request, response) {
495
492
  !request.dmsSessionRetry
496
493
  ) {
497
494
  request.dmsSessionRetry = true;
498
- if (await refreshSession(request, response)) {
495
+ if (await (await currentAuth()).refresh(request, response)) {
499
496
  await handleRequestSafely(request, response);
500
497
  return;
501
498
  }
@@ -524,7 +521,7 @@ if (
524
521
  handleClientError(error, socket, maxHeaderSize),
525
522
  );
526
523
  const port = Number(process.env.PORT ?? 3001);
527
- const host = process.env.HOST ?? "0.0.0.0";
524
+ const host = listenHost();
528
525
  const onListenError = (error) => void reportListenError(error, host, port);
529
526
  frontendHttpServer.once("error", onListenError);
530
527
  frontendHttpServer.listen(port, host, () => {
@@ -533,6 +530,17 @@ if (
533
530
  });
534
531
  }
535
532
 
533
+ /** Prints who owns which paths, once the server is up. */
534
+ async function reportRouting() {
535
+ try {
536
+ const modules = await loadServerModules(await developmentServer());
537
+ for (const line of routingReport(modules.proxies, modules.routes.length))
538
+ console.log(line);
539
+ } catch (error) {
540
+ console.error("DMS server modules failed to load", error);
541
+ }
542
+ }
543
+
536
544
  /**
537
545
  * Report the server ready. In development Vite would otherwise start with the
538
546
  * first request, so it is started here: "ready" then means the first page is
@@ -547,6 +555,7 @@ async function announceReady() {
547
555
  }
548
556
  const { address, port } = frontendHttpServer.address();
549
557
  await reportReady(address, port);
558
+ await reportRouting();
550
559
  // Only the production build produces the email bundle otherwise. Started
551
560
  // once the server answers, so it never delays the first page.
552
561
  if (process.env.DMS_DEV === "true") watchEmailBundle();