@antelopejs/dms-frontend 0.9.1 → 0.9.3

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 (103) hide show
  1. package/README.md +9 -1
  2. package/dist/cancellation.js +2 -1
  3. package/dist/config.js +11 -7
  4. package/dist/derived-outputs.js +6 -113
  5. package/dist/dev-modules.js +3 -2
  6. package/dist/discovery.js +19 -12
  7. package/dist/fs-sync.js +1 -2
  8. package/dist/internal/guards.d.ts +5 -0
  9. package/dist/internal/guards.js +22 -0
  10. package/dist/internal/locale-catalogs.d.ts +6 -0
  11. package/dist/internal/locale-catalogs.js +115 -0
  12. package/dist/layer-aliases.js +2 -1
  13. package/dist/layers.js +2 -1
  14. package/dist/locale-files.js +9 -4
  15. package/dist/manifest-secrets.js +11 -4
  16. package/dist/manifest.js +11 -5
  17. package/dist/materialize.js +6 -5
  18. package/dist/module-set.js +2 -4
  19. package/dist/ports.js +8 -1
  20. package/dist/private-values.js +26 -17
  21. package/dist/project-modules.js +17 -11
  22. package/dist/renderer-range.js +2 -1
  23. package/dist/server-process.js +17 -16
  24. package/dist/update-check.js +23 -16
  25. package/dist/verify-source-runner.js +2 -2
  26. package/dist/workspace-setup.js +1 -2
  27. package/dist/workspace.js +11 -10
  28. package/package.json +7 -4
  29. package/skills/dms-frontend/SKILL.md +1 -1
  30. package/skills/dms-frontend/cases.json +77 -0
  31. package/templates/vue/app-runtime.ts +88 -7
  32. package/templates/vue/client-chunking.mjs +0 -5
  33. package/templates/vue/client-server-guard.mjs +10 -13
  34. package/templates/vue/entry-weight.mjs +12 -14
  35. package/templates/vue/frontend-module.ts +159 -3563
  36. package/templates/vue/frontend-runtime/async-data-entry.ts +320 -0
  37. package/templates/vue/frontend-runtime/async-data.ts +404 -0
  38. package/templates/vue/frontend-runtime/components.ts +386 -0
  39. package/templates/vue/frontend-runtime/cookies.ts +125 -0
  40. package/templates/vue/frontend-runtime/data-access.ts +132 -0
  41. package/templates/vue/frontend-runtime/dms-error.ts +11 -0
  42. package/templates/vue/frontend-runtime/errors.ts +145 -0
  43. package/templates/vue/frontend-runtime/fetch.ts +306 -0
  44. package/templates/vue/frontend-runtime/module-registration.ts +247 -0
  45. package/templates/vue/frontend-runtime/paged-data.ts +191 -0
  46. package/templates/vue/frontend-runtime/payload.ts +172 -0
  47. package/templates/vue/frontend-runtime/route.ts +328 -0
  48. package/templates/vue/frontend-runtime/sdk.ts +300 -0
  49. package/templates/vue/frontend-runtime/state.ts +167 -0
  50. package/templates/vue/frontend-runtime/types.ts +361 -0
  51. package/templates/vue/frontend-runtime/ui.ts +247 -0
  52. package/templates/vue/globals.d.ts +5 -0
  53. package/templates/vue/head-order.mjs +11 -13
  54. package/templates/vue/icon-bundle.ts +31 -0
  55. package/templates/vue/message-subset.ts +65 -0
  56. package/templates/vue/network-error.ts +4 -0
  57. package/templates/vue/prepare-types.mjs +0 -8
  58. package/templates/vue/server/auth/backend.mjs +4 -2
  59. package/templates/vue/server/auth/establish-endpoints.mjs +14 -9
  60. package/templates/vue/server/auth/session.mjs +4 -3
  61. package/templates/vue/server/auth-hooks.mjs +6 -6
  62. package/templates/vue/server/backend-proxies.mjs +6 -6
  63. package/templates/vue/server/backend-response.mjs +15 -14
  64. package/templates/vue/server/built-files.mjs +10 -8
  65. package/templates/vue/server/cli-report.mjs +7 -5
  66. package/templates/vue/server/client-error.mjs +11 -8
  67. package/templates/vue/server/client-manifest.mjs +9 -8
  68. package/templates/vue/server/content.mjs +2 -1
  69. package/templates/vue/server/dev-private-files.mjs +20 -15
  70. package/templates/vue/server/dev-styles.mjs +22 -25
  71. package/templates/vue/server/email-bundle-watch.mjs +0 -12
  72. package/templates/vue/server/email.mjs +20 -15
  73. package/templates/vue/server/favicon.mjs +12 -11
  74. package/templates/vue/server/homepage.mjs +8 -10
  75. package/templates/vue/server/icons.mjs +18 -9
  76. package/templates/vue/server/in-place-render.mjs +6 -8
  77. package/templates/vue/server/listen-host.mjs +2 -2
  78. package/templates/vue/server/module-event.mjs +5 -5
  79. package/templates/vue/server/module-routes.mjs +11 -7
  80. package/templates/vue/server/module-server.mjs +8 -7
  81. package/templates/vue/server/module-testing.mjs +7 -7
  82. package/templates/vue/server/page-props.mjs +14 -14
  83. package/templates/vue/server/path-patterns.mjs +4 -4
  84. package/templates/vue/server/proxy-compression.mjs +21 -13
  85. package/templates/vue/server/proxy-headers.mjs +5 -3
  86. package/templates/vue/server/proxy-redirects.mjs +7 -6
  87. package/templates/vue/server/proxy-transfers.mjs +15 -11
  88. package/templates/vue/server/public-endpoints.mjs +27 -19
  89. package/templates/vue/server/public-options.mjs +9 -7
  90. package/templates/vue/server/request-failure.mjs +4 -6
  91. package/templates/vue/server/request-routing.mjs +8 -11
  92. package/templates/vue/server/request-target.mjs +6 -6
  93. package/templates/vue/server/route-rules.mjs +7 -5
  94. package/templates/vue/server/static-files.mjs +19 -10
  95. package/templates/vue/server/web-body.mjs +3 -4
  96. package/templates/vue/server.mjs +32 -19
  97. package/templates/vue/sfc-global-types.mjs +27 -24
  98. package/templates/vue/ssr-renderer.ts +10 -1
  99. package/templates/vue/ui-inertia-page.mjs +7 -7
  100. package/templates/vue/ui-kbd.mjs +10 -10
  101. package/templates/vue/ui-lazy-icons.mjs +44 -0
  102. package/templates/vue/ui-request-state.mjs +9 -9
  103. package/templates/vue/vite.config.ts +2 -0
@@ -1,18 +1,19 @@
1
- // The favicon placeholder of a page whose render declares no icon.
2
- //
3
- // A document without any `<link rel="icon">` makes the browser ask for
4
- // `/favicon.ico`, a request the frontend server would pass on to the backend
5
- // on every page load. An empty data URL stops it. The template used to carry
6
- // that placeholder itself, but a module declaring real icons through `useHead`
7
- // (light and dark, one per `prefers-color-scheme`) then competed with it, and
8
- // the browser could keep the empty one: it only goes in when the rendered head
9
- // has no icon of its own.
10
-
11
1
  const FAVICON_PLACEHOLDER = '<link rel="icon" href="data:," />';
12
2
  /** A `<link>` whose `rel` holds the `icon` token, `shortcut icon` included. */
13
3
  const ICON_LINK = /<link\b[^>]*\srel=(["'])(?:[^"']*\s)?icon(?:\s[^"']*)?\1/i;
14
4
 
15
- /** The rendered head tags, behind the placeholder when they hold no icon. */
5
+ /**
6
+ * The rendered head tags, behind the placeholder when they hold no icon.
7
+ *
8
+ * The favicon placeholder of a page whose render declares no icon. A
9
+ * document without any `<link rel="icon">` makes the browser ask for
10
+ * `/favicon.ico`, a request the frontend server would pass on to the backend
11
+ * on every page load; an empty data URL stops it. It only goes in when the
12
+ * rendered head has no icon of its own: a module declaring real icons
13
+ * through `useHead` (light and dark, one per `prefers-color-scheme`)
14
+ * competed with a placeholder carried by the template, and the browser could
15
+ * keep the empty one.
16
+ */
16
17
  export function withFaviconPlaceholder(headTags) {
17
18
  return ICON_LINK.test(headTags)
18
19
  ? headTags
@@ -1,13 +1,3 @@
1
- // Working out which page a visit to `/` means.
2
- //
3
- // The answer lives in the module registry the workspace was materialized with:
4
- // a module declares `homepage` somewhere in its options, at whatever depth its
5
- // own configuration shape puts it, and the highest-priority declaration wins —
6
- // the registry is already sorted. Read once at boot, because the registry is a
7
- // build artifact and cannot change while the server is up.
8
- //
9
- // Split out of server.mjs, whose size the linter caps.
10
-
11
1
  import { existsSync, readFileSync } from "node:fs";
12
2
  import { join } from "node:path";
13
3
  import { fileURLToPath } from "node:url";
@@ -29,6 +19,14 @@ function findHomepage(options) {
29
19
 
30
20
  const OPTIONS = publicOptionsOf(REGISTRY);
31
21
 
22
+ /**
23
+ * Works out which page a visit to `/` means. The answer lives in the module
24
+ * registry the workspace was materialized with: a module declares `homepage`
25
+ * somewhere in its options, at whatever depth its own configuration shape
26
+ * puts it, and the highest-priority declaration wins, since the registry is
27
+ * already sorted. Read once at boot, because the registry is a build
28
+ * artifact and cannot change while the server is up.
29
+ */
32
30
  export const HOMEPAGE =
33
31
  REGISTRY.modules
34
32
  .map((module) => findHomepage(OPTIONS[module.id]))
@@ -9,20 +9,27 @@ import { writeContent } from "./content.mjs";
9
9
  const WORKSPACE_ROOT = fileURLToPath(new URL("..", import.meta.url));
10
10
  const JSON_TYPE = "application/json";
11
11
  const READ_METHODS = new Set(["GET", "HEAD"]);
12
- // Iconify's own naming rule, for prefixes and icon names alike. It is also
13
- // what keeps a request from naming anything but a collection file.
12
+ /**
13
+ * Iconify's own naming rule, for prefixes and icon names alike. It is also
14
+ * what keeps a request from naming anything but a collection file.
15
+ */
14
16
  const ICON_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
15
17
  const COLLECTION_FILE = /^([^/]+)\.json$/;
16
18
  const ICON_NAME_SEPARATOR = ",";
17
19
  const COLLECTION_PACKAGES = join("node_modules", "@iconify-json");
18
- // An icon's data only changes with the installed collection, which a
19
- // deployment replaces; a week bounds how long an upgraded icon can lag.
20
+ /**
21
+ * An icon's data only changes with the installed collection, which a
22
+ * deployment replaces; a week bounds how long an upgraded icon can lag.
23
+ */
20
24
  const ICON_CACHE_CONTROL =
21
25
  "public, max-age=604800, stale-while-revalidate=86400";
22
26
  const ERROR_CACHE_CONTROL = "no-store";
23
27
  const collections = new Map();
24
- // By renderer module: a development server evaluates a new one after a layer
25
- // changes, and each must find the collections loaded before it renders.
28
+ /**
29
+ * Kept by renderer module: a development server evaluates a new one after a
30
+ * layer changes, and each must find the collections loaded before it
31
+ * renders.
32
+ */
26
33
  const rendererCollections = new WeakMap();
27
34
 
28
35
  function collectionSpecifiers(prefix) {
@@ -32,14 +39,16 @@ function collectionSpecifiers(prefix) {
32
39
  ];
33
40
  }
34
41
 
42
+ /**
43
+ * The file of an icon collection from the first source installed under its
44
+ * name; a source not installed is skipped and the next one tried.
45
+ */
35
46
  function resolveCollectionFile(root, prefix) {
36
47
  const require = createRequire(join(root, "package.json"));
37
48
  for (const specifier of collectionSpecifiers(prefix)) {
38
49
  try {
39
50
  return require.resolve(specifier);
40
- } catch {
41
- // Not installed under this name: try the next source.
42
- }
51
+ } catch {}
43
52
  }
44
53
  return undefined;
45
54
  }
@@ -1,9 +1,3 @@
1
- // A page that completes its query as it renders, rendered at that query in
2
- // the same request rather than answered with a redirect.
3
- //
4
- // Lives apart from server.mjs so the file stays under the size the linter
5
- // allows.
6
-
7
1
  import { ofetch } from "ofetch";
8
2
 
9
3
  /**
@@ -34,6 +28,9 @@ function reusableReadKey(path, options, headers) {
34
28
  * `backendHeaders()` and the read's own. After `reuseCompletedReads()`, a later
35
29
  * render of the same request is given a copy of each read a previous render
36
30
  * completed instead of reading the backend again.
31
+ *
32
+ * A read returns a copy: the render that read it may still change the object it
33
+ * got. A body that cannot be copied is read again.
37
34
  */
38
35
  export function serverReads(backendHeaders) {
39
36
  const completed = new Map();
@@ -52,8 +49,6 @@ export function serverReads(backendHeaders) {
52
49
  headers,
53
50
  });
54
51
  if (!key) return reading;
55
- // A copy: the render that read it may still change the object it got. A
56
- // body that cannot be copied is read again.
57
52
  return reading.then((data) => {
58
53
  try {
59
54
  completed.set(key, structuredClone(data));
@@ -89,6 +84,9 @@ function rendersInPlace(request, from, location) {
89
84
  * the first render made (`fetch`), instead of being answered with a redirect
90
85
  * the browser would follow with a whole new request. The page served then
91
86
  * names the URL the document shows, which the browser puts in the address bar.
87
+ *
88
+ * A page that completes its query as it renders is rendered at that query in
89
+ * the same request, rather than answered with a redirect.
92
90
  */
93
91
  export async function renderInPlace(request, page, fetch, render, propsAt) {
94
92
  let rendered = await render(page);
@@ -1,5 +1,3 @@
1
- // The address the frontend server listens on.
2
-
3
1
  const LOOPBACK = "127.0.0.1";
4
2
  const EVERY_INTERFACE = "0.0.0.0";
5
3
 
@@ -10,6 +8,8 @@ const EVERY_INTERFACE = "0.0.0.0";
10
8
  * reach. `HOST=0.0.0.0` opens it to them. A production server listens on
11
9
  * every interface, as a container expects.
12
10
  *
11
+ * The address the frontend server listens on.
12
+ *
13
13
  * @param env The server's environment
14
14
  */
15
15
  export function listenHost(env = process.env) {
@@ -1,8 +1,3 @@
1
- // What a module route handler receives and how its answer is written: the
2
- // Web-standard request, the session, the module's sealed cookies, the backend,
3
- // and the defaults every route gets once (same-origin on unsafe methods, a body
4
- // limit, `no-store`). See `server-module.ts` for the types a module reads.
5
-
6
1
  import { Readable } from "node:stream";
7
2
  import { backend, json, RequestBodyError } from "./auth/backend.mjs";
8
3
  import {
@@ -165,6 +160,11 @@ function frozenCopy(value) {
165
160
  /**
166
161
  * The event a handler receives.
167
162
  *
163
+ * What a module route handler receives and how its answer is written: the Web-
164
+ * standard request, the session, the module's sealed cookies, the backend, and
165
+ * the defaults every route gets once (same-origin on unsafe methods, a body
166
+ * limit, `no-store`). See `server-module.ts` for the types a module reads.
167
+ *
168
168
  * @param context `{ request, response, route, params, module, app, view,
169
169
  * authOwner }`, where `module` is `{ id, options, privateOptions }`, `app`
170
170
  * the deployment's read-only data, `view` what a session shows a page and
@@ -1,8 +1,3 @@
1
- // The routes frontend modules declare in their `dms.server.ts`: what each
2
- // declaration is checked against, how conflicts are settled, and which route a
3
- // request reaches. Pure: the server hands it the declarations it loaded, and
4
- // `module-event.mjs` runs the handler a request reaches.
5
-
6
1
  import { resolveBackendProxies } from "./backend-proxies.mjs";
7
2
  import { compilePathPattern } from "./path-patterns.mjs";
8
3
  import { matchPublicEndpoint } from "./public-endpoints.mjs";
@@ -93,6 +88,10 @@ function optionsRejection(options) {
93
88
  return undefined;
94
89
  }
95
90
 
91
+ /**
92
+ * Two declarations of one path whatever their parameters are named share a
93
+ * shape.
94
+ */
96
95
  function compilePath(path) {
97
96
  const normalized = normalizedPath(path);
98
97
  const segments = normalized === "/" ? [] : normalized.slice(1).split("/");
@@ -107,7 +106,6 @@ function compilePath(path) {
107
106
  .join("");
108
107
  return {
109
108
  path: normalized,
110
- // Two declarations of one path whatever their parameters are named.
111
109
  shape: segments
112
110
  .map((segment) => (PARAM_SEGMENT.test(segment) ? ":" : segment))
113
111
  .join("/"),
@@ -432,6 +430,11 @@ function resolveRules(modules, warn) {
432
430
  * `proxies`: the backend prefixes the modules and the manifest declare, the
433
431
  * most specific first (see `backend-proxies.mjs`).
434
432
  *
433
+ * The routes frontend modules declare in their `dms.server.ts`: what each
434
+ * declaration is checked against, how conflicts are settled, and which route a
435
+ * request reaches. Pure: the server hands it the declarations it loaded, and
436
+ * `module-event.mjs` runs the handler a request reaches.
437
+ *
435
438
  * @param declarations `{ id, priority, setup }` per module with a dms.server.ts
436
439
  * @param warn Reports a declaration problem
437
440
  * @param publicEndpoints The deployment's public endpoints (#108), to report a
@@ -478,13 +481,14 @@ function decodedParams(route, match) {
478
481
  /**
479
482
  * The route a request reaches, with its parameters, or `undefined`.
480
483
  *
484
+ * A `HEAD` no route declares is the `GET`'s, without its body.
485
+ *
481
486
  * @param routes What `resolveServerDeclarations` returned
482
487
  * @param method The request's method
483
488
  * @param pathname The request's normalized path, without its query string
484
489
  */
485
490
  export function matchModuleRoute(routes, method, pathname) {
486
491
  const reached = matchMethod(routes, method, pathname);
487
- // A `HEAD` no route declares is the `GET`'s, without its body.
488
492
  if (reached || method !== "HEAD") return reached;
489
493
  return matchMethod(routes, "GET", pathname);
490
494
  }
@@ -1,9 +1,3 @@
1
- // Loads what the frontend modules declare in their `dms.server.ts` and hands
2
- // a request to the module route it reaches. In development the declarations
3
- // come from Vite (`ssrLoadModule`), so an edited entry or handler answers on
4
- // the next request; in production from the bundle `ajs dms build` makes
5
- // (`dist/server-modules`). A workspace built before the feature has none.
6
-
7
1
  import { existsSync, readFileSync } from "node:fs";
8
2
  import { fileURLToPath } from "node:url";
9
3
  import { allowedEstablishEndpoints } from "./auth/establish-endpoints.mjs";
@@ -185,6 +179,8 @@ async function currentDeclarations(devServer) {
185
179
  /**
186
180
  * A set of server modules, resolved for a deployment.
187
181
  *
182
+ * `authOwner` is the module whose sealed cookies are the `dms_*` ones.
183
+ *
188
184
  * @param declarations `{ id, priority, setup }` per module with a dms.server.ts
189
185
  * @param deployment `{ publicOptions, privateOptions, authEstablishEndpoints,
190
186
  * publicEndpoints, backendProxies, warn }`: each module's options by id,
@@ -211,7 +207,6 @@ export function createServerModules(declarations, deployment) {
211
207
  app: {
212
208
  authEstablishEndpoints: deployment.authEstablishEndpoints?.() ?? [],
213
209
  },
214
- // The module whose sealed cookies are the `dms_*` ones.
215
210
  authOwner: resolution.auth?.id,
216
211
  });
217
212
  return {
@@ -244,6 +239,12 @@ function serverModules(declarations) {
244
239
  /**
245
240
  * The server modules of this deployment, resolved.
246
241
  *
242
+ * Loads what the frontend modules declare in their `dms.server.ts` and hands a
243
+ * request to the module route it reaches. In development the declarations come
244
+ * from Vite (`ssrLoadModule`), so an edited entry or handler answers on the
245
+ * next request; in production from the bundle `ajs dms build` makes
246
+ * (`dist/server-modules`). A workspace built before the feature has none.
247
+ *
247
248
  * @param devServer Vite in development, `undefined` in production
248
249
  */
249
250
  export async function loadServerModules(devServer) {
@@ -1,10 +1,3 @@
1
- // Serves a frontend module's `dms.server.ts` routes the way the frontend
2
- // server does, for the module's own tests: same router, same event, same
3
- // cookie jar and the same defaults. Imported from
4
- // `@antelopejs/dms-frontend/templates/vue/server/module-testing.mjs`; the
5
- // server reads `DMS_API_BASE_URL` and `DMS_SESSION_SECRET` from the
6
- // environment, as in production.
7
-
8
1
  import { createServer } from "node:http";
9
2
  import { RequestBodyError, UpstreamError } from "./auth/backend.mjs";
10
3
  import { UNEXPECTED_ERROR_BODY } from "./backend-response.mjs";
@@ -26,6 +19,13 @@ function writeFailure(error, response) {
26
19
  * Starts an HTTP server on a free local port answering the modules' routes,
27
20
  * and `404` to anything else.
28
21
  *
22
+ * Serves a frontend module's `dms.server.ts` routes the way the frontend server
23
+ * does, for the module's own tests: same router, same event, same cookie jar
24
+ * and the same defaults. Imported from
25
+ * `@antelopejs/dms-frontend/templates/vue/server/module-testing.mjs`; the server reads
26
+ * `DMS_API_BASE_URL` and `DMS_SESSION_SECRET` from the environment, as in
27
+ * production.
28
+ *
29
29
  * @param declarations `{ id, priority?, setup }` per module, `setup` being
30
30
  * the default export of its `dms.server.ts`
31
31
  * @param deployment `{ publicOptions?, privateOptions?, authEstablishEndpoints?,
@@ -1,8 +1,3 @@
1
- // The props of a DMS page visit, and which of them the browser already has.
2
- //
3
- // Lives apart from server.mjs so the file stays under the size the linter
4
- // allows.
5
-
6
1
  import { createHash } from "node:crypto";
7
2
  import { readSession } from "./auth/session.mjs";
8
3
  import { BackendResponseError } from "./backend-response.mjs";
@@ -43,8 +38,10 @@ function holdsOnlyThisLayout(held, sharedKey) {
43
38
  );
44
39
  }
45
40
 
46
- // The parameters of the page request itself, which a page query of the same
47
- // name cannot override.
41
+ /**
42
+ * The parameters of the page request itself, which a page query of the same
43
+ * name cannot override.
44
+ */
48
45
  const PAGE_REQUEST_PARAMETERS = new Set(["path", SHARED_PROP]);
49
46
 
50
47
  /**
@@ -118,6 +115,16 @@ export async function propsForQuery(props, url, backend, auth) {
118
115
  * asks for it, and otherwise asks the backend for the page alone. A partial
119
116
  * reload gets the props it names, and the layout too when the browser's copy
120
117
  * is not this session's.
118
+ *
119
+ * The sealed session names the account and workspace, whatever the module
120
+ * owning sign-in shows a page of it.
121
+ *
122
+ * A partial reload merges what it gets into the page on screen: a session that
123
+ * ended has to be sent as one, not left out.
124
+ *
125
+ * The shared layout is declared only when the browser ends up with this
126
+ * session's layout, sent now or already held: declaring it otherwise would let
127
+ * a layout of another session pass for this one.
121
128
  */
122
129
  export async function loadPageProps(request, backend, auth) {
123
130
  const url = new URL(request.url, "http://frontend.local");
@@ -125,15 +132,11 @@ export async function loadPageProps(request, backend, auth) {
125
132
  const sealed = readSession(request);
126
133
  const { user, session } = auth.view(sealed);
127
134
  const { partial, wants } = requestedProps(request);
128
- // The sealed session names the account and workspace, whatever the module
129
- // owning sign-in shows a page of it.
130
135
  const sharedKey = sharedOnceKey(sealed);
131
136
  const holdsShared = holdsOnlyThisLayout(heldOnceProps(request), sharedKey);
132
137
  const withShared = (partial && wants(SHARED_PROP)) || !holdsShared;
133
138
  const props = {};
134
139
  if (wants("path")) props.path = pathname;
135
- // A partial reload merges what it gets into the page on screen: a session
136
- // that ended has to be sent as one, not left out.
137
140
  if (wants("user") && (user || partial)) props.user = user ?? null;
138
141
  if (wants("session") && (session || partial)) props.session = session ?? null;
139
142
  let shared;
@@ -145,9 +148,6 @@ export async function loadPageProps(request, backend, auth) {
145
148
  shared = await backend.fetchJson(SITE_LAYOUT_PATH);
146
149
  }
147
150
  if (shared) props[SHARED_PROP] = shared;
148
- // Declared only when the browser ends up with this session's layout: sent
149
- // now, or already held. Declaring it otherwise would let a layout of
150
- // another session pass for this one.
151
151
  const onceProps =
152
152
  shared || holdsShared
153
153
  ? {
@@ -1,7 +1,3 @@
1
- // Page path patterns, as the modules' `dms.server.ts` name them: `/x` is the
2
- // page itself, `/x/*` one segment below it, `/x/**` the page and everything
3
- // below it. Nothing else: no prefix match inside a segment, no query string.
4
-
5
1
  const SEGMENT = /^[A-Za-z0-9._~-]+$/;
6
2
 
7
3
  /**
@@ -9,6 +5,10 @@ const SEGMENT = /^[A-Za-z0-9._~-]+$/;
9
5
  *
10
6
  * `specificity` orders matching patterns from the least to the most specific:
11
7
  * more literal segments first, then a single-segment `*` over `**`.
8
+ *
9
+ * Page path patterns, as the modules' `dms.server.ts` name them: `/x` is the
10
+ * page itself, `/x/*` one segment below it, `/x/**` the page and everything
11
+ * below it. Nothing else: no prefix match inside a segment, no query string.
12
12
  */
13
13
  export function compilePathPattern(pattern) {
14
14
  if (typeof pattern !== "string" || !pattern.startsWith("/")) return undefined;
@@ -10,21 +10,26 @@ import { pipeWebBody } from "./web-body.mjs";
10
10
  */
11
11
  export const PLAIN_ANSWER_HEADERS = { "accept-encoding": "identity" };
12
12
 
13
- // Text the backend sends uncompressed: JSON above all, which the antelope API
14
- // module never compresses. An event stream is left as it is: its events are
15
- // small and many, and proxies between here and the browser expect it plain.
13
+ /**
14
+ * Text the backend sends uncompressed: JSON above all, which the antelope
15
+ * API module never compresses. An event stream is left as it is: its events
16
+ * are small and many, and proxies between here and the browser expect it
17
+ * plain.
18
+ */
16
19
  const COMPRESSIBLE_TYPE =
17
20
  /^(?:text\/(?!event-stream)[\w.+-]+|application\/(?:[\w.-]+\+)?(?:json|xml|javascript|ecmascript)|image\/svg\+xml)\s*(?:;|$)/i;
18
- // Statuses that carry no body to compress.
19
21
  const BODILESS_STATUSES = new Set([204, 205, 304]);
20
22
 
21
- /** Whether the backend answer is one to compress on its way to the client. */
23
+ /**
24
+ * Whether the backend answer is one to compress on its way to the client.
25
+ *
26
+ * A response already encoded, or a range of the bytes, is not compressed:
27
+ * compressing would change what the encoding or the range describes.
28
+ */
22
29
  function isCompressible(request, upstream) {
23
30
  if (request.method === "HEAD" || !upstream.body) return false;
24
31
  if (BODILESS_STATUSES.has(upstream.status)) return false;
25
32
  const headers = upstream.headers;
26
- // Already encoded, or a range of the bytes: compressing would change what
27
- // the encoding or the range describes.
28
33
  const encoding = headers.get("content-encoding");
29
34
  if (encoding && encoding.toLowerCase() !== "identity") return false;
30
35
  if (upstream.status === 206 || headers.has("content-range")) return false;
@@ -40,10 +45,11 @@ function isCompressible(request, upstream) {
40
45
  /**
41
46
  * The compressor for a backend answer the proxy relays, in the encoding the
42
47
  * client accepts, or `undefined` to relay it as it came.
48
+ *
49
+ * Streaming: an answer of unknown length (chunked JSON, streamed text) reaches
50
+ * the client part by part, as the backend writes it.
43
51
  */
44
52
  function proxyEncoder(request, upstream) {
45
- // Streaming: an answer of unknown length (chunked JSON, streamed text)
46
- // reaches the client part by part, as the backend writes it.
47
53
  return isCompressible(request, upstream)
48
54
  ? acceptedEncoder(request, { streaming: true })
49
55
  : undefined;
@@ -52,6 +58,12 @@ function proxyEncoder(request, upstream) {
52
58
  /**
53
59
  * Relays a backend answer to the client, compressed when it should be.
54
60
  * `isProxiedPath` decides which backend `Location` becomes a frontend path.
61
+ *
62
+ * Only native fetch bodies are decoded. Synthetic or opaque responses may still
63
+ * contain the encoded bytes, and unknown coding chains stay opaque.
64
+ *
65
+ * A decoded partial representation cannot retain the encoded byte offsets, and
66
+ * a complete 200 response is not manufactured from a partial 206 either.
55
67
  */
56
68
  export async function relayAnswer(
57
69
  request,
@@ -59,8 +71,6 @@ export async function relayAnswer(
59
71
  upstream,
60
72
  { fetched = false, isProxiedPath } = {},
61
73
  ) {
62
- // Only native fetch bodies are decoded. Synthetic/opaque responses may
63
- // still contain the encoded bytes, and unknown coding chains stay opaque.
64
74
  const codings = (upstream.headers.get("content-encoding") ?? "")
65
75
  .toLowerCase()
66
76
  .split(",")
@@ -74,8 +84,6 @@ export async function relayAnswer(
74
84
  ["gzip", "x-gzip", "deflate", "br"].includes(coding),
75
85
  )
76
86
  ) {
77
- // A decoded partial representation cannot retain the encoded byte offsets.
78
- // Do not manufacture a complete 200 response from a partial 206 either.
79
87
  if (upstream.status === 206 || upstream.headers.has("content-range")) {
80
88
  await upstream.body.cancel();
81
89
  response.writeHead(502);
@@ -1,8 +1,10 @@
1
1
  import { relayedLocation } from "./proxy-redirects.mjs";
2
2
 
3
- // Hop-by-hop headers describe the backend connection, not the client's one.
4
- // Forwarded, an explicit `connection: keep-alive` overrode the client's own
5
- // `Connection: close`, so Node kept the socket open until its idle timeout.
3
+ /**
4
+ * Hop-by-hop headers describe the backend connection, not the client's one.
5
+ * Forwarded, an explicit `connection: keep-alive` overrode the client's own
6
+ * `Connection: close`, so Node kept the socket open until its idle timeout.
7
+ */
6
8
  const HOP_BY_HOP_HEADERS = [
7
9
  "connection",
8
10
  "keep-alive",
@@ -1,8 +1,3 @@
1
- // A backend redirect is the browser's to follow. Followed server-side, a
2
- // redirect to another site served that site's page, and set its cookies, on
3
- // the frontend's origin, and a redirect on the backend lost its Location.
4
- // The proxy fetches with `redirect: "manual"` and relays the redirect instead.
5
-
6
1
  /** The statuses that send the client to their `Location`. */
7
2
  export const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
8
3
 
@@ -32,6 +27,13 @@ export function isBackendUrl(
32
27
  * declared backend proxy) stays an absolute backend URL, which the browser
33
28
  * follows to the backend. Another site's location is kept.
34
29
  *
30
+ * A backend redirect is the browser's to follow. Followed server-side, a
31
+ * redirect to another site served that site's page, and set its cookies, on the
32
+ * frontend's origin, and a redirect on the backend lost its Location. The proxy
33
+ * fetches with `redirect: "manual"` and relays the redirect instead.
34
+ *
35
+ * `//host` would name another host, so `/.` keeps the location a path.
36
+ *
35
37
  * @param location The backend's `Location`
36
38
  * @param fetchedUrl The backend URL the answer came from
37
39
  * @param isProxiedPath `(pathname) => boolean`, whether the frontend server
@@ -51,7 +53,6 @@ export function relayedLocation(
51
53
  if (target.origin !== originOf(fetchedUrl)) return location;
52
54
  if (!isProxiedPath(target.pathname)) return target.href;
53
55
  const path = `${target.pathname}${target.search}${target.hash}`;
54
- // `//host` would name another host: `/.` keeps it a path.
55
56
  return path.startsWith("//") ? `/.${path}` : path;
56
57
  }
57
58
 
@@ -1,17 +1,15 @@
1
- // Large transfers through the proxy, a file upload above all. The request
2
- // body is streamed to the backend, never buffered, and held to the
3
- // `maxBodyBytes` of the proxy it reached: a declared Content-Length over the
4
- // limit is refused before a byte is passed on, a chunked body is cut off as
5
- // soon as it goes over.
6
-
7
1
  const PAYLOAD_TOO_LARGE = 413;
8
2
  const TOO_LARGE_BODY = JSON.stringify({ message: "Payload Too Large" });
9
- // The refused body is never passed on: the iteration stops where it went
10
- // over, and the request stream stays open for the lingering close below.
3
+ /**
4
+ * The refused body is never passed on: the iteration stops where it went
5
+ * over, and the request stream stays open for the lingering close below.
6
+ */
11
7
  const BODY_ITERATION = { destroyOnReturn: false };
12
- // How long a refused body is read and dropped before the connection closes
13
- // anyway: long enough for a client to finish sending what was in flight,
14
- // short enough to bound what a refused client can make the server read.
8
+ /**
9
+ * How long a refused body is read and dropped before the connection closes
10
+ * anyway: long enough for a client to finish sending what was in flight,
11
+ * short enough to bound what a refused client can make the server read.
12
+ */
15
13
  const LINGER_MS = 5_000;
16
14
 
17
15
  const BODILESS_METHODS = new Set(["GET", "HEAD"]);
@@ -92,6 +90,12 @@ function refuseTooLargeBody(request, response) {
92
90
  /**
93
91
  * Sends a proxied request, its body held to `maxBodyBytes`.
94
92
  *
93
+ * Large transfers through the proxy, a file upload above all. The request body
94
+ * is streamed to the backend, never buffered, and held to the `maxBodyBytes` of
95
+ * the proxy it reached: a declared Content-Length over the limit is refused
96
+ * before a byte is passed on, a chunked body is cut off as soon as it goes
97
+ * over.
98
+ *
95
99
  * @param send `(body) => Promise<Response>`, the backend request with the
96
100
  * body to stream: none for a GET or a HEAD, the request itself, or its
97
101
  * bytes cut off past the limit