@antelopejs/dms-frontend 0.3.0 → 0.3.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 +24 -0
- package/dist/commands/dev.js +4 -1
- package/dist/commands/start.js +3 -0
- package/dist/common.d.ts +1 -0
- package/dist/common.js +1 -0
- package/dist/config.js +1 -0
- package/dist/layer-watch.js +42 -2
- package/dist/manifest-secrets.d.ts +31 -0
- package/dist/manifest-secrets.js +101 -0
- package/dist/materialize.d.ts +2 -0
- package/dist/materialize.js +102 -10
- package/dist/ports.js +1 -1
- package/dist/workspace-setup.d.ts +2 -0
- package/dist/workspace-setup.js +2 -0
- package/package.json +1 -1
- package/templates/vue/app-runtime.ts +27 -2
- package/templates/vue/frontend-module.ts +61 -5
- package/templates/vue/gitignore +9 -0
- package/templates/vue/network-error.ts +70 -0
- package/templates/vue/server/auth/backend.mjs +9 -2
- package/templates/vue/server/backend-response.mjs +85 -0
- package/templates/vue/server/email-bundle-watch.mjs +31 -0
- package/templates/vue/server/email.mjs +76 -1
- package/templates/vue/server/inertia.mjs +9 -0
- package/templates/vue/server/request-failure.mjs +54 -0
- package/templates/vue/server.mjs +47 -41
- package/templates/vue/ssr-renderer.ts +93 -61
- package/templates/vue/vite.email.config.ts +5 -1
package/templates/vue/server.mjs
CHANGED
|
@@ -18,17 +18,25 @@ import {
|
|
|
18
18
|
refreshSession,
|
|
19
19
|
} from "./server/auth/routes.mjs";
|
|
20
20
|
import { readSession } from "./server/auth/session.mjs";
|
|
21
|
+
import {
|
|
22
|
+
BackendResponseError,
|
|
23
|
+
backendResponseError,
|
|
24
|
+
redirectAccessRefusal,
|
|
25
|
+
UNEXPECTED_ERROR_BODY,
|
|
26
|
+
} from "./server/backend-response.mjs";
|
|
21
27
|
import { productionHtmlTemplate } from "./server/client-manifest.mjs";
|
|
22
28
|
import { documentStyleTags } from "./server/dev-styles.mjs";
|
|
23
|
-
import { handleEmailRender } from "./server/email.mjs";
|
|
29
|
+
import { handleEmailRender, watchEmailBundle } from "./server/email.mjs";
|
|
24
30
|
import { HOMEPAGE } from "./server/homepage.mjs";
|
|
25
31
|
import { htmlTag } from "./server/html-tag.mjs";
|
|
32
|
+
import * as requestFailure from "./server/request-failure.mjs";
|
|
26
33
|
import { handleTester } from "./server/tester.mjs";
|
|
27
34
|
import {
|
|
28
35
|
createInertiaPage,
|
|
29
36
|
htmlHeaders,
|
|
30
37
|
inertiaAppHtml,
|
|
31
38
|
inertiaHeaders,
|
|
39
|
+
isFrontendVisit,
|
|
32
40
|
redirectFrontendVisit,
|
|
33
41
|
handleAssetVersionMismatch,
|
|
34
42
|
} from "./server/inertia.mjs";
|
|
@@ -73,13 +81,6 @@ let vite;
|
|
|
73
81
|
let vitePromise;
|
|
74
82
|
let frontendHttpServer;
|
|
75
83
|
|
|
76
|
-
class BackendResponseError extends Error {
|
|
77
|
-
constructor(status) {
|
|
78
|
-
super(`DMS backend returned ${status}`);
|
|
79
|
-
this.status = status;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
84
|
async function developmentServer() {
|
|
84
85
|
if (process.env.DMS_DEV !== "true") return undefined;
|
|
85
86
|
if (vite) return vite;
|
|
@@ -139,7 +140,7 @@ async function backendJson(path, request) {
|
|
|
139
140
|
headers: backendHeaders(request),
|
|
140
141
|
});
|
|
141
142
|
if (!response.ok) {
|
|
142
|
-
throw
|
|
143
|
+
throw await backendResponseError(response);
|
|
143
144
|
}
|
|
144
145
|
return response.json();
|
|
145
146
|
}
|
|
@@ -185,14 +186,6 @@ function writeProxyHeaders(response, upstream) {
|
|
|
185
186
|
response.writeHead(upstream.status, headers);
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
function isFrontendVisit(request) {
|
|
189
|
-
if (request.method !== "GET") return false;
|
|
190
|
-
return (
|
|
191
|
-
Boolean(request.headers[INERTIA_HEADER]) ||
|
|
192
|
-
request.headers.accept?.includes("text/html")
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
189
|
function redirectToHomepage(request, response, pathname) {
|
|
197
190
|
if (pathname !== "/" || HOMEPAGE === "/") return false;
|
|
198
191
|
redirectFrontendVisit(request, response, HOMEPAGE);
|
|
@@ -232,7 +225,12 @@ async function proxy(request, response, fallbackOnNotFound = false) {
|
|
|
232
225
|
return true;
|
|
233
226
|
}
|
|
234
227
|
const target = new URL(request.url, process.env.DMS_API_BASE_URL);
|
|
228
|
+
// Cancelling the body alone misses a client that leaves before the backend
|
|
229
|
+
// answers: its stream, an SSE feed above all, then stayed open for good.
|
|
230
|
+
const abandoned = new AbortController();
|
|
231
|
+
response.once("close", () => abandoned.abort());
|
|
235
232
|
const upstream = await fetch(target, {
|
|
233
|
+
signal: abandoned.signal,
|
|
236
234
|
method: request.method,
|
|
237
235
|
headers: backendHeaders(request),
|
|
238
236
|
body: ["GET", "HEAD"].includes(request.method) ? undefined : request,
|
|
@@ -386,14 +384,16 @@ async function writeBackendError(error, request, response) {
|
|
|
386
384
|
redirectFrontendVisit(request, response, location);
|
|
387
385
|
return;
|
|
388
386
|
}
|
|
387
|
+
const loadRenderer = async () => ssrRenderer(await developmentServer());
|
|
388
|
+
if (await redirectAccessRefusal(error, request, response, loadRenderer))
|
|
389
|
+
return;
|
|
389
390
|
console.error("DMS backend request failed", error);
|
|
390
391
|
if (isFrontendVisit(request)) {
|
|
391
|
-
const statusMessage = "DMS backend request failed";
|
|
392
392
|
const props = {
|
|
393
393
|
path: pathname,
|
|
394
394
|
page: {},
|
|
395
395
|
...publicSession(readSession(request)),
|
|
396
|
-
error: { statusCode: status
|
|
396
|
+
error: { statusCode: status },
|
|
397
397
|
};
|
|
398
398
|
const page = createInertiaPage(request.url, props);
|
|
399
399
|
if (request.headers[INERTIA_HEADER]) {
|
|
@@ -411,7 +411,7 @@ async function writeBackendError(error, request, response) {
|
|
|
411
411
|
const payload =
|
|
412
412
|
error instanceof UpstreamError || error instanceof RequestBodyError
|
|
413
413
|
? { message: error.message }
|
|
414
|
-
:
|
|
414
|
+
: UNEXPECTED_ERROR_BODY;
|
|
415
415
|
response.writeHead(status, { "content-type": JSON_TYPE });
|
|
416
416
|
response.end(JSON.stringify(payload));
|
|
417
417
|
}
|
|
@@ -433,6 +433,8 @@ export async function handleRequest(request, response) {
|
|
|
433
433
|
return handleAuth(request, response, url);
|
|
434
434
|
if (requestOwnership(request.method, pathname) === "server")
|
|
435
435
|
return proxy(request, response);
|
|
436
|
+
if (requestFailure.hasMalformedPath(request.url))
|
|
437
|
+
return requestFailure.writeBadRequest(response);
|
|
436
438
|
if (!isFrontendVisit(request) && (await proxy(request, response, true)))
|
|
437
439
|
return;
|
|
438
440
|
const devServer = await developmentServer();
|
|
@@ -472,31 +474,32 @@ export async function handleRequest(request, response) {
|
|
|
472
474
|
);
|
|
473
475
|
}
|
|
474
476
|
|
|
477
|
+
async function writeRequestFailure(error, request, response) {
|
|
478
|
+
if (requestFailure.isResponseGone(error, response)) return;
|
|
479
|
+
if (
|
|
480
|
+
error instanceof BackendResponseError &&
|
|
481
|
+
error.status === 401 &&
|
|
482
|
+
request.method === "GET" &&
|
|
483
|
+
readSession(request) &&
|
|
484
|
+
!request.dmsSessionRetry
|
|
485
|
+
) {
|
|
486
|
+
request.dmsSessionRetry = true;
|
|
487
|
+
if (await refreshSession(request, response)) {
|
|
488
|
+
await handleRequestSafely(request, response);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
await writeBackendError(error, request, response);
|
|
493
|
+
}
|
|
494
|
+
|
|
475
495
|
export async function handleRequestSafely(request, response) {
|
|
476
496
|
try {
|
|
477
497
|
await handleRequest(request, response);
|
|
478
498
|
} catch (error) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
response.headersSent
|
|
484
|
-
)
|
|
485
|
-
return;
|
|
486
|
-
if (
|
|
487
|
-
error instanceof BackendResponseError &&
|
|
488
|
-
error.status === 401 &&
|
|
489
|
-
request.method === "GET" &&
|
|
490
|
-
readSession(request) &&
|
|
491
|
-
!request.dmsSessionRetry
|
|
492
|
-
) {
|
|
493
|
-
request.dmsSessionRetry = true;
|
|
494
|
-
if (await refreshSession(request, response)) {
|
|
495
|
-
await handleRequestSafely(request, response);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
await writeBackendError(error, request, response);
|
|
499
|
+
// An error page failing too must not reject: that would end the process.
|
|
500
|
+
await writeRequestFailure(error, request, response).catch((failure) =>
|
|
501
|
+
requestFailure.abandonResponse(failure, response),
|
|
502
|
+
);
|
|
500
503
|
}
|
|
501
504
|
}
|
|
502
505
|
|
|
@@ -531,4 +534,7 @@ async function announceReady() {
|
|
|
531
534
|
? `[${address}]`
|
|
532
535
|
: address;
|
|
533
536
|
console.log(`✓ Server ready on http://${host}:${port}`);
|
|
537
|
+
// Only the production build produces the email bundle otherwise. Started
|
|
538
|
+
// once the server answers, so it never delays the first page.
|
|
539
|
+
if (process.env.DMS_DEV === "true") watchEmailBundle();
|
|
534
540
|
}
|
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
createDmsFrontendRuntime,
|
|
19
19
|
hasDmsPage,
|
|
20
20
|
preloadDmsPage,
|
|
21
|
+
resolveDmsAccessRedirect,
|
|
21
22
|
setDmsServerRuntimeResolver,
|
|
22
23
|
setupFrontendModules,
|
|
24
|
+
trackDmsServerScopes,
|
|
23
25
|
} from "./frontend-module";
|
|
24
26
|
import { frontendModules } from "./frontend-modules.generated";
|
|
25
27
|
|
|
@@ -51,8 +53,14 @@ interface DmsSsrContext {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
const SSR_ASYNC_DATA_ID = "dms-ssr-async-data";
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
// Holds the runtime rather than being it: every socket, timer and promise a
|
|
57
|
+
// render starts keeps this store for its whole life, and a pooled backend
|
|
58
|
+
// connection then serving an SSE stream lives for hours. Emptied once the
|
|
59
|
+
// render is over, the store no longer ties the render to them.
|
|
60
|
+
const runtimeStorage = new AsyncLocalStorage<{
|
|
61
|
+
runtime?: DmsFrontendRuntime;
|
|
62
|
+
}>();
|
|
63
|
+
setDmsServerRuntimeResolver(() => runtimeStorage.getStore()?.runtime);
|
|
56
64
|
|
|
57
65
|
function asyncDataScript(runtime: DmsFrontendRuntime): string {
|
|
58
66
|
const value = JSON.stringify(serializeDmsAsyncData(runtime)).replaceAll(
|
|
@@ -73,6 +81,10 @@ export function isDmsFrontendPage(path: string): boolean {
|
|
|
73
81
|
return hasDmsPage(path);
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
export function accessRedirect(code: string): string | undefined {
|
|
85
|
+
return resolveDmsAccessRedirect(code);
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
export async function renderDmsPage(
|
|
77
89
|
page: DmsSsrPage,
|
|
78
90
|
serverFetch?: DmsServerFetch,
|
|
@@ -84,9 +96,14 @@ export async function renderDmsPage(
|
|
|
84
96
|
true,
|
|
85
97
|
requestCookies,
|
|
86
98
|
);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
const store = { runtime };
|
|
100
|
+
try {
|
|
101
|
+
return await runtimeStorage.run(store, () =>
|
|
102
|
+
renderDmsPageWithRuntime(page, runtime, serverFetch),
|
|
103
|
+
);
|
|
104
|
+
} finally {
|
|
105
|
+
store.runtime = undefined;
|
|
106
|
+
}
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
async function renderDmsPageWithRuntime(
|
|
@@ -95,64 +112,79 @@ async function renderDmsPageWithRuntime(
|
|
|
95
112
|
serverFetch?: DmsServerFetch,
|
|
96
113
|
): Promise<DmsSsrResult> {
|
|
97
114
|
await preloadDmsPage(page.props);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
app
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
let stopScopes = () => {};
|
|
116
|
+
try {
|
|
117
|
+
const head = createHead();
|
|
118
|
+
const ssrContext: DmsSsrContext = {};
|
|
119
|
+
let asyncComponents = new Set<string>();
|
|
120
|
+
const inertiaResult = await createInertiaApp({
|
|
121
|
+
page,
|
|
122
|
+
render: (app) => renderToString(app, ssrContext),
|
|
123
|
+
resolve: resolveDmsInertiaPage,
|
|
124
|
+
async setup({ App, props, plugin }) {
|
|
125
|
+
const app = createSSRApp({ render: () => h(App, props) });
|
|
126
|
+
asyncComponents = trackDmsAsyncComponents(app);
|
|
127
|
+
stopScopes = trackDmsServerScopes(app);
|
|
128
|
+
const configured = await configureDmsApp({
|
|
129
|
+
app,
|
|
130
|
+
head,
|
|
131
|
+
initialPageProps: page.props,
|
|
132
|
+
initialPageUrl: page.url,
|
|
133
|
+
inertiaPlugin: plugin,
|
|
134
|
+
runtime,
|
|
135
|
+
serverFetch,
|
|
136
|
+
});
|
|
137
|
+
return app;
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
if (!inertiaResult) throw new Error("Inertia SSR returned no result");
|
|
141
|
+
await runtime.pendingNavigation;
|
|
142
|
+
if (runtime.serverRedirect) {
|
|
143
|
+
return {
|
|
144
|
+
body: "",
|
|
145
|
+
head: { bodyAttrs: "", headTags: "", htmlAttrs: "" },
|
|
146
|
+
overlays: "",
|
|
147
|
+
redirect: runtime.serverRedirect,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (runtime.currentError.value && !page.props.error) {
|
|
151
|
+
const failure = runtime.currentError.value as Error & DmsErrorData;
|
|
152
|
+
const expected =
|
|
153
|
+
Number.isInteger(failure.statusCode) &&
|
|
154
|
+
failure.statusCode! >= 400 &&
|
|
155
|
+
failure.statusCode! <= 599;
|
|
156
|
+
// An unexpected failure reaches the page as its status code alone, which
|
|
157
|
+
// the DMS error page describes in the visitor's language; the technical
|
|
158
|
+
// cause only reaches the server log.
|
|
159
|
+
if (!expected) console.error("DMS page render failed", failure);
|
|
160
|
+
const error = expected
|
|
161
|
+
? {
|
|
162
|
+
statusCode: failure.statusCode,
|
|
163
|
+
message: failure.message,
|
|
164
|
+
statusMessage: failure.statusMessage,
|
|
165
|
+
}
|
|
166
|
+
: { statusCode: 500 };
|
|
167
|
+
return renderDmsPage(
|
|
168
|
+
{ ...page, props: { ...page.props, error } },
|
|
115
169
|
serverFetch,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!inertiaResult) throw new Error("Inertia SSR returned no result");
|
|
121
|
-
await runtime.pendingNavigation;
|
|
122
|
-
if (runtime.serverRedirect) {
|
|
170
|
+
runtime.requestCookies,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const renderedHead = await renderSSRHead(head);
|
|
123
174
|
return {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const expected =
|
|
133
|
-
Number.isInteger(failure.statusCode) &&
|
|
134
|
-
failure.statusCode! >= 400 &&
|
|
135
|
-
failure.statusCode! <= 599;
|
|
136
|
-
const error = {
|
|
137
|
-
statusCode: expected ? failure.statusCode : 500,
|
|
138
|
-
message: expected ? failure.message : "An unexpected error occurred",
|
|
139
|
-
statusMessage: expected ? failure.statusMessage : "Application error",
|
|
175
|
+
error: page.props.error,
|
|
176
|
+
body: `${inertiaResult.body}${asyncDataScript(runtime)}${asyncComponentsScript(asyncComponents)}`,
|
|
177
|
+
head: {
|
|
178
|
+
bodyAttrs: renderedHead.bodyAttrs,
|
|
179
|
+
headTags: `${inertiaResult.head.join("")}${renderedHead.headTags}`,
|
|
180
|
+
htmlAttrs: renderedHead.htmlAttrs,
|
|
181
|
+
},
|
|
182
|
+
overlays: ssrContext.teleports?.["#dms-overlays"] ?? "",
|
|
140
183
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
);
|
|
184
|
+
} finally {
|
|
185
|
+
// Only once the head and the async data are serialized: both still read
|
|
186
|
+
// state the rendered components own.
|
|
187
|
+
stopScopes();
|
|
188
|
+
runtime.scope.stop();
|
|
146
189
|
}
|
|
147
|
-
const renderedHead = await renderSSRHead(head);
|
|
148
|
-
return {
|
|
149
|
-
error: page.props.error,
|
|
150
|
-
body: `${inertiaResult.body}${asyncDataScript(runtime)}${asyncComponentsScript(asyncComponents)}`,
|
|
151
|
-
head: {
|
|
152
|
-
bodyAttrs: renderedHead.bodyAttrs,
|
|
153
|
-
headTags: `${inertiaResult.head.join("")}${renderedHead.headTags}`,
|
|
154
|
-
htmlAttrs: renderedHead.htmlAttrs,
|
|
155
|
-
},
|
|
156
|
-
overlays: ssrContext.teleports?.["#dms-overlays"] ?? "",
|
|
157
|
-
};
|
|
158
190
|
}
|
|
@@ -42,10 +42,14 @@ export default defineConfig({
|
|
|
42
42
|
for (const file of readdirSync(directory).filter((name) =>
|
|
43
43
|
name.endsWith(".json"),
|
|
44
44
|
)) {
|
|
45
|
+
const catalog = resolve(directory, file);
|
|
46
|
+
// Read as data rather than imported: without this, a build in watch
|
|
47
|
+
// mode would keep emitting the catalogs of its first run.
|
|
48
|
+
this.addWatchFile(catalog);
|
|
45
49
|
this.emitFile({
|
|
46
50
|
type: "asset",
|
|
47
51
|
fileName: `locales/${file}`,
|
|
48
|
-
source: readFileSync(
|
|
52
|
+
source: readFileSync(catalog, "utf8"),
|
|
49
53
|
});
|
|
50
54
|
}
|
|
51
55
|
},
|