@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
@@ -0,0 +1,65 @@
1
+ /** The `<script>` the server render leaves in the document for {@link DmsMessageSubset}. */
2
+ export const SSR_MESSAGES_ID = "dms-ssr-messages";
3
+
4
+ type Messages = Record<string, unknown>;
5
+
6
+ /**
7
+ * The messages a server render read from a locale's catalog, as a catalog of
8
+ * their own, with the locale they belong to.
9
+ */
10
+ export interface DmsMessageSubset {
11
+ locale: string;
12
+ messages: Messages;
13
+ }
14
+
15
+ export interface MessageReadRecorder {
16
+ /** The catalog to give the render: every read through it is recorded. */
17
+ messages: Messages;
18
+ /** What the render read so far. */
19
+ subset: () => Messages;
20
+ }
21
+
22
+ function isTraversable(value: unknown): value is object {
23
+ return typeof value === "object" && value !== null;
24
+ }
25
+
26
+ function isMessages(value: unknown): value is Messages {
27
+ return isTraversable(value);
28
+ }
29
+
30
+ type Slot = () => Messages;
31
+
32
+ /**
33
+ * Wraps a catalog so that the messages read through it are collected, with
34
+ * the path they sit at, into a catalog of their own.
35
+ *
36
+ * A message is recorded when a string is read from the catalog, whether by a
37
+ * lookup (`t`), an existence check (`te`) or a walk over a subtree (`tm`).
38
+ */
39
+ export function recordMessageReads(messages: Messages): MessageReadRecorder {
40
+ const used: Messages = {};
41
+ const place = (source: object, parent: Slot, key: string): Slot => {
42
+ return () => {
43
+ const holder = parent();
44
+ const existing = holder[key];
45
+ if (isMessages(existing)) return existing;
46
+ const container: Messages = Array.isArray(source) ? [] : {};
47
+ holder[key] = container;
48
+ return container;
49
+ };
50
+ };
51
+ const wrap = <T extends object>(source: T, slot: Slot): T =>
52
+ new Proxy(source, {
53
+ get(target, key, receiver) {
54
+ const value: unknown = Reflect.get(target, key, receiver);
55
+ if (typeof key !== "string" || key === "__proto__") return value;
56
+ if (typeof value === "string") {
57
+ slot()[key] = value;
58
+ return value;
59
+ }
60
+ if (isTraversable(value)) return wrap(value, place(value, slot, key));
61
+ return value;
62
+ },
63
+ });
64
+ return { messages: wrap(messages, () => used), subset: () => used };
65
+ }
@@ -15,6 +15,10 @@ export interface DmsNetworkErrorTranslator {
15
15
 
16
16
  const NETWORK_ERROR_TOAST_ID = "dms-network-error";
17
17
  const NETWORK_ERROR_KEY = "error.network";
18
+ /** The messages a module overrides the built-in texts with. */
19
+ export const NETWORK_ERROR_KEYS = ["title", "description"].map(
20
+ (name) => `${NETWORK_ERROR_KEY}.${name}`,
21
+ );
18
22
  const DEFAULT_LOCALE = "en";
19
23
  // Shipped with the renderer, since no module is guaranteed to translate the
20
24
  // case; a module overrides them with `error.network.title` and
@@ -1,10 +1,3 @@
1
- // Writes the files Vite generates and the type check reads (auto-imports.d.ts
2
- // and the Nuxt UI templates) without building anything: it resolves
3
- // vite.config.ts and runs its build hooks over an empty entry, in seconds
4
- // where a client build takes a minute. `ajs dms verify-source` runs it first
5
- // (`pnpm run build:types`), so the type check no longer waits for the client
6
- // bundle.
7
-
8
1
  import { build } from "vite";
9
2
 
10
3
  const ENTRY = "dms-prepare-types-entry";
@@ -23,7 +16,6 @@ await build({
23
16
  build: {
24
17
  write: false,
25
18
  emptyOutDir: false,
26
- // The entry is empty on purpose; Rolldown does not warn about it.
27
19
  rolldownOptions: { input: ENTRY },
28
20
  },
29
21
  });
@@ -21,8 +21,10 @@ export class UpstreamError extends Error {
21
21
  }
22
22
 
23
23
  const PUBLIC_AUTH_MESSAGE = /^error\.[a-z0-9_.-]{1,100}$/;
24
- // The API error contract's generic key: the technical cause of a failure the
25
- // backend did not describe stays in the server log.
24
+ /**
25
+ * The API error contract's generic key: the technical cause of a failure the
26
+ * backend did not describe stays in the server log.
27
+ */
26
28
  export const GENERIC_ERROR_MESSAGE = "error.500.description";
27
29
 
28
30
  export function publicBackendMessage(data) {
@@ -1,17 +1,17 @@
1
- // The backend endpoints a module may open a session from: what the backend's
2
- // modules declared in the frontend manifest, and what the environment adds
3
- // (`DMS_AUTH_ESTABLISH_ENDPOINTS`). The module that owns sign-in reads them as
4
- // `event.app.authEstablishEndpoints`.
5
-
6
1
  import { readFileSync } from "node:fs";
7
2
 
8
- // Absolute backend API paths only: no scheme, no authority, no query string,
9
- // and no segment that could climb out of `/api/`.
3
+ /**
4
+ * Absolute backend API paths only: no scheme, no authority, no query string,
5
+ * and no segment that could climb out of `/api/`.
6
+ */
10
7
  const BACKEND_API_PATH =
11
8
  /^\/api\/[A-Za-z0-9][A-Za-z0-9._~-]*(?:\/[A-Za-z0-9][A-Za-z0-9._~-]*)*$/;
12
9
 
13
- // Written at the workspace root by the loader, from the endpoints each backend
14
- // module declared in the frontend manifest. This file sits two directories up.
10
+ /**
11
+ * Written at the workspace root by the loader, from the endpoints each
12
+ * backend module declared in the frontend manifest. This file sits two
13
+ * directories up.
14
+ */
15
15
  const DECLARED_ENDPOINTS_FILE = new URL(
16
16
  "../../generated-auth-establish.json",
17
17
  import.meta.url,
@@ -49,6 +49,11 @@ export function readDeclaredEstablishEndpoints(file = DECLARED_ENDPOINTS_FILE) {
49
49
  * stays refused, so no backend endpoint that happens to mint a token pair can
50
50
  * be turned into a login by a request from the browser.
51
51
  *
52
+ * The backend endpoints a module may open a session from: what the backend's
53
+ * modules declared in the frontend manifest, and what the environment adds
54
+ * (`DMS_AUTH_ESTABLISH_ENDPOINTS`). The module that owns sign-in reads them as
55
+ * `event.app.authEstablishEndpoints`.
56
+ *
52
57
  * @param declaration Comma-separated absolute backend paths from the environment
53
58
  * @param declared Paths declared by the backend's frontend modules
54
59
  * @returns The well-formed backend API paths, without duplicates
@@ -15,6 +15,9 @@ const MAX_AGE = 60 * 60 * 24 * 30;
15
15
  * frontends on localhost:3000 and localhost:3001 would overwrite each other's
16
16
  * session, each failing to decrypt the other's and signing the user out. The
17
17
  * suffix follows the backend, so a restart keeps the cookies it wrote.
18
+ *
19
+ * A `DMS_API_BASE_URL` that is not a URL is hashed as written: the raw string
20
+ * still tells two backends apart.
18
21
  */
19
22
  function cookieScope() {
20
23
  const backendUrl = (process.env.DMS_API_BASE_URL ?? "").trim();
@@ -24,9 +27,7 @@ function cookieScope() {
24
27
  if (url.hostname === "localhost" || url.hostname === "[::1]")
25
28
  url.hostname = "127.0.0.1";
26
29
  canonical = url.toString().replace(/\/$/, "");
27
- } catch {
28
- // Not a URL: the raw string still tells two backends apart.
29
- }
30
+ } catch {}
30
31
  return createHash("sha256").update(canonical).digest("hex").slice(0, 12);
31
32
  }
32
33
 
@@ -1,9 +1,3 @@
1
- // The session logic the server runs outside any route: renewing a session the
2
- // backend refused, the session a page carries, and where a signed-out visit
3
- // goes. The module that owns sign-in provides it through `registerAuth` in its
4
- // `dms.server.ts`. Without one, a page carries no session and a refused
5
- // session is not renewed.
6
-
7
1
  import { readSession, setRequestSession } from "./auth/session.mjs";
8
2
  import { createServerEvent } from "./module-event.mjs";
9
3
  import { compilePathPattern } from "./path-patterns.mjs";
@@ -40,6 +34,12 @@ function hookView(publicSession, session) {
40
34
  /**
41
35
  * The server's session logic for a set of server modules.
42
36
  *
37
+ * The session logic the server runs outside any route: renewing a session the
38
+ * backend refused, the session a page carries, and where a signed-out visit
39
+ * goes. The module that owns sign-in provides it through `registerAuth` in its
40
+ * `dms.server.ts`. Without one, a page carries no session and a refused session
41
+ * is not renewed.
42
+ *
43
43
  * @param provider The resolved auth hooks, `undefined` when no module
44
44
  * registers any
45
45
  * @param context `(id) => ({ module, app })`, a module's event context
@@ -1,9 +1,3 @@
1
- // The backend prefixes the frontend server proxies. The renderer hardcodes
2
- // none: a frontend module declares them in its `dms.server.ts`
3
- // (`registerBackendProxy`), and a backend module in the frontend manifest
4
- // (`backendProxies`, next to its `publicEndpoints`). A path no prefix covers
5
- // is never sent to the backend.
6
-
7
1
  import { compilePathPattern } from "./path-patterns.mjs";
8
2
 
9
3
  /** How the proxy authenticates a request to the backend. */
@@ -60,6 +54,12 @@ function compileProxy(pattern, options, owner) {
60
54
  /**
61
55
  * The deployment's backend proxies, one per pattern.
62
56
  *
57
+ * The backend prefixes the frontend server proxies. The renderer hardcodes
58
+ * none: a frontend module declares them in its `dms.server.ts`
59
+ * (`registerBackendProxy`), and a backend module in the frontend manifest
60
+ * (`backendProxies`, next to its `publicEndpoints`). A path no prefix covers is
61
+ * never sent to the backend.
62
+ *
63
63
  * @param declared `{ id, pattern, options }` per `registerBackendProxy` call,
64
64
  * from the highest-priority module down
65
65
  * @param manifest `{ module, path, auth?, maxBodyBytes? }` per backend
@@ -1,18 +1,17 @@
1
- // Reading a failed DMS backend response for the page a visitor gets instead.
2
- //
3
- // Lives apart from server.mjs so the file stays under the size the linter
4
- // allows.
5
-
6
1
  import { GENERIC_ERROR_MESSAGE } from "./auth/backend.mjs";
7
2
  import { isFrontendVisit, redirectFrontendVisit } from "./inertia.mjs";
8
3
 
9
- // What a JSON caller gets when the backend fails: the generic i18n key of the
10
- // API error contract. A page gets the status code alone, which the DMS error
11
- // page describes in the visitor's language; the technical cause only reaches
12
- // the server log.
4
+ /**
5
+ * What a JSON caller gets when the backend fails: the generic i18n key of
6
+ * the API error contract. A page gets the status code alone, which the DMS
7
+ * error page describes in the visitor's language; the technical cause only
8
+ * reaches the server log.
9
+ */
13
10
  export const UNEXPECTED_ERROR_BODY = { message: GENERIC_ERROR_MESSAGE };
14
- // A typed refusal carries a machine-readable i18n key as its body, bare or
15
- // JSON-encoded, e.g. `saas.errors.workspace.access_blocked`.
11
+ /**
12
+ * A typed refusal carries a machine-readable i18n key as its body, bare or
13
+ * JSON-encoded, e.g. `saas.errors.workspace.access_blocked`.
14
+ */
16
15
  const REFUSAL_CODE = /^[a-z0-9_-]+(?:\.[a-z0-9_-]+)+$/i;
17
16
  const REFUSAL_CODE_MAX_LENGTH = 200;
18
17
  const FORBIDDEN = 403;
@@ -26,6 +25,10 @@ export class BackendResponseError extends Error {
26
25
  }
27
26
  }
28
27
 
28
+ /**
29
+ * The refusal code in a backend answer: the string or the `message` of its
30
+ * JSON body, or the body itself when it is not JSON (a bare code).
31
+ */
29
32
  async function refusalCode(response) {
30
33
  const text = (await response.text().catch(() => "")).trim();
31
34
  if (text.length > REFUSAL_CODE_MAX_LENGTH) return undefined;
@@ -33,9 +36,7 @@ async function refusalCode(response) {
33
36
  try {
34
37
  const data = JSON.parse(text);
35
38
  value = typeof data === "string" ? data : data?.message;
36
- } catch {
37
- // A bare code.
38
- }
39
+ } catch {}
39
40
  return typeof value === "string" && REFUSAL_CODE.test(value)
40
41
  ? value
41
42
  : undefined;
@@ -1,10 +1,3 @@
1
- // The built files the production server hands to a browser: what the client
2
- // build wrote to `dist/client`, except a file or directory whose name starts
3
- // with a dot. Vite keeps its build metadata there (`.vite/manifest.json`, the
4
- // source path of every module file), and a module's `public/` may hold dot
5
- // files too. `/.well-known/`, which RFC 8615 reserves for files meant to be
6
- // public, stays served.
7
-
8
1
  import { existsSync, statSync } from "node:fs";
9
2
  import { relative, resolve, sep } from "node:path";
10
3
 
@@ -14,6 +7,13 @@ const PUBLIC_DOT_DIRECTORY = ".well-known";
14
7
  * Whether a path relative to the build root may be served: no segment starts
15
8
  * with a dot, but a leading `.well-known`. Split on both separators: on
16
9
  * Windows, `relative` answers with backslashes, and a decoded `%5C` is one.
10
+ *
11
+ * The built files the production server hands to a browser: what the client
12
+ * build wrote to `dist/client`, except a file or directory whose name starts
13
+ * with a dot. Vite keeps its build metadata there (`.vite/manifest.json`, the
14
+ * source path of every module file), and a module's `public/` may hold dot
15
+ * files too. `/.well-known/`, which RFC 8615 reserves for files meant to be
16
+ * public, stays served.
17
17
  */
18
18
  export function isPublicBuildPath(relativePath) {
19
19
  return relativePath
@@ -30,6 +30,9 @@ export function isPublicBuildPath(relativePath) {
30
30
  * serves none there: the path is malformed, leaves `root`, names a dot file or
31
31
  * directory, or is not a regular file. Decoded first, as the browser encoded
32
32
  * it, so the file checked is the file served.
33
+ *
34
+ * The prefix uses the native separator: `resolve` answers with backslashes on
35
+ * Windows.
33
36
  */
34
37
  export function publicBuildFile(root, pathname) {
35
38
  let decoded;
@@ -40,7 +43,6 @@ export function publicBuildFile(root, pathname) {
40
43
  }
41
44
  const base = resolve(root);
42
45
  const file = resolve(base, `.${decoded}`);
43
- // Native separator: `resolve` answers with backslashes on Windows.
44
46
  if (!file.startsWith(`${base}${sep}`)) return undefined;
45
47
  if (!isPublicBuildPath(relative(base, file))) return undefined;
46
48
  if (!existsSync(file) || !statSync(file).isFile()) return undefined;
@@ -1,7 +1,3 @@
1
- // What the server tells whoever started it. Started by `ajs dms dev` or
2
- // `ajs dms start`, it reports over IPC and the CLI words what the user reads.
3
- // Started on its own, it prints the line itself.
4
-
5
1
  import { readFileSync } from "node:fs";
6
2
  import { resolve } from "node:path";
7
3
  import { stripVTControlCharacters } from "node:util";
@@ -13,7 +9,13 @@ async function report(message, line, write) {
13
9
  await new Promise((sent) => process.send(message, () => sent()));
14
10
  }
15
11
 
16
- /** The server answers at `address`:`port`. */
12
+ /**
13
+ * The server answers at `address`:`port`.
14
+ *
15
+ * What the server tells whoever started it. Started by `ajs dms dev` or `ajs
16
+ * dms start`, it reports over IPC and the CLI words what the user reads.
17
+ * Started on its own, it prints the line itself.
18
+ */
17
19
  export async function reportReady(address, port) {
18
20
  const host = WILDCARD_ADDRESSES.includes(address)
19
21
  ? "localhost"
@@ -1,14 +1,12 @@
1
- // Answers for requests Node rejects before the request handler runs. Without
2
- // a `clientError` listener Node answers on its own and logs nothing, so a page
3
- // refused for its headers left no trace in the server output.
4
-
5
1
  const HEADER_OVERFLOW = "HPE_HEADER_OVERFLOW";
6
2
  const HEADER_OVERFLOW_RESPONSE =
7
3
  "HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n";
8
4
  const BAD_REQUEST_RESPONSE =
9
5
  "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
10
- // A browser retries a refused page with the same cookies: one hint per minute
11
- // is enough to explain them all.
6
+ /**
7
+ * A browser retries a refused page with the same cookies: one hint per
8
+ * minute is enough to explain them all.
9
+ */
12
10
  const HEADER_OVERFLOW_HINT_INTERVAL_MS = 60_000;
13
11
 
14
12
  let lastHeaderOverflowHint = -Infinity;
@@ -27,6 +25,13 @@ function warnHeaderOverflow(maxHeaderSize) {
27
25
  /**
28
26
  * The server's `clientError` listener: explains a 431 in the logs, and
29
27
  * otherwise answers as Node does without a listener.
28
+ *
29
+ * Answers requests Node rejects before the request handler runs. Without a
30
+ * `clientError` listener Node answers on its own and logs nothing, so a page
31
+ * refused for its headers left no trace in the server output.
32
+ *
33
+ * Once a response went out on this connection, a status line would land in the
34
+ * middle of it.
30
35
  */
31
36
  export function handleClientError(error, socket, maxHeaderSize) {
32
37
  let response = BAD_REQUEST_RESPONSE;
@@ -34,8 +39,6 @@ export function handleClientError(error, socket, maxHeaderSize) {
34
39
  response = HEADER_OVERFLOW_RESPONSE;
35
40
  warnHeaderOverflow(maxHeaderSize);
36
41
  }
37
- // Once a response went out on this connection, a status line would land in
38
- // the middle of it.
39
42
  if (socket.writable && socket.bytesWritten === 0) socket.write(response);
40
43
  socket.destroy();
41
44
  }
@@ -1,9 +1,3 @@
1
- // Reading the built client manifest and turning the components a server render
2
- // created into the stylesheet links its HTML needs.
3
- //
4
- // Lives apart from server.mjs so the file stays under the size the linter
5
- // allows. Production-only: in dev, Vite serves these itself.
6
-
7
1
  import { existsSync, readFileSync } from "node:fs";
8
2
  import { join } from "node:path";
9
3
  import { fileURLToPath } from "node:url";
@@ -16,11 +10,18 @@ const CLIENT_MANIFEST_PATH = join(
16
10
  );
17
11
  const SOURCE_TEMPLATE_PATH = join(PROJECT_ROOT, "index.html");
18
12
 
19
- // Read once and kept: both files are build output and cannot change while the
20
- // server is up.
13
+ /**
14
+ * Read once and kept: both files are build output and cannot change while
15
+ * the server is up.
16
+ */
21
17
  let clientManifest;
22
18
  let productionTemplate;
23
19
 
20
+ /**
21
+ * Reads the built client manifest and turns the components a server render
22
+ * created into the stylesheet links its HTML needs. Production-only: in dev,
23
+ * Vite serves these itself.
24
+ */
24
25
  export function productionHtmlTemplate() {
25
26
  if (productionTemplate) return productionTemplate;
26
27
  const templatePath = existsSync(BUILT_TEMPLATE_PATH)
@@ -15,6 +15,8 @@ const DYNAMIC_BROTLI_QUALITY = 4;
15
15
  * accepts (Brotli first), or `undefined` when it accepts neither. `streaming`
16
16
  * flushes what each write gives at once, for a body written over time: the
17
17
  * client gets each part when it is written, not when a block fills.
18
+ *
19
+ * Implicit identity is a fallback, not a preference over compression.
18
20
  */
19
21
  export function acceptedEncoder(request, { streaming = false } = {}) {
20
22
  const accepted = request.headers["accept-encoding"] ?? "";
@@ -38,7 +40,6 @@ export function acceptedEncoder(request, { streaming = false } = {}) {
38
40
  const quality = (token) => qualities.get(token) ?? qualities.get("*") ?? 0;
39
41
  const br = quality("br");
40
42
  const gzip = quality("gzip");
41
- // Implicit identity is a fallback, not a preference over compression.
42
43
  const identity = qualities.get("identity") ?? 0;
43
44
  if (identity > Math.max(br, gzip)) return undefined;
44
45
  if (br > 0 && br >= gzip)
@@ -1,15 +1,3 @@
1
- // The files of the workspace the development server never hands to a browser.
2
- //
3
- // Vite's root is the workspace root, where the generated server options (the
4
- // modules' private options: the backend's secrets) and the cached manifest are
5
- // written, and the modules' server code is materialized under it. Vite serves
6
- // any file under its root, as is, `?raw`, `?import` or `?url`. Its
7
- // `server.fs.deny` list covers most of these, but not every query combination
8
- // (`?import&raw` got through in Vite 7.2; Vite 8.3 refuses it itself), so a middleware ahead of Vite's own
9
- // answers 403 by path, whatever the query, and a load guard refuses them to
10
- // any client request that reaches the transform pipeline another way.
11
- // `ssrLoadModule`, which loads the server modules, is left alone.
12
-
13
1
  import { relative, resolve, sep } from "node:path";
14
2
 
15
3
  const PRIVATE_FILE_NAMES = new Set([
@@ -22,7 +10,8 @@ const FORBIDDEN_STATUS = 403;
22
10
 
23
11
  /**
24
12
  * Vite's `server.fs.deny` for the development server: Vite's own defaults,
25
- * which a `deny` list replaces, and the private files.
13
+ * which a `deny` list replaces, and the private files: the generated secrets
14
+ * and the modules' server code are never served.
26
15
  */
27
16
  export const DEV_FS_DENY = [
28
17
  ".env",
@@ -68,7 +57,24 @@ function requestedFile(url, root) {
68
57
  return resolve(root, `.${sep}${pathname.replace(/^[\\/]+/, "")}`);
69
58
  }
70
59
 
71
- /** Answers 403 to private files, and fails a client load of one. */
60
+ /**
61
+ * Answers 403 to private files, and fails a client load of one.
62
+ *
63
+ * The files of the workspace the development server never hands to a
64
+ * browser. Vite's root is the workspace root, where the generated server
65
+ * options (the modules' private options: the backend's secrets) and the
66
+ * cached manifest are written, and the modules' server code is materialized
67
+ * under it. Vite serves any file under its root, as is, `?raw`, `?import` or
68
+ * `?url`. Its `server.fs.deny` list covers most of these, but not every
69
+ * query combination (`?import&raw` got through in Vite 7.2; Vite 8.3 refuses
70
+ * it itself), so a middleware ahead of Vite's own answers 403 by path,
71
+ * whatever the query, and a load guard refuses them to any client request
72
+ * that reaches the transform pipeline another way. `ssrLoadModule`, which
73
+ * loads the server modules, is left alone.
74
+ *
75
+ * The middleware is registered directly, not returned: it runs before Vite's
76
+ * middlewares.
77
+ */
72
78
  export function denyPrivateFiles() {
73
79
  let root = process.cwd();
74
80
  return {
@@ -79,7 +85,6 @@ export function denyPrivateFiles() {
79
85
  root = config.root;
80
86
  },
81
87
  configureServer(server) {
82
- // Registered directly, not returned: it runs before Vite's middlewares.
83
88
  server.middlewares.use((request, response, next) => {
84
89
  const file = requestedFile(request.url, root);
85
90
  if (!file || !isPrivateFile(file, root)) return next();
@@ -1,21 +1,3 @@
1
- // Putting the page's stylesheets in the document the development server
2
- // returns, instead of letting the client entry inject them from JavaScript
3
- // once it has executed.
4
- //
5
- // Vite serves every stylesheet as a JavaScript module in development, so a
6
- // plain SSR document carries no styles at all and the browser paints raw HTML
7
- // until `/main.ts` and its module graph have run. Production has no such gap:
8
- // the built `index.html` already links the entry stylesheet, which is why this
9
- // module is development-only and lives beside client-manifest.mjs rather than
10
- // inside server.mjs, whose size the linter caps.
11
- //
12
- // Each tag carries the module id Vite will use for its own copy
13
- // (`data-vite-dev-id`), under our own attribute name. `main.ts` hands the
14
- // stylesheet over as soon as Vite injects that copy — see `adoptViteDevStyles`.
15
- // We deliberately do NOT reuse `data-vite-dev-id` itself: Vite's client adopts
16
- // any pre-existing link with that attribute and then skips `updateStyle` for
17
- // it, which would leave CSS edits stranded.
18
-
19
1
  import { join, sep } from "node:path";
20
2
  import { fileURLToPath } from "node:url";
21
3
  import { renderedModuleStyleTags } from "./client-manifest.mjs";
@@ -47,12 +29,14 @@ function ssrModuleGraph(devServer) {
47
29
  return devServer.environments?.ssr?.moduleGraph ?? devServer.moduleGraph;
48
30
  }
49
31
 
50
- // Vite only treats an id as a stylesheet when the language is the LAST thing in
51
- // it, and a component's style block ends in `&lang.css`. Appending `&direct`
52
- // after that makes Vite skip its stylesheet pipeline, so the block comes back
53
- // as written: `@import "package/dist/style.css"` is left for the browser, which
54
- // resolves it against the page URL and gets a 404. `direct` therefore goes in
55
- // front of the trailing language segment.
32
+ /**
33
+ * Vite only treats an id as a stylesheet when the language is the last thing
34
+ * in it, and a component's style block ends in `&lang.css`. Appending
35
+ * `&direct` after that makes Vite skip its stylesheet pipeline, so the block
36
+ * comes back as written: `@import "package/dist/style.css"` is left for the
37
+ * browser, which resolves it against the page URL and gets a 404. `direct`
38
+ * therefore goes in front of the trailing language segment.
39
+ */
56
40
  const TRAILING_LANG = /&(lang\.[^&?]*)$/;
57
41
 
58
42
  function directUrl(url) {
@@ -90,7 +74,20 @@ async function inlineTag(devServer, { url, id }) {
90
74
  return `<style ${STYLE_ATTRIBUTE}="${id}">${css}</style>`;
91
75
  }
92
76
 
93
- /** Style tags for everything the development render touched, entry sheet first. */
77
+ /**
78
+ * Style tags for everything the development render touched, entry sheet
79
+ * first.
80
+ *
81
+ * Puts the page's stylesheets in the document the development server
82
+ * returns, instead of letting the client entry inject them from JavaScript
83
+ * once it has executed. Vite serves every stylesheet as a JavaScript module
84
+ * in development, so a plain SSR document carries no styles at all and the
85
+ * browser paints raw HTML until `/main.ts` and its module graph have run.
86
+ * Production has no such gap: the built `index.html` already links the entry
87
+ * stylesheet, which is why this module is development-only and lives beside
88
+ * client-manifest.mjs rather than inside server.mjs, whose size the linter
89
+ * caps.
90
+ */
94
91
  export async function developmentStyleTags(devServer) {
95
92
  const modules = styleModules(devServer);
96
93
  const links = [
@@ -1,20 +1,8 @@
1
- // Development-only email bundle builder, forked by the frontend server.
2
- //
3
- // Runs Vite's email build in watch mode so `dist/server` exists as soon as the
4
- // dev server does and follows every template edit the layer watchers mirror
5
- // into the workspace. It lives in its own process so its builds never hold the
6
- // dev server's event loop, and it reports each build over IPC so the server
7
- // renders from a finished bundle only. A build empties `dist/server` first, so
8
- // it asks the server before it starts and waits for the go-ahead, which the
9
- // server gives once no render reads the previous bundle any more.
10
-
11
1
  import { fileURLToPath } from "node:url";
12
2
  import { build } from "vite";
13
3
 
14
4
  const PROJECT_ROOT = fileURLToPath(new URL("..", import.meta.url));
15
5
 
16
- // The server owns this process: when its IPC channel closes, however the
17
- // server ended, there is nobody left to build for.
18
6
  process.on("disconnect", () => process.exit(0));
19
7
 
20
8
  let startBuild;
@@ -10,18 +10,23 @@ const EMAIL_BUNDLE_WATCH_URL = new URL(
10
10
  "./email-bundle-watch.mjs",
11
11
  import.meta.url,
12
12
  );
13
- // Bound the request's wait, but never read a bundle being replaced.
13
+ /** Bound the request's wait, but never read a bundle being replaced. */
14
14
  const EMAIL_BUILD_WAIT_MS = 60_000;
15
15
 
16
- // Settled whenever no email build is running. The version changes with each
17
- // finished build: Node caches an imported module by URL, so the renderer is
18
- // imported under a new one to pick up rebuilt templates.
16
+ /**
17
+ * Settled whenever no email build is running. The version changes with each
18
+ * finished build: Node caches an imported module by URL, so the renderer is
19
+ * imported under a new one to pick up rebuilt templates.
20
+ */
19
21
  let emailBuild = Promise.resolve();
20
22
  let settleEmailBuild;
21
23
  let emailBundleVersion = 0;
22
- // A build empties dist/server before it writes the new bundle, so it never
23
- // starts while a render still imports from the previous one: the builder asks
24
- // first, and gets the go-ahead once the renders in progress have finished.
24
+ /**
25
+ * A build empties dist/server before it writes the new bundle, so it never
26
+ * starts while a render still imports from the previous one: the builder
27
+ * asks first, and gets the go-ahead once the renders in progress have
28
+ * finished.
29
+ */
25
30
  let rendersInProgress = 0;
26
31
  let startPendingBuild;
27
32
 
@@ -56,7 +61,8 @@ function renderFinished() {
56
61
  * Build the email bundle now and again on every template change, for
57
62
  * development: only the production build otherwise produces it. The builder
58
63
  * runs in a child process at low priority, so the dev server keeps answering
59
- * while it works.
64
+ * while it works. Lowering the priority is skipped where the platform does not
65
+ * permit it: the build then runs at normal priority.
60
66
  */
61
67
  export function watchEmailBundle() {
62
68
  emailBuildStarted();
@@ -65,9 +71,7 @@ export function watchEmailBundle() {
65
71
  });
66
72
  try {
67
73
  setPriority(builder.pid, constants.priority.PRIORITY_BELOW_NORMAL);
68
- } catch {
69
- // Not permitted on this platform; the build only runs at normal priority.
70
- }
74
+ } catch {}
71
75
  builder.on("message", (message) => {
72
76
  if (message?.state !== "building") {
73
77
  emailBuildSettled();
@@ -85,9 +89,9 @@ export function watchEmailBundle() {
85
89
  return builder;
86
90
  }
87
91
 
92
+ /** Another build may start while a render waits for the previous one. */
88
93
  async function waitForEmailBuild() {
89
94
  const deadline = Date.now() + EMAIL_BUILD_WAIT_MS;
90
- // Another build may start while a render waits for the previous one.
91
95
  while (settleEmailBuild && Date.now() < deadline) {
92
96
  let timer;
93
97
  await Promise.race([
@@ -113,15 +117,16 @@ function loadEmailRenderer() {
113
117
  * Renders an email template of the application's email bundle, waiting for a
114
118
  * development build in progress.
115
119
  *
120
+ * The render is counted with no await in between, so a build the builder asks
121
+ * for now waits for this render, which imports templates and reads catalogs
122
+ * from the bundle until it ends.
123
+ *
116
124
  * @param template The template's name
117
125
  * @param props The template's props
118
126
  * @param options `locale`, the language to render in
119
127
  */
120
128
  export async function renderEmailTemplate(template, props, options = {}) {
121
129
  await waitForEmailBuild();
122
- // Counted from here, with no await in between, so a build the builder asks
123
- // for now waits for this render, which imports templates and reads catalogs
124
- // from the bundle until it ends.
125
130
  rendersInProgress += 1;
126
131
  try {
127
132
  const { renderEmail } = await loadEmailRenderer();