@antelopejs/dms-frontend 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/cancellation.js +2 -1
- package/dist/config.js +11 -7
- package/dist/derived-outputs.js +6 -113
- package/dist/dev-modules.js +3 -2
- package/dist/discovery.js +19 -12
- package/dist/fs-sync.js +1 -2
- package/dist/internal/guards.d.ts +5 -0
- package/dist/internal/guards.js +22 -0
- package/dist/internal/locale-catalogs.d.ts +6 -0
- package/dist/internal/locale-catalogs.js +115 -0
- package/dist/layer-aliases.js +2 -1
- package/dist/layers.js +2 -1
- package/dist/locale-files.js +9 -4
- package/dist/manifest-secrets.js +11 -4
- package/dist/manifest.js +11 -5
- package/dist/materialize.js +6 -5
- package/dist/module-set.js +2 -4
- package/dist/ports.js +8 -1
- package/dist/private-values.js +26 -17
- package/dist/project-modules.js +17 -11
- package/dist/renderer-range.js +2 -1
- package/dist/server-process.js +17 -16
- package/dist/update-check.js +23 -16
- package/dist/verify-source-runner.js +2 -2
- package/dist/workspace-setup.js +1 -2
- package/dist/workspace.js +11 -10
- package/package.json +7 -4
- package/skills/dms-frontend/SKILL.md +1 -1
- package/skills/dms-frontend/cases.json +77 -0
- package/templates/vue/app-runtime.ts +88 -7
- package/templates/vue/client-chunking.mjs +0 -5
- package/templates/vue/client-server-guard.mjs +10 -13
- package/templates/vue/entry-weight.mjs +12 -14
- package/templates/vue/frontend-module.ts +159 -3563
- package/templates/vue/frontend-runtime/async-data-entry.ts +320 -0
- package/templates/vue/frontend-runtime/async-data.ts +404 -0
- package/templates/vue/frontend-runtime/components.ts +386 -0
- package/templates/vue/frontend-runtime/cookies.ts +125 -0
- package/templates/vue/frontend-runtime/data-access.ts +132 -0
- package/templates/vue/frontend-runtime/dms-error.ts +11 -0
- package/templates/vue/frontend-runtime/errors.ts +145 -0
- package/templates/vue/frontend-runtime/fetch.ts +306 -0
- package/templates/vue/frontend-runtime/module-registration.ts +247 -0
- package/templates/vue/frontend-runtime/paged-data.ts +191 -0
- package/templates/vue/frontend-runtime/payload.ts +172 -0
- package/templates/vue/frontend-runtime/route.ts +328 -0
- package/templates/vue/frontend-runtime/sdk.ts +300 -0
- package/templates/vue/frontend-runtime/state.ts +167 -0
- package/templates/vue/frontend-runtime/types.ts +361 -0
- package/templates/vue/frontend-runtime/ui.ts +247 -0
- package/templates/vue/globals.d.ts +5 -0
- package/templates/vue/head-order.mjs +11 -13
- package/templates/vue/icon-bundle.ts +31 -0
- package/templates/vue/message-subset.ts +65 -0
- package/templates/vue/network-error.ts +4 -0
- package/templates/vue/prepare-types.mjs +0 -8
- package/templates/vue/server/auth/backend.mjs +4 -2
- package/templates/vue/server/auth/establish-endpoints.mjs +14 -9
- package/templates/vue/server/auth/session.mjs +4 -3
- package/templates/vue/server/auth-hooks.mjs +6 -6
- package/templates/vue/server/backend-proxies.mjs +6 -6
- package/templates/vue/server/backend-response.mjs +15 -14
- package/templates/vue/server/built-files.mjs +10 -8
- package/templates/vue/server/cli-report.mjs +7 -5
- package/templates/vue/server/client-error.mjs +11 -8
- package/templates/vue/server/client-manifest.mjs +9 -8
- package/templates/vue/server/content.mjs +2 -1
- package/templates/vue/server/dev-private-files.mjs +20 -15
- package/templates/vue/server/dev-styles.mjs +22 -25
- package/templates/vue/server/email-bundle-watch.mjs +0 -12
- package/templates/vue/server/email.mjs +20 -15
- package/templates/vue/server/favicon.mjs +12 -11
- package/templates/vue/server/homepage.mjs +8 -10
- package/templates/vue/server/icons.mjs +18 -9
- package/templates/vue/server/in-place-render.mjs +6 -8
- package/templates/vue/server/listen-host.mjs +2 -2
- package/templates/vue/server/module-event.mjs +5 -5
- package/templates/vue/server/module-routes.mjs +11 -7
- package/templates/vue/server/module-server.mjs +8 -7
- package/templates/vue/server/module-testing.mjs +7 -7
- package/templates/vue/server/page-props.mjs +14 -14
- package/templates/vue/server/path-patterns.mjs +4 -4
- package/templates/vue/server/proxy-compression.mjs +21 -13
- package/templates/vue/server/proxy-headers.mjs +5 -3
- package/templates/vue/server/proxy-redirects.mjs +7 -6
- package/templates/vue/server/proxy-transfers.mjs +15 -11
- package/templates/vue/server/public-endpoints.mjs +27 -19
- package/templates/vue/server/public-options.mjs +9 -7
- package/templates/vue/server/request-failure.mjs +4 -6
- package/templates/vue/server/request-routing.mjs +8 -11
- package/templates/vue/server/request-target.mjs +6 -6
- package/templates/vue/server/route-rules.mjs +7 -5
- package/templates/vue/server/static-files.mjs +19 -10
- package/templates/vue/server/web-body.mjs +3 -4
- package/templates/vue/server.mjs +32 -19
- package/templates/vue/sfc-global-types.mjs +27 -24
- package/templates/vue/ssr-renderer.ts +10 -1
- package/templates/vue/ui-inertia-page.mjs +7 -7
- package/templates/vue/ui-kbd.mjs +10 -10
- package/templates/vue/ui-lazy-icons.mjs +44 -0
- package/templates/vue/ui-request-state.mjs +9 -9
- package/templates/vue/vite.config.ts +2 -0
|
@@ -2,18 +2,23 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { json } from "./auth/backend.mjs";
|
|
3
3
|
import { CROSS_ORIGIN_ERROR, isSameOrigin } from "./auth/client-ip.mjs";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Written at the workspace root by the loader, from the endpoints each
|
|
7
|
+
* backend module declared in the frontend manifest. This file sits one
|
|
8
|
+
* directory up.
|
|
9
|
+
*/
|
|
7
10
|
const DECLARED_ENDPOINTS_FILE = new URL(
|
|
8
11
|
"../generated-public-endpoints.json",
|
|
9
12
|
import.meta.url,
|
|
10
13
|
);
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* An absolute backend API path whose first segment is literal (a `*` there
|
|
17
|
+
* would open a route of every module) and whose next ones are literal or `*`
|
|
18
|
+
* (exactly one segment): no prefix match, no query string, no segment that
|
|
19
|
+
* climbs out. Kept in step with `PUBLIC_PATH` in the DMS
|
|
20
|
+
* (`frontend-public-endpoints.ts`), which validates the modules' declarations.
|
|
21
|
+
*/
|
|
17
22
|
const PUBLIC_PATH =
|
|
18
23
|
/^\/api\/[A-Za-z0-9][A-Za-z0-9._~-]*(?:\/(?:\*|[A-Za-z0-9][A-Za-z0-9._~-]*))*$/;
|
|
19
24
|
const SEGMENT_WILDCARD = "*";
|
|
@@ -27,9 +32,11 @@ const ALLOWED_METHODS = new Set([
|
|
|
27
32
|
"DELETE",
|
|
28
33
|
]);
|
|
29
34
|
const DEFAULT_METHODS = ["POST"];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
/**
|
|
36
|
+
* A declared header is a module's own `x-` header. Those the proxy, a
|
|
37
|
+
* reverse proxy or the backend give a meaning to are never taken from the
|
|
38
|
+
* caller. Kept in step with the DMS's `RESERVED_HEADER`.
|
|
39
|
+
*/
|
|
33
40
|
const HEADER_NAME = /^x-[a-z0-9-]+$/;
|
|
34
41
|
const RESERVED_HEADER =
|
|
35
42
|
/^x-(?:forwarded-|real-ip$|dms-|inertia|http-method|method-override|original-|rewrite-)/;
|
|
@@ -56,6 +63,8 @@ export function readDeclaredPublicEndpoints(file = DECLARED_ENDPOINTS_FILE) {
|
|
|
56
63
|
* The backend proxies the backend modules declared in the manifest, as
|
|
57
64
|
* `{ module, path, auth?, maxBodyBytes? }`, unchecked.
|
|
58
65
|
*
|
|
66
|
+
* A missing or broken file declares no proxy.
|
|
67
|
+
*
|
|
59
68
|
* @param file Generated file to read
|
|
60
69
|
*/
|
|
61
70
|
export function readDeclaredBackendProxies(file = DECLARED_ENDPOINTS_FILE) {
|
|
@@ -65,16 +74,15 @@ export function readDeclaredBackendProxies(file = DECLARED_ENDPOINTS_FILE) {
|
|
|
65
74
|
try {
|
|
66
75
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
67
76
|
if (Array.isArray(parsed?.proxies)) proxies = parsed.proxies;
|
|
68
|
-
} catch {
|
|
69
|
-
// No file, or a broken one: no proxy declared.
|
|
70
|
-
}
|
|
77
|
+
} catch {}
|
|
71
78
|
if (file === DECLARED_ENDPOINTS_FILE) declaredProxies = proxies;
|
|
72
79
|
return proxies;
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
/**
|
|
76
83
|
* The backend proxies `DMS_BACKEND_PROXIES` adds: a JSON array of
|
|
77
|
-
* `{ path, auth?, maxBodyBytes? }`, as the manifest's `backendProxies`, for an
|
|
84
|
+
* `{ path, auth?, maxBodyBytes? }`, as the manifest's `backendProxies`, for an
|
|
85
|
+
* operator. A value that is not a JSON array is reported and ignored.
|
|
78
86
|
*
|
|
79
87
|
* @param declaration The variable's value
|
|
80
88
|
*/
|
|
@@ -89,9 +97,7 @@ export function environmentBackendProxies(
|
|
|
89
97
|
...entry,
|
|
90
98
|
module: "DMS_BACKEND_PROXIES",
|
|
91
99
|
}));
|
|
92
|
-
} catch {
|
|
93
|
-
// Reported below.
|
|
94
|
-
}
|
|
100
|
+
} catch {}
|
|
95
101
|
console.warn("Ignoring DMS_BACKEND_PROXIES: not a JSON array");
|
|
96
102
|
return [];
|
|
97
103
|
}
|
|
@@ -180,8 +186,10 @@ export function allowedPublicEndpoints(
|
|
|
180
186
|
);
|
|
181
187
|
}
|
|
182
188
|
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Compiled once per value of the environment variable, so a malformed entry
|
|
191
|
+
* is reported once rather than on every request.
|
|
192
|
+
*/
|
|
185
193
|
let compiled;
|
|
186
194
|
|
|
187
195
|
function currentPublicEndpoints() {
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// The modules' public options the running server serves: computed by
|
|
2
|
-
// `ajs dms start` from the project's configuration and handed over in
|
|
3
|
-
// `DMS_PUBLIC_OPTIONS`, a JSON object of options by module id. A build never
|
|
4
|
-
// holds them, so a deployment's variables reach the dashboard without a
|
|
5
|
-
// rebuild. Unset (`ajs dms dev`), the registry's options stand.
|
|
6
|
-
|
|
7
1
|
const VARIABLE = "DMS_PUBLIC_OPTIONS";
|
|
8
2
|
export const PUBLIC_OPTIONS_ELEMENT_ID = "dms-public-options";
|
|
9
3
|
|
|
@@ -22,7 +16,15 @@ function parse(raw) {
|
|
|
22
16
|
}
|
|
23
17
|
}
|
|
24
18
|
|
|
25
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The options by module id the server runs with, undefined when unset.
|
|
21
|
+
*
|
|
22
|
+
* The modules' public options the running server serves: computed by `ajs
|
|
23
|
+
* dms start` from the project's configuration and handed over in
|
|
24
|
+
* `DMS_PUBLIC_OPTIONS`, a JSON object of options by module id. A build never
|
|
25
|
+
* holds them, so a deployment's variables reach the dashboard without a
|
|
26
|
+
* rebuild. Unset (`ajs dms dev`), the registry's options stand.
|
|
27
|
+
*/
|
|
26
28
|
export const RUNTIME_PUBLIC_OPTIONS = parse(process.env[VARIABLE]);
|
|
27
29
|
|
|
28
30
|
/** The options of each module of the registry, the runtime ones first. */
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// Answers for requests the server cannot serve at all: an unreadable URL, or a
|
|
2
|
-
// failure while it was already rendering the error page.
|
|
3
|
-
//
|
|
4
|
-
// Lives apart from server.mjs so the file stays under the size the linter
|
|
5
|
-
// allows.
|
|
6
|
-
|
|
7
1
|
const FRONTEND_ORIGIN = "http://frontend.local";
|
|
8
2
|
const TEXT_TYPE = "text/plain; charset=utf-8";
|
|
9
3
|
|
|
@@ -33,6 +27,10 @@ export function isResponseGone(error, response) {
|
|
|
33
27
|
);
|
|
34
28
|
}
|
|
35
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Answers for requests the server cannot serve at all: an unreadable URL, or
|
|
32
|
+
* a failure while it was already rendering the error page.
|
|
33
|
+
*/
|
|
36
34
|
export function writeBadRequest(response) {
|
|
37
35
|
response.writeHead(400, { "content-type": TEXT_TYPE });
|
|
38
36
|
response.end("Bad Request");
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
// Who answers a request: one owner per path, whoever asks and whatever the
|
|
2
|
-
// method, in this order:
|
|
3
|
-
//
|
|
4
|
-
// 1. the renderer: its icon endpoint, Vite's URLs and the built assets, and
|
|
5
|
-
// any file it has (a development source, a built asset);
|
|
6
|
-
// 2. the module routes of the modules' `dms.server.ts`;
|
|
7
|
-
// 3. the backend proxies the modules declare (`registerBackendProxy`, the
|
|
8
|
-
// manifest's `backendProxies`);
|
|
9
|
-
// 4. the pages, for everything else: a method other than `GET` and `HEAD` on a
|
|
10
|
-
// page path is a `404`.
|
|
11
|
-
|
|
12
1
|
import { matchBackendProxy } from "./backend-proxies.mjs";
|
|
13
2
|
import { isRendererFilePath, moduleRouteMethods } from "./module-routes.mjs";
|
|
14
3
|
|
|
@@ -17,6 +6,14 @@ const PAGE_METHODS = new Set(["GET", "HEAD"]);
|
|
|
17
6
|
/**
|
|
18
7
|
* The owner of a request.
|
|
19
8
|
*
|
|
9
|
+
* Who answers a request: one owner per path, whoever asks and whatever the
|
|
10
|
+
* method, in this order: 1. the renderer: its icon endpoint, Vite's URLs and
|
|
11
|
+
* the built assets, and any file it has (a development source, a built asset);
|
|
12
|
+
* 2. the module routes of the modules' `dms.server.ts`; 3. the backend proxies
|
|
13
|
+
* the modules declare (`registerBackendProxy`, the manifest's
|
|
14
|
+
* `backendProxies`); 4. the pages, for everything else: a method other than
|
|
15
|
+
* `GET` and `HEAD` on a page path is a `404`.
|
|
16
|
+
*
|
|
20
17
|
* @param method The request's method
|
|
21
18
|
* @param pathname The request's normalized path
|
|
22
19
|
* @param modules What `loadServerModules` returned: `routes` and `proxies`
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { writeBadRequest } from "./request-failure.mjs";
|
|
2
2
|
|
|
3
|
-
// The request target as the server reads it. Node hands `request.url` over
|
|
4
|
-
// verbatim, so it is not always a path: `//host/x`, `/\host/x` and the
|
|
5
|
-
// absolute form `http://host/x` all name another host once resolved, which
|
|
6
|
-
// turned the proxy into an open one. Every routing decision and the URL sent to
|
|
7
|
-
// the backend read the same normalized path from here.
|
|
8
|
-
|
|
9
3
|
const FRONTEND_ORIGIN = "http://frontend.local";
|
|
10
4
|
|
|
11
5
|
/**
|
|
@@ -45,6 +39,12 @@ export function backendUrl(
|
|
|
45
39
|
/**
|
|
46
40
|
* Answers 400 to a request whose target is not origin-form, before any route
|
|
47
41
|
* reads it. Returns whether it answered.
|
|
42
|
+
*
|
|
43
|
+
* The request target as the server reads it. Node hands `request.url` over
|
|
44
|
+
* verbatim, so it is not always a path: `//host/x`, `/\host/x` and the absolute
|
|
45
|
+
* form `http://host/x` all name another host once resolved, which turned the
|
|
46
|
+
* proxy into an open one. Every routing decision and the URL sent to the
|
|
47
|
+
* backend read the same normalized path from here.
|
|
48
48
|
*/
|
|
49
49
|
export function refuseForeignTarget(request, response) {
|
|
50
50
|
if (isOriginForm(request.url)) return false;
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// Route rules the modules declare in their `dms.server.ts`: how a page is
|
|
2
|
-
// rendered and which headers its document and Inertia answers carry. They
|
|
3
|
-
// change rendering and caching, never who may see a page.
|
|
4
|
-
|
|
5
1
|
import { compilePathPattern } from "./path-patterns.mjs";
|
|
6
2
|
|
|
7
3
|
const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9a-z-]+$/;
|
|
@@ -60,7 +56,13 @@ function cacheRejection(cache) {
|
|
|
60
56
|
return undefined;
|
|
61
57
|
}
|
|
62
58
|
|
|
63
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Why a rule is ignored, or `undefined`.
|
|
61
|
+
*
|
|
62
|
+
* Route rules the modules declare in their `dms.server.ts`: how a page is
|
|
63
|
+
* rendered and which headers its document and Inertia answers carry. They
|
|
64
|
+
* change rendering and caching, never who may see a page.
|
|
65
|
+
*/
|
|
64
66
|
export function ruleRejection(pattern, rule) {
|
|
65
67
|
if (!compilePathPattern(pattern))
|
|
66
68
|
return "the pattern is not /x, /x/* or /x/**";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
2
2
|
import { extname } from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* What a build or a module's `public/` directory may hold. A type missing
|
|
6
|
+
* here went out as `application/octet-stream`: fonts among them, which a
|
|
7
|
+
* browser then reports as a mismatch against the `font/woff2` it preloaded.
|
|
8
|
+
*/
|
|
7
9
|
const MIME_TYPES = {
|
|
8
10
|
".avif": "image/avif",
|
|
9
11
|
".css": "text/css",
|
|
@@ -32,12 +34,16 @@ const MIME_TYPES = {
|
|
|
32
34
|
".xml": "application/xml",
|
|
33
35
|
};
|
|
34
36
|
const DEFAULT_MIME_TYPE = "application/octet-stream";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Vite names everything under `assets/` after its content: a new build
|
|
39
|
+
* writes new names, so a client never needs to ask again.
|
|
40
|
+
*/
|
|
37
41
|
const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Any other file keeps its name across builds (a module's `public/` file, a
|
|
44
|
+
* favicon): the client asks again on each use, and is answered 304 while it
|
|
45
|
+
* has not changed.
|
|
46
|
+
*/
|
|
41
47
|
const REVALIDATED_CACHE = "no-cache";
|
|
42
48
|
|
|
43
49
|
const isFile = (path) => existsSync(path) && statSync(path).isFile();
|
|
@@ -62,7 +68,11 @@ function entityTag(stats) {
|
|
|
62
68
|
|
|
63
69
|
const opaqueTag = (tag) => tag.trim().replace(/^W\//, "");
|
|
64
70
|
|
|
65
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* RFC 9110 §13.1.2 and §13.1.3: `If-None-Match` first, the date otherwise.
|
|
73
|
+
*
|
|
74
|
+
* The header has a one-second resolution.
|
|
75
|
+
*/
|
|
66
76
|
function isNotModified(request, tag, stats) {
|
|
67
77
|
const noneMatch = request.headers["if-none-match"];
|
|
68
78
|
if (noneMatch !== undefined) {
|
|
@@ -73,7 +83,6 @@ function isNotModified(request, tag, stats) {
|
|
|
73
83
|
.some((candidate) => opaqueTag(candidate) === wanted);
|
|
74
84
|
}
|
|
75
85
|
const since = Date.parse(request.headers["if-modified-since"] ?? "");
|
|
76
|
-
// The header has a one-second resolution.
|
|
77
86
|
return (
|
|
78
87
|
!Number.isNaN(since) && Math.floor(stats.mtimeMs / 1000) * 1000 <= since
|
|
79
88
|
);
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// Writes a Web-standard body, a backend answer above all, to a Node response.
|
|
2
|
-
//
|
|
3
|
-
// Lives apart from the proxy and the module routes, which both send one.
|
|
4
|
-
|
|
5
1
|
import { Readable } from "node:stream";
|
|
6
2
|
import { pipeline } from "node:stream/promises";
|
|
7
3
|
|
|
@@ -12,6 +8,9 @@ import { pipeline } from "node:stream/promises";
|
|
|
12
8
|
* listener, and its next failure (the backend closing, or undici's body
|
|
13
9
|
* timeout on an event stream gone quiet) ended the whole server. The body is
|
|
14
10
|
* cancelled instead, which closes its backend connection too.
|
|
11
|
+
*
|
|
12
|
+
* Writes a Web-standard body, a backend answer above all, to a Node response.
|
|
13
|
+
* It lives apart from the proxy and the module routes, which both send one.
|
|
15
14
|
*/
|
|
16
15
|
export async function pipeWebBody(body, ...streams) {
|
|
17
16
|
const response = streams.at(-1);
|
package/templates/vue/server.mjs
CHANGED
|
@@ -82,17 +82,26 @@ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
|
82
82
|
const LOCAL_ROUTES = [[icons.isIconRequest, icons.handleIcons]];
|
|
83
83
|
const CLIENT_ROOT = join(PROJECT_ROOT, "dist/client");
|
|
84
84
|
const SOURCE_TEMPLATE_PATH = join(PROJECT_ROOT, "index.html");
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
/**
|
|
86
|
+
* A URL, not a path: on Windows, `import()` reads `C:\...` as a URL whose
|
|
87
|
+
* scheme is `c:` and refuses it.
|
|
88
|
+
*/
|
|
87
89
|
const BUILT_SSR_RENDERER_URL = new URL(
|
|
88
90
|
"dist/ssr/ssr-renderer.js",
|
|
89
91
|
import.meta.url,
|
|
90
92
|
);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The SSR bundle keeps vue-i18n external, so Node loads its esm-bundler build,
|
|
96
|
+
* which reads compile-time flags only a bundler replaces. Vite inlines them in
|
|
97
|
+
* the client bundle; without the same value here, installing vue-i18n under
|
|
98
|
+
* NODE_ENV=production throws a ReferenceError on the first render.
|
|
99
|
+
*/
|
|
100
|
+
function defineVueCompileFlags() {
|
|
101
|
+
globalThis.__VUE_PROD_DEVTOOLS__ ??= false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
defineVueCompileFlags();
|
|
96
105
|
let productionSsrRenderer;
|
|
97
106
|
let vite;
|
|
98
107
|
let vitePromise;
|
|
@@ -105,7 +114,6 @@ async function developmentServer() {
|
|
|
105
114
|
.then(({ createServer: createViteServer }) => {
|
|
106
115
|
const serverOptions = {
|
|
107
116
|
middlewareMode: true,
|
|
108
|
-
// The generated secrets and the modules' server code, never served.
|
|
109
117
|
fs: { deny: DEV_FS_DENY },
|
|
110
118
|
};
|
|
111
119
|
if (frontendHttpServer) serverOptions.ws = { server: frontendHttpServer };
|
|
@@ -206,6 +214,16 @@ function redirectSignedOutPrivatePage(request, response, props, auth) {
|
|
|
206
214
|
* Sends a request to the backend through the proxy it reached.
|
|
207
215
|
* `isProxiedPath` says which backend paths a relayed `Location` may name on
|
|
208
216
|
* the frontend's origin.
|
|
217
|
+
*
|
|
218
|
+
* A client that left while the request was routed closed the response already,
|
|
219
|
+
* and would never abort the request below.
|
|
220
|
+
*
|
|
221
|
+
* Cancelling the body alone misses a client that leaves before the backend
|
|
222
|
+
* answers: its stream, an SSE feed above all, then stayed open for good.
|
|
223
|
+
*
|
|
224
|
+
* A backend redirect is the browser's to follow: Media sends a file's link to
|
|
225
|
+
* the storage's signed URL, on the public origin the server itself may not
|
|
226
|
+
* reach, which turned every download into a 502.
|
|
209
227
|
*/
|
|
210
228
|
async function proxy(request, response, backendProxy, isProxiedPath) {
|
|
211
229
|
const access = proxyAccess(request, SAFE_METHODS, backendProxy);
|
|
@@ -213,11 +231,7 @@ async function proxy(request, response, backendProxy, isProxiedPath) {
|
|
|
213
231
|
refuseCrossOrigin(response);
|
|
214
232
|
return true;
|
|
215
233
|
}
|
|
216
|
-
// A client that left while the request was routed closed the response
|
|
217
|
-
// already, and would never abort the request below.
|
|
218
234
|
if (response.closed) return;
|
|
219
|
-
// Cancelling the body alone misses a client that leaves before the backend
|
|
220
|
-
// answers: its stream, an SSE feed above all, then stayed open for good.
|
|
221
235
|
const abandoned = new AbortController();
|
|
222
236
|
response.once("close", () => abandoned.abort());
|
|
223
237
|
const init = {
|
|
@@ -225,9 +239,6 @@ async function proxy(request, response, backendProxy, isProxiedPath) {
|
|
|
225
239
|
method: request.method,
|
|
226
240
|
headers: { ...backendHeaders(request, access), ...PLAIN_ANSWER_HEADERS },
|
|
227
241
|
duplex: "half",
|
|
228
|
-
// A backend redirect is the browser's to follow: Media sends a file's
|
|
229
|
-
// link to the storage's signed URL, on the public origin the server
|
|
230
|
-
// itself may not reach, which turned every download into a 502.
|
|
231
242
|
redirect: "manual",
|
|
232
243
|
};
|
|
233
244
|
const upstream = await sendWithinLimit(
|
|
@@ -409,11 +420,12 @@ async function answeredByVite(devServer, request, response) {
|
|
|
409
420
|
* Whether the renderer has a file at `pathname`: a development source Vite
|
|
410
421
|
* serves (under the workspace or its `public/`), or a built asset the
|
|
411
422
|
* production server serves. Only a path with an extension names one.
|
|
423
|
+
*
|
|
424
|
+
* The path is decoded as Vite and the asset server read it: `%2E` is a dot.
|
|
412
425
|
*/
|
|
413
426
|
function hasRendererFile(devServer, requested) {
|
|
414
427
|
let pathname;
|
|
415
428
|
try {
|
|
416
|
-
// As Vite and the asset server read it: `%2E` is a dot.
|
|
417
429
|
pathname = decodeURIComponent(requested);
|
|
418
430
|
} catch {
|
|
419
431
|
return false;
|
|
@@ -519,12 +531,12 @@ async function writeRequestFailure(error, request, response) {
|
|
|
519
531
|
await writeBackendError(error, request, response);
|
|
520
532
|
}
|
|
521
533
|
|
|
534
|
+
/** An error page failing too must not reject: that would end the process. */
|
|
522
535
|
export async function handleRequestSafely(request, response) {
|
|
523
536
|
try {
|
|
524
537
|
if (refuseForeignTarget(request, response)) return;
|
|
525
538
|
await handleRequest(request, response);
|
|
526
539
|
} catch (error) {
|
|
527
|
-
// An error page failing too must not reject: that would end the process.
|
|
528
540
|
await writeRequestFailure(error, request, response).catch((failure) =>
|
|
529
541
|
requestFailure.abandonResponse(failure, response),
|
|
530
542
|
);
|
|
@@ -564,6 +576,9 @@ async function reportRouting() {
|
|
|
564
576
|
* Report the server ready. In development Vite would otherwise start with the
|
|
565
577
|
* first request, so it is started here: "ready" then means the first page is
|
|
566
578
|
* served without that wait.
|
|
579
|
+
*
|
|
580
|
+
* Only the production build produces the email bundle otherwise. The watcher
|
|
581
|
+
* starts once the server answers, so it never delays the first page.
|
|
567
582
|
*/
|
|
568
583
|
async function announceReady() {
|
|
569
584
|
try {
|
|
@@ -575,7 +590,5 @@ async function announceReady() {
|
|
|
575
590
|
const { address, port } = frontendHttpServer.address();
|
|
576
591
|
await reportReady(address, port);
|
|
577
592
|
await reportRouting();
|
|
578
|
-
// Only the production build produces the email bundle otherwise. Started
|
|
579
|
-
// once the server answers, so it never delays the first page.
|
|
580
593
|
if (process.env.DMS_DEV === "true") watchEmailBundle();
|
|
581
594
|
}
|
|
@@ -1,24 +1,12 @@
|
|
|
1
|
-
// Vue's SFC compiler turns `defineProps<Props>()` into runtime prop types, and
|
|
2
|
-
// it reads a type from the component's own imports only. A type a module
|
|
3
|
-
// auto-imports (its declared `app/types`, `app/composables`…, and Vue's own
|
|
4
|
-
// `Ref`, `Component`…) is visible to TypeScript through auto-imports.d.ts but
|
|
5
|
-
// not to the compiler, which drops it from the prop without a word:
|
|
6
|
-
// `boolean | RowActionConfig` became `{ type: Boolean }` in a production build.
|
|
7
|
-
//
|
|
8
|
-
// The compiler does read ambient declarations from `script.globalTypeFiles`,
|
|
9
|
-
// but not the `export type { … } from` re-exports auto-imports.d.ts is made
|
|
10
|
-
// of. This writes those types as ambient aliases it can follow, from the same
|
|
11
|
-
// directories and through the same scanner as the auto-imports, before Vite
|
|
12
|
-
// compiles anything, and rewrites them when a dev server sees a module's
|
|
13
|
-
// declared directories change.
|
|
14
|
-
|
|
15
1
|
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
2
|
import { createRequire } from "node:module";
|
|
17
3
|
import { pathToFileURL } from "node:url";
|
|
18
4
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The very unimport the auto-import plugin scans with, so both read the
|
|
7
|
+
* directories the same way. Imported by URL, not by path: on Windows,
|
|
8
|
+
* `import()` reads `C:\...` as a URL whose scheme is `c:` and refuses it.
|
|
9
|
+
*/
|
|
22
10
|
const unimportUrl = pathToFileURL(
|
|
23
11
|
createRequire(
|
|
24
12
|
createRequire(import.meta.url).resolve("unplugin-auto-import/package.json"),
|
|
@@ -41,6 +29,19 @@ const withoutExtension = (path) => path.replace(/\.[cm]?[jt]sx?$/, "");
|
|
|
41
29
|
* the last of two imports of the same name. A module that exports its own
|
|
42
30
|
* `Component` thus shadows Vue's, for the compiler as for TypeScript.
|
|
43
31
|
*
|
|
32
|
+
* Vue's SFC compiler turns `defineProps<Props>()` into runtime prop types and
|
|
33
|
+
* reads a type from the component's own imports only. A type a module auto-
|
|
34
|
+
* imports (its declared `app/types`, `app/composables`..., and Vue's own `Ref`,
|
|
35
|
+
* `Component`...) is visible to TypeScript through auto-imports.d.ts but not to
|
|
36
|
+
* the compiler, which drops it from the prop without a word: `boolean |
|
|
37
|
+
* RowActionConfig` became `{ type: Boolean }` in a production build. The
|
|
38
|
+
* compiler does read ambient declarations from `script.globalTypeFiles`, but
|
|
39
|
+
* not the `export type { ... } from` re-exports auto-imports.d.ts is made of.
|
|
40
|
+
* These types are therefore written as ambient aliases it can follow, from the
|
|
41
|
+
* same directories and through the same scanner as the auto-imports, before
|
|
42
|
+
* Vite compiles anything, and rewritten when a dev server sees a module's
|
|
43
|
+
* declared directories change.
|
|
44
|
+
*
|
|
44
45
|
* @param {(string | { glob: string, types?: boolean })[]} dirs the auto-import
|
|
45
46
|
* directories, as handed to the auto-import plugin
|
|
46
47
|
* @param {string} cwd the workspace
|
|
@@ -61,6 +62,11 @@ export async function sfcGlobalTypes(dirs, cwd) {
|
|
|
61
62
|
return `${HEADER}${[...aliases.values()].join("\n")}\n`;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Written through a temporary file: a build starting meanwhile reads the
|
|
67
|
+
* previous declarations or the new ones, never a half-written file the
|
|
68
|
+
* compiler would then keep in its cache.
|
|
69
|
+
*/
|
|
64
70
|
function writeIfChanged(file, content) {
|
|
65
71
|
let current;
|
|
66
72
|
try {
|
|
@@ -69,9 +75,6 @@ function writeIfChanged(file, content) {
|
|
|
69
75
|
current = undefined;
|
|
70
76
|
}
|
|
71
77
|
if (current === content) return false;
|
|
72
|
-
// Through a temporary file: a build starting meanwhile reads the previous
|
|
73
|
-
// declarations or the new ones, never a half-written file the compiler
|
|
74
|
-
// would then keep in its cache.
|
|
75
78
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
76
79
|
writeFileSync(temporary, content);
|
|
77
80
|
renameSync(temporary, file);
|
|
@@ -87,6 +90,10 @@ export async function writeSfcGlobalTypes(dirs, cwd, file) {
|
|
|
87
90
|
* Keeps the declarations in step on a dev server: a type file added, renamed
|
|
88
91
|
* or removed rewrites them, and the Vue plugin reloads the components whose
|
|
89
92
|
* props read the file.
|
|
93
|
+
*
|
|
94
|
+
* The file is under the workspace, which Vite watches: rewriting it is the
|
|
95
|
+
* change the Vue plugin reloads the dependent components on. A failed scan or
|
|
96
|
+
* write keeps the previous declarations, since it must not end the dev server.
|
|
90
97
|
*/
|
|
91
98
|
export function sfcGlobalTypesWatcher(dirs, cwd, file) {
|
|
92
99
|
return {
|
|
@@ -98,10 +105,6 @@ export function sfcGlobalTypesWatcher(dirs, cwd, file) {
|
|
|
98
105
|
if (!/\.[cm]?[jt]sx?$/.test(path) || path === file) return;
|
|
99
106
|
clearTimeout(pending);
|
|
100
107
|
pending = setTimeout(() => {
|
|
101
|
-
// The file is under the workspace, which Vite watches: rewriting it
|
|
102
|
-
// is the change the Vue plugin reloads the dependent components on.
|
|
103
|
-
// A failed scan or write keeps the previous declarations: it must
|
|
104
|
-
// not end the dev server.
|
|
105
108
|
sfcGlobalTypes(dirs, cwd)
|
|
106
109
|
.then((content) => writeIfChanged(file, content))
|
|
107
110
|
.catch((error) =>
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
trackDmsServerScopes,
|
|
31
31
|
withPublicOptions,
|
|
32
32
|
} from "./frontend-module";
|
|
33
|
+
import { type DmsMessageSubset, SSR_MESSAGES_ID } from "./message-subset";
|
|
33
34
|
import { parsePublicOptions } from "./public-options";
|
|
34
35
|
import { frontendModules } from "./frontend-modules.generated";
|
|
35
36
|
import {
|
|
@@ -129,6 +130,12 @@ function asyncComponentsScript(names: Set<string>): string {
|
|
|
129
130
|
return `<script id="${SSR_ASYNC_COMPONENTS_ID}" type="application/json">${value}</script>`;
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
function messagesScript(subset: DmsMessageSubset | undefined): string {
|
|
134
|
+
if (!subset) return "";
|
|
135
|
+
const value = JSON.stringify(subset).replaceAll("<", "\\u003c");
|
|
136
|
+
return `<script id="${SSR_MESSAGES_ID}" type="application/json">${value}</script>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
132
139
|
function iconsScript(names: Set<string>): string {
|
|
133
140
|
const value = JSON.stringify(serializeDmsIcons(names)).replaceAll(
|
|
134
141
|
"<",
|
|
@@ -195,6 +202,7 @@ async function renderDmsPageWithRuntime(
|
|
|
195
202
|
let iconSymbols = new Map<string, string>();
|
|
196
203
|
let payload: DmsPayload = { data: {}, errors: {}, state: {} };
|
|
197
204
|
let catalogLocale: (() => string | undefined) | undefined;
|
|
205
|
+
let messageSubset: (() => DmsMessageSubset | undefined) | undefined;
|
|
198
206
|
const inertiaResult = await createInertiaApp({
|
|
199
207
|
page,
|
|
200
208
|
// Read as the HTML is complete: data a run settles afterwards was never
|
|
@@ -221,6 +229,7 @@ async function renderDmsPageWithRuntime(
|
|
|
221
229
|
serverFetch,
|
|
222
230
|
});
|
|
223
231
|
catalogLocale = configured.catalogLocale;
|
|
232
|
+
messageSubset = configured.messageSubset;
|
|
224
233
|
return app;
|
|
225
234
|
},
|
|
226
235
|
});
|
|
@@ -255,7 +264,7 @@ async function renderDmsPageWithRuntime(
|
|
|
255
264
|
const renderedHead = await renderSSRHead(head);
|
|
256
265
|
return {
|
|
257
266
|
error: page.props.error,
|
|
258
|
-
body: `${inertiaResult.body}${dmsIconSprite(iconSymbols)}${payloadScript(payload)}${asyncComponentsScript(asyncComponents)}${iconsScript(icons)}`,
|
|
267
|
+
body: `${inertiaResult.body}${dmsIconSprite(iconSymbols)}${payloadScript(payload)}${asyncComponentsScript(asyncComponents)}${iconsScript(icons)}${messagesScript(messageSubset?.())}`,
|
|
259
268
|
head: {
|
|
260
269
|
bodyAttrs: renderedHead.bodyAttrs,
|
|
261
270
|
headTags: `${inertiaResult.head.join("")}${renderedHead.headTags}`,
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
// Nuxt UI reads Inertia's page (`usePage()`) for its links and route stub.
|
|
2
|
-
// Inertia's page is module state, shared by the concurrent requests of a
|
|
3
|
-
// server: a render could read another request's URL. Nuxt UI's files read
|
|
4
|
-
// the DMS's per-request page instead, and the build fails if one reads
|
|
5
|
-
// Inertia's in a form this rewrite does not know, rather than letting the
|
|
6
|
-
// leak come back unnoticed with a Nuxt UI release.
|
|
7
|
-
|
|
8
1
|
const INERTIA_PAGE_IMPORT =
|
|
9
2
|
/import\s*\{\s*usePage\s*\}\s*from\s*["']@inertiajs\/vue3["']/g;
|
|
10
3
|
const DMS_PAGE_IMPORT =
|
|
@@ -16,6 +9,13 @@ const ANY_INERTIA_PAGE_IMPORT =
|
|
|
16
9
|
/**
|
|
17
10
|
* The rewrite, as a Vite plugin.
|
|
18
11
|
*
|
|
12
|
+
* Nuxt UI reads Inertia's page (`usePage()`) for its links and route stub.
|
|
13
|
+
* Inertia's page is module state, shared by the concurrent requests of a
|
|
14
|
+
* server: a render could read another request's URL. Nuxt UI's files read the
|
|
15
|
+
* DMS's per-request page instead, and the build fails if one reads Inertia's in
|
|
16
|
+
* a form this rewrite does not know, rather than letting the leak come back
|
|
17
|
+
* unnoticed with a Nuxt UI release.
|
|
18
|
+
*
|
|
19
19
|
* @param isNuxtUiFile `(id) => boolean`, whether a module is Nuxt UI's
|
|
20
20
|
*/
|
|
21
21
|
export function uiInertiaPage(isNuxtUiFile) {
|
package/templates/vue/ui-kbd.mjs
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
// Nuxt UI prints the `meta`, `ctrl` and `alt` of a key hint (`UKbd`, the
|
|
2
|
-
// `kbds` of its menus) from a map every hint shares, blank until the first
|
|
3
|
-
// hint mounts: a hint hydrating after that, in an async component, printed
|
|
4
|
-
// "⌘" or "Ctrl" over the server's blank, a hydration mismatch on every
|
|
5
|
-
// platform. Its `useKbd` is routed to the DMS runtime's `useDmsKbd`, which
|
|
6
|
-
// keeps each hint on the server's value until it mounts and lets the
|
|
7
|
-
// application give the platform to the server render
|
|
8
|
-
// (`provideDmsKbdPlatform`). The build fails if Nuxt UI changes the form
|
|
9
|
-
// rewritten here, rather than letting the mismatch come back unnoticed.
|
|
10
|
-
|
|
11
1
|
/** Nuxt UI's key hint composable. */
|
|
12
2
|
const USE_KBD = /\/@nuxt\/ui\/dist\/runtime\/composables\/useKbd\.js$/;
|
|
13
3
|
/** Its export as Nuxt UI 4 writes it: one composable shared by every hint. */
|
|
@@ -21,6 +11,16 @@ const ROUTED_EXPORT =
|
|
|
21
11
|
/**
|
|
22
12
|
* The rewrite, as a Vite plugin.
|
|
23
13
|
*
|
|
14
|
+
* Nuxt UI prints the `meta`, `ctrl` and `alt` of a key hint (`UKbd`, the `kbds`
|
|
15
|
+
* of its menus) from a map every hint shares, blank until the first hint
|
|
16
|
+
* mounts: a hint hydrating after that, in an async component, printed "⌘" or
|
|
17
|
+
* "Ctrl" over the server's blank, a hydration mismatch on every platform. Its
|
|
18
|
+
* `useKbd` is routed to the DMS runtime's `useDmsKbd`, which keeps each hint on
|
|
19
|
+
* the server's value until it mounts and lets the application give the platform
|
|
20
|
+
* to the server render (`provideDmsKbdPlatform`). The build fails if Nuxt UI
|
|
21
|
+
* changes the form rewritten here, rather than letting the mismatch come back
|
|
22
|
+
* unnoticed.
|
|
23
|
+
*
|
|
24
24
|
* @param isNuxtUiFile `(id) => boolean`, whether a module is Nuxt UI's
|
|
25
25
|
*/
|
|
26
26
|
export function uiKbd(isNuxtUiFile) {
|