@antelopejs/dms-frontend 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/commands/build-action.d.ts +8 -5
- package/dist/commands/build-action.js +16 -41
- package/dist/commands/build.js +9 -15
- package/dist/commands/dev-action.d.ts +2 -0
- package/dist/commands/dev-action.js +14 -3
- package/dist/commands/dev.js +3 -1
- package/dist/commands/start-action.d.ts +2 -0
- package/dist/commands/start-action.js +65 -47
- package/dist/commands/start.js +8 -6
- package/dist/common.d.ts +1 -0
- package/dist/common.js +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/derived-outputs.d.ts +1 -5
- package/dist/derived-outputs.js +48 -35
- package/dist/dev-modules.d.ts +5 -0
- package/dist/dev-modules.js +88 -0
- package/dist/entry-weight.d.ts +24 -0
- package/dist/entry-weight.js +86 -0
- package/dist/fs-sync.d.ts +1 -0
- package/dist/fs-sync.js +6 -0
- package/dist/help.js +12 -4
- package/dist/index.js +4 -6
- package/dist/layer-watch.js +7 -2
- package/dist/layers.d.ts +2 -3
- package/dist/layers.js +1 -95
- package/dist/locale-files.d.ts +6 -0
- package/dist/locale-files.js +75 -0
- package/dist/manifest.d.ts +8 -8
- package/dist/manifest.js +17 -39
- package/dist/materialize.js +6 -0
- package/dist/module-set.d.ts +14 -12
- package/dist/module-set.js +39 -31
- package/dist/private-values.d.ts +3 -2
- package/dist/private-values.js +16 -5
- package/dist/project-modules-child.d.ts +13 -0
- package/dist/project-modules-child.js +26 -0
- package/dist/project-modules.d.ts +47 -0
- package/dist/project-modules.js +268 -0
- package/dist/workspace-setup.d.ts +3 -7
- package/dist/workspace-setup.js +79 -62
- package/dist/workspace.d.ts +8 -4
- package/dist/workspace.js +1 -1
- package/package.json +13 -8
- package/skills/dms-frontend/SKILL.md +2 -2
- package/templates/vue/app-runtime.ts +8 -1
- package/templates/vue/client-chunking.mjs +61 -0
- package/templates/vue/email-renderer.ts +6 -1
- package/templates/vue/entry-weight.mjs +132 -0
- package/templates/vue/frontend-build-loader.ts +1 -1
- package/templates/vue/frontend-module.ts +168 -1
- package/templates/vue/gitignore +1 -0
- package/templates/vue/main.ts +32 -2
- package/templates/vue/network-error.ts +3 -1
- package/templates/vue/package.json +3 -3
- package/templates/vue/patches/@vueuse__core@14.4.0.patch +24 -0
- package/templates/vue/pnpm-workspace.yaml +5 -0
- package/templates/vue/prepare-types.mjs +2 -7
- package/templates/vue/public-options.ts +19 -0
- package/templates/vue/server/auth/backend.mjs +20 -3
- package/templates/vue/server/auth/client-ip.mjs +64 -22
- package/templates/vue/server/auth/session.mjs +4 -11
- package/templates/vue/server/backend-proxies.mjs +13 -2
- package/templates/vue/server/backend-response.mjs +2 -13
- package/templates/vue/server/cli-report.mjs +42 -18
- package/templates/vue/server/client-manifest.mjs +34 -58
- package/templates/vue/server/dev-private-files.mjs +1 -1
- package/templates/vue/server/dev-styles.mjs +16 -5
- package/templates/vue/server/homepage.mjs +4 -1
- package/templates/vue/server/in-place-render.mjs +110 -0
- package/templates/vue/server/module-event.mjs +23 -8
- package/templates/vue/server/module-server.mjs +4 -4
- package/templates/vue/server/module-testing.mjs +5 -3
- package/templates/vue/server/page-props.mjs +10 -0
- package/templates/vue/server/proxy-compression.mjs +15 -8
- package/templates/vue/server/proxy-headers.mjs +12 -1
- package/templates/vue/server/proxy-redirects.mjs +66 -0
- package/templates/vue/server/proxy-transfers.mjs +110 -0
- package/templates/vue/server/public-endpoints.mjs +2 -2
- package/templates/vue/server/public-options.mjs +49 -0
- package/templates/vue/server/request-routing.mjs +20 -0
- package/templates/vue/server/web-body.mjs +24 -0
- package/templates/vue/server-module.ts +19 -7
- package/templates/vue/server.mjs +98 -78
- package/templates/vue/sfc-global-types.mjs +119 -0
- package/templates/vue/ssr-renderer.ts +27 -1
- package/templates/vue/ui-kbd.mjs +45 -0
- package/templates/vue/vite.config.ts +104 -47
- package/templates/vue/vite.email.config.ts +6 -3
- package/templates/vue/vite.server.config.ts +5 -2
- package/dist/commands/snapshot-action.d.ts +0 -6
- package/dist/commands/snapshot-action.js +0 -53
- package/dist/commands/snapshot.d.ts +0 -2
- package/dist/commands/snapshot.js +0 -55
- package/dist/snapshot.d.ts +0 -25
- package/dist/snapshot.js +0 -90
- package/templates/vue/server/backend-wake.mjs +0 -129
|
@@ -29,8 +29,8 @@ export interface DmsSession {
|
|
|
29
29
|
refreshToken?: string;
|
|
30
30
|
accountId?: string;
|
|
31
31
|
activeTenantId?: string;
|
|
32
|
-
/**
|
|
33
|
-
persistent
|
|
32
|
+
/** True when the session outlives the browser (30 days), false when it ends with it. */
|
|
33
|
+
persistent: boolean;
|
|
34
34
|
/** Stamped by the frontend server on every write. */
|
|
35
35
|
updatedAt?: number;
|
|
36
36
|
[key: string]: unknown;
|
|
@@ -84,8 +84,8 @@ export interface DmsServerEvent {
|
|
|
84
84
|
*/
|
|
85
85
|
view(): DmsPublicSession;
|
|
86
86
|
/**
|
|
87
|
-
* Writes the session cookie, stamped with `updatedAt
|
|
88
|
-
*
|
|
87
|
+
* Writes the session cookie, stamped with `updatedAt`. It lasts 30 days
|
|
88
|
+
* when `persistent` is true and ends with the browser when it is false.
|
|
89
89
|
*/
|
|
90
90
|
write(session: DmsSession): void;
|
|
91
91
|
clear(): void;
|
|
@@ -112,16 +112,21 @@ export interface DmsServerEvent {
|
|
|
112
112
|
/**
|
|
113
113
|
* Calls the backend, JSON in and out. A refusal throws an error carrying
|
|
114
114
|
* the status and the backend's public `error.*` message, which the server
|
|
115
|
-
* answers as is when the handler lets it through.
|
|
115
|
+
* answers as is when the handler lets it through. A redirect is not
|
|
116
|
+
* followed: it throws a 502.
|
|
116
117
|
*/
|
|
117
118
|
backend<T = unknown>(path: string, init?: DmsBackendRequest): Promise<T>;
|
|
118
119
|
/**
|
|
119
120
|
* The backend URL of an absolute path, for a call `backend` cannot make (a
|
|
120
121
|
* raw response, no relayed header): always on the backend's origin,
|
|
121
|
-
* whatever the path holds.
|
|
122
|
+
* whatever the path holds. Fetch it with `redirect: "manual"` to relay a
|
|
123
|
+
* backend redirect: the browser then follows it.
|
|
122
124
|
*/
|
|
123
125
|
backendUrl(path: string): URL;
|
|
124
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* The client's address, from the `DMS_CLIENT_IP_HEADER` header or behind
|
|
128
|
+
* `DMS_TRUSTED_PROXY_HOPS` proxies.
|
|
129
|
+
*/
|
|
125
130
|
clientIp(): string;
|
|
126
131
|
/** This module, and only this one. */
|
|
127
132
|
module: {
|
|
@@ -273,6 +278,13 @@ export interface DmsBackendProxyOptions {
|
|
|
273
278
|
* needed.
|
|
274
279
|
*/
|
|
275
280
|
auth?: "session" | "none";
|
|
281
|
+
/**
|
|
282
|
+
* The largest request body, in bytes, the server streams to the backend.
|
|
283
|
+
* A larger declared `Content-Length` gets a `413` before any byte is read;
|
|
284
|
+
* a chunked body is cut off with a `413` as soon as it goes over. No limit
|
|
285
|
+
* by default: the backend applies its own.
|
|
286
|
+
*/
|
|
287
|
+
maxBodyBytes?: number;
|
|
276
288
|
}
|
|
277
289
|
|
|
278
290
|
export type DmsServerSetup = (server: DmsServer) => void;
|
package/templates/vue/server.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { createServer, maxHeaderSize } from "node:http";
|
|
3
3
|
import { extname, join, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { ofetch } from "ofetch";
|
|
6
5
|
import {
|
|
6
|
+
CLIENT_HEADERS,
|
|
7
7
|
relayedHeaders,
|
|
8
8
|
RequestBodyError,
|
|
9
9
|
UpstreamError,
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
redirectAccessRefusal,
|
|
18
18
|
UNEXPECTED_ERROR_BODY,
|
|
19
19
|
} from "./server/backend-response.mjs";
|
|
20
|
-
import { fetchWaking } from "./server/backend-wake.mjs";
|
|
21
20
|
import { handleClientError } from "./server/client-error.mjs";
|
|
22
21
|
import {
|
|
23
22
|
reportListenError,
|
|
@@ -26,7 +25,10 @@ import {
|
|
|
26
25
|
} from "./server/cli-report.mjs";
|
|
27
26
|
import { publicBuildFile } from "./server/built-files.mjs";
|
|
28
27
|
import { serveStaticFile } from "./server/static-files.mjs";
|
|
29
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
localeCatalogPreloadTag,
|
|
30
|
+
productionHtmlTemplate,
|
|
31
|
+
} from "./server/client-manifest.mjs";
|
|
30
32
|
import { writeContent } from "./server/content.mjs";
|
|
31
33
|
import {
|
|
32
34
|
captureViteErrors,
|
|
@@ -37,7 +39,11 @@ import { documentStyleTags } from "./server/dev-styles.mjs";
|
|
|
37
39
|
import { watchEmailBundle } from "./server/email.mjs";
|
|
38
40
|
import { withFaviconPlaceholder } from "./server/favicon.mjs";
|
|
39
41
|
import { DEV_FS_DENY, denyPrivateFiles } from "./server/dev-private-files.mjs";
|
|
40
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
ownerLabel,
|
|
44
|
+
reachesBackend,
|
|
45
|
+
requestOwner,
|
|
46
|
+
} from "./server/request-routing.mjs";
|
|
41
47
|
import { routingReport } from "./server/backend-proxies.mjs";
|
|
42
48
|
import { routeRuleFor, ruleHeaders } from "./server/route-rules.mjs";
|
|
43
49
|
import {
|
|
@@ -45,7 +51,9 @@ import {
|
|
|
45
51
|
loadServerModules,
|
|
46
52
|
} from "./server/module-server.mjs";
|
|
47
53
|
import { HOMEPAGE } from "./server/homepage.mjs";
|
|
48
|
-
import {
|
|
54
|
+
import { publicOptionsScript } from "./server/public-options.mjs";
|
|
55
|
+
import { renderInPlace, serverReads } from "./server/in-place-render.mjs";
|
|
56
|
+
import { loadPageProps, propsForQuery } from "./server/page-props.mjs";
|
|
49
57
|
import { htmlTag } from "./server/html-tag.mjs";
|
|
50
58
|
import { listenHost } from "./server/listen-host.mjs";
|
|
51
59
|
import * as icons from "./server/icons.mjs";
|
|
@@ -55,6 +63,7 @@ import {
|
|
|
55
63
|
relayAnswer,
|
|
56
64
|
} from "./server/proxy-compression.mjs";
|
|
57
65
|
import { backendUrl, refuseForeignTarget } from "./server/request-target.mjs";
|
|
66
|
+
import { sendWithinLimit, SERVER_OPTIONS } from "./server/proxy-transfers.mjs";
|
|
58
67
|
import {
|
|
59
68
|
createInertiaPage,
|
|
60
69
|
htmlHeaders,
|
|
@@ -73,7 +82,12 @@ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
|
73
82
|
const LOCAL_ROUTES = [[icons.isIconRequest, icons.handleIcons]];
|
|
74
83
|
const CLIENT_ROOT = join(PROJECT_ROOT, "dist/client");
|
|
75
84
|
const SOURCE_TEMPLATE_PATH = join(PROJECT_ROOT, "index.html");
|
|
76
|
-
|
|
85
|
+
// A URL, not a path: on Windows, `import()` reads `C:\...` as a URL whose
|
|
86
|
+
// scheme is `c:` and refuses it.
|
|
87
|
+
const BUILT_SSR_RENDERER_URL = new URL(
|
|
88
|
+
"dist/ssr/ssr-renderer.js",
|
|
89
|
+
import.meta.url,
|
|
90
|
+
);
|
|
77
91
|
// The SSR bundle keeps vue-i18n external, so Node loads its esm-bundler build,
|
|
78
92
|
// which reads compile-time flags only a bundler replaces. Vite inlines them in
|
|
79
93
|
// the client bundle; without the same value here, installing vue-i18n under
|
|
@@ -94,8 +108,7 @@ async function developmentServer() {
|
|
|
94
108
|
// The generated secrets and the modules' server code, never served.
|
|
95
109
|
fs: { deny: DEV_FS_DENY },
|
|
96
110
|
};
|
|
97
|
-
if (frontendHttpServer)
|
|
98
|
-
serverOptions.hmr = { server: frontendHttpServer };
|
|
111
|
+
if (frontendHttpServer) serverOptions.ws = { server: frontendHttpServer };
|
|
99
112
|
return createViteServer({
|
|
100
113
|
server: serverOptions,
|
|
101
114
|
appType: "custom",
|
|
@@ -127,9 +140,7 @@ function backendHeaders(request, access = { anonymous: false, headers: [] }) {
|
|
|
127
140
|
"if-match",
|
|
128
141
|
"origin",
|
|
129
142
|
"referer",
|
|
130
|
-
|
|
131
|
-
"x-content-language",
|
|
132
|
-
"x-realtime-session",
|
|
143
|
+
...CLIENT_HEADERS,
|
|
133
144
|
...access.headers,
|
|
134
145
|
]),
|
|
135
146
|
...forwardedHeaders(request),
|
|
@@ -141,24 +152,13 @@ function backendHeaders(request, access = { anonymous: false, headers: [] }) {
|
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
function serverComponentFetch(request) {
|
|
144
|
-
return (
|
|
145
|
-
const headers = new Headers(backendHeaders(request));
|
|
146
|
-
new Headers(options.headers).forEach((value, name) => {
|
|
147
|
-
headers.set(name, value);
|
|
148
|
-
});
|
|
149
|
-
return ofetch(path, {
|
|
150
|
-
...options,
|
|
151
|
-
baseURL: process.env.DMS_API_BASE_URL,
|
|
152
|
-
headers,
|
|
153
|
-
});
|
|
154
|
-
};
|
|
155
|
+
return serverReads(() => backendHeaders(request));
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
async function backendJson(path, request) {
|
|
158
|
-
const response = await
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
);
|
|
159
|
+
const response = await fetch(new URL(path, process.env.DMS_API_BASE_URL), {
|
|
160
|
+
headers: backendHeaders(request),
|
|
161
|
+
});
|
|
162
162
|
if (!response.ok) {
|
|
163
163
|
throw await backendResponseError(response);
|
|
164
164
|
}
|
|
@@ -170,18 +170,18 @@ async function currentAuth() {
|
|
|
170
170
|
return (await loadServerModules(await developmentServer())).serverAuth;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function pageBackend(request) {
|
|
174
|
+
return {
|
|
175
|
+
fetchJson: (path) => backendJson(path, request),
|
|
176
|
+
isFrontendPage: async (pathname) =>
|
|
177
|
+
(await ssrRenderer(await developmentServer())).isDmsFrontendPage(
|
|
178
|
+
pathname,
|
|
179
|
+
),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
173
183
|
function pageProps(request, auth) {
|
|
174
|
-
return loadPageProps(
|
|
175
|
-
request,
|
|
176
|
-
{
|
|
177
|
-
fetchJson: (path) => backendJson(path, request),
|
|
178
|
-
isFrontendPage: async (pathname) =>
|
|
179
|
-
(await ssrRenderer(await developmentServer())).isDmsFrontendPage(
|
|
180
|
-
pathname,
|
|
181
|
-
),
|
|
182
|
-
},
|
|
183
|
-
auth,
|
|
184
|
-
);
|
|
184
|
+
return loadPageProps(request, pageBackend(request), auth);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
function redirectToHomepage(request, response, pathname) {
|
|
@@ -202,25 +202,45 @@ function redirectSignedOutPrivatePage(request, response, props, auth) {
|
|
|
202
202
|
return true;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
/**
|
|
206
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Sends a request to the backend through the proxy it reached.
|
|
207
|
+
* `isProxiedPath` says which backend paths a relayed `Location` may name on
|
|
208
|
+
* the frontend's origin.
|
|
209
|
+
*/
|
|
210
|
+
async function proxy(request, response, backendProxy, isProxiedPath) {
|
|
207
211
|
const access = proxyAccess(request, SAFE_METHODS, backendProxy);
|
|
208
212
|
if (access.isRefused) {
|
|
209
213
|
refuseCrossOrigin(response);
|
|
210
214
|
return true;
|
|
211
215
|
}
|
|
216
|
+
// A client that left while the request was routed closed the response
|
|
217
|
+
// already, and would never abort the request below.
|
|
218
|
+
if (response.closed) return;
|
|
212
219
|
// Cancelling the body alone misses a client that leaves before the backend
|
|
213
220
|
// answers: its stream, an SSE feed above all, then stayed open for good.
|
|
214
221
|
const abandoned = new AbortController();
|
|
215
222
|
response.once("close", () => abandoned.abort());
|
|
216
|
-
const
|
|
223
|
+
const init = {
|
|
217
224
|
signal: abandoned.signal,
|
|
218
225
|
method: request.method,
|
|
219
226
|
headers: { ...backendHeaders(request, access), ...PLAIN_ANSWER_HEADERS },
|
|
220
|
-
body: ["GET", "HEAD"].includes(request.method) ? undefined : request,
|
|
221
227
|
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
|
+
redirect: "manual",
|
|
232
|
+
};
|
|
233
|
+
const upstream = await sendWithinLimit(
|
|
234
|
+
request,
|
|
235
|
+
response,
|
|
236
|
+
backendProxy.maxBodyBytes,
|
|
237
|
+
(body) => fetch(backendUrl(request.url), { ...init, body }),
|
|
238
|
+
);
|
|
239
|
+
if (!upstream) return true;
|
|
240
|
+
await relayAnswer(request, response, upstream, {
|
|
241
|
+
fetched: true,
|
|
242
|
+
isProxiedPath,
|
|
222
243
|
});
|
|
223
|
-
await relayAnswer(request, response, upstream, { fetched: true });
|
|
224
244
|
}
|
|
225
245
|
|
|
226
246
|
/**
|
|
@@ -240,7 +260,7 @@ function serveAsset(pathname, request, response) {
|
|
|
240
260
|
async function ssrRenderer(devServer) {
|
|
241
261
|
const renderer = devServer
|
|
242
262
|
? await devServer.ssrLoadModule("/ssr-renderer.ts")
|
|
243
|
-
: await (productionSsrRenderer ??= import(
|
|
263
|
+
: await (productionSsrRenderer ??= import(BUILT_SSR_RENDERER_URL.href));
|
|
244
264
|
await icons.provideIconCollections(renderer);
|
|
245
265
|
return renderer;
|
|
246
266
|
}
|
|
@@ -250,6 +270,7 @@ function clientRenderedShell(page) {
|
|
|
250
270
|
body: inertiaAppHtml(page),
|
|
251
271
|
head: { headTags: "", htmlAttrs: "", bodyAttrs: "" },
|
|
252
272
|
overlays: "",
|
|
273
|
+
modules: [],
|
|
253
274
|
};
|
|
254
275
|
}
|
|
255
276
|
|
|
@@ -290,7 +311,10 @@ export async function renderHtml(
|
|
|
290
311
|
: await serverRendered(page, serverFetch, requestCookies, devServer);
|
|
291
312
|
if (rendered.redirect)
|
|
292
313
|
return { html: "", status: 200, redirect: rendered.redirect };
|
|
293
|
-
const
|
|
314
|
+
const headEnd =
|
|
315
|
+
(await documentStyleTags(devServer, rendered.modules, template)) +
|
|
316
|
+
(devServer ? "" : localeCatalogPreloadTag(rendered.catalogLocale)) +
|
|
317
|
+
publicOptionsScript();
|
|
294
318
|
const html = template
|
|
295
319
|
.replace("<title>Antelope DMS</title>", () =>
|
|
296
320
|
withFaviconPlaceholder(rendered.head.headTags),
|
|
@@ -299,7 +323,7 @@ export async function renderHtml(
|
|
|
299
323
|
htmlTag(attributes, rendered.head.htmlAttrs),
|
|
300
324
|
)
|
|
301
325
|
.replace("<body", `<body ${rendered.head.bodyAttrs}`)
|
|
302
|
-
.replace("</head>", `${
|
|
326
|
+
.replace("</head>", `${headEnd}</head>`)
|
|
303
327
|
.replace(
|
|
304
328
|
'<div id="dms-overlays" class="isolate"></div>',
|
|
305
329
|
`<div id="dms-overlays" class="isolate">${rendered.overlays}</div>`,
|
|
@@ -308,13 +332,28 @@ export async function renderHtml(
|
|
|
308
332
|
return { html, status: rendered.error?.statusCode ?? 200 };
|
|
309
333
|
}
|
|
310
334
|
|
|
311
|
-
function renderRequestHtml(page, request, rule = {}) {
|
|
312
|
-
const fetch = serverComponentFetch(request);
|
|
335
|
+
function renderRequestHtml(page, request, rule = {}, reads) {
|
|
336
|
+
const fetch = reads ?? serverComponentFetch(request);
|
|
313
337
|
return renderHtml(page, request.url, fetch, request.headers.cookie, {
|
|
314
338
|
ssr: rule.ssr,
|
|
315
339
|
});
|
|
316
340
|
}
|
|
317
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Renders a page document, at the query the page completes as it renders:
|
|
344
|
+
* see `renderInPlace`.
|
|
345
|
+
*/
|
|
346
|
+
function renderDocument(page, request, rule, auth) {
|
|
347
|
+
const fetch = serverComponentFetch(request);
|
|
348
|
+
return renderInPlace(
|
|
349
|
+
request,
|
|
350
|
+
page,
|
|
351
|
+
fetch,
|
|
352
|
+
(served) => renderRequestHtml(served, request, rule, fetch),
|
|
353
|
+
(props, url) => propsForQuery(props, url, pageBackend(request), auth),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
318
357
|
async function writeBackendError(error, request, response) {
|
|
319
358
|
const auth = await currentAuth();
|
|
320
359
|
const status = Number.isInteger(error?.status) ? error.status : 502;
|
|
@@ -332,9 +371,6 @@ async function writeBackendError(error, request, response) {
|
|
|
332
371
|
if (await redirectAccessRefusal(error, request, response, loadRenderer))
|
|
333
372
|
return;
|
|
334
373
|
console.error("DMS backend request failed", error);
|
|
335
|
-
const retryHeaders = error?.retryAfter
|
|
336
|
-
? { "retry-after": error.retryAfter }
|
|
337
|
-
: {};
|
|
338
374
|
if (isFrontendVisit(request)) {
|
|
339
375
|
const props = {
|
|
340
376
|
path: pathname,
|
|
@@ -348,24 +384,18 @@ async function writeBackendError(error, request, response) {
|
|
|
348
384
|
request,
|
|
349
385
|
response,
|
|
350
386
|
status,
|
|
351
|
-
|
|
387
|
+
inertiaHeaders(page.version),
|
|
352
388
|
JSON.stringify(page),
|
|
353
389
|
);
|
|
354
390
|
}
|
|
355
391
|
const { html } = await renderRequestHtml(page, request);
|
|
356
|
-
return writeContent(
|
|
357
|
-
request,
|
|
358
|
-
response,
|
|
359
|
-
status,
|
|
360
|
-
{ ...htmlHeaders(), ...retryHeaders },
|
|
361
|
-
html,
|
|
362
|
-
);
|
|
392
|
+
return writeContent(request, response, status, htmlHeaders(), html);
|
|
363
393
|
}
|
|
364
394
|
const payload =
|
|
365
395
|
error instanceof UpstreamError || error instanceof RequestBodyError
|
|
366
396
|
? { message: error.message }
|
|
367
397
|
: UNEXPECTED_ERROR_BODY;
|
|
368
|
-
response.writeHead(status, { "content-type": JSON_TYPE
|
|
398
|
+
response.writeHead(status, { "content-type": JSON_TYPE });
|
|
369
399
|
response.end(JSON.stringify(payload));
|
|
370
400
|
}
|
|
371
401
|
|
|
@@ -425,15 +455,16 @@ export async function handleRequest(request, response) {
|
|
|
425
455
|
if (localRoute) return localRoute[1](request, response);
|
|
426
456
|
const devServer = await developmentServer();
|
|
427
457
|
const modules = await loadServerModules(devServer);
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
);
|
|
458
|
+
const hasFile = (path) => hasRendererFile(devServer, path);
|
|
459
|
+
const owner = requestOwner(request.method, pathname, modules, hasFile);
|
|
431
460
|
if (devServer) response.setHeader("x-dms-route-owner", ownerLabel(owner));
|
|
432
461
|
if (owner.kind === "renderer")
|
|
433
462
|
return serveRendererFile(devServer, request, response, pathname);
|
|
434
463
|
if (owner.kind === "module")
|
|
435
464
|
return handleModuleRoute(request, response, pathname, modules);
|
|
436
|
-
|
|
465
|
+
const isProxiedPath = (path) => reachesBackend(path, modules, hasFile);
|
|
466
|
+
if (owner.kind === "backend")
|
|
467
|
+
return proxy(request, response, owner.proxy, isProxiedPath);
|
|
437
468
|
if (owner.kind === "not-found") return writeNotFound(response);
|
|
438
469
|
if (requestFailure.hasMalformedPath(request.url))
|
|
439
470
|
return requestFailure.writeBadRequest(response);
|
|
@@ -455,16 +486,17 @@ export async function handleRequest(request, response) {
|
|
|
455
486
|
JSON.stringify(page),
|
|
456
487
|
);
|
|
457
488
|
}
|
|
458
|
-
const rendered = await
|
|
489
|
+
const { rendered, served } = await renderDocument(page, request, rule, auth);
|
|
459
490
|
if (rendered.redirect) {
|
|
460
491
|
redirectFrontendVisit(request, response, rendered.redirect);
|
|
461
492
|
return;
|
|
462
493
|
}
|
|
494
|
+
const location = served === page ? {} : { "content-location": served.url };
|
|
463
495
|
return writeContent(
|
|
464
496
|
request,
|
|
465
497
|
response,
|
|
466
498
|
rendered.status,
|
|
467
|
-
{ ...htmlHeaders(), ...headersFor(rendered.status) },
|
|
499
|
+
{ ...htmlHeaders(), ...headersFor(rendered.status), ...location },
|
|
468
500
|
rendered.html,
|
|
469
501
|
);
|
|
470
502
|
}
|
|
@@ -487,19 +519,7 @@ async function writeRequestFailure(error, request, response) {
|
|
|
487
519
|
await writeBackendError(error, request, response);
|
|
488
520
|
}
|
|
489
521
|
|
|
490
|
-
/**
|
|
491
|
-
* Aborted once the client is gone, ending a wait for a waking backend. An
|
|
492
|
-
* embedder's response that emits no events never aborts it.
|
|
493
|
-
*/
|
|
494
|
-
function abandonedSignal(response) {
|
|
495
|
-
const abandoned = new AbortController();
|
|
496
|
-
if (typeof response.once === "function")
|
|
497
|
-
response.once("close", () => abandoned.abort());
|
|
498
|
-
return abandoned.signal;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
522
|
export async function handleRequestSafely(request, response) {
|
|
502
|
-
request.dmsAbandoned = abandonedSignal(response);
|
|
503
523
|
try {
|
|
504
524
|
if (refuseForeignTarget(request, response)) return;
|
|
505
525
|
await handleRequest(request, response);
|
|
@@ -515,7 +535,7 @@ if (
|
|
|
515
535
|
process.argv[1] &&
|
|
516
536
|
import.meta.url === new URL(`file://${process.argv[1]}`).href
|
|
517
537
|
) {
|
|
518
|
-
frontendHttpServer = createServer(handleRequestSafely);
|
|
538
|
+
frontendHttpServer = createServer(SERVER_OPTIONS, handleRequestSafely);
|
|
519
539
|
frontendHttpServer.on("clientError", (error, socket) =>
|
|
520
540
|
handleClientError(error, socket, maxHeaderSize),
|
|
521
541
|
);
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import { pathToFileURL } from "node:url";
|
|
18
|
+
|
|
19
|
+
// The very unimport the auto-import plugin scans with, so both read the
|
|
20
|
+
// directories the same way. Imported by URL, not by path: on Windows,
|
|
21
|
+
// `import()` reads `C:\...` as a URL whose scheme is `c:` and refuses it.
|
|
22
|
+
const unimportUrl = pathToFileURL(
|
|
23
|
+
createRequire(
|
|
24
|
+
createRequire(import.meta.url).resolve("unplugin-auto-import/package.json"),
|
|
25
|
+
).resolve("unimport"),
|
|
26
|
+
);
|
|
27
|
+
const { builtinPresets, resolvePreset, scanDirExports } = await import(
|
|
28
|
+
unimportUrl.href
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const HEADER =
|
|
32
|
+
"// Generated by @antelopejs/dms-frontend from the auto-imported types, for\n" +
|
|
33
|
+
"// Vue's SFC compiler (script.globalTypeFiles). Not read by TypeScript.\n";
|
|
34
|
+
|
|
35
|
+
const withoutExtension = (path) => path.replace(/\.[cm]?[jt]sx?$/, "");
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The ambient aliases of the auto-imported types: one per name, in the order
|
|
39
|
+
* the auto-import plugin reads them (Vue's preset, then the scanned
|
|
40
|
+
* directories), the last one winning, as unimport's own deduplication keeps
|
|
41
|
+
* the last of two imports of the same name. A module that exports its own
|
|
42
|
+
* `Component` thus shadows Vue's, for the compiler as for TypeScript.
|
|
43
|
+
*
|
|
44
|
+
* @param {(string | { glob: string, types?: boolean })[]} dirs the auto-import
|
|
45
|
+
* directories, as handed to the auto-import plugin
|
|
46
|
+
* @param {string} cwd the workspace
|
|
47
|
+
*/
|
|
48
|
+
export async function sfcGlobalTypes(dirs, cwd) {
|
|
49
|
+
const vue = await resolvePreset(builtinPresets.vue);
|
|
50
|
+
const scanned = await scanDirExports(dirs, { cwd, types: true });
|
|
51
|
+
const aliases = new Map();
|
|
52
|
+
for (const entry of [...vue, ...scanned]) {
|
|
53
|
+
if (!entry.type) continue;
|
|
54
|
+
const name = entry.as ?? entry.name;
|
|
55
|
+
const source = JSON.stringify(withoutExtension(entry.from));
|
|
56
|
+
aliases.set(
|
|
57
|
+
name,
|
|
58
|
+
`declare type ${name} = import(${source}).${entry.name};`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return `${HEADER}${[...aliases.values()].join("\n")}\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeIfChanged(file, content) {
|
|
65
|
+
let current;
|
|
66
|
+
try {
|
|
67
|
+
current = readFileSync(file, "utf8");
|
|
68
|
+
} catch {
|
|
69
|
+
current = undefined;
|
|
70
|
+
}
|
|
71
|
+
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
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
76
|
+
writeFileSync(temporary, content);
|
|
77
|
+
renameSync(temporary, file);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Writes the declarations once, before Vite's config is returned. */
|
|
82
|
+
export async function writeSfcGlobalTypes(dirs, cwd, file) {
|
|
83
|
+
writeIfChanged(file, await sfcGlobalTypes(dirs, cwd));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Keeps the declarations in step on a dev server: a type file added, renamed
|
|
88
|
+
* or removed rewrites them, and the Vue plugin reloads the components whose
|
|
89
|
+
* props read the file.
|
|
90
|
+
*/
|
|
91
|
+
export function sfcGlobalTypesWatcher(dirs, cwd, file) {
|
|
92
|
+
return {
|
|
93
|
+
name: "dms-sfc-global-types",
|
|
94
|
+
apply: "serve",
|
|
95
|
+
configureServer(server) {
|
|
96
|
+
let pending;
|
|
97
|
+
const refresh = (path) => {
|
|
98
|
+
if (!/\.[cm]?[jt]sx?$/.test(path) || path === file) return;
|
|
99
|
+
clearTimeout(pending);
|
|
100
|
+
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
|
+
sfcGlobalTypes(dirs, cwd)
|
|
106
|
+
.then((content) => writeIfChanged(file, content))
|
|
107
|
+
.catch((error) =>
|
|
108
|
+
server.config.logger.warn(
|
|
109
|
+
`Could not update ${file}, the types Vue's SFC compiler ` +
|
|
110
|
+
`reads: ${error instanceof Error ? error.message : error}`,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
}, 100);
|
|
114
|
+
};
|
|
115
|
+
for (const event of ["add", "change", "unlink"])
|
|
116
|
+
server.watcher.on(event, refresh);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -28,7 +28,9 @@ import {
|
|
|
28
28
|
setDmsServerRuntimeResolver,
|
|
29
29
|
setupFrontendModules,
|
|
30
30
|
trackDmsServerScopes,
|
|
31
|
+
withPublicOptions,
|
|
31
32
|
} from "./frontend-module";
|
|
33
|
+
import { parsePublicOptions } from "./public-options";
|
|
32
34
|
import { frontendModules } from "./frontend-modules.generated";
|
|
33
35
|
import {
|
|
34
36
|
SSR_ICONS_ID,
|
|
@@ -53,8 +55,19 @@ export interface DmsSsrResult {
|
|
|
53
55
|
body: string;
|
|
54
56
|
head: DmsSsrHead;
|
|
55
57
|
overlays: string;
|
|
58
|
+
/**
|
|
59
|
+
* The id of every single-file component the render created, its path from
|
|
60
|
+
* the workspace root as the client manifest keys it: the server links their
|
|
61
|
+
* stylesheets.
|
|
62
|
+
*/
|
|
63
|
+
modules: string[];
|
|
56
64
|
error?: DmsErrorData;
|
|
57
65
|
redirect?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The locale of the catalog the page was rendered with: the server preloads
|
|
68
|
+
* that catalog's file, which the browser loads before it hydrates.
|
|
69
|
+
*/
|
|
70
|
+
catalogLocale?: string;
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
export interface DmsSsrHead {
|
|
@@ -64,6 +77,8 @@ export interface DmsSsrHead {
|
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
interface DmsSsrContext {
|
|
80
|
+
/** Filled by `@vitejs/plugin-vue`: each component adds its id as it sets up. */
|
|
81
|
+
modules?: Set<string>;
|
|
67
82
|
teleports?: Record<string, string>;
|
|
68
83
|
}
|
|
69
84
|
|
|
@@ -122,7 +137,13 @@ function iconsScript(names: Set<string>): string {
|
|
|
122
137
|
return `<script id="${SSR_ICONS_ID}" type="application/json">${value}</script>`;
|
|
123
138
|
}
|
|
124
139
|
|
|
125
|
-
await setupFrontendModules(
|
|
140
|
+
await setupFrontendModules(
|
|
141
|
+
withPublicOptions(
|
|
142
|
+
frontendModules,
|
|
143
|
+
parsePublicOptions(process.env.DMS_PUBLIC_OPTIONS),
|
|
144
|
+
),
|
|
145
|
+
appConfigs,
|
|
146
|
+
);
|
|
126
147
|
|
|
127
148
|
export function isDmsFrontendPage(path: string): boolean {
|
|
128
149
|
return hasDmsPage(path);
|
|
@@ -173,6 +194,7 @@ async function renderDmsPageWithRuntime(
|
|
|
173
194
|
let icons = new Set<string>();
|
|
174
195
|
let iconSymbols = new Map<string, string>();
|
|
175
196
|
let payload: DmsPayload = { data: {}, errors: {}, state: {} };
|
|
197
|
+
let catalogLocale: (() => string | undefined) | undefined;
|
|
176
198
|
const inertiaResult = await createInertiaApp({
|
|
177
199
|
page,
|
|
178
200
|
// Read as the HTML is complete: data a run settles afterwards was never
|
|
@@ -198,6 +220,7 @@ async function renderDmsPageWithRuntime(
|
|
|
198
220
|
runtime,
|
|
199
221
|
serverFetch,
|
|
200
222
|
});
|
|
223
|
+
catalogLocale = configured.catalogLocale;
|
|
201
224
|
return app;
|
|
202
225
|
},
|
|
203
226
|
});
|
|
@@ -208,6 +231,7 @@ async function renderDmsPageWithRuntime(
|
|
|
208
231
|
body: "",
|
|
209
232
|
head: { bodyAttrs: "", headTags: "", htmlAttrs: "" },
|
|
210
233
|
overlays: "",
|
|
234
|
+
modules: [],
|
|
211
235
|
redirect: runtime.serverRedirect,
|
|
212
236
|
};
|
|
213
237
|
}
|
|
@@ -238,6 +262,8 @@ async function renderDmsPageWithRuntime(
|
|
|
238
262
|
htmlAttrs: renderedHead.htmlAttrs,
|
|
239
263
|
},
|
|
240
264
|
overlays: ssrContext.teleports?.["#dms-overlays"] ?? "",
|
|
265
|
+
modules: [...(ssrContext.modules ?? [])],
|
|
266
|
+
catalogLocale: catalogLocale?.(),
|
|
241
267
|
};
|
|
242
268
|
} finally {
|
|
243
269
|
// Only once the head and the async data are serialized: both still read
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
/** Nuxt UI's key hint composable. */
|
|
12
|
+
const USE_KBD = /\/@nuxt\/ui\/dist\/runtime\/composables\/useKbd\.js$/;
|
|
13
|
+
/** Its export as Nuxt UI 4 writes it: one composable shared by every hint. */
|
|
14
|
+
const SHARED_EXPORT =
|
|
15
|
+
/export const useKbd = \/\* @__PURE__ \*\/ createSharedComposable\(_useKbd\);/;
|
|
16
|
+
const ROUTED_EXPORT =
|
|
17
|
+
'import { useDmsKbd } from "#dms/frontend-module";\n' +
|
|
18
|
+
"const useSharedKbd = /* @__PURE__ */ createSharedComposable(_useKbd);\n" +
|
|
19
|
+
"export const useKbd = () => useDmsKbd(useSharedKbd, kbdKeysMap);";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The rewrite, as a Vite plugin.
|
|
23
|
+
*
|
|
24
|
+
* @param isNuxtUiFile `(id) => boolean`, whether a module is Nuxt UI's
|
|
25
|
+
*/
|
|
26
|
+
export function uiKbd(isNuxtUiFile) {
|
|
27
|
+
return {
|
|
28
|
+
name: "dms-ui-kbd",
|
|
29
|
+
enforce: "pre",
|
|
30
|
+
transform(code, id) {
|
|
31
|
+
const path = id.replaceAll("\\", "/").split("?")[0];
|
|
32
|
+
if (!isNuxtUiFile(id) || !USE_KBD.test(path)) return null;
|
|
33
|
+
if (
|
|
34
|
+
!SHARED_EXPORT.test(code) ||
|
|
35
|
+
!code.includes("export const kbdKeysMap")
|
|
36
|
+
)
|
|
37
|
+
this.error(
|
|
38
|
+
`${id} defines useKbd in a form the DMS does not rewrite: its key ` +
|
|
39
|
+
"hints would not hydrate cleanly. Update ui-kbd.mjs for this " +
|
|
40
|
+
"Nuxt UI release.",
|
|
41
|
+
);
|
|
42
|
+
return { code: code.replace(SHARED_EXPORT, ROUTED_EXPORT), map: null };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|