@antelopejs/dms-frontend 0.9.3 → 0.10.1

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 (140) hide show
  1. package/README.md +46 -29
  2. package/dist/commands/build-action.d.ts +3 -0
  3. package/dist/commands/build-action.js +77 -8
  4. package/dist/commands/build.js +7 -0
  5. package/dist/commands/clean-action.js +1 -1
  6. package/dist/commands/clean.js +1 -1
  7. package/dist/commands/dev-action.js +4 -3
  8. package/dist/commands/dev.js +1 -1
  9. package/dist/commands/prepare-action.js +1 -1
  10. package/dist/commands/start-action.js +3 -24
  11. package/dist/commands/workspace-task.js +2 -1
  12. package/dist/config.js +1 -0
  13. package/dist/derived-outputs.d.ts +1 -0
  14. package/dist/derived-outputs.js +6 -4
  15. package/dist/discovery.js +17 -6
  16. package/dist/entry-weight.js +15 -0
  17. package/dist/fs-sync.js +7 -4
  18. package/dist/help.js +1 -1
  19. package/dist/index.js +2 -1
  20. package/dist/internal/built-workspace.d.ts +4 -0
  21. package/dist/internal/built-workspace.js +43 -0
  22. package/dist/internal/ensure.d.ts +1 -0
  23. package/dist/internal/ensure.js +8 -0
  24. package/dist/internal/locale-catalogs.d.ts +8 -3
  25. package/dist/internal/locale-catalogs.js +16 -6
  26. package/dist/internal/manifest-decoder.d.ts +7 -0
  27. package/dist/internal/manifest-decoder.js +81 -0
  28. package/dist/internal/own-release.d.ts +5 -0
  29. package/dist/internal/own-release.js +20 -0
  30. package/dist/internal/plain-object.d.ts +1 -0
  31. package/dist/internal/plain-object.js +6 -0
  32. package/dist/internal/prepared-workspace.d.ts +12 -0
  33. package/dist/internal/prepared-workspace.js +74 -0
  34. package/dist/internal/text-list.d.ts +1 -0
  35. package/dist/internal/text-list.js +10 -0
  36. package/dist/internal/vite-manifest.d.ts +6 -0
  37. package/dist/internal/vite-manifest.js +24 -0
  38. package/dist/layer-watch.js +25 -24
  39. package/dist/layers.d.ts +1 -1
  40. package/dist/layers.js +3 -1
  41. package/dist/locale-files.d.ts +6 -3
  42. package/dist/locale-files.js +15 -9
  43. package/dist/manifest.d.ts +1 -1
  44. package/dist/manifest.js +46 -32
  45. package/dist/materialize.js +6 -2
  46. package/dist/module-set.js +2 -1
  47. package/dist/node-options.d.ts +1 -0
  48. package/dist/node-options.js +8 -0
  49. package/dist/output.d.ts +15 -9
  50. package/dist/output.js +30 -18
  51. package/dist/ports.d.ts +3 -1
  52. package/dist/ports.js +1 -1
  53. package/dist/private-values.d.ts +1 -1
  54. package/dist/private-values.js +5 -3
  55. package/dist/project-modules.d.ts +5 -1
  56. package/dist/project-modules.js +23 -15
  57. package/dist/renderer-range.js +5 -3
  58. package/dist/server-process.js +1 -1
  59. package/dist/verify-source-build.js +10 -2
  60. package/dist/verify-source-client-bundle.d.ts +1 -0
  61. package/dist/verify-source-client-bundle.js +30 -0
  62. package/dist/verify-source-runner.js +36 -94
  63. package/dist/verify-source-sources.js +49 -11
  64. package/dist/verify-source-workspace.d.ts +2 -0
  65. package/dist/verify-source-workspace.js +111 -0
  66. package/dist/workspace-setup.d.ts +1 -0
  67. package/dist/workspace-setup.js +4 -3
  68. package/package.json +11 -7
  69. package/skills/dms-frontend/SKILL.md +1 -1
  70. package/templates/vue/app-runtime.ts +2 -9
  71. package/templates/vue/backend-boundary.mjs +132 -0
  72. package/templates/vue/client-chunking.mjs +2 -0
  73. package/templates/vue/client-server-guard.mjs +16 -0
  74. package/templates/vue/compress-assets.mjs +9 -1
  75. package/templates/vue/entry-weight.mjs +29 -3
  76. package/templates/vue/frontend-build.ts +4 -3
  77. package/templates/vue/frontend-module.ts +1 -0
  78. package/templates/vue/frontend-runtime/module-registration.ts +2 -2
  79. package/templates/vue/frontend-runtime/state.ts +2 -2
  80. package/templates/vue/frontend-runtime/types.ts +8 -2
  81. package/templates/vue/head-order.mjs +10 -2
  82. package/templates/vue/icon-api.mjs +2 -0
  83. package/templates/vue/icon-scan.mjs +4 -0
  84. package/templates/vue/server/auth/backend.mjs +42 -5
  85. package/templates/vue/server/auth/client-ip.mjs +52 -4
  86. package/templates/vue/server/auth/establish-endpoints.mjs +11 -7
  87. package/templates/vue/server/auth/session.mjs +86 -4
  88. package/templates/vue/server/auth-declarations.mjs +46 -0
  89. package/templates/vue/server/auth-hooks.mjs +55 -7
  90. package/templates/vue/server/backend-proxies.mjs +78 -18
  91. package/templates/vue/server/backend-request.mjs +61 -0
  92. package/templates/vue/server/backend-response.mjs +23 -6
  93. package/templates/vue/server/built-files.mjs +7 -0
  94. package/templates/vue/server/cli-report.mjs +75 -12
  95. package/templates/vue/server/client-error.mjs +5 -0
  96. package/templates/vue/server/client-manifest.mjs +23 -1
  97. package/templates/vue/server/content.mjs +40 -10
  98. package/templates/vue/server/dev-private-files.mjs +17 -1
  99. package/templates/vue/server/dev-styles.mjs +38 -0
  100. package/templates/vue/server/email-bundle-watch.mjs +9 -3
  101. package/templates/vue/server/email.mjs +14 -6
  102. package/templates/vue/server/favicon.mjs +3 -0
  103. package/templates/vue/server/generated-files.mjs +221 -0
  104. package/templates/vue/server/homepage.mjs +21 -9
  105. package/templates/vue/server/html-tag.mjs +4 -0
  106. package/templates/vue/server/icons.mjs +93 -16
  107. package/templates/vue/server/in-place-render.mjs +35 -0
  108. package/templates/vue/server/inertia.mjs +76 -3
  109. package/templates/vue/server/module-event.mjs +118 -25
  110. package/templates/vue/server/module-route-types.mjs +78 -0
  111. package/templates/vue/server/module-routes.mjs +151 -96
  112. package/templates/vue/server/module-server.mjs +121 -40
  113. package/templates/vue/server/module-testing.mjs +36 -12
  114. package/templates/vue/server/page-props.mjs +94 -3
  115. package/templates/vue/server/page-redirects.mjs +37 -0
  116. package/templates/vue/server/path-patterns.mjs +35 -12
  117. package/templates/vue/server/proxy-compression.mjs +22 -0
  118. package/templates/vue/server/proxy-headers.mjs +12 -2
  119. package/templates/vue/server/proxy-redirects.mjs +21 -5
  120. package/templates/vue/server/proxy-request.mjs +58 -0
  121. package/templates/vue/server/proxy-transfers.mjs +44 -10
  122. package/templates/vue/server/public-endpoints.mjs +139 -28
  123. package/templates/vue/server/public-options.mjs +20 -6
  124. package/templates/vue/server/request-failure.mjs +24 -1
  125. package/templates/vue/server/request-routing.mjs +32 -10
  126. package/templates/vue/server/request-target.mjs +30 -11
  127. package/templates/vue/server/route-rules.mjs +48 -18
  128. package/templates/vue/server/ssr-module.mjs +32 -0
  129. package/templates/vue/server/static-files.mjs +35 -7
  130. package/templates/vue/server/type-guards.mjs +71 -0
  131. package/templates/vue/server/vite-errors.mjs +45 -7
  132. package/templates/vue/server/web-body.mjs +5 -1
  133. package/templates/vue/server-module.ts +12 -2
  134. package/templates/vue/server.mjs +162 -157
  135. package/templates/vue/sfc-global-types.mjs +112 -13
  136. package/templates/vue/ssr-renderer.ts +2 -2
  137. package/templates/vue/ui-inertia-page.mjs +16 -1
  138. package/templates/vue/ui-kbd.mjs +2 -1
  139. package/templates/vue/ui-request-state.mjs +2 -1
  140. package/templates/vue/vite.config.ts +9 -0
@@ -0,0 +1,132 @@
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, relative, sep } from "node:path";
3
+ import { isRecord, isString } from "./server/type-guards.mjs";
4
+
5
+ const BACKEND_SOURCE = "src";
6
+ const QUERY_OR_HASH = /[?#].*$/;
7
+
8
+ /**
9
+ * @param {string} manifest
10
+ * @returns {string | undefined}
11
+ */
12
+ function manifestName(manifest) {
13
+ try {
14
+ /** @type {unknown} */
15
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
16
+ return isRecord(parsed) && isString(parsed.name) ? parsed.name : undefined;
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * @typedef {{ root: string, name: string }} BackendPackage
24
+ */
25
+
26
+ /**
27
+ * The backend package that holds `directory`: the nearest `package.json` above
28
+ * it, when it names one of `backendPackages`.
29
+ *
30
+ * @param {string} directory
31
+ * @param {ReadonlySet<string>} backendPackages
32
+ * @param {Map<string, BackendPackage | null>} known
33
+ * @returns {BackendPackage | null}
34
+ */
35
+ function owningBackend(directory, backendPackages, known) {
36
+ const cached = known.get(directory);
37
+ if (cached !== undefined) return cached;
38
+ const manifest = join(directory, "package.json");
39
+ const parent = dirname(directory);
40
+ /** @type {BackendPackage | null} */
41
+ let owner = null;
42
+ if (existsSync(manifest)) {
43
+ const name = manifestName(manifest);
44
+ owner =
45
+ name !== undefined && backendPackages.has(name)
46
+ ? { root: directory, name }
47
+ : null;
48
+ } else if (parent !== directory) {
49
+ owner = owningBackend(parent, backendPackages, known);
50
+ }
51
+ known.set(directory, owner);
52
+ return owner;
53
+ }
54
+
55
+ /**
56
+ * The real path of a resolved module id under the `src` of a backend package,
57
+ * and that package's name, or `undefined`. The package that owns the file
58
+ * decides, not where it lies, so a backend linked or installed into the
59
+ * workspace is caught as well as its source checkout. Virtual ids and ids that
60
+ * are not files are ignored.
61
+ *
62
+ * @param {string} id
63
+ * @param {ReadonlySet<string>} backendPackages
64
+ * @param {Map<string, BackendPackage | null>} known
65
+ * @returns {{ file: string, name: string } | undefined}
66
+ */
67
+ function backendSourceFile(id, backendPackages, known) {
68
+ const path = id.replace(QUERY_OR_HASH, "");
69
+ if (!isAbsolute(path)) return undefined;
70
+ let file;
71
+ try {
72
+ file = realpathSync(path);
73
+ } catch {
74
+ return undefined;
75
+ }
76
+ const owner = owningBackend(dirname(file), backendPackages, known);
77
+ if (owner === null) return undefined;
78
+ return relative(owner.root, file).split(sep)[0] === BACKEND_SOURCE
79
+ ? { file, name: owner.name }
80
+ : undefined;
81
+ }
82
+
83
+ /**
84
+ * @param {ReadonlySet<string>} backendPackages
85
+ * @returns {import("vite").Plugin}
86
+ */
87
+ function boundaryCheck(backendPackages) {
88
+ /** @type {Map<string, BackendPackage | null>} */
89
+ const known = new Map();
90
+ return {
91
+ name: "dms-backend-boundary",
92
+ enforce: "pre",
93
+ async resolveId(source, importer, options) {
94
+ const resolved = await this.resolve(source, importer, {
95
+ ...options,
96
+ skipSelf: true,
97
+ });
98
+ if (resolved === null) return null;
99
+ const backend = backendSourceFile(resolved.id, backendPackages, known);
100
+ if (backend === undefined) return resolved;
101
+ const from =
102
+ importer === undefined
103
+ ? "an entry point"
104
+ : importer.replace(QUERY_OR_HASH, "");
105
+ return this.error(
106
+ `Backend boundary: ${from} imports ${backend.file}, which is under the src of ${backend.name}. ` +
107
+ "The frontend reaches the backend only through its HTTP interface.",
108
+ );
109
+ },
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Fail the build, a worker build and the dev server when a module of the
115
+ * frontend resolves to a file under the `src` of a backend package: the package
116
+ * that holds a module's layer, as the registry names it. The browser and the
117
+ * server renderer reach the backend only through its HTTP interface.
118
+ *
119
+ * The check reads what the whole resolution chain returns, so an alias, a
120
+ * symlink and an id marked external are all seen through. Vite builds workers
121
+ * with their own plugin list, so the plugin adds the check there too.
122
+ *
123
+ * @param {readonly string[]} backendPackages
124
+ * @returns {import("vite").Plugin}
125
+ */
126
+ export function rejectBackendSources(backendPackages) {
127
+ const names = new Set(backendPackages);
128
+ return {
129
+ ...boundaryCheck(names),
130
+ config: () => ({ worker: { plugins: () => [boundaryCheck(names)] } }),
131
+ };
132
+ }
@@ -18,6 +18,8 @@
18
18
  * quarter to four fifths, but every group is loaded whole by each page that
19
19
  * uses any of it, and the anonymous pages pay 70 to 115 KB of Brotli for code
20
20
  * only the signed-in shell uses.
21
+ *
22
+ * @type {import("vite").Rolldown.CodeSplittingOptions}
21
23
  */
22
24
  export const clientCodeSplitting = {
23
25
  groups: [{ name: "entry", tags: ["$initial"] }],
@@ -2,11 +2,20 @@ import { relative, sep } from "node:path";
2
2
 
3
3
  const SERVER_ENTRY = /(?:^|[\\/])dms\.server\.[cm]?[jt]sx?$/;
4
4
 
5
+ /**
6
+ * @param {string} id
7
+ * @returns {string}
8
+ */
5
9
  function stripQuery(id) {
6
10
  const query = id.indexOf("?");
7
11
  return query === -1 ? id : id.slice(0, query);
8
12
  }
9
13
 
14
+ /**
15
+ * @param {string} file
16
+ * @param {readonly string[]} moduleRoots
17
+ * @returns {boolean}
18
+ */
10
19
  function insideModuleServerDirectory(file, moduleRoots) {
11
20
  return moduleRoots.some((root) => {
12
21
  const path = relative(root, file);
@@ -20,6 +29,10 @@ function insideModuleServerDirectory(file, moduleRoots) {
20
29
  * Whether `id`, a resolved module id, is server code: a module's server entry
21
30
  * (`dms.server.*`) or a file under a `server/` directory of a module. Installed
22
31
  * packages never are: `node_modules` has its own `server` directories.
32
+ *
33
+ * @param {string} id
34
+ * @param {readonly string[]} moduleRoots
35
+ * @returns {boolean}
23
36
  */
24
37
  export function isServerSource(id, moduleRoots) {
25
38
  if (id.startsWith("\0")) return false;
@@ -44,6 +57,9 @@ export function isServerSource(id, moduleRoots) {
44
57
  * import; the guard sits in the module graph instead, where every resolved file
45
58
  * passes. Kept beside vite.config.ts rather than inside it so the plugin can be
46
59
  * exercised on its own, the way head-order.mjs is.
60
+ *
61
+ * @param {readonly string[]} moduleRoots
62
+ * @returns {import("vite").Plugin}
47
63
  */
48
64
  export function rejectServerSourcesInClient(moduleRoots) {
49
65
  let ssrBuild = false;
@@ -16,10 +16,14 @@ const MINIMUM_SIZE_BYTES = 1_024;
16
16
  const compressBrotli = promisify(brotliCompress);
17
17
  const compressGzip = promisify(gzip);
18
18
 
19
+ /**
20
+ * @param {string} directory
21
+ * @returns {Promise<string[]>}
22
+ */
19
23
  async function assetFiles(directory) {
20
24
  const entries = await readdir(directory, { withFileTypes: true });
21
25
  const files = await Promise.all(
22
- entries.map((entry) => {
26
+ entries.map(async (entry) => {
23
27
  const path = join(directory, entry.name);
24
28
  return entry.isDirectory() ? assetFiles(path) : [path];
25
29
  }),
@@ -27,6 +31,10 @@ async function assetFiles(directory) {
27
31
  return files.flat();
28
32
  }
29
33
 
34
+ /**
35
+ * @param {string} path
36
+ * @returns {Promise<void>}
37
+ */
30
38
  async function compressAsset(path) {
31
39
  if (!COMPRESSIBLE_EXTENSIONS.has(extname(path))) return;
32
40
  const content = await readFile(path);
@@ -10,11 +10,20 @@ export const TRANSLATIONS_OWNER = "translations";
10
10
 
11
11
  const LOCALE_CATALOG = /[\\/]locales\.generated[\\/]/;
12
12
 
13
+ /**
14
+ * @param {string} id
15
+ * @returns {string}
16
+ */
13
17
  function stripQuery(id) {
14
18
  const query = id.indexOf("?");
15
19
  return query === -1 ? id : id.slice(0, query);
16
20
  }
17
21
 
22
+ /**
23
+ * @param {string} root
24
+ * @param {string} file
25
+ * @returns {boolean}
26
+ */
18
27
  function isInside(root, file) {
19
28
  const path = relative(root, file);
20
29
  return Boolean(path) && !path.startsWith("..") && !path.startsWith(sep);
@@ -26,7 +35,9 @@ function isInside(root, file) {
26
35
  * what every page shares (the renderer's runtime, the UI and icon libraries,
27
36
  * an installed package).
28
37
  *
38
+ * @param {string} id
29
39
  * @param {{ root: string, name?: string, packageName?: string, id: string }[]} modules
40
+ * @returns {string}
30
41
  */
31
42
  export function entryOwner(id, modules) {
32
43
  if (id.startsWith("\0")) return SHARED_OWNER;
@@ -42,20 +53,29 @@ export function entryOwner(id, modules) {
42
53
  /**
43
54
  * The files a page loads before its first line runs: the entry chunks and
44
55
  * their static imports, transitively, by file name.
56
+ *
57
+ * @param {import("vite").Rollup.OutputBundle} bundle
58
+ * @returns {import("vite").Rollup.OutputChunk[]}
45
59
  */
46
60
  function entryClosure(bundle) {
47
61
  const chunks = Object.values(bundle).filter(
62
+ /**
63
+ * @param {import("vite").Rollup.OutputChunk | import("vite").Rollup.OutputAsset} output
64
+ * @returns {output is import("vite").Rollup.OutputChunk}
65
+ */
48
66
  (output) => output.type === "chunk",
49
67
  );
50
68
  const byName = new Map(chunks.map((chunk) => [chunk.fileName, chunk]));
51
- const seen = new Set();
69
+ /** @type {Map<string, import("vite").Rollup.OutputChunk>} */
70
+ const seen = new Map();
71
+ /** @param {import("vite").Rollup.OutputChunk | undefined} chunk */
52
72
  const visit = (chunk) => {
53
73
  if (!chunk || seen.has(chunk.fileName)) return;
54
- seen.add(chunk.fileName);
74
+ seen.set(chunk.fileName, chunk);
55
75
  for (const file of chunk.imports ?? []) visit(byName.get(file));
56
76
  };
57
77
  for (const chunk of chunks) if (chunk.isEntry) visit(chunk);
58
- return [...seen].map((file) => byName.get(file));
78
+ return [...seen.values()];
59
79
  }
60
80
 
61
81
  /**
@@ -64,10 +84,13 @@ function entryClosure(bundle) {
64
84
  * rendered for each (`renderedLength`, before minification): an estimate that
65
85
  * needs nothing the bundle does not already hold.
66
86
  *
87
+ * @param {import("vite").Rollup.OutputBundle} bundle
88
+ * @param {{ root: string, name?: string, packageName?: string, id: string }[]} modules
67
89
  * @returns {{ files: string[], bytes: number, owners: Record<string, number> }}
68
90
  */
69
91
  export function attributeEntry(bundle, modules) {
70
92
  const files = entryClosure(bundle);
93
+ /** @type {Record<string, number>} */
71
94
  const owners = {};
72
95
  let bytes = 0;
73
96
  for (const chunk of files) {
@@ -109,6 +132,9 @@ export function attributeEntry(bundle, modules) {
109
132
  *
110
133
  * The weight is recorded once the bundle is written: plugins that generate the
111
134
  * bundle after this one still rewrite the chunks' code.
135
+ *
136
+ * @param {{ root: string, name?: string, packageName?: string, id: string }[]} modules
137
+ * @returns {import("vite").Plugin}
112
138
  */
113
139
  export function recordEntryWeight(modules) {
114
140
  let ssrBuild = false;
@@ -31,10 +31,11 @@ export interface DmsFrontendBuild {
31
31
 
32
32
  /**
33
33
  * Adds Vite plugins to the application's build, for `ajs dms dev`,
34
- * `ajs dms build` and the preview alike.
34
+ * `ajs dms build` and the preview alike, and to the bundle of each web worker
35
+ * it starts with `new Worker(new URL(...))`.
35
36
  *
36
- * `create` runs in the build process each time Vite loads its configuration,
37
- * and returns real Vite plugin objects (an array, each with a `name`; false,
37
+ * `create` runs in the build process each time Vite loads its configuration
38
+ * and once more for each worker bundle, and returns real Vite plugin objects (an array, each with a `name`; false,
38
39
  * null and undefined entries are skipped and nested arrays flattened, as in a
39
40
  * Vite configuration) or nothing. It is synchronous: a Promise is refused,
40
41
  * because the configuration is built synchronously. The plugins come after
@@ -34,6 +34,7 @@ export type {
34
34
  DmsFetchTransformContext,
35
35
  UseAsyncDataOptions,
36
36
  UseFetchOptions,
37
+ DmsServerFetch,
37
38
  DmsErrorData,
38
39
  DmsPayload,
39
40
  DmsStateOptions,
@@ -1,5 +1,4 @@
1
1
  import { usePage } from "@inertiajs/vue3";
2
- import { ofetch } from "ofetch";
3
2
  import {
4
3
  type Component,
5
4
  type EffectScope,
@@ -19,6 +18,7 @@ import {
19
18
  type DmsPluginRegistrationOptions,
20
19
  type DmsPluginSetup,
21
20
  type DmsRoute,
21
+ type DmsServerFetch,
22
22
  type DmsSharedPagePayload,
23
23
  type RouteLocationRaw,
24
24
  } from "./types";
@@ -212,7 +212,7 @@ export interface DmsFrontendRuntime {
212
212
  /** Whether to warn about what a development render does wrong. */
213
213
  devWarnings?: boolean;
214
214
  runtimeHooks: Map<string, DmsRuntimeHook[]>;
215
- serverFetch?: typeof ofetch;
215
+ serverFetch?: DmsServerFetch;
216
216
  currentError: Ref<Error | null>;
217
217
  route: DmsRoute;
218
218
  currentRoutePattern?: string;
@@ -1,4 +1,3 @@
1
- import { ofetch } from "ofetch";
2
1
  import {
3
2
  type App,
4
3
  effectScope,
@@ -19,6 +18,7 @@ import {
19
18
  import {
20
19
  type DmsAppContext,
21
20
  type DmsRoute,
21
+ type DmsServerFetch,
22
22
  type DmsRuntimeConfig,
23
23
  } from "./types";
24
24
 
@@ -65,7 +65,7 @@ export function forgetBrowserRuntime(runtime: DmsFrontendRuntime): void {
65
65
 
66
66
  /** Creates mutable runtime state owned by one Vue application. */
67
67
  export function createDmsFrontendRuntime(
68
- serverFetch?: typeof ofetch,
68
+ serverFetch?: DmsServerFetch,
69
69
  hydratedAsyncData?: Record<string, unknown>,
70
70
  isServer = false,
71
71
  requestCookies?: string,
@@ -1,5 +1,5 @@
1
1
  import { type InertiaLinkProps } from "@inertiajs/vue3";
2
- import { type FetchOptions, ofetch } from "ofetch";
2
+ import { type FetchOptions } from "ofetch";
3
3
  import {
4
4
  type App,
5
5
  type ComputedRef,
@@ -246,6 +246,12 @@ export interface UseAsyncDataOptions<T> {
246
246
  */
247
247
  refreshInterval?: MaybeRefOrGetter<number | null | undefined>;
248
248
  }
249
+ /** What the runtime calls to read the backend: `ofetch`, or a stand-in for it. */
250
+ export type DmsServerFetch = (
251
+ request: string,
252
+ options?: FetchOptions,
253
+ ) => Promise<unknown>;
254
+
249
255
  /**
250
256
  * `useDmsFetch`'s options: ofetch's request options, of which the method, the
251
257
  * query, the params and the body may be refs or getters, and the async data
@@ -271,7 +277,7 @@ export interface UseFetchOptions<T>
271
277
  context: DmsFetchTransformContext,
272
278
  ) => T | Promise<T>;
273
279
  /** Replaces the fetch function; it receives the run's signal. */
274
- $fetch?: typeof ofetch;
280
+ $fetch?: DmsServerFetch;
275
281
  }
276
282
  export interface DmsErrorData {
277
283
  statusCode?: number;
@@ -1,6 +1,10 @@
1
1
  const SCRIPT = /<script[^>]*\stype="module"[^>]*><\/script>/;
2
2
  const STYLESHEET = /[ \t]*<link rel="stylesheet"[^>]*>\n?/;
3
3
 
4
+ /**
5
+ * @param {string} html
6
+ * @returns {string}
7
+ */
4
8
  function deprioritize(html) {
5
9
  return html.replace(SCRIPT, (tag) =>
6
10
  tag.includes("fetchpriority")
@@ -24,12 +28,16 @@ function deprioritize(html) {
24
28
  * HTTP/1.1 the generated server speaks. Kept beside vite.config.ts rather than
25
29
  * inside it so the transform can be exercised on its own, the way
26
30
  * client-manifest.mjs is.
31
+ *
32
+ * @param {string} html
33
+ * @returns {string}
27
34
  */
28
35
  export function orderHeadForFirstPaint(html) {
29
36
  const prioritized = deprioritize(html);
30
37
  const stylesheet = prioritized.match(STYLESHEET);
31
- const script = prioritized.match(SCRIPT);
32
- if (!stylesheet || !script || stylesheet.index < script.index)
38
+ const stylesheetAt = prioritized.search(STYLESHEET);
39
+ const scriptAt = prioritized.search(SCRIPT);
40
+ if (!stylesheet || scriptAt < 0 || stylesheetAt < scriptAt)
33
41
  return prioritized;
34
42
  return prioritized
35
43
  .replace(STYLESHEET, "")
@@ -9,6 +9,8 @@ export const ICON_API_PATH = "/api/_dms/icons/";
9
9
  /**
10
10
  * The `@iconify/vue` API provider serving every icon the client bundle lacks
11
11
  * from `origin`, instead of `api.iconify.design` and its fallbacks.
12
+ *
13
+ * @param {string} origin
12
14
  */
13
15
  export function sameOriginIconProvider(origin) {
14
16
  return { resources: [origin], path: ICON_API_PATH };
@@ -19,6 +19,10 @@ const SOURCE_SCRIPT_GLOB = "app/**/*.ts";
19
19
  * fetched from the Iconify API at run time. Every frontend source root lives
20
20
  * under the workspace (`frontend-modules/`, enforced when the registry is
21
21
  * written), so a relative glob reaches it. Globs are POSIX on every platform.
22
+ *
23
+ * @param {string} viteRoot
24
+ * @param {readonly string[]} sourceRoots
25
+ * @returns {string[]}
22
26
  */
23
27
  export function iconScanGlobs(viteRoot, sourceRoots) {
24
28
  const sourceGlobs = sourceRoots.map((root) =>
@@ -1,4 +1,5 @@
1
1
  import { REDIRECT_STATUSES } from "../proxy-redirects.mjs";
2
+ import { isObjectLike, isString } from "../type-guards.mjs";
2
3
  import { clientCountryHeader, clientIp, clientIpHeader } from "./client-ip.mjs";
3
4
 
4
5
  /**
@@ -14,6 +15,10 @@ export const CLIENT_HEADERS = [
14
15
  ];
15
16
 
16
17
  export class UpstreamError extends Error {
18
+ /**
19
+ * @param {number} status
20
+ * @param {string} message
21
+ */
17
22
  constructor(status, message) {
18
23
  super(message);
19
24
  this.status = status;
@@ -27,9 +32,15 @@ const PUBLIC_AUTH_MESSAGE = /^error\.[a-z0-9_.-]{1,100}$/;
27
32
  */
28
33
  export const GENERIC_ERROR_MESSAGE = "error.500.description";
29
34
 
35
+ /**
36
+ * @param {unknown} data A backend answer: any JSON value, or the text of one
37
+ * that is not JSON
38
+ * @returns {string | undefined}
39
+ */
30
40
  export function publicBackendMessage(data) {
31
- const message = typeof data === "string" ? data : data?.message;
32
- return typeof message === "string" && PUBLIC_AUTH_MESSAGE.test(message)
41
+ /** @type {unknown} */
42
+ const message = isObjectLike(data) ? data.message : data;
43
+ return isString(message) && PUBLIC_AUTH_MESSAGE.test(message)
33
44
  ? message
34
45
  : undefined;
35
46
  }
@@ -37,8 +48,10 @@ export function publicBackendMessage(data) {
37
48
  /**
38
49
  * The headers of a client request the server relays to the backend.
39
50
  *
40
- * @param request The client request
41
- * @param names Lowercase names of the request headers to copy when present
51
+ * @param {import("./client-ip.mjs").ForwardedRequest} request The client
52
+ * request
53
+ * @param {readonly string[]} names Lowercase names of the request headers to
54
+ * copy when present
42
55
  * @returns The client country header, then the copied headers
43
56
  */
44
57
  export function relayedHeaders(request, names) {
@@ -46,7 +59,7 @@ export function relayedHeaders(request, names) {
46
59
  ...clientCountryHeader(request),
47
60
  ...Object.fromEntries(
48
61
  names.flatMap((name) =>
49
- request.headers[name] ? [[name, request.headers[name]]] : [],
62
+ request.headers[name] ? [[name, String(request.headers[name])]] : [],
50
63
  ),
51
64
  ),
52
65
  };
@@ -58,8 +71,22 @@ export function relayedHeaders(request, names) {
58
71
  * `options.headers` adds request headers; the client's address and the
59
72
  * relayed client headers are set after them, so no caller replaces those.
60
73
  * A redirect is not followed: it is no JSON answer, so it fails with a 502.
74
+ *
75
+ * @param {string} path
76
+ * @param {import("./client-ip.mjs").ForwardedRequest} request The client
77
+ * request
78
+ * @param {{
79
+ * method?: string,
80
+ * body?: unknown,
81
+ * token?: string,
82
+ * relay?: boolean,
83
+ * headers?: Record<string, string>
84
+ * }} [options]
85
+ * @returns {Promise<unknown>} The answer's JSON, `{}` for a successful one that
86
+ * is not JSON
61
87
  */
62
88
  export async function backend(path, request, options = {}) {
89
+ /** @type {Record<string, string>} */
63
90
  const headers = Object.fromEntries(
64
91
  Object.entries(options.headers ?? {}).map(([name, value]) => [
65
92
  name.toLowerCase(),
@@ -87,6 +114,7 @@ export async function backend(path, request, options = {}) {
87
114
  throw new UpstreamError(502, GENERIC_ERROR_MESSAGE);
88
115
  }
89
116
  const text = await response.text();
117
+ /** @type {unknown} */
90
118
  let data;
91
119
  try {
92
120
  data = JSON.parse(text);
@@ -105,12 +133,21 @@ export async function backend(path, request, options = {}) {
105
133
  }
106
134
 
107
135
  export class RequestBodyError extends Error {
136
+ /**
137
+ * @param {number} status
138
+ * @param {string} message
139
+ */
108
140
  constructor(status, message) {
109
141
  super(message);
110
142
  this.status = status;
111
143
  }
112
144
  }
113
145
 
146
+ /**
147
+ * @param {import("node:http").ServerResponse} response
148
+ * @param {number} status
149
+ * @param {unknown} value Anything `JSON.stringify` writes
150
+ */
114
151
  export function json(response, status, value) {
115
152
  response.writeHead(status, { "content-type": "application/json" });
116
153
  response.end(JSON.stringify(value));