@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.
- package/README.md +46 -29
- package/dist/commands/build-action.d.ts +3 -0
- package/dist/commands/build-action.js +77 -8
- package/dist/commands/build.js +7 -0
- package/dist/commands/clean-action.js +1 -1
- package/dist/commands/clean.js +1 -1
- package/dist/commands/dev-action.js +4 -3
- package/dist/commands/dev.js +1 -1
- package/dist/commands/prepare-action.js +1 -1
- package/dist/commands/start-action.js +3 -24
- package/dist/commands/workspace-task.js +2 -1
- package/dist/config.js +1 -0
- package/dist/derived-outputs.d.ts +1 -0
- package/dist/derived-outputs.js +6 -4
- package/dist/discovery.js +17 -6
- package/dist/entry-weight.js +15 -0
- package/dist/fs-sync.js +7 -4
- package/dist/help.js +1 -1
- package/dist/index.js +2 -1
- package/dist/internal/built-workspace.d.ts +4 -0
- package/dist/internal/built-workspace.js +43 -0
- package/dist/internal/ensure.d.ts +1 -0
- package/dist/internal/ensure.js +8 -0
- package/dist/internal/locale-catalogs.d.ts +8 -3
- package/dist/internal/locale-catalogs.js +16 -6
- package/dist/internal/manifest-decoder.d.ts +7 -0
- package/dist/internal/manifest-decoder.js +81 -0
- package/dist/internal/own-release.d.ts +5 -0
- package/dist/internal/own-release.js +20 -0
- package/dist/internal/plain-object.d.ts +1 -0
- package/dist/internal/plain-object.js +6 -0
- package/dist/internal/prepared-workspace.d.ts +12 -0
- package/dist/internal/prepared-workspace.js +74 -0
- package/dist/internal/text-list.d.ts +1 -0
- package/dist/internal/text-list.js +10 -0
- package/dist/internal/vite-manifest.d.ts +6 -0
- package/dist/internal/vite-manifest.js +24 -0
- package/dist/layer-watch.js +25 -24
- package/dist/layers.d.ts +1 -1
- package/dist/layers.js +3 -1
- package/dist/locale-files.d.ts +6 -3
- package/dist/locale-files.js +15 -9
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +46 -32
- package/dist/materialize.js +6 -2
- package/dist/module-set.js +2 -1
- package/dist/node-options.d.ts +1 -0
- package/dist/node-options.js +8 -0
- package/dist/output.d.ts +15 -9
- package/dist/output.js +30 -18
- package/dist/ports.d.ts +3 -1
- package/dist/ports.js +1 -1
- package/dist/private-values.d.ts +1 -1
- package/dist/private-values.js +5 -3
- package/dist/project-modules.d.ts +5 -1
- package/dist/project-modules.js +23 -15
- package/dist/renderer-range.js +5 -3
- package/dist/server-process.js +1 -1
- package/dist/verify-source-build.js +10 -2
- package/dist/verify-source-client-bundle.d.ts +1 -0
- package/dist/verify-source-client-bundle.js +30 -0
- package/dist/verify-source-runner.js +36 -94
- package/dist/verify-source-sources.js +49 -11
- package/dist/verify-source-workspace.d.ts +2 -0
- package/dist/verify-source-workspace.js +111 -0
- package/dist/workspace-setup.d.ts +1 -0
- package/dist/workspace-setup.js +4 -3
- package/package.json +11 -7
- package/skills/dms-frontend/SKILL.md +1 -1
- package/templates/vue/app-runtime.ts +2 -9
- package/templates/vue/backend-boundary.mjs +132 -0
- package/templates/vue/client-chunking.mjs +2 -0
- package/templates/vue/client-server-guard.mjs +16 -0
- package/templates/vue/compress-assets.mjs +9 -1
- package/templates/vue/entry-weight.mjs +29 -3
- package/templates/vue/frontend-build.ts +4 -3
- package/templates/vue/frontend-module.ts +1 -0
- package/templates/vue/frontend-runtime/module-registration.ts +2 -2
- package/templates/vue/frontend-runtime/state.ts +2 -2
- package/templates/vue/frontend-runtime/types.ts +8 -2
- package/templates/vue/head-order.mjs +10 -2
- package/templates/vue/icon-api.mjs +2 -0
- package/templates/vue/icon-scan.mjs +4 -0
- package/templates/vue/server/auth/backend.mjs +42 -5
- package/templates/vue/server/auth/client-ip.mjs +52 -4
- package/templates/vue/server/auth/establish-endpoints.mjs +11 -7
- package/templates/vue/server/auth/session.mjs +86 -4
- package/templates/vue/server/auth-declarations.mjs +46 -0
- package/templates/vue/server/auth-hooks.mjs +55 -7
- package/templates/vue/server/backend-proxies.mjs +78 -18
- package/templates/vue/server/backend-request.mjs +61 -0
- package/templates/vue/server/backend-response.mjs +23 -6
- package/templates/vue/server/built-files.mjs +7 -0
- package/templates/vue/server/cli-report.mjs +75 -12
- package/templates/vue/server/client-error.mjs +5 -0
- package/templates/vue/server/client-manifest.mjs +23 -1
- package/templates/vue/server/content.mjs +40 -10
- package/templates/vue/server/dev-private-files.mjs +17 -1
- package/templates/vue/server/dev-styles.mjs +38 -0
- package/templates/vue/server/email-bundle-watch.mjs +9 -3
- package/templates/vue/server/email.mjs +14 -6
- package/templates/vue/server/favicon.mjs +3 -0
- package/templates/vue/server/generated-files.mjs +221 -0
- package/templates/vue/server/homepage.mjs +21 -9
- package/templates/vue/server/html-tag.mjs +4 -0
- package/templates/vue/server/icons.mjs +93 -16
- package/templates/vue/server/in-place-render.mjs +35 -0
- package/templates/vue/server/inertia.mjs +76 -3
- package/templates/vue/server/module-event.mjs +118 -25
- package/templates/vue/server/module-route-types.mjs +78 -0
- package/templates/vue/server/module-routes.mjs +151 -96
- package/templates/vue/server/module-server.mjs +121 -40
- package/templates/vue/server/module-testing.mjs +36 -12
- package/templates/vue/server/page-props.mjs +94 -3
- package/templates/vue/server/page-redirects.mjs +37 -0
- package/templates/vue/server/path-patterns.mjs +35 -12
- package/templates/vue/server/proxy-compression.mjs +22 -0
- package/templates/vue/server/proxy-headers.mjs +12 -2
- package/templates/vue/server/proxy-redirects.mjs +21 -5
- package/templates/vue/server/proxy-request.mjs +58 -0
- package/templates/vue/server/proxy-transfers.mjs +44 -10
- package/templates/vue/server/public-endpoints.mjs +139 -28
- package/templates/vue/server/public-options.mjs +20 -6
- package/templates/vue/server/request-failure.mjs +24 -1
- package/templates/vue/server/request-routing.mjs +32 -10
- package/templates/vue/server/request-target.mjs +30 -11
- package/templates/vue/server/route-rules.mjs +48 -18
- package/templates/vue/server/ssr-module.mjs +32 -0
- package/templates/vue/server/static-files.mjs +35 -7
- package/templates/vue/server/type-guards.mjs +71 -0
- package/templates/vue/server/vite-errors.mjs +45 -7
- package/templates/vue/server/web-body.mjs +5 -1
- package/templates/vue/server-module.ts +12 -2
- package/templates/vue/server.mjs +162 -157
- package/templates/vue/sfc-global-types.mjs +112 -13
- package/templates/vue/ssr-renderer.ts +2 -2
- package/templates/vue/ui-inertia-page.mjs +16 -1
- package/templates/vue/ui-kbd.mjs +2 -1
- package/templates/vue/ui-request-state.mjs +2 -1
- package/templates/vue/vite.config.ts +9 -0
|
@@ -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 { isObjectLike, 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 =
|
|
48
|
+
value = isObjectLike(data) ? data.message : data;
|
|
39
49
|
} catch {}
|
|
40
|
-
return
|
|
41
|
-
? value
|
|
42
|
-
: undefined;
|
|
50
|
+
return isString(value) && REFUSAL_CODE.test(value) ? value : undefined;
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
/**
|
|
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
|
-
* @
|
|
69
|
+
* @param {unknown} error What the request failed with
|
|
70
|
+
* @param {import("./request-target.mjs").ServerRequest} 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,12 +1,23 @@
|
|
|
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
|
+
/** @type {Promise<void>} */
|
|
17
|
+
const sent = new Promise((resolve) =>
|
|
18
|
+
process.send?.(message, () => resolve()),
|
|
19
|
+
);
|
|
20
|
+
await sent;
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
/**
|
|
@@ -15,6 +26,10 @@ async function report(message, line, write) {
|
|
|
15
26
|
* What the server tells whoever started it. Started by `ajs dms dev` or `ajs
|
|
16
27
|
* dms start`, it reports over IPC and the CLI words what the user reads.
|
|
17
28
|
* Started on its own, it prints the line itself.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} address
|
|
31
|
+
* @param {number} port
|
|
32
|
+
* @returns {Promise<void>}
|
|
18
33
|
*/
|
|
19
34
|
export async function reportReady(address, port) {
|
|
20
35
|
const host = WILDCARD_ADDRESSES.includes(address)
|
|
@@ -29,10 +44,32 @@ export async function reportReady(address, port) {
|
|
|
29
44
|
);
|
|
30
45
|
}
|
|
31
46
|
|
|
47
|
+
/** The `ANTELOPEJS_QUIET` values that leave a run verbose, as `@antelopejs/core` reads them. */
|
|
48
|
+
const DISABLED_FLAG_VALUES = ["0", "false"];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Prints the routing summary, unless the run is quiet: `-q` given to `ajs` or
|
|
52
|
+
* `ajs dms` reaches the server as `ANTELOPEJS_QUIET`, and a quiet ready server
|
|
53
|
+
* prints only its `Local` url.
|
|
54
|
+
*
|
|
55
|
+
* @param {readonly string[]} lines What `routingReport` returned
|
|
56
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
57
|
+
*/
|
|
58
|
+
export function reportRouting(lines, env = process.env) {
|
|
59
|
+
const quiet = env.ANTELOPEJS_QUIET;
|
|
60
|
+
if (quiet && !DISABLED_FLAG_VALUES.includes(quiet.toLowerCase())) return;
|
|
61
|
+
for (const line of lines) console.log(line);
|
|
62
|
+
}
|
|
63
|
+
|
|
32
64
|
/**
|
|
33
65
|
* The server cannot listen: a port taken by another process, an address this
|
|
34
66
|
* host does not have. Reported instead of Node's unhandled-error dump, then
|
|
35
67
|
* the server exits.
|
|
68
|
+
*
|
|
69
|
+
* @param {NodeJS.ErrnoException} error
|
|
70
|
+
* @param {string} host
|
|
71
|
+
* @param {number} port
|
|
72
|
+
* @returns {Promise<void>}
|
|
36
73
|
*/
|
|
37
74
|
export async function reportListenError(error, host, port) {
|
|
38
75
|
await report(
|
|
@@ -52,31 +89,46 @@ export async function reportListenError(error, host, port) {
|
|
|
52
89
|
/** The `[PARSE_ERROR] ` Rolldown puts in front of what it says. */
|
|
53
90
|
const RULE_PREFIX = /^\[[A-Z_]+\]\s*/;
|
|
54
91
|
|
|
55
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* The first problem Rolldown reports when it cannot bundle a config.
|
|
94
|
+
*
|
|
95
|
+
* @param {unknown} error What the startup threw
|
|
96
|
+
*/
|
|
56
97
|
function bundlerError(error) {
|
|
57
|
-
const
|
|
58
|
-
|
|
98
|
+
const errors = isObjectLike(error) ? error.errors : undefined;
|
|
99
|
+
const first = isObjectLike(errors) ? errors[0] : undefined;
|
|
100
|
+
return isRecord(first) ? first : undefined;
|
|
59
101
|
}
|
|
60
102
|
|
|
61
103
|
/**
|
|
62
104
|
* Where a startup error points, when it says: Rolldown's location for a config
|
|
63
105
|
* it could not bundle (the line is read back from the file, which Rolldown
|
|
64
106
|
* only draws in a colored frame), Rollup's for a plugin that failed on a file.
|
|
107
|
+
*
|
|
108
|
+
* @param {unknown} error What the startup threw
|
|
65
109
|
*/
|
|
66
110
|
function errorLocation(error) {
|
|
67
|
-
const source =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
111
|
+
const source =
|
|
112
|
+
bundlerError(error) ?? (isObjectLike(error) ? error : undefined);
|
|
113
|
+
const loc = isObjectLike(source?.loc) ? source.loc : undefined;
|
|
114
|
+
const file = loc?.file ?? source?.id;
|
|
115
|
+
if (!isString(file)) return undefined;
|
|
116
|
+
const line = isNumber(loc?.line) ? loc.line : undefined;
|
|
71
117
|
return {
|
|
72
118
|
file: resolve(file),
|
|
73
119
|
line,
|
|
74
|
-
column,
|
|
120
|
+
column: isNumber(loc?.column) ? loc.column : undefined,
|
|
75
121
|
lineText: line === undefined ? undefined : readLine(file, line),
|
|
76
122
|
};
|
|
77
123
|
}
|
|
78
124
|
|
|
79
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Line `line` (1-based) of `file`, `undefined` when it cannot be read.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} file
|
|
129
|
+
* @param {number} line
|
|
130
|
+
* @returns {string | undefined}
|
|
131
|
+
*/
|
|
80
132
|
function readLine(file, line) {
|
|
81
133
|
try {
|
|
82
134
|
return readFileSync(file, "utf8").split(/\r?\n/)[line - 1];
|
|
@@ -85,7 +137,11 @@ function readLine(file, line) {
|
|
|
85
137
|
}
|
|
86
138
|
}
|
|
87
139
|
|
|
88
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* The first line of what went wrong, without Rolldown's rule and frame.
|
|
142
|
+
*
|
|
143
|
+
* @param {unknown} error What the startup threw
|
|
144
|
+
*/
|
|
89
145
|
function errorText(error) {
|
|
90
146
|
const reported = bundlerError(error);
|
|
91
147
|
const message = reported
|
|
@@ -101,6 +157,8 @@ function errorText(error) {
|
|
|
101
157
|
/**
|
|
102
158
|
* Vite could not start, so no page can be served: the error and where it
|
|
103
159
|
* points instead of a stack trace, then the server exits.
|
|
160
|
+
*
|
|
161
|
+
* @param {unknown} error What the startup threw
|
|
104
162
|
*/
|
|
105
163
|
export async function reportStartError(error) {
|
|
106
164
|
const message = errorText(error);
|
|
@@ -109,7 +167,12 @@ export async function reportStartError(error) {
|
|
|
109
167
|
? `${location.file}${location.line ? `:${location.line}:${location.column ?? 0}` : ""}: `
|
|
110
168
|
: "";
|
|
111
169
|
await report(
|
|
112
|
-
{
|
|
170
|
+
{
|
|
171
|
+
type: "dms:start-error",
|
|
172
|
+
message,
|
|
173
|
+
code: isObjectLike(error) ? error.code : undefined,
|
|
174
|
+
location,
|
|
175
|
+
},
|
|
113
176
|
`DMS development server failed to start: ${where}${message}`,
|
|
114
177
|
console.error,
|
|
115
178
|
);
|
|
@@ -11,6 +11,7 @@ const HEADER_OVERFLOW_HINT_INTERVAL_MS = 60_000;
|
|
|
11
11
|
|
|
12
12
|
let lastHeaderOverflowHint = -Infinity;
|
|
13
13
|
|
|
14
|
+
/** @param {number} maxHeaderSize */
|
|
14
15
|
function warnHeaderOverflow(maxHeaderSize) {
|
|
15
16
|
const now = Date.now();
|
|
16
17
|
if (now - lastHeaderOverflowHint < HEADER_OVERFLOW_HINT_INTERVAL_MS) return;
|
|
@@ -32,6 +33,10 @@ function warnHeaderOverflow(maxHeaderSize) {
|
|
|
32
33
|
*
|
|
33
34
|
* Once a response went out on this connection, a status line would land in the
|
|
34
35
|
* middle of it.
|
|
36
|
+
*
|
|
37
|
+
* @param {NodeJS.ErrnoException} error
|
|
38
|
+
* @param {import("node:stream").Duplex & { bytesWritten?: number }} socket
|
|
39
|
+
* @param {number} maxHeaderSize
|
|
35
40
|
*/
|
|
36
41
|
export function handleClientError(error, socket, maxHeaderSize) {
|
|
37
42
|
let response = BAD_REQUEST_RESPONSE;
|
|
@@ -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 =
|
|
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();
|
|
@@ -11,15 +11,16 @@ export const MINIMUM_COMPRESSION_BYTES = 1_024;
|
|
|
11
11
|
const DYNAMIC_BROTLI_QUALITY = 4;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* client gets each part when it is written, not when a block fills.
|
|
14
|
+
* The codings among `br` and `gzip` the client accepts, most wanted first
|
|
15
|
+
* (Brotli on a tie). Empty when it accepts neither, or ranks identity above
|
|
16
|
+
* both: implicit identity is a fallback, not a preference over compression.
|
|
18
17
|
*
|
|
19
|
-
*
|
|
18
|
+
* @param {import("node:http").IncomingMessage} request
|
|
19
|
+
* @returns {("br" | "gzip")[]}
|
|
20
20
|
*/
|
|
21
|
-
export function
|
|
21
|
+
export function acceptedCodings(request) {
|
|
22
22
|
const accepted = request.headers["accept-encoding"] ?? "";
|
|
23
|
+
/** @type {Map<string, number>} */
|
|
23
24
|
const qualities = new Map();
|
|
24
25
|
for (const entry of accepted.split(",")) {
|
|
25
26
|
const [token, ...parameters] = entry.trim().toLowerCase().split(";");
|
|
@@ -37,12 +38,29 @@ export function acceptedEncoder(request, { streaming = false } = {}) {
|
|
|
37
38
|
Math.min(qualities.get(token.trim()) ?? 1, quality),
|
|
38
39
|
);
|
|
39
40
|
}
|
|
41
|
+
/** @param {string} token */
|
|
40
42
|
const quality = (token) => qualities.get(token) ?? qualities.get("*") ?? 0;
|
|
41
43
|
const br = quality("br");
|
|
42
44
|
const gzip = quality("gzip");
|
|
43
45
|
const identity = qualities.get("identity") ?? 0;
|
|
44
|
-
if (identity > Math.max(br, gzip)) return
|
|
45
|
-
|
|
46
|
+
if (identity > Math.max(br, gzip)) return [];
|
|
47
|
+
/** @type {("br" | "gzip")[]} */
|
|
48
|
+
const ranked = gzip > br ? ["gzip", "br"] : ["br", "gzip"];
|
|
49
|
+
return ranked.filter((coding) => quality(coding) > 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A compressor for a body generated or relayed now, in the encoding the client
|
|
54
|
+
* wants most (see `acceptedCodings`), or `undefined` when it accepts neither.
|
|
55
|
+
* `streaming` flushes what each write gives at once, for a body written over
|
|
56
|
+
* time: the client gets each part when it is written, not when a block fills.
|
|
57
|
+
*
|
|
58
|
+
* @param {import("node:http").IncomingMessage} request
|
|
59
|
+
* @param {{ streaming?: boolean }} [options]
|
|
60
|
+
*/
|
|
61
|
+
export function acceptedEncoder(request, { streaming = false } = {}) {
|
|
62
|
+
const [coding] = acceptedCodings(request);
|
|
63
|
+
if (coding === "br")
|
|
46
64
|
return {
|
|
47
65
|
encoding: "br",
|
|
48
66
|
stream: createBrotliCompress({
|
|
@@ -52,7 +70,7 @@ export function acceptedEncoder(request, { streaming = false } = {}) {
|
|
|
52
70
|
},
|
|
53
71
|
}),
|
|
54
72
|
};
|
|
55
|
-
if (
|
|
73
|
+
if (coding === "gzip")
|
|
56
74
|
return {
|
|
57
75
|
encoding: "gzip",
|
|
58
76
|
stream: createGzip(
|
|
@@ -61,12 +79,24 @@ export function acceptedEncoder(request, { streaming = false } = {}) {
|
|
|
61
79
|
};
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
/**
|
|
83
|
+
* @param {import("node:http").IncomingMessage} request
|
|
84
|
+
* @param {string | Buffer} content
|
|
85
|
+
*/
|
|
64
86
|
function responseEncoder(request, content) {
|
|
65
87
|
if (Buffer.byteLength(content) < MINIMUM_COMPRESSION_BYTES) return undefined;
|
|
66
88
|
return acceptedEncoder(request);
|
|
67
89
|
}
|
|
68
90
|
|
|
69
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Send a generated body, compressed when the client accepts it.
|
|
93
|
+
*
|
|
94
|
+
* @param {import("node:http").IncomingMessage} request
|
|
95
|
+
* @param {import("node:http").ServerResponse} response
|
|
96
|
+
* @param {number} status
|
|
97
|
+
* @param {import("node:http").OutgoingHttpHeaders} headers
|
|
98
|
+
* @param {string | Buffer} content
|
|
99
|
+
*/
|
|
70
100
|
export async function writeContent(
|
|
71
101
|
request,
|
|
72
102
|
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
|
-
/**
|
|
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
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { build } from "vite";
|
|
3
|
+
import { isObjectLike } from "./type-guards.mjs";
|
|
3
4
|
|
|
4
5
|
const PROJECT_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
5
6
|
|
|
6
7
|
process.on("disconnect", () => process.exit(0));
|
|
7
8
|
|
|
9
|
+
/** @type {(() => void) | undefined} */
|
|
8
10
|
let startBuild;
|
|
9
11
|
process.on("message", (message) => {
|
|
10
|
-
if (message
|
|
12
|
+
if (!isObjectLike(message) || message.state !== "build") return;
|
|
11
13
|
startBuild?.();
|
|
12
14
|
startBuild = undefined;
|
|
13
15
|
});
|
|
@@ -17,10 +19,12 @@ const serverHandoff = {
|
|
|
17
19
|
name: "dms-email-bundle-handoff",
|
|
18
20
|
buildStart() {
|
|
19
21
|
if (!process.send) return;
|
|
20
|
-
|
|
22
|
+
/** @type {Promise<void>} */
|
|
23
|
+
const held = new Promise((start) => {
|
|
21
24
|
startBuild = start;
|
|
22
|
-
process.send({ state: "building" });
|
|
25
|
+
process.send?.({ state: "building" });
|
|
23
26
|
});
|
|
27
|
+
return held;
|
|
24
28
|
},
|
|
25
29
|
};
|
|
26
30
|
|
|
@@ -34,6 +38,8 @@ const watcher = await build({
|
|
|
34
38
|
build: { watch: {} },
|
|
35
39
|
});
|
|
36
40
|
|
|
41
|
+
if (!("on" in watcher))
|
|
42
|
+
throw new TypeError("The email bundle build did not start a watcher");
|
|
37
43
|
watcher.on("event", (event) => {
|
|
38
44
|
if (event.code === "END") process.send?.({ state: "ready" });
|
|
39
45
|
else if (event.code === "ERROR") process.send?.({ state: "failed" });
|