@antelopejs/dms-frontend 0.9.3 → 0.10.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 (116) hide show
  1. package/README.md +18 -24
  2. package/dist/commands/build-action.js +3 -2
  3. package/dist/commands/dev-action.js +2 -1
  4. package/dist/commands/prepare-action.js +1 -1
  5. package/dist/commands/start-action.js +3 -24
  6. package/dist/commands/workspace-task.js +2 -1
  7. package/dist/derived-outputs.js +3 -3
  8. package/dist/discovery.js +17 -6
  9. package/dist/entry-weight.js +15 -0
  10. package/dist/fs-sync.js +7 -4
  11. package/dist/index.js +2 -1
  12. package/dist/internal/built-workspace.d.ts +4 -0
  13. package/dist/internal/built-workspace.js +43 -0
  14. package/dist/internal/ensure.d.ts +1 -0
  15. package/dist/internal/ensure.js +8 -0
  16. package/dist/internal/locale-catalogs.d.ts +8 -3
  17. package/dist/internal/locale-catalogs.js +16 -6
  18. package/dist/internal/manifest-decoder.d.ts +7 -0
  19. package/dist/internal/manifest-decoder.js +81 -0
  20. package/dist/internal/own-release.d.ts +5 -0
  21. package/dist/internal/own-release.js +20 -0
  22. package/dist/internal/plain-object.d.ts +1 -0
  23. package/dist/internal/plain-object.js +6 -0
  24. package/dist/internal/text-list.d.ts +1 -0
  25. package/dist/internal/text-list.js +10 -0
  26. package/dist/internal/vite-manifest.d.ts +6 -0
  27. package/dist/internal/vite-manifest.js +24 -0
  28. package/dist/layer-watch.js +25 -24
  29. package/dist/layers.d.ts +1 -1
  30. package/dist/layers.js +3 -1
  31. package/dist/locale-files.d.ts +2 -1
  32. package/dist/locale-files.js +4 -0
  33. package/dist/manifest.d.ts +1 -1
  34. package/dist/manifest.js +44 -31
  35. package/dist/materialize.js +6 -2
  36. package/dist/module-set.js +2 -1
  37. package/dist/node-options.d.ts +1 -0
  38. package/dist/node-options.js +8 -0
  39. package/dist/output.d.ts +15 -9
  40. package/dist/output.js +30 -18
  41. package/dist/private-values.d.ts +1 -1
  42. package/dist/private-values.js +5 -3
  43. package/dist/project-modules.js +13 -8
  44. package/dist/renderer-range.js +5 -3
  45. package/dist/server-process.js +1 -1
  46. package/dist/verify-source-build.js +10 -2
  47. package/dist/verify-source-client-bundle.d.ts +1 -0
  48. package/dist/verify-source-client-bundle.js +30 -0
  49. package/dist/verify-source-runner.js +44 -60
  50. package/dist/verify-source-sources.js +49 -11
  51. package/dist/workspace-setup.js +1 -1
  52. package/package.json +6 -4
  53. package/skills/dms-frontend/SKILL.md +1 -1
  54. package/templates/vue/app-runtime.ts +2 -9
  55. package/templates/vue/client-server-guard.mjs +16 -0
  56. package/templates/vue/compress-assets.mjs +9 -1
  57. package/templates/vue/entry-weight.mjs +22 -0
  58. package/templates/vue/frontend-module.ts +1 -0
  59. package/templates/vue/frontend-runtime/module-registration.ts +2 -2
  60. package/templates/vue/frontend-runtime/state.ts +2 -2
  61. package/templates/vue/frontend-runtime/types.ts +8 -2
  62. package/templates/vue/head-order.mjs +7 -0
  63. package/templates/vue/icon-scan.mjs +4 -0
  64. package/templates/vue/server/auth/backend.mjs +41 -4
  65. package/templates/vue/server/auth/client-ip.mjs +52 -4
  66. package/templates/vue/server/auth/establish-endpoints.mjs +10 -6
  67. package/templates/vue/server/auth/session.mjs +81 -2
  68. package/templates/vue/server/auth-hooks.mjs +15 -5
  69. package/templates/vue/server/backend-proxies.mjs +65 -9
  70. package/templates/vue/server/backend-request.mjs +61 -0
  71. package/templates/vue/server/backend-response.mjs +23 -6
  72. package/templates/vue/server/built-files.mjs +7 -0
  73. package/templates/vue/server/cli-report.mjs +53 -11
  74. package/templates/vue/server/client-manifest.mjs +23 -1
  75. package/templates/vue/server/content.mjs +18 -1
  76. package/templates/vue/server/dev-private-files.mjs +17 -1
  77. package/templates/vue/server/dev-styles.mjs +38 -0
  78. package/templates/vue/server/email.mjs +9 -3
  79. package/templates/vue/server/favicon.mjs +3 -0
  80. package/templates/vue/server/generated-files.mjs +211 -0
  81. package/templates/vue/server/homepage.mjs +21 -9
  82. package/templates/vue/server/html-tag.mjs +4 -0
  83. package/templates/vue/server/icons.mjs +80 -7
  84. package/templates/vue/server/in-place-render.mjs +35 -0
  85. package/templates/vue/server/inertia.mjs +61 -2
  86. package/templates/vue/server/module-event.mjs +98 -17
  87. package/templates/vue/server/module-route-types.mjs +78 -0
  88. package/templates/vue/server/module-routes.mjs +138 -31
  89. package/templates/vue/server/module-server.mjs +98 -27
  90. package/templates/vue/server/module-testing.mjs +14 -5
  91. package/templates/vue/server/page-props.mjs +92 -2
  92. package/templates/vue/server/page-redirects.mjs +37 -0
  93. package/templates/vue/server/path-patterns.mjs +13 -1
  94. package/templates/vue/server/proxy-compression.mjs +22 -0
  95. package/templates/vue/server/proxy-headers.mjs +11 -1
  96. package/templates/vue/server/proxy-redirects.mjs +20 -5
  97. package/templates/vue/server/proxy-request.mjs +57 -0
  98. package/templates/vue/server/proxy-transfers.mjs +33 -5
  99. package/templates/vue/server/public-endpoints.mjs +138 -23
  100. package/templates/vue/server/public-options.mjs +20 -6
  101. package/templates/vue/server/request-failure.mjs +23 -0
  102. package/templates/vue/server/request-routing.mjs +26 -9
  103. package/templates/vue/server/request-target.mjs +9 -3
  104. package/templates/vue/server/route-rules.mjs +39 -15
  105. package/templates/vue/server/ssr-module.mjs +32 -0
  106. package/templates/vue/server/static-files.mjs +24 -1
  107. package/templates/vue/server/type-guards.mjs +71 -0
  108. package/templates/vue/server/vite-errors.mjs +32 -5
  109. package/templates/vue/server/web-body.mjs +3 -0
  110. package/templates/vue/server-module.ts +10 -1
  111. package/templates/vue/server.mjs +132 -148
  112. package/templates/vue/sfc-global-types.mjs +112 -13
  113. package/templates/vue/ssr-renderer.ts +2 -2
  114. package/templates/vue/ui-inertia-page.mjs +16 -1
  115. package/templates/vue/ui-kbd.mjs +2 -1
  116. package/templates/vue/ui-request-state.mjs +2 -1
@@ -1,4 +1,36 @@
1
1
  import { compilePathPattern } from "./path-patterns.mjs";
2
+ import { isNonNullObject } from "./type-guards.mjs";
3
+
4
+ /** @typedef {import("../server-module.ts").DmsBackendProxyOptions} ProxyOptions */
5
+ /**
6
+ * A backend prefix the server proxies, compiled from a pattern `proxyRejection`
7
+ * accepted.
8
+ *
9
+ * @typedef {object} BackendProxy
10
+ * @property {import("./path-patterns.mjs").CompiledPathPattern} pattern
11
+ * @property {string} path The pattern as declared
12
+ * @property {string} owner Who declared it
13
+ * @property {NonNullable<ProxyOptions["auth"]>} auth
14
+ * @property {number} [maxBodyBytes]
15
+ */
16
+ /**
17
+ * What `registerBackendProxy` declared, per call.
18
+ *
19
+ * @typedef {object} ProxyDeclaration
20
+ * @property {string} id The declaring frontend module
21
+ * @property {string} pattern
22
+ * @property {ProxyOptions} [options]
23
+ */
24
+ /**
25
+ * What a backend module declared in the manifest, or an operator in
26
+ * `DMS_BACKEND_PROXIES`.
27
+ *
28
+ * @typedef {object} ManifestProxy
29
+ * @property {string} [module]
30
+ * @property {string} [path]
31
+ * @property {ProxyOptions["auth"]} [auth]
32
+ * @property {number} [maxBodyBytes]
33
+ */
2
34
 
3
35
  /** How the proxy authenticates a request to the backend. */
4
36
  export const PROXY_AUTH_MODES = Object.freeze(["session", "none"]);
@@ -6,8 +38,10 @@ const DEFAULT_AUTH = "session";
6
38
  /** The renderer's own paths, never a backend's. */
7
39
  const RENDERER_PREFIXES = ["/api/_dms", "/@", "/assets", "/node_modules"];
8
40
 
41
+ /** @param {string} pattern */
9
42
  function literalPrefix(pattern) {
10
43
  const segments = pattern === "/" ? [] : pattern.slice(1).split("/");
44
+ /** @type {string[]} */
11
45
  const literal = [];
12
46
  for (const segment of segments) {
13
47
  if (segment === "*" || segment === "**") break;
@@ -16,7 +50,12 @@ function literalPrefix(pattern) {
16
50
  return `/${literal.join("/")}`;
17
51
  }
18
52
 
19
- /** Why a proxy declaration is ignored, or `undefined`. */
53
+ /**
54
+ * Why a proxy declaration is ignored, or `undefined`.
55
+ *
56
+ * @param {string} pattern
57
+ * @param {ProxyOptions | undefined} options
58
+ */
20
59
  export function proxyRejection(pattern, options) {
21
60
  const compiled = compilePathPattern(pattern);
22
61
  if (!compiled) return "the pattern is not /x, /x/* or /x/**";
@@ -29,7 +68,7 @@ export function proxyRejection(pattern, options) {
29
68
  )
30
69
  )
31
70
  return `${RENDERER_PREFIXES.join(", ")} are the renderer's own paths`;
32
- if (options !== undefined && (typeof options !== "object" || !options))
71
+ if (options !== undefined && !isNonNullObject(options))
33
72
  return "its options are not an object";
34
73
  if (options?.auth !== undefined && !PROXY_AUTH_MODES.includes(options.auth))
35
74
  return `auth is not one of ${PROXY_AUTH_MODES.map((mode) => `"${mode}"`).join(", ")}`;
@@ -41,6 +80,12 @@ export function proxyRejection(pattern, options) {
41
80
  return undefined;
42
81
  }
43
82
 
83
+ /**
84
+ * @param {string} pattern A pattern `proxyRejection` accepted
85
+ * @param {ProxyOptions | undefined} options
86
+ * @param {string} owner Who declared it
87
+ * @returns {BackendProxy}
88
+ */
44
89
  function compileProxy(pattern, options, owner) {
45
90
  return {
46
91
  pattern: compilePathPattern(pattern),
@@ -60,16 +105,22 @@ function compileProxy(pattern, options, owner) {
60
105
  * (`backendProxies`, next to its `publicEndpoints`). A path no prefix covers is
61
106
  * never sent to the backend.
62
107
  *
63
- * @param declared `{ id, pattern, options }` per `registerBackendProxy` call,
108
+ * @param {readonly ProxyDeclaration[]} declared `{ id, pattern, options }` per `registerBackendProxy` call,
64
109
  * from the highest-priority module down
65
- * @param manifest `{ module, path, auth?, maxBodyBytes? }` per backend
110
+ * @param {readonly (ManifestProxy | null | undefined)[]} manifest `{ module, path, auth?, maxBodyBytes? }` per backend
66
111
  * module declaration
67
- * @param warn Reports a declaration problem
68
- * @returns The proxies, the most specific pattern first: a request reaches
112
+ * @param {(message: string) => void} warn Reports a declaration problem
113
+ * @returns {BackendProxy[]} The proxies, the most specific pattern first: a request reaches
69
114
  * the first that matches
70
115
  */
71
116
  export function resolveBackendProxies(declared, manifest, warn) {
117
+ /** @type {Map<string | undefined, BackendProxy>} */
72
118
  const kept = new Map();
119
+ /**
120
+ * @param {string | undefined} pattern
121
+ * @param {ProxyOptions | undefined} options
122
+ * @param {string} owner
123
+ */
73
124
  const add = (pattern, options, owner) => {
74
125
  const reason = proxyRejection(pattern, options);
75
126
  const current = kept.get(pattern);
@@ -94,7 +145,12 @@ export function resolveBackendProxies(declared, manifest, warn) {
94
145
  );
95
146
  }
96
147
 
97
- /** The backend proxy a path reaches, or `undefined`. */
148
+ /**
149
+ * The backend proxy a path reaches, or `undefined`.
150
+ *
151
+ * @param {readonly BackendProxy[]} proxies
152
+ * @param {string} pathname
153
+ */
98
154
  export function matchBackendProxy(proxies, pathname) {
99
155
  return proxies.find((proxy) => proxy.pattern.test(pathname));
100
156
  }
@@ -103,8 +159,8 @@ export function matchBackendProxy(proxies, pathname) {
103
159
  * The lines the server prints at startup: each backend prefix, who declared
104
160
  * it and how it authenticates, then who owns the rest.
105
161
  *
106
- * @param proxies What `resolveBackendProxies` returned
107
- * @param routeCount The number of module routes
162
+ * @param {readonly BackendProxy[]} proxies What `resolveBackendProxies` returned
163
+ * @param {number} routeCount The number of module routes
108
164
  */
109
165
  export function routingReport(proxies, routeCount) {
110
166
  const width = Math.max(0, ...proxies.map((proxy) => proxy.path.length));
@@ -0,0 +1,61 @@
1
+ import { CLIENT_HEADERS, relayedHeaders } from "./auth/backend.mjs";
2
+ import { forwardedHeaders } from "./auth/client-ip.mjs";
3
+ import { readSession } from "./auth/session.mjs";
4
+ import { backendResponseError } from "./backend-response.mjs";
5
+ import { serverReads } from "./in-place-render.mjs";
6
+
7
+ /**
8
+ * The headers of a backend request: the relayed client headers, the client's
9
+ * Origin and Referer as received, the `X-Forwarded-*` headers (set last, so no
10
+ * caller writes them), and the session's access token unless the request goes
11
+ * anonymously.
12
+ *
13
+ * @param {import("node:http").IncomingMessage} request
14
+ * @param {{ anonymous: boolean, headers: readonly string[] }} [access]
15
+ * @returns {Record<string, string>}
16
+ */
17
+ export function backendHeaders(
18
+ request,
19
+ access = { anonymous: false, headers: [] },
20
+ ) {
21
+ /** @type {Record<string, string>} */
22
+ const headers = {
23
+ ...relayedHeaders(request, [
24
+ "accept",
25
+ "content-type",
26
+ "if-match",
27
+ "origin",
28
+ "referer",
29
+ ...CLIENT_HEADERS,
30
+ ...access.headers,
31
+ ]),
32
+ ...forwardedHeaders(request),
33
+ };
34
+ const session = access.anonymous ? undefined : readSession(request);
35
+ if (session?.accessToken)
36
+ headers.authorization = `Bearer ${session.accessToken}`;
37
+ return headers;
38
+ }
39
+
40
+ /**
41
+ * @param {import("node:http").IncomingMessage} request
42
+ * @returns {import("./in-place-render.mjs").ServerReads}
43
+ */
44
+ export function serverComponentFetch(request) {
45
+ return serverReads(() => backendHeaders(request));
46
+ }
47
+
48
+ /**
49
+ * @param {string} path
50
+ * @param {import("node:http").IncomingMessage} request
51
+ * @returns {Promise<unknown>}
52
+ */
53
+ export async function backendJson(path, request) {
54
+ const response = await fetch(new URL(path, process.env.DMS_API_BASE_URL), {
55
+ headers: backendHeaders(request),
56
+ });
57
+ if (!response.ok) {
58
+ throw await backendResponseError(response);
59
+ }
60
+ return response.json();
61
+ }
@@ -1,5 +1,6 @@
1
1
  import { GENERIC_ERROR_MESSAGE } from "./auth/backend.mjs";
2
2
  import { isFrontendVisit, redirectFrontendVisit } from "./inertia.mjs";
3
+ import { isString } from "./type-guards.mjs";
3
4
 
4
5
  /**
5
6
  * What a JSON caller gets when the backend fails: the generic i18n key of
@@ -18,6 +19,10 @@ const FORBIDDEN = 403;
18
19
  const FRONTEND_ORIGIN = "http://frontend.local";
19
20
 
20
21
  export class BackendResponseError extends Error {
22
+ /**
23
+ * @param {number} status
24
+ * @param {string | undefined} code The typed refusal code of the answer
25
+ */
21
26
  constructor(status, code) {
22
27
  super(`DMS backend returned ${status}`);
23
28
  this.status = status;
@@ -28,21 +33,28 @@ export class BackendResponseError extends Error {
28
33
  /**
29
34
  * The refusal code in a backend answer: the string or the `message` of its
30
35
  * JSON body, or the body itself when it is not JSON (a bare code).
36
+ *
37
+ * @param {Response} response
38
+ * @returns {Promise<string | undefined>}
31
39
  */
32
40
  async function refusalCode(response) {
33
41
  const text = (await response.text().catch(() => "")).trim();
34
42
  if (text.length > REFUSAL_CODE_MAX_LENGTH) return undefined;
43
+ /** @type {unknown} */
35
44
  let value = text;
36
45
  try {
46
+ /** @type {unknown} */
37
47
  const data = JSON.parse(text);
38
- value = typeof data === "string" ? data : data?.message;
48
+ value = isString(data) ? data : data?.message;
39
49
  } catch {}
40
- return typeof value === "string" && REFUSAL_CODE.test(value)
41
- ? value
42
- : undefined;
50
+ return isString(value) && REFUSAL_CODE.test(value) ? value : undefined;
43
51
  }
44
52
 
45
- /** The error for a failed backend response, with its typed code if any. */
53
+ /**
54
+ * The error for a failed backend response, with its typed code if any.
55
+ *
56
+ * @param {Response} response
57
+ */
46
58
  export async function backendResponseError(response) {
47
59
  return new BackendResponseError(response.status, await refusalCode(response));
48
60
  }
@@ -54,7 +66,11 @@ export async function backendResponseError(response) {
54
66
  * `loadRenderer` resolves. Only a same-origin path other than the refused one
55
67
  * qualifies: a refused destination sent to itself again would loop.
56
68
  *
57
- * @returns Whether the visit was redirected
69
+ * @param {unknown} error What the request failed with
70
+ * @param {import("node:http").IncomingMessage} request
71
+ * @param {import("node:http").ServerResponse} response
72
+ * @param {() => Promise<typeof import("../ssr-renderer.ts")>} loadRenderer
73
+ * @returns {Promise<boolean>} Whether the visit was redirected
58
74
  */
59
75
  export async function redirectAccessRefusal(
60
76
  error,
@@ -69,6 +85,7 @@ export async function redirectAccessRefusal(
69
85
  !error.code
70
86
  )
71
87
  return false;
88
+ /** @type {string | undefined} */
72
89
  let location;
73
90
  try {
74
91
  location = (await loadRenderer()).accessRedirect?.(error.code);
@@ -14,6 +14,9 @@ const PUBLIC_DOT_DIRECTORY = ".well-known";
14
14
  * source path of every module file), and a module's `public/` may hold dot
15
15
  * files too. `/.well-known/`, which RFC 8615 reserves for files meant to be
16
16
  * public, stays served.
17
+ *
18
+ * @param {string} relativePath
19
+ * @returns {boolean}
17
20
  */
18
21
  export function isPublicBuildPath(relativePath) {
19
22
  return relativePath
@@ -33,6 +36,10 @@ export function isPublicBuildPath(relativePath) {
33
36
  *
34
37
  * The prefix uses the native separator: `resolve` answers with backslashes on
35
38
  * Windows.
39
+ *
40
+ * @param {string} root
41
+ * @param {string} pathname
42
+ * @returns {string | undefined}
36
43
  */
37
44
  export function publicBuildFile(root, pathname) {
38
45
  let decoded;
@@ -1,9 +1,16 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { stripVTControlCharacters } from "node:util";
4
+ import { isNumber, isObjectLike, isRecord, isString } from "./type-guards.mjs";
4
5
 
5
6
  const WILDCARD_ADDRESSES = ["0.0.0.0", "::"];
6
7
 
8
+ /**
9
+ * @param {import("node:child_process").Serializable} message
10
+ * @param {string} line
11
+ * @param {(line: string) => void} write
12
+ * @returns {Promise<void>}
13
+ */
7
14
  async function report(message, line, write) {
8
15
  if (!process.send) return write(line);
9
16
  await new Promise((sent) => process.send(message, () => sent()));
@@ -15,6 +22,10 @@ async function report(message, line, write) {
15
22
  * What the server tells whoever started it. Started by `ajs dms dev` or `ajs
16
23
  * dms start`, it reports over IPC and the CLI words what the user reads.
17
24
  * Started on its own, it prints the line itself.
25
+ *
26
+ * @param {string} address
27
+ * @param {number} port
28
+ * @returns {Promise<void>}
18
29
  */
19
30
  export async function reportReady(address, port) {
20
31
  const host = WILDCARD_ADDRESSES.includes(address)
@@ -33,6 +44,11 @@ export async function reportReady(address, port) {
33
44
  * The server cannot listen: a port taken by another process, an address this
34
45
  * host does not have. Reported instead of Node's unhandled-error dump, then
35
46
  * the server exits.
47
+ *
48
+ * @param {NodeJS.ErrnoException} error
49
+ * @param {string} host
50
+ * @param {number} port
51
+ * @returns {Promise<void>}
36
52
  */
37
53
  export async function reportListenError(error, host, port) {
38
54
  await report(
@@ -52,31 +68,46 @@ export async function reportListenError(error, host, port) {
52
68
  /** The `[PARSE_ERROR] ` Rolldown puts in front of what it says. */
53
69
  const RULE_PREFIX = /^\[[A-Z_]+\]\s*/;
54
70
 
55
- /** The first problem Rolldown reports when it cannot bundle a config. */
71
+ /**
72
+ * The first problem Rolldown reports when it cannot bundle a config.
73
+ *
74
+ * @param {unknown} error What the startup threw
75
+ */
56
76
  function bundlerError(error) {
57
- const first = error?.errors?.[0];
58
- return first && typeof first === "object" ? first : undefined;
77
+ const errors = isObjectLike(error) ? error.errors : undefined;
78
+ const first = isObjectLike(errors) ? errors[0] : undefined;
79
+ return isRecord(first) ? first : undefined;
59
80
  }
60
81
 
61
82
  /**
62
83
  * Where a startup error points, when it says: Rolldown's location for a config
63
84
  * it could not bundle (the line is read back from the file, which Rolldown
64
85
  * only draws in a colored frame), Rollup's for a plugin that failed on a file.
86
+ *
87
+ * @param {unknown} error What the startup threw
65
88
  */
66
89
  function errorLocation(error) {
67
- const source = bundlerError(error) ?? error;
68
- const file = source?.loc?.file ?? source?.id;
69
- if (typeof file !== "string") return undefined;
70
- const { line, column } = source.loc ?? {};
90
+ const source =
91
+ bundlerError(error) ?? (isObjectLike(error) ? error : undefined);
92
+ const loc = isObjectLike(source?.loc) ? source.loc : undefined;
93
+ const file = loc?.file ?? source?.id;
94
+ if (!isString(file)) return undefined;
95
+ const line = isNumber(loc?.line) ? loc.line : undefined;
71
96
  return {
72
97
  file: resolve(file),
73
98
  line,
74
- column,
99
+ column: isNumber(loc?.column) ? loc.column : undefined,
75
100
  lineText: line === undefined ? undefined : readLine(file, line),
76
101
  };
77
102
  }
78
103
 
79
- /** Line `line` (1-based) of `file`, `undefined` when it cannot be read. */
104
+ /**
105
+ * Line `line` (1-based) of `file`, `undefined` when it cannot be read.
106
+ *
107
+ * @param {string} file
108
+ * @param {number} line
109
+ * @returns {string | undefined}
110
+ */
80
111
  function readLine(file, line) {
81
112
  try {
82
113
  return readFileSync(file, "utf8").split(/\r?\n/)[line - 1];
@@ -85,7 +116,11 @@ function readLine(file, line) {
85
116
  }
86
117
  }
87
118
 
88
- /** The first line of what went wrong, without Rolldown's rule and frame. */
119
+ /**
120
+ * The first line of what went wrong, without Rolldown's rule and frame.
121
+ *
122
+ * @param {unknown} error What the startup threw
123
+ */
89
124
  function errorText(error) {
90
125
  const reported = bundlerError(error);
91
126
  const message = reported
@@ -101,6 +136,8 @@ function errorText(error) {
101
136
  /**
102
137
  * Vite could not start, so no page can be served: the error and where it
103
138
  * points instead of a stack trace, then the server exits.
139
+ *
140
+ * @param {unknown} error What the startup threw
104
141
  */
105
142
  export async function reportStartError(error) {
106
143
  const message = errorText(error);
@@ -109,7 +146,12 @@ export async function reportStartError(error) {
109
146
  ? `${location.file}${location.line ? `:${location.line}:${location.column ?? 0}` : ""}: `
110
147
  : "";
111
148
  await report(
112
- { type: "dms:start-error", message, code: error?.code, location },
149
+ {
150
+ type: "dms:start-error",
151
+ message,
152
+ code: isObjectLike(error) ? error.code : undefined,
153
+ location,
154
+ },
113
155
  `DMS development server failed to start: ${where}${message}`,
114
156
  console.error,
115
157
  );
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { viteManifestOf } from "./generated-files.mjs";
4
5
 
5
6
  const PROJECT_ROOT = fileURLToPath(new URL("../", import.meta.url));
6
7
  const BUILT_TEMPLATE_PATH = join(PROJECT_ROOT, "dist/client/index.html");
@@ -14,7 +15,9 @@ const SOURCE_TEMPLATE_PATH = join(PROJECT_ROOT, "index.html");
14
15
  * Read once and kept: both files are build output and cannot change while
15
16
  * the server is up.
16
17
  */
18
+ /** @type {import("vite").Manifest | undefined} */
17
19
  let clientManifest;
20
+ /** @type {string | undefined} */
18
21
  let productionTemplate;
19
22
 
20
23
  /**
@@ -31,13 +34,22 @@ export function productionHtmlTemplate() {
31
34
  return productionTemplate;
32
35
  }
33
36
 
37
+ /** @returns {import("vite").Manifest | undefined} */
34
38
  function productionClientManifest() {
35
39
  if (clientManifest) return clientManifest;
36
40
  if (!existsSync(CLIENT_MANIFEST_PATH)) return undefined;
37
- clientManifest = JSON.parse(readFileSync(CLIENT_MANIFEST_PATH, "utf8"));
41
+ clientManifest = viteManifestOf(
42
+ JSON.parse(readFileSync(CLIENT_MANIFEST_PATH, "utf8")),
43
+ );
38
44
  return clientManifest;
39
45
  }
40
46
 
47
+ /**
48
+ * @param {import("vite").Manifest} manifest
49
+ * @param {string} key
50
+ * @param {Set<string>} styles
51
+ * @param {Set<string>} visited
52
+ */
41
53
  function collectManifestStyles(manifest, key, styles, visited) {
42
54
  if (visited.has(key)) return;
43
55
  visited.add(key);
@@ -56,11 +68,16 @@ function collectManifestStyles(manifest, key, styles, visited) {
56
68
  * component, so the lookup needs no guessing from names. A component bundled
57
69
  * into another chunk is no key of its own: its stylesheet comes with that
58
70
  * chunk's, reached through the `imports` of the component that loaded it.
71
+ *
72
+ * @param {Iterable<string>} [modules]
73
+ * @returns {Set<string>}
59
74
  */
60
75
  function renderedModuleStyles(modules = []) {
61
76
  const manifest = productionClientManifest();
77
+ /** @type {Set<string>} */
62
78
  const styles = new Set();
63
79
  if (!manifest) return styles;
80
+ /** @type {Set<string>} */
64
81
  const visited = new Set();
65
82
  for (const id of modules)
66
83
  collectManifestStyles(manifest, id, styles, visited);
@@ -72,6 +89,9 @@ function renderedModuleStyles(modules = []) {
72
89
  * template does not link already, so that none of them waits for its chunk's
73
90
  * JavaScript to arrive: the HTML would otherwise paint unstyled and shift once
74
91
  * it does. A component the render did not reach gets no link.
92
+ *
93
+ * @param {Iterable<string> | undefined} modules
94
+ * @param {string} template
75
95
  */
76
96
  export function renderedModuleStyleTags(modules, template) {
77
97
  return [...renderedModuleStyles(modules)]
@@ -85,6 +105,8 @@ export function renderedModuleStyleTags(modules, template) {
85
105
  * rendered with: the browser fetches it next to the entry rather than once the
86
106
  * entry runs and asks for it, which would put a round trip before hydration.
87
107
  * Nothing without a production build (Vite serves the sources in development).
108
+ *
109
+ * @param {string | undefined} locale
88
110
  */
89
111
  export function localeCatalogPreloadTag(locale) {
90
112
  const manifest = productionClientManifest();
@@ -17,9 +17,13 @@ const DYNAMIC_BROTLI_QUALITY = 4;
17
17
  * client gets each part when it is written, not when a block fills.
18
18
  *
19
19
  * Implicit identity is a fallback, not a preference over compression.
20
+ *
21
+ * @param {import("node:http").IncomingMessage} request
22
+ * @param {{ streaming?: boolean }} [options]
20
23
  */
21
24
  export function acceptedEncoder(request, { streaming = false } = {}) {
22
25
  const accepted = request.headers["accept-encoding"] ?? "";
26
+ /** @type {Map<string, number>} */
23
27
  const qualities = new Map();
24
28
  for (const entry of accepted.split(",")) {
25
29
  const [token, ...parameters] = entry.trim().toLowerCase().split(";");
@@ -37,6 +41,7 @@ export function acceptedEncoder(request, { streaming = false } = {}) {
37
41
  Math.min(qualities.get(token.trim()) ?? 1, quality),
38
42
  );
39
43
  }
44
+ /** @param {string} token */
40
45
  const quality = (token) => qualities.get(token) ?? qualities.get("*") ?? 0;
41
46
  const br = quality("br");
42
47
  const gzip = quality("gzip");
@@ -61,12 +66,24 @@ export function acceptedEncoder(request, { streaming = false } = {}) {
61
66
  };
62
67
  }
63
68
 
69
+ /**
70
+ * @param {import("node:http").IncomingMessage} request
71
+ * @param {string | Buffer} content
72
+ */
64
73
  function responseEncoder(request, content) {
65
74
  if (Buffer.byteLength(content) < MINIMUM_COMPRESSION_BYTES) return undefined;
66
75
  return acceptedEncoder(request);
67
76
  }
68
77
 
69
- /** Send a generated body, compressed when the client accepts it. */
78
+ /**
79
+ * Send a generated body, compressed when the client accepts it.
80
+ *
81
+ * @param {import("node:http").IncomingMessage} request
82
+ * @param {import("node:http").ServerResponse} response
83
+ * @param {number} status
84
+ * @param {import("node:http").OutgoingHttpHeaders} headers
85
+ * @param {string | Buffer} content
86
+ */
70
87
  export async function writeContent(
71
88
  request,
72
89
  response,
@@ -28,6 +28,10 @@ export const DEV_FS_DENY = [
28
28
  * file, a module's server entry, or a file under a `server/` directory of a
29
29
  * module materialized under `root`. Compared without case, as a
30
30
  * case-insensitive file system would serve them.
31
+ *
32
+ * @param {string} file
33
+ * @param {string} root
34
+ * @returns {boolean}
31
35
  */
32
36
  export function isPrivateFile(file, root) {
33
37
  const name = (resolve(file).split(/[\\/]/).at(-1) ?? "").toLowerCase();
@@ -39,12 +43,22 @@ export function isPrivateFile(file, root) {
39
43
  );
40
44
  }
41
45
 
46
+ /**
47
+ * @param {string} id
48
+ * @returns {string}
49
+ */
42
50
  function stripQuery(id) {
43
51
  const query = id.search(/[?#]/);
44
52
  return query === -1 ? id : id.slice(0, query);
45
53
  }
46
54
 
47
- /** The file a request URL names under `root`, `undefined` when it names none. */
55
+ /**
56
+ * The file a request URL names under `root`, `undefined` when it names none.
57
+ *
58
+ * @param {string | undefined} url
59
+ * @param {string} root
60
+ * @returns {string | undefined}
61
+ */
48
62
  function requestedFile(url, root) {
49
63
  let pathname;
50
64
  try {
@@ -74,6 +88,8 @@ function requestedFile(url, root) {
74
88
  *
75
89
  * The middleware is registered directly, not returned: it runs before Vite's
76
90
  * middlewares.
91
+ *
92
+ * @returns {import("vite").Plugin}
77
93
  */
78
94
  export function denyPrivateFiles() {
79
95
  let root = process.cwd();
@@ -8,6 +8,13 @@ const SFC_STYLE = /[?&]vue&type=style/;
8
8
  const STYLE_EXTENSION =
9
9
  /\.(?:css|less|pcss|postcss|sass|scss|styl|stylus)(?:$|\?)/;
10
10
 
11
+ /**
12
+ * A stylesheet module of the development server's graph: its URL, and the id
13
+ * its tag is matched by.
14
+ *
15
+ * @typedef {{ url: string, id: string }} StyleModule
16
+ */
17
+
11
18
  /**
12
19
  * The workspace's generated entry stylesheet. `main.ts` is its only importer,
13
20
  * so it never enters the SSR module graph, yet it carries Tailwind, Nuxt UI and
@@ -25,6 +32,16 @@ const ENTRY_STYLESHEET_ID = join(PROJECT_ROOT, "dms-main.css")
25
32
  .split(sep)
26
33
  .join("/");
27
34
 
35
+ /**
36
+ * @typedef {{ urlToModuleMap: ReadonlyMap<string, { id: string | null } | undefined> }} StyleGraph
37
+ * @typedef {{
38
+ * environments?: { ssr?: { moduleGraph?: StyleGraph } },
39
+ * moduleGraph?: StyleGraph,
40
+ * transformRequest(url: string): Promise<{ code: string } | null>,
41
+ * }} StyleServer
42
+ */
43
+
44
+ /** @param {StyleServer} devServer */
28
45
  function ssrModuleGraph(devServer) {
29
46
  return devServer.environments?.ssr?.moduleGraph ?? devServer.moduleGraph;
30
47
  }
@@ -39,6 +56,10 @@ function ssrModuleGraph(devServer) {
39
56
  */
40
57
  const TRAILING_LANG = /&(lang\.[^&?]*)$/;
41
58
 
59
+ /**
60
+ * @param {string} url
61
+ * @returns {string}
62
+ */
42
63
  function directUrl(url) {
43
64
  if (SFC_STYLE.test(url) && TRAILING_LANG.test(url)) {
44
65
  return url.replace(TRAILING_LANG, "&direct&$1");
@@ -46,6 +67,10 @@ function directUrl(url) {
46
67
  return `${url}${url.includes("?") ? "&" : "?"}direct`;
47
68
  }
48
69
 
70
+ /**
71
+ * @param {StyleServer} devServer
72
+ * @returns {StyleModule[]}
73
+ */
49
74
  function styleModules(devServer) {
50
75
  const graph = ssrModuleGraph(devServer);
51
76
  if (!graph) return [];
@@ -54,6 +79,7 @@ function styleModules(devServer) {
54
79
  .map(([url, module]) => ({ url, id: module?.id ?? url }));
55
80
  }
56
81
 
82
+ /** @param {StyleModule} module */
57
83
  function linkTag({ url, id }) {
58
84
  return `<link rel="stylesheet" ${STYLE_ATTRIBUTE}="${id}" href="${directUrl(url)}">`;
59
85
  }
@@ -64,6 +90,10 @@ function linkTag({ url, id }) {
64
90
  * stylesheet. They are small; inline them instead. A block that could close its
65
91
  * own tag is dropped rather than escaped — CSS has no escape that is valid in
66
92
  * every position, and a scoped component rule is not worth the risk.
93
+ *
94
+ * @param {StyleServer} devServer
95
+ * @param {StyleModule} module
96
+ * @returns {Promise<string>}
67
97
  */
68
98
  async function inlineTag(devServer, { url, id }) {
69
99
  const result = await devServer
@@ -87,6 +117,9 @@ async function inlineTag(devServer, { url, id }) {
87
117
  * stylesheet, which is why this module is development-only and lives beside
88
118
  * client-manifest.mjs rather than inside server.mjs, whose size the linter
89
119
  * caps.
120
+ *
121
+ * @param {StyleServer} devServer
122
+ * @returns {Promise<string>}
90
123
  */
91
124
  export async function developmentStyleTags(devServer) {
92
125
  const modules = styleModules(devServer);
@@ -106,6 +139,11 @@ export async function developmentStyleTags(devServer) {
106
139
  * The style tags a document needs, whichever server is answering it: collected
107
140
  * from the live module graph in development, read from the build manifest for
108
141
  * the components the render created (`modules`) in production.
142
+ *
143
+ * @param {import("vite").ViteDevServer | undefined} devServer
144
+ * @param {Iterable<string> | undefined} modules
145
+ * @param {string} template
146
+ * @returns {Promise<string>}
109
147
  */
110
148
  export async function documentStyleTags(devServer, modules, template) {
111
149
  return devServer