@antelopejs/dms-frontend 0.10.0 → 0.10.2
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 +34 -11
- package/dist/commands/build-action.d.ts +3 -0
- package/dist/commands/build-action.js +74 -6
- 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 +2 -2
- package/dist/commands/dev.js +1 -1
- package/dist/config.js +1 -0
- package/dist/derived-outputs.d.ts +1 -0
- package/dist/derived-outputs.js +3 -1
- package/dist/help.js +1 -1
- package/dist/internal/prepared-workspace.d.ts +12 -0
- package/dist/internal/prepared-workspace.js +74 -0
- package/dist/layer-watch.js +1 -1
- package/dist/locale-files.d.ts +4 -2
- package/dist/locale-files.js +11 -9
- package/dist/manifest.js +2 -1
- package/dist/ports.d.ts +3 -1
- package/dist/ports.js +1 -1
- package/dist/project-modules.d.ts +6 -1
- package/dist/project-modules.js +26 -16
- package/dist/verify-source-runner.js +2 -44
- 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 +3 -2
- package/dist/workspace.js +1 -1
- package/package.json +20 -7
- package/templates/vue/backend-boundary.mjs +132 -0
- package/templates/vue/client-chunking.mjs +2 -0
- package/templates/vue/entry-weight.mjs +9 -5
- package/templates/vue/frontend-build.ts +4 -3
- package/templates/vue/head-order.mjs +3 -2
- package/templates/vue/icon-api.mjs +2 -0
- package/templates/vue/server/auth/backend.mjs +4 -4
- package/templates/vue/server/auth/establish-endpoints.mjs +2 -2
- package/templates/vue/server/auth/session.mjs +5 -2
- package/templates/vue/server/auth-declarations.mjs +46 -0
- package/templates/vue/server/auth-hooks.mjs +41 -3
- package/templates/vue/server/backend-proxies.mjs +16 -12
- package/templates/vue/server/backend-response.mjs +3 -3
- package/templates/vue/server/cli-report.mjs +22 -1
- package/templates/vue/server/client-error.mjs +5 -0
- package/templates/vue/server/content.mjs +24 -11
- package/templates/vue/server/email-bundle-watch.mjs +9 -3
- package/templates/vue/server/email.mjs +5 -3
- package/templates/vue/server/generated-files.mjs +16 -6
- package/templates/vue/server/icons.mjs +15 -11
- package/templates/vue/server/in-place-render.mjs +3 -3
- package/templates/vue/server/inertia.mjs +17 -3
- package/templates/vue/server/module-event.mjs +28 -16
- package/templates/vue/server/module-route-types.mjs +2 -2
- package/templates/vue/server/module-routes.mjs +24 -76
- package/templates/vue/server/module-server.mjs +27 -17
- package/templates/vue/server/module-testing.mjs +23 -8
- package/templates/vue/server/page-props.mjs +6 -5
- package/templates/vue/server/page-redirects.mjs +1 -1
- package/templates/vue/server/path-patterns.mjs +22 -11
- package/templates/vue/server/proxy-headers.mjs +1 -1
- package/templates/vue/server/proxy-redirects.mjs +1 -0
- package/templates/vue/server/proxy-request.mjs +2 -1
- package/templates/vue/server/proxy-transfers.mjs +17 -11
- package/templates/vue/server/public-endpoints.mjs +12 -16
- package/templates/vue/server/request-failure.mjs +1 -1
- package/templates/vue/server/request-routing.mjs +6 -1
- package/templates/vue/server/request-target.mjs +22 -9
- package/templates/vue/server/route-rules.mjs +12 -6
- package/templates/vue/server/static-files.mjs +12 -7
- package/templates/vue/server/vite-errors.mjs +13 -2
- package/templates/vue/server/web-body.mjs +2 -1
- package/templates/vue/server-module.ts +2 -1
- package/templates/vue/server.mjs +41 -20
- package/templates/vue/ui-inertia-page.mjs +1 -1
- package/templates/vue/vite.config.ts +9 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readSession, setRequestSession } from "./auth/session.mjs";
|
|
2
2
|
import { createServerEvent } from "./module-event.mjs";
|
|
3
|
-
import { compilePathPattern } from "./path-patterns.mjs";
|
|
3
|
+
import { compilePathPattern, PATH_PATTERN_FORMS } from "./path-patterns.mjs";
|
|
4
|
+
import { isFunction, isRecord, isString } from "./type-guards.mjs";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_SIGN_IN_PATH = "/auth";
|
|
6
7
|
const DEFAULT_ONBOARDING_PATH = "/onboarding";
|
|
@@ -17,7 +18,44 @@ const HOOK_ROUTE = Object.freeze({ bodyLimit: 0 });
|
|
|
17
18
|
*/
|
|
18
19
|
function defaultExempt(signInPath) {
|
|
19
20
|
const signIn = signInPath === "/" ? "/" : `${signInPath}/**`;
|
|
20
|
-
return [signIn, DEFAULT_ONBOARDING_PATH].
|
|
21
|
+
return [signIn, DEFAULT_ONBOARDING_PATH].flatMap(
|
|
22
|
+
(pattern) => compilePathPattern(pattern) ?? [],
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const AUTH_HOOKS = ["refreshSession", "publicSession"];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Why an auth hooks registration is ignored, or `undefined`.
|
|
30
|
+
*
|
|
31
|
+
* @param {unknown} auth
|
|
32
|
+
*/
|
|
33
|
+
export function authRejection(auth) {
|
|
34
|
+
if (!isRecord(auth)) return "it is not an object";
|
|
35
|
+
for (const hook of AUTH_HOOKS)
|
|
36
|
+
if (auth[hook] !== undefined && !isFunction(auth[hook]))
|
|
37
|
+
return `${hook} is not a function`;
|
|
38
|
+
if (
|
|
39
|
+
auth.signInPath !== undefined &&
|
|
40
|
+
(!isString(auth.signInPath) ||
|
|
41
|
+
!compilePathPattern(auth.signInPath) ||
|
|
42
|
+
auth.signInPath.includes("*"))
|
|
43
|
+
)
|
|
44
|
+
return "signInPath is not a page path";
|
|
45
|
+
return exemptionRejection(auth.signInExempt);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {unknown} signInExempt */
|
|
49
|
+
function exemptionRejection(signInExempt) {
|
|
50
|
+
if (signInExempt === undefined) return undefined;
|
|
51
|
+
if (!Array.isArray(signInExempt))
|
|
52
|
+
return `signInExempt is not a list of ${PATH_PATTERN_FORMS} patterns`;
|
|
53
|
+
for (const pattern of signInExempt)
|
|
54
|
+
if (!compilePathPattern(pattern))
|
|
55
|
+
return isString(pattern)
|
|
56
|
+
? `signInExempt holds "${pattern}", which is not ${PATH_PATTERN_FORMS}`
|
|
57
|
+
: "signInExempt holds an entry that is not a string";
|
|
58
|
+
return undefined;
|
|
21
59
|
}
|
|
22
60
|
|
|
23
61
|
/**
|
|
@@ -72,7 +110,7 @@ export function serverAuth(provider, context) {
|
|
|
72
110
|
* Renews the request's session after the backend refused it. Returns the
|
|
73
111
|
* new session, which the rest of the request runs with, or `undefined`.
|
|
74
112
|
*
|
|
75
|
-
* @param {import("
|
|
113
|
+
* @param {import("./request-target.mjs").ServerRequest} request
|
|
76
114
|
* @param {import("node:http").ServerResponse} response
|
|
77
115
|
*/
|
|
78
116
|
async refresh(request, response) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { compilePathPattern } from "./path-patterns.mjs";
|
|
2
|
-
import { isNonNullObject } from "./type-guards.mjs";
|
|
1
|
+
import { compilePathPattern, PATH_PATTERN_FORMS } from "./path-patterns.mjs";
|
|
2
|
+
import { isNonNullObject, isString } from "./type-guards.mjs";
|
|
3
3
|
|
|
4
4
|
/** @typedef {import("../server-module.ts").DmsBackendProxyOptions} ProxyOptions */
|
|
5
5
|
/**
|
|
@@ -53,12 +53,13 @@ function literalPrefix(pattern) {
|
|
|
53
53
|
/**
|
|
54
54
|
* Why a proxy declaration is ignored, or `undefined`.
|
|
55
55
|
*
|
|
56
|
-
* @param {string} pattern
|
|
56
|
+
* @param {string | undefined} pattern
|
|
57
57
|
* @param {ProxyOptions | undefined} options
|
|
58
58
|
*/
|
|
59
59
|
export function proxyRejection(pattern, options) {
|
|
60
60
|
const compiled = compilePathPattern(pattern);
|
|
61
|
-
if (!compiled
|
|
61
|
+
if (!compiled || !isString(pattern))
|
|
62
|
+
return `the pattern is not ${PATH_PATTERN_FORMS}`;
|
|
62
63
|
const prefix = literalPrefix(pattern);
|
|
63
64
|
if (prefix === "/")
|
|
64
65
|
return "its first segment is not literal: it would take every page";
|
|
@@ -81,15 +82,15 @@ export function proxyRejection(pattern, options) {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/**
|
|
84
|
-
* @param {
|
|
85
|
+
* @param {import("./path-patterns.mjs").CompiledPathPattern} compiled A pattern `proxyRejection` accepted
|
|
85
86
|
* @param {ProxyOptions | undefined} options
|
|
86
87
|
* @param {string} owner Who declared it
|
|
87
88
|
* @returns {BackendProxy}
|
|
88
89
|
*/
|
|
89
|
-
function compileProxy(
|
|
90
|
+
function compileProxy(compiled, options, owner) {
|
|
90
91
|
return {
|
|
91
|
-
pattern:
|
|
92
|
-
path: pattern,
|
|
92
|
+
pattern: compiled,
|
|
93
|
+
path: compiled.pattern,
|
|
93
94
|
owner,
|
|
94
95
|
auth: options?.auth ?? DEFAULT_AUTH,
|
|
95
96
|
maxBodyBytes: options?.maxBodyBytes,
|
|
@@ -122,15 +123,18 @@ export function resolveBackendProxies(declared, manifest, warn) {
|
|
|
122
123
|
* @param {string} owner
|
|
123
124
|
*/
|
|
124
125
|
const add = (pattern, options, owner) => {
|
|
125
|
-
const
|
|
126
|
+
const compiled = compilePathPattern(pattern);
|
|
126
127
|
const current = kept.get(pattern);
|
|
127
|
-
|
|
128
|
+
const reason =
|
|
129
|
+
proxyRejection(pattern, options) ??
|
|
130
|
+
(current && `${current.owner} declares it`);
|
|
131
|
+
if (!compiled || reason) {
|
|
128
132
|
warn(
|
|
129
|
-
`${owner} declares the backend proxy "${pattern}", which is ignored: ${reason
|
|
133
|
+
`${owner} declares the backend proxy "${pattern}", which is ignored: ${reason}.`,
|
|
130
134
|
);
|
|
131
135
|
return;
|
|
132
136
|
}
|
|
133
|
-
kept.set(pattern, compileProxy(
|
|
137
|
+
kept.set(pattern, compileProxy(compiled, options, owner));
|
|
134
138
|
};
|
|
135
139
|
for (const { id, pattern, options } of declared)
|
|
136
140
|
add(pattern, options, `Frontend module ${id}`);
|
|
@@ -1,6 +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
|
+
import { isObjectLike, isString } from "./type-guards.mjs";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* What a JSON caller gets when the backend fails: the generic i18n key of
|
|
@@ -45,7 +45,7 @@ async function refusalCode(response) {
|
|
|
45
45
|
try {
|
|
46
46
|
/** @type {unknown} */
|
|
47
47
|
const data = JSON.parse(text);
|
|
48
|
-
value =
|
|
48
|
+
value = isObjectLike(data) ? data.message : data;
|
|
49
49
|
} catch {}
|
|
50
50
|
return isString(value) && REFUSAL_CODE.test(value) ? value : undefined;
|
|
51
51
|
}
|
|
@@ -67,7 +67,7 @@ export async function backendResponseError(response) {
|
|
|
67
67
|
* qualifies: a refused destination sent to itself again would loop.
|
|
68
68
|
*
|
|
69
69
|
* @param {unknown} error What the request failed with
|
|
70
|
-
* @param {import("
|
|
70
|
+
* @param {import("./request-target.mjs").ServerRequest} request
|
|
71
71
|
* @param {import("node:http").ServerResponse} response
|
|
72
72
|
* @param {() => Promise<typeof import("../ssr-renderer.ts")>} loadRenderer
|
|
73
73
|
* @returns {Promise<boolean>} Whether the visit was redirected
|
|
@@ -13,7 +13,11 @@ const WILDCARD_ADDRESSES = ["0.0.0.0", "::"];
|
|
|
13
13
|
*/
|
|
14
14
|
async function report(message, line, write) {
|
|
15
15
|
if (!process.send) return write(line);
|
|
16
|
-
|
|
16
|
+
/** @type {Promise<void>} */
|
|
17
|
+
const sent = new Promise((resolve) =>
|
|
18
|
+
process.send?.(message, () => resolve()),
|
|
19
|
+
);
|
|
20
|
+
await sent;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -40,6 +44,23 @@ export async function reportReady(address, port) {
|
|
|
40
44
|
);
|
|
41
45
|
}
|
|
42
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
|
+
|
|
43
64
|
/**
|
|
44
65
|
* The server cannot listen: a port taken by another process, an address this
|
|
45
66
|
* host does not have. Reported instead of Node's unhandled-error dump, then
|
|
@@ -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;
|
|
@@ -11,17 +11,14 @@ 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.
|
|
18
|
-
*
|
|
19
|
-
* Implicit identity is a fallback, not a preference over compression.
|
|
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.
|
|
20
17
|
*
|
|
21
18
|
* @param {import("node:http").IncomingMessage} request
|
|
22
|
-
* @
|
|
19
|
+
* @returns {("br" | "gzip")[]}
|
|
23
20
|
*/
|
|
24
|
-
export function
|
|
21
|
+
export function acceptedCodings(request) {
|
|
25
22
|
const accepted = request.headers["accept-encoding"] ?? "";
|
|
26
23
|
/** @type {Map<string, number>} */
|
|
27
24
|
const qualities = new Map();
|
|
@@ -46,8 +43,24 @@ export function acceptedEncoder(request, { streaming = false } = {}) {
|
|
|
46
43
|
const br = quality("br");
|
|
47
44
|
const gzip = quality("gzip");
|
|
48
45
|
const identity = qualities.get("identity") ?? 0;
|
|
49
|
-
if (identity > Math.max(br, gzip)) return
|
|
50
|
-
|
|
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")
|
|
51
64
|
return {
|
|
52
65
|
encoding: "br",
|
|
53
66
|
stream: createBrotliCompress({
|
|
@@ -57,7 +70,7 @@ export function acceptedEncoder(request, { streaming = false } = {}) {
|
|
|
57
70
|
},
|
|
58
71
|
}),
|
|
59
72
|
};
|
|
60
|
-
if (
|
|
73
|
+
if (coding === "gzip")
|
|
61
74
|
return {
|
|
62
75
|
encoding: "gzip",
|
|
63
76
|
stream: createGzip(
|
|
@@ -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" });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fork } from "node:child_process";
|
|
2
2
|
import { constants, setPriority } from "node:os";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { isObjectLike } from "./type-guards.mjs";
|
|
4
5
|
|
|
5
6
|
const EMAIL_RENDERER_URL = new URL(
|
|
6
7
|
"../dist/server/email-renderer.js",
|
|
@@ -75,10 +76,11 @@ export function watchEmailBundle() {
|
|
|
75
76
|
cwd: fileURLToPath(new URL("..", import.meta.url)),
|
|
76
77
|
});
|
|
77
78
|
try {
|
|
78
|
-
|
|
79
|
+
if (builder.pid !== undefined)
|
|
80
|
+
setPriority(builder.pid, constants.priority.PRIORITY_BELOW_NORMAL);
|
|
79
81
|
} catch {}
|
|
80
82
|
builder.on("message", (message) => {
|
|
81
|
-
if (message
|
|
83
|
+
if (!isObjectLike(message) || message.state !== "building") {
|
|
82
84
|
emailBuildSettled();
|
|
83
85
|
return;
|
|
84
86
|
}
|
|
@@ -115,7 +117,7 @@ async function waitForEmailBuild() {
|
|
|
115
117
|
/** @returns {Promise<typeof import("../email-renderer.ts")>} */
|
|
116
118
|
function loadEmailRenderer() {
|
|
117
119
|
const url = new URL(EMAIL_RENDERER_URL);
|
|
118
|
-
if (emailBundleVersion) url.searchParams.set("v", emailBundleVersion);
|
|
120
|
+
if (emailBundleVersion) url.searchParams.set("v", String(emailBundleVersion));
|
|
119
121
|
return import(url.href);
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -115,6 +115,14 @@ function isManifestChunk(value) {
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* @param {unknown} value
|
|
120
|
+
* @returns {value is import("vite").Manifest}
|
|
121
|
+
*/
|
|
122
|
+
function isManifest(value) {
|
|
123
|
+
return isPlainRecord(value) && Object.values(value).every(isManifestChunk);
|
|
124
|
+
}
|
|
125
|
+
|
|
118
126
|
/**
|
|
119
127
|
* The manifest `vite build` wrote to `dist/client/.vite/manifest.json`.
|
|
120
128
|
*
|
|
@@ -123,7 +131,7 @@ function isManifestChunk(value) {
|
|
|
123
131
|
* @throws {Error} When it is not an object of chunks
|
|
124
132
|
*/
|
|
125
133
|
export function viteManifestOf(content) {
|
|
126
|
-
if (!
|
|
134
|
+
if (!isManifest(content))
|
|
127
135
|
throw new Error("the client manifest is not an object of Vite chunks");
|
|
128
136
|
return content;
|
|
129
137
|
}
|
|
@@ -132,8 +140,12 @@ export function viteManifestOf(content) {
|
|
|
132
140
|
* The members of an Iconify collection that `iconCollectionOf` checks, the
|
|
133
141
|
* ones an icon query and a server render read.
|
|
134
142
|
*
|
|
143
|
+
* @typedef {Parameters<typeof import("@iconify/utils").getIcons>[0]} Iconify
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
/**
|
|
135
147
|
* @typedef {Pick<
|
|
136
|
-
*
|
|
148
|
+
* Iconify,
|
|
137
149
|
* | "prefix"
|
|
138
150
|
* | "icons"
|
|
139
151
|
* | "aliases"
|
|
@@ -142,10 +154,8 @@ export function viteManifestOf(content) {
|
|
|
142
154
|
* | "top"
|
|
143
155
|
* | "width"
|
|
144
156
|
* | "height"
|
|
145
|
-
*
|
|
146
|
-
* | "hFlip"
|
|
147
|
-
* | "vFlip"
|
|
148
|
-
* >} IconCollection
|
|
157
|
+
* > &
|
|
158
|
+
* Pick<Iconify["icons"][string], "rotate" | "hFlip" | "vFlip">} IconCollection
|
|
149
159
|
*/
|
|
150
160
|
|
|
151
161
|
/**
|
|
@@ -104,12 +104,12 @@ async function loadCollection(root, prefix) {
|
|
|
104
104
|
*/
|
|
105
105
|
function collection(root, prefix) {
|
|
106
106
|
const key = join(root, prefix);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return
|
|
107
|
+
const cached = collections.get(key);
|
|
108
|
+
if (cached) return cached;
|
|
109
|
+
const loading = loadCollection(root, prefix);
|
|
110
|
+
loading.catch(() => collections.delete(key));
|
|
111
|
+
collections.set(key, loading);
|
|
112
|
+
return loading;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
@@ -128,7 +128,7 @@ export async function installedCollections(root) {
|
|
|
128
128
|
.filter((prefix) => ICON_NAME.test(prefix))
|
|
129
129
|
.map((prefix) => collection(root, prefix)),
|
|
130
130
|
);
|
|
131
|
-
return loaded.filter(
|
|
131
|
+
return loaded.filter((loadedCollection) => loadedCollection !== undefined);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
@@ -193,13 +193,17 @@ function requestedIcons(url) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
* Whether the generated server answers `pathname` with `iconResponse
|
|
196
|
+
* Whether the generated server answers `pathname` with `iconResponse`: the
|
|
197
|
+
* icon endpoint, with or without its trailing slash, and every path under it.
|
|
197
198
|
*
|
|
198
199
|
* @param {string} pathname
|
|
199
200
|
* @returns {boolean}
|
|
200
201
|
*/
|
|
201
202
|
export function isIconRequest(pathname) {
|
|
202
|
-
return
|
|
203
|
+
return (
|
|
204
|
+
pathname === ICON_API_PATH.slice(0, -1) ||
|
|
205
|
+
pathname.startsWith(ICON_API_PATH)
|
|
206
|
+
);
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
/**
|
|
@@ -214,7 +218,7 @@ export function isIconRequest(pathname) {
|
|
|
214
218
|
* @returns {Promise<IconAnswer>}
|
|
215
219
|
*/
|
|
216
220
|
export async function iconResponse(root, method, url) {
|
|
217
|
-
if (!READ_METHODS.has(method)) {
|
|
221
|
+
if (method === undefined || !READ_METHODS.has(method)) {
|
|
218
222
|
const response = errorResponse(405, "Method not allowed");
|
|
219
223
|
response.headers.allow = [...READ_METHODS].join(", ");
|
|
220
224
|
return response;
|
|
@@ -234,7 +238,7 @@ export async function iconResponse(root, method, url) {
|
|
|
234
238
|
* Serve an Iconify API query from the collections installed in the workspace,
|
|
235
239
|
* the same way under `ajs dms dev` and `ajs dms start`: both run this server.
|
|
236
240
|
*
|
|
237
|
-
* @param {import("
|
|
241
|
+
* @param {import("./request-target.mjs").ServerRequest} request
|
|
238
242
|
* @param {import("node:http").ServerResponse} response
|
|
239
243
|
*/
|
|
240
244
|
export async function handleIcons(request, response) {
|
|
@@ -44,7 +44,7 @@ function reusableReadKey(path, options, headers) {
|
|
|
44
44
|
* A read returns a copy: the render that read it may still change the object it
|
|
45
45
|
* got. A body that cannot be copied is read again.
|
|
46
46
|
*
|
|
47
|
-
* @param {() =>
|
|
47
|
+
* @param {() => ConstructorParameters<typeof Headers>[0]} backendHeaders
|
|
48
48
|
* @returns {ServerReads}
|
|
49
49
|
*/
|
|
50
50
|
export function serverReads(backendHeaders) {
|
|
@@ -90,7 +90,7 @@ export function serverReads(backendHeaders) {
|
|
|
90
90
|
* same path. Any other redirect keeps its own response.
|
|
91
91
|
*
|
|
92
92
|
* @param {import("node:http").IncomingMessage} request
|
|
93
|
-
* @param {string
|
|
93
|
+
* @param {string} from
|
|
94
94
|
* @param {string} location
|
|
95
95
|
* @returns {boolean}
|
|
96
96
|
*/
|
|
@@ -114,7 +114,7 @@ function rendersInPlace(request, from, location) {
|
|
|
114
114
|
* A page that completes its query as it renders is rendered at that query in
|
|
115
115
|
* the same request, rather than answered with a redirect.
|
|
116
116
|
*
|
|
117
|
-
* @template {{ url: string
|
|
117
|
+
* @template {{ url: string, props: unknown }} Page
|
|
118
118
|
* @template {{ redirect?: string }} Rendered
|
|
119
119
|
* @param {import("node:http").IncomingMessage} request
|
|
120
120
|
* @param {Page} page
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { isObjectLike, isString } from "./type-guards.mjs";
|
|
4
5
|
|
|
5
6
|
const CLIENT_MANIFEST_PATH = fileURLToPath(
|
|
6
7
|
new URL("../dist/client/.vite/manifest.json", import.meta.url),
|
|
@@ -53,14 +54,26 @@ export const DMS_PAGE_COMPONENT = "DmsDynamicPage";
|
|
|
53
54
|
* @typedef {{
|
|
54
55
|
* component: string,
|
|
55
56
|
* props: Record<string, unknown>,
|
|
56
|
-
* url: string
|
|
57
|
+
* url: string,
|
|
57
58
|
* version: string,
|
|
58
59
|
* onceProps?: OnceProps
|
|
59
60
|
* }} InertiaPage
|
|
60
61
|
*/
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
|
-
*
|
|
64
|
+
* Whether the page has the props the server render needs: the path and the
|
|
65
|
+
* page the backend described. A full visit carries both; the page payload is
|
|
66
|
+
* the backend's JSON, read as the renderer reads it.
|
|
67
|
+
*
|
|
68
|
+
* @param {InertiaPage} page
|
|
69
|
+
* @returns {page is InertiaPage & import("../ssr-renderer.ts").DmsSsrPage}
|
|
70
|
+
*/
|
|
71
|
+
export function isSsrPage(page) {
|
|
72
|
+
return isString(page.props.path) && isObjectLike(page.props.page);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} url
|
|
64
77
|
* @param {Record<string, unknown> | undefined} props
|
|
65
78
|
* @param {string} [version]
|
|
66
79
|
* @param {OnceProps} [onceProps]
|
|
@@ -72,6 +85,7 @@ export function createInertiaPage(
|
|
|
72
85
|
version = assetVersion(),
|
|
73
86
|
onceProps,
|
|
74
87
|
) {
|
|
88
|
+
/** @type {InertiaPage} */
|
|
75
89
|
const page = {
|
|
76
90
|
component: DMS_PAGE_COMPONENT,
|
|
77
91
|
props: { ...props, errors: props?.errors ?? {} },
|
|
@@ -180,7 +194,7 @@ export function isFrontendVisit(request) {
|
|
|
180
194
|
if (request.method !== "GET") return false;
|
|
181
195
|
return (
|
|
182
196
|
Boolean(request.headers["x-inertia"]) ||
|
|
183
|
-
request.headers.accept?.includes("text/html")
|
|
197
|
+
request.headers.accept?.includes("text/html") === true
|
|
184
198
|
);
|
|
185
199
|
}
|
|
186
200
|
|
|
@@ -25,13 +25,15 @@ import {
|
|
|
25
25
|
import { backendUrl } from "./request-target.mjs";
|
|
26
26
|
import {
|
|
27
27
|
isFunction,
|
|
28
|
-
isNonNullObject,
|
|
29
28
|
isNumber,
|
|
29
|
+
isObjectLike,
|
|
30
|
+
isRecord,
|
|
30
31
|
isString,
|
|
31
32
|
} from "./type-guards.mjs";
|
|
32
33
|
import { pipeWebBody } from "./web-body.mjs";
|
|
33
34
|
|
|
34
35
|
/** @typedef {import("node:http").IncomingMessage} IncomingMessage */
|
|
36
|
+
/** @typedef {import("./request-target.mjs").ServerRequest} ServerRequest */
|
|
35
37
|
/** @typedef {import("node:http").ServerResponse} ServerResponse */
|
|
36
38
|
/** @typedef {import("../server-module.ts").DmsServerEvent} ServerEvent */
|
|
37
39
|
/** @typedef {import("../server-module.ts").DmsSession} Session */
|
|
@@ -40,7 +42,7 @@ import { pipeWebBody } from "./web-body.mjs";
|
|
|
40
42
|
* What an event is made from.
|
|
41
43
|
*
|
|
42
44
|
* @typedef {object} EventContext
|
|
43
|
-
* @property {
|
|
45
|
+
* @property {ServerRequest} request
|
|
44
46
|
* @property {ServerResponse} response
|
|
45
47
|
* @property {Pick<import("./module-route-types.mjs").ServerRoute, "bodyLimit">} route
|
|
46
48
|
* @property {Readonly<Record<string, string>>} params
|
|
@@ -97,7 +99,7 @@ function limitedBody(request, limit) {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/**
|
|
100
|
-
* @param {
|
|
102
|
+
* @param {ServerRequest} request
|
|
101
103
|
* @param {EventContext["route"]} route
|
|
102
104
|
*/
|
|
103
105
|
function webRequest(request, route) {
|
|
@@ -124,9 +126,13 @@ async function readJson(webRequestValue) {
|
|
|
124
126
|
const chunks = [];
|
|
125
127
|
const reader = webRequestValue.body.getReader();
|
|
126
128
|
for (;;) {
|
|
127
|
-
const
|
|
128
|
-
if (done) break;
|
|
129
|
-
|
|
129
|
+
const read = await reader.read();
|
|
130
|
+
if (read.done) break;
|
|
131
|
+
/** @type {unknown} */
|
|
132
|
+
const chunk = read.value;
|
|
133
|
+
if (!(chunk instanceof Uint8Array))
|
|
134
|
+
throw new RequestBodyError(400, "Invalid request body");
|
|
135
|
+
chunks.push(chunk);
|
|
130
136
|
}
|
|
131
137
|
if (!chunks.length) return {};
|
|
132
138
|
try {
|
|
@@ -237,8 +243,16 @@ function frozenCopy(value) {
|
|
|
237
243
|
*/
|
|
238
244
|
export function createServerEvent(context) {
|
|
239
245
|
const { request, response, route, params } = context;
|
|
240
|
-
const
|
|
241
|
-
|
|
246
|
+
const webRequestValue = webRequest(request, route);
|
|
247
|
+
// SAFETY: `server-module.ts` lets a handler name the JSON shape it expects from the backend and from the client, as `Response.json()` does; the server parses that JSON and hands it on unchecked, which these two members are the only place to state.
|
|
248
|
+
const parsedJson = /** @type {Pick<ServerEvent, "backend" | "readJson">} */ ({
|
|
249
|
+
backend: (/** @type {string} */ path, init = {}) =>
|
|
250
|
+
backend(path, request, init),
|
|
251
|
+
readJson: () => readJson(webRequestValue),
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
...parsedJson,
|
|
255
|
+
request: webRequestValue,
|
|
242
256
|
params: Object.freeze({ ...params }),
|
|
243
257
|
session: sessionAccess(request, response, context.view),
|
|
244
258
|
cookies: cookieAccess(
|
|
@@ -247,8 +261,6 @@ export function createServerEvent(context) {
|
|
|
247
261
|
context.module.id,
|
|
248
262
|
context.authOwner,
|
|
249
263
|
),
|
|
250
|
-
backend: (/** @type {string} */ path, init = {}) =>
|
|
251
|
-
backend(path, request, init),
|
|
252
264
|
backendUrl: (/** @type {string} */ path) => backendUrl(path),
|
|
253
265
|
clientIp: () => clientIp(request),
|
|
254
266
|
module: Object.freeze({
|
|
@@ -266,19 +278,18 @@ export function createServerEvent(context) {
|
|
|
266
278
|
...(context.app?.authEstablishEndpoints ?? []),
|
|
267
279
|
]),
|
|
268
280
|
}),
|
|
281
|
+
url: new URL(webRequestValue.url),
|
|
269
282
|
};
|
|
270
|
-
event.url = new URL(event.request.url);
|
|
271
|
-
event.readJson = () => readJson(event.request);
|
|
272
|
-
return event;
|
|
273
283
|
}
|
|
274
284
|
|
|
275
285
|
/** @param {unknown} value */
|
|
276
286
|
function isResponse(value) {
|
|
277
287
|
return (
|
|
278
288
|
value instanceof Response ||
|
|
279
|
-
(
|
|
289
|
+
(isRecord(value) &&
|
|
280
290
|
isNumber(value.status) &&
|
|
281
|
-
|
|
291
|
+
isObjectLike(value.headers) &&
|
|
292
|
+
isFunction(value.headers.get) &&
|
|
282
293
|
"body" in value)
|
|
283
294
|
);
|
|
284
295
|
}
|
|
@@ -304,7 +315,7 @@ function asResponse(result) {
|
|
|
304
315
|
* on the frontend's origin when the frontend proxies its path, no cookie of
|
|
305
316
|
* another site its fetch reached.
|
|
306
317
|
*
|
|
307
|
-
* @param {
|
|
318
|
+
* @param {ServerRequest} request
|
|
308
319
|
* @param {ServerResponse} response
|
|
309
320
|
* @param {unknown} result
|
|
310
321
|
* @param {(path: string) => boolean} isProxiedPath
|
|
@@ -313,6 +324,7 @@ async function writeServerResponse(request, response, result, isProxiedPath) {
|
|
|
313
324
|
const answer = asResponse(result);
|
|
314
325
|
const cookies = [response.getHeader("set-cookie") ?? []]
|
|
315
326
|
.flat()
|
|
327
|
+
.map(String)
|
|
316
328
|
.concat(relayableCookies(answer));
|
|
317
329
|
answer.headers.forEach((value, name) => {
|
|
318
330
|
if (name === "set-cookie") return;
|
|
@@ -2,8 +2,8 @@ export {};
|
|
|
2
2
|
|
|
3
3
|
/** @typedef {import("../server-module.ts").DmsServerMethod} ServerMethod */
|
|
4
4
|
/**
|
|
5
|
-
* What `registerAuth` declared. The
|
|
6
|
-
*
|
|
5
|
+
* What `registerAuth` declared. The server binds each hook to the declared
|
|
6
|
+
* object, so a hook may use `this`.
|
|
7
7
|
*
|
|
8
8
|
* @typedef {object} AuthDeclaration
|
|
9
9
|
* @property {string} [signInPath]
|