@antelopejs/dms-frontend 0.5.4 → 0.5.6
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 +1 -1
- package/dist/config.js +1 -0
- package/dist/verify-source-build.d.ts +11 -0
- package/dist/verify-source-build.js +113 -0
- package/dist/verify-source-runner.js +3 -58
- package/package.json +1 -1
- package/templates/vue/package.json +1 -0
- package/templates/vue/prepare-types.mjs +34 -0
- package/templates/vue/server/proxy-headers.mjs +25 -0
- package/templates/vue/server/request-target.mjs +53 -0
- package/templates/vue/server.mjs +4 -10
package/README.md
CHANGED
|
@@ -615,6 +615,6 @@ ajs dms verify-source \
|
|
|
615
615
|
|
|
616
616
|
The checks look for what the core layer ships: its lazily loaded chart and rich-text libraries, the pages every DMS serves and its e-mail templates. A `--layer` that holds no core layer, such as a project's own `frontend-vue`, is refused before anything is installed, with exit code 2 and the command that verifies the same folder with `--module` instead.
|
|
617
617
|
|
|
618
|
-
A supplied package whose declared range excludes this adapter version is refused before anything is installed (see [Declaring the loader releases a module supports](#declaring-the-loader-releases-a-module-supports)). The verifier generates a temporary workspace, installs it, builds client, SSR
|
|
618
|
+
A supplied package whose declared range excludes this adapter version is refused before anything is installed (see [Declaring the loader releases a module supports](#declaring-the-loader-releases-a-module-supports)). The verifier generates a temporary workspace, installs it, builds the client, SSR and email bundles while `vue-tsc` runs alongside them (it does not compress the client assets, which only a served build needs), checks what the client bundle loads, renders eight DMS email templates, and checks that the email bundle excludes browser-only modules. Each check is one line that ends in ✔ or ✖, and the run ends with `Verified <n> modules` and its duration, or with the first failure: a type or Vite error points at the file in the package you passed (`./frontend-vue/app/components/Callout.vue:17:7`), not at its copy in the workspace, and the command exits 1. `--verbose` streams the pnpm, Vite and `vue-tsc` output. The workspace is removed when the run ends. It does not start a backend or publish packages. Repository development can invoke the same runner through `pnpm test:real-source`, with `DMS_LAYER_SOURCE` set to a core layer root and `DMS_MODULE_SOURCES` to a JSON array of module roots.
|
|
619
619
|
|
|
620
620
|
Refresh rotation is single-flight within one frontend process. Horizontally scaled deployments must use sticky routing so a browser reaches the same process, or replace this process-local behavior with an external session adapter. It does not provide a distributed rotation guarantee.
|
package/dist/config.js
CHANGED
|
@@ -59,6 +59,7 @@ exports.TEMPLATE_FILES = [
|
|
|
59
59
|
["email-markup.ts", "email-markup.ts"],
|
|
60
60
|
["tsconfig.json", "tsconfig.json"],
|
|
61
61
|
["typecheck-loader.mjs", "typecheck-loader.mjs"],
|
|
62
|
+
["prepare-types.mjs", "prepare-types.mjs"],
|
|
62
63
|
["app-config-stub.mjs", "app-config-stub.mjs"],
|
|
63
64
|
["DmsDynamicPage.vue", "DmsDynamicPage.vue"],
|
|
64
65
|
["server.mjs", "server.mjs"],
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type PathMapper } from "./child-output";
|
|
2
|
+
import { ViteWarnings } from "./vite-output";
|
|
3
|
+
export interface VerificationContext {
|
|
4
|
+
workspace: string;
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
mapLine: PathMapper;
|
|
7
|
+
warnings: ViteWarnings;
|
|
8
|
+
isVerbose: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function prepareTypes(context: VerificationContext): Promise<void>;
|
|
11
|
+
export declare function buildAndTypeCheck(context: VerificationContext): Promise<void>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prepareTypes = prepareTypes;
|
|
4
|
+
exports.buildAndTypeCheck = buildAndTypeCheck;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const cli_1 = require("@antelopejs/core/cli");
|
|
8
|
+
const child_output_1 = require("./child-output");
|
|
9
|
+
const build_action_1 = require("./commands/build-action");
|
|
10
|
+
const common_1 = require("./common");
|
|
11
|
+
const typecheck_output_1 = require("./typecheck-output");
|
|
12
|
+
const verify_source_result_1 = require("./verify-source-result");
|
|
13
|
+
const vite_output_1 = require("./vite-output");
|
|
14
|
+
const VERIFY_BUILD_STEPS = build_action_1.BUILD_STEPS.filter((step) => step.script !== "build:compress");
|
|
15
|
+
async function prepareTypes(context) {
|
|
16
|
+
await (0, cli_1.getProcessTasks)().run("Generating the type declarations", async (task) => {
|
|
17
|
+
const result = await (0, common_1.runFramedCommand)("pnpm", ["--silent", "run", "build:types"], {
|
|
18
|
+
name: "types",
|
|
19
|
+
cwd: context.workspace,
|
|
20
|
+
env: context.env,
|
|
21
|
+
mapLine: context.mapLine,
|
|
22
|
+
});
|
|
23
|
+
if (result.code === 0)
|
|
24
|
+
return;
|
|
25
|
+
task.fail("Type declarations failed");
|
|
26
|
+
throw new cli_1.CliError((0, child_output_1.describeChildFailure)({
|
|
27
|
+
title: "Generating the type declarations failed",
|
|
28
|
+
command: "pnpm run build:types",
|
|
29
|
+
code: result.code,
|
|
30
|
+
lines: result.lines,
|
|
31
|
+
isVerbose: context.isVerbose,
|
|
32
|
+
}));
|
|
33
|
+
}, { done: "Generated the type declarations" });
|
|
34
|
+
snapshotTypeDeclarations(context.workspace);
|
|
35
|
+
}
|
|
36
|
+
const AUTO_IMPORTS_DECLARATIONS = "auto-imports.d.ts";
|
|
37
|
+
const AUTO_IMPORTS_SNAPSHOT = "auto-imports.verify.d.ts";
|
|
38
|
+
const TYPECHECK_CONFIG = "tsconfig.verify.json";
|
|
39
|
+
function snapshotTypeDeclarations(workspace) {
|
|
40
|
+
(0, node_fs_1.cpSync)((0, node_path_1.join)(workspace, AUTO_IMPORTS_DECLARATIONS), (0, node_path_1.join)(workspace, AUTO_IMPORTS_SNAPSHOT));
|
|
41
|
+
const config = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(workspace, "tsconfig.json"), "utf8"));
|
|
42
|
+
config.files = (config.files ?? []).map((file) => file === AUTO_IMPORTS_DECLARATIONS ? AUTO_IMPORTS_SNAPSHOT : file);
|
|
43
|
+
if (!config.files.includes(AUTO_IMPORTS_SNAPSHOT))
|
|
44
|
+
throw new cli_1.CliError({
|
|
45
|
+
title: `The workspace tsconfig.json does not list ${AUTO_IMPORTS_DECLARATIONS}`,
|
|
46
|
+
});
|
|
47
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(workspace, TYPECHECK_CONFIG), `${JSON.stringify(config, null, 2)}\n`);
|
|
48
|
+
}
|
|
49
|
+
async function buildBundle(context, step, warnings) {
|
|
50
|
+
await (0, cli_1.getProcessTasks)().run(step.running, async (task) => {
|
|
51
|
+
const result = await (0, common_1.runFramedCommand)("pnpm", ["--silent", "run", step.script], {
|
|
52
|
+
name: step.name,
|
|
53
|
+
cwd: context.workspace,
|
|
54
|
+
env: context.env,
|
|
55
|
+
mapLine: context.mapLine,
|
|
56
|
+
onLine: (line) => warnings.read(line),
|
|
57
|
+
});
|
|
58
|
+
if (result.code === 0)
|
|
59
|
+
return;
|
|
60
|
+
task.fail(step.failed);
|
|
61
|
+
throw (0, build_action_1.buildStepFailure)(step, result, context.isVerbose, verify_source_result_1.VERIFY_SOURCE_COMMAND);
|
|
62
|
+
}, { done: step.done });
|
|
63
|
+
}
|
|
64
|
+
async function buildAndTypeCheck(context) {
|
|
65
|
+
const stepWarnings = VERIFY_BUILD_STEPS.map(() => new vite_output_1.ViteWarnings());
|
|
66
|
+
const outcomes = await Promise.allSettled([
|
|
67
|
+
...VERIFY_BUILD_STEPS.map((step, index) => buildBundle(context, step, stepWarnings[index])),
|
|
68
|
+
typeCheck(context),
|
|
69
|
+
]);
|
|
70
|
+
for (const warnings of stepWarnings)
|
|
71
|
+
for (const line of warnings.lines())
|
|
72
|
+
context.warnings.read(line);
|
|
73
|
+
const failure = outcomes.find((outcome) => outcome.status === "rejected");
|
|
74
|
+
if (failure)
|
|
75
|
+
throw failure.reason;
|
|
76
|
+
(0, vite_output_1.reportViteWarnings)(context.warnings, context.isVerbose, (0, cli_1.getProcessUi)());
|
|
77
|
+
}
|
|
78
|
+
async function typeCheck(context) {
|
|
79
|
+
const typecheckArguments = [
|
|
80
|
+
"--import",
|
|
81
|
+
"./typecheck-loader.mjs",
|
|
82
|
+
"./node_modules/vue-tsc/bin/vue-tsc.js",
|
|
83
|
+
"--project",
|
|
84
|
+
TYPECHECK_CONFIG,
|
|
85
|
+
"--noEmit",
|
|
86
|
+
"--pretty",
|
|
87
|
+
];
|
|
88
|
+
const isWindows = process.platform === "win32";
|
|
89
|
+
const command = isWindows ? process.execPath : "env";
|
|
90
|
+
const commandArguments = isWindows
|
|
91
|
+
? typecheckArguments
|
|
92
|
+
: ["-u", "NODE_OPTIONS", process.execPath, ...typecheckArguments];
|
|
93
|
+
await (0, cli_1.getProcessTasks)().run("Type checking", async (task) => {
|
|
94
|
+
const result = await (0, common_1.runFramedCommand)(command, commandArguments, {
|
|
95
|
+
name: "vue-tsc",
|
|
96
|
+
cwd: context.workspace,
|
|
97
|
+
mapLine: context.mapLine,
|
|
98
|
+
});
|
|
99
|
+
if (result.code === 0)
|
|
100
|
+
return;
|
|
101
|
+
const report = (0, typecheck_output_1.parseTypeCheckOutput)(result.lines);
|
|
102
|
+
const count = report.count > 0 ? [(0, cli_1.pluralize)(report.count, "error")] : [];
|
|
103
|
+
task.fail(["Type check failed", ...count].join((0, cli_1.getProcessUi)().symbols.separator));
|
|
104
|
+
throw new cli_1.CliError((0, typecheck_output_1.describeTypeCheckErrors)(report, verify_source_result_1.VERIFY_SOURCE_COMMAND) ??
|
|
105
|
+
(0, child_output_1.describeChildFailure)({
|
|
106
|
+
title: "Type check failed",
|
|
107
|
+
command: "vue-tsc --noEmit",
|
|
108
|
+
code: result.code,
|
|
109
|
+
lines: result.lines,
|
|
110
|
+
isVerbose: context.isVerbose,
|
|
111
|
+
}));
|
|
112
|
+
}, { done: "Type check passed" });
|
|
113
|
+
}
|
|
@@ -37,11 +37,10 @@ const node_fs_1 = require("node:fs");
|
|
|
37
37
|
const node_path_1 = require("node:path");
|
|
38
38
|
const cli_1 = require("@antelopejs/core/cli");
|
|
39
39
|
const child_output_1 = require("./child-output");
|
|
40
|
-
const build_action_1 = require("./commands/build-action");
|
|
41
40
|
const common_1 = require("./common");
|
|
42
41
|
const output_1 = require("./output");
|
|
43
42
|
const temporary_workspace_1 = require("./temporary-workspace");
|
|
44
|
-
const
|
|
43
|
+
const verify_source_build_1 = require("./verify-source-build");
|
|
45
44
|
const verify_source_result_1 = require("./verify-source-result");
|
|
46
45
|
const verify_source_sources_1 = require("./verify-source-sources");
|
|
47
46
|
const vite_output_1 = require("./vite-output");
|
|
@@ -95,26 +94,6 @@ function materializeWorkspace(workspace, layers, localPackages) {
|
|
|
95
94
|
};
|
|
96
95
|
(0, node_fs_1.writeFileSync)(packagePath, `${JSON.stringify(workspacePackage, null, 2)}\n`);
|
|
97
96
|
}
|
|
98
|
-
async function buildBundles(context) {
|
|
99
|
-
const [firstStep] = build_action_1.BUILD_STEPS;
|
|
100
|
-
await (0, cli_1.getProcessTasks)().run(firstStep.running, async (task) => {
|
|
101
|
-
for (const step of build_action_1.BUILD_STEPS) {
|
|
102
|
-
task.update(step.running);
|
|
103
|
-
const result = await (0, common_1.runFramedCommand)("pnpm", ["--silent", "run", step.script], {
|
|
104
|
-
name: step.name,
|
|
105
|
-
cwd: context.workspace,
|
|
106
|
-
env: context.env,
|
|
107
|
-
mapLine: context.mapLine,
|
|
108
|
-
onLine: (line) => context.warnings.read(line),
|
|
109
|
-
});
|
|
110
|
-
if (result.code !== 0) {
|
|
111
|
-
task.fail(step.failed);
|
|
112
|
-
throw (0, build_action_1.buildStepFailure)(step, result, context.isVerbose, verify_source_result_1.VERIFY_SOURCE_COMMAND);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}, { done: "Built client, SSR and e-mail bundles" });
|
|
116
|
-
(0, vite_output_1.reportViteWarnings)(context.warnings, context.isVerbose, (0, cli_1.getProcessUi)());
|
|
117
|
-
}
|
|
118
97
|
function checkClientBundle(workspace) {
|
|
119
98
|
const clientRoot = (0, node_path_1.join)(workspace, "dist", "client");
|
|
120
99
|
const manifest = readJson((0, node_path_1.join)(clientRoot, ".vite", "manifest.json"));
|
|
@@ -133,40 +112,6 @@ function checkClientBundle(workspace) {
|
|
|
133
112
|
for (const marker of RUNTIME_MARKERS)
|
|
134
113
|
ensure(javascript.includes(marker), `${marker} is not reachable at runtime`);
|
|
135
114
|
}
|
|
136
|
-
async function typeCheck(context) {
|
|
137
|
-
const typecheckArguments = [
|
|
138
|
-
"--import",
|
|
139
|
-
"./typecheck-loader.mjs",
|
|
140
|
-
"./node_modules/vue-tsc/bin/vue-tsc.js",
|
|
141
|
-
"--noEmit",
|
|
142
|
-
"--pretty",
|
|
143
|
-
];
|
|
144
|
-
const isWindows = process.platform === "win32";
|
|
145
|
-
const command = isWindows ? process.execPath : "env";
|
|
146
|
-
const commandArguments = isWindows
|
|
147
|
-
? typecheckArguments
|
|
148
|
-
: ["-u", "NODE_OPTIONS", process.execPath, ...typecheckArguments];
|
|
149
|
-
await (0, cli_1.getProcessTasks)().run("Type checking", async (task) => {
|
|
150
|
-
const result = await (0, common_1.runFramedCommand)(command, commandArguments, {
|
|
151
|
-
name: "vue-tsc",
|
|
152
|
-
cwd: context.workspace,
|
|
153
|
-
mapLine: context.mapLine,
|
|
154
|
-
});
|
|
155
|
-
if (result.code === 0)
|
|
156
|
-
return;
|
|
157
|
-
const report = (0, typecheck_output_1.parseTypeCheckOutput)(result.lines);
|
|
158
|
-
const count = report.count > 0 ? [(0, cli_1.pluralize)(report.count, "error")] : [];
|
|
159
|
-
task.fail(["Type check failed", ...count].join((0, cli_1.getProcessUi)().symbols.separator));
|
|
160
|
-
throw new cli_1.CliError((0, typecheck_output_1.describeTypeCheckErrors)(report, verify_source_result_1.VERIFY_SOURCE_COMMAND) ??
|
|
161
|
-
(0, child_output_1.describeChildFailure)({
|
|
162
|
-
title: "Type check failed",
|
|
163
|
-
command: "vue-tsc --noEmit",
|
|
164
|
-
code: result.code,
|
|
165
|
-
lines: result.lines,
|
|
166
|
-
isVerbose: context.isVerbose,
|
|
167
|
-
}));
|
|
168
|
-
}, { done: "Type check passed" });
|
|
169
|
-
}
|
|
170
115
|
const EMAIL_CASES = [
|
|
171
116
|
{
|
|
172
117
|
name: "EmailAdminInvite",
|
|
@@ -293,9 +238,9 @@ async function verifySources() {
|
|
|
293
238
|
const tasks = (0, cli_1.getProcessTasks)();
|
|
294
239
|
await tasks.run(`Materializing ${(0, cli_1.pluralize)(layers.length, "module")}`, async () => materializeWorkspace(workspace, layers, options.localPackages), { done: `Materialized ${(0, cli_1.pluralize)(layers.length, "module")}` });
|
|
295
240
|
await (0, common_1.installDeps)(workspace, context.mapLine, ["--ignore-scripts"]);
|
|
296
|
-
await
|
|
241
|
+
await (0, verify_source_build_1.prepareTypes)(context);
|
|
242
|
+
await (0, verify_source_build_1.buildAndTypeCheck)(context);
|
|
297
243
|
await tasks.run("Checking the client bundle", async () => checkClientBundle(workspace), { done: "Client bundle checked", failed: "Client bundle check failed" });
|
|
298
|
-
await typeCheck(context);
|
|
299
244
|
await tasks.run("Checking the e-mail contracts", async () => checkEmailContracts(workspace), { done: (line) => line, failed: "E-mail contracts failed" });
|
|
300
245
|
return { ok: true, modules: layers.length };
|
|
301
246
|
}
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"build:compress": "node compress-assets.mjs",
|
|
10
10
|
"build:ssr": "vite build --ssr ssr-renderer.ts --outDir dist/ssr",
|
|
11
11
|
"build:email": "vite build --config vite.email.config.ts",
|
|
12
|
+
"build:types": "node prepare-types.mjs",
|
|
12
13
|
"typecheck": "node --import ./typecheck-loader.mjs ./node_modules/vue-tsc/bin/vue-tsc.js --noEmit",
|
|
13
14
|
"start": "node server.mjs"
|
|
14
15
|
},
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Writes the files Vite generates and the type check reads (auto-imports.d.ts
|
|
2
|
+
// and the Nuxt UI templates) without building anything: it resolves
|
|
3
|
+
// vite.config.ts and runs its build hooks over an empty entry, in seconds
|
|
4
|
+
// where a client build takes a minute. `ajs dms verify-source` runs it first
|
|
5
|
+
// (`pnpm run build:types`), so the type check no longer waits for the client
|
|
6
|
+
// bundle.
|
|
7
|
+
|
|
8
|
+
import { build } from "vite";
|
|
9
|
+
|
|
10
|
+
const ENTRY = "dms-prepare-types-entry";
|
|
11
|
+
const RESOLVED_ENTRY = `\0${ENTRY}`;
|
|
12
|
+
|
|
13
|
+
await build({
|
|
14
|
+
configFile: "vite.config.ts",
|
|
15
|
+
plugins: [
|
|
16
|
+
{
|
|
17
|
+
name: "dms-prepare-types-entry",
|
|
18
|
+
enforce: "pre",
|
|
19
|
+
resolveId: (id) => (id === ENTRY ? RESOLVED_ENTRY : null),
|
|
20
|
+
load: (id) => (id === RESOLVED_ENTRY ? "export {};" : null),
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
build: {
|
|
24
|
+
write: false,
|
|
25
|
+
emptyOutDir: false,
|
|
26
|
+
rollupOptions: {
|
|
27
|
+
input: ENTRY,
|
|
28
|
+
// The entry is empty on purpose.
|
|
29
|
+
onwarn: (warning, warn) => {
|
|
30
|
+
if (warning.code !== "EMPTY_BUNDLE") warn(warning);
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Hop-by-hop headers describe the backend connection, not the client's one.
|
|
2
|
+
// Forwarded, an explicit `connection: keep-alive` overrode the client's own
|
|
3
|
+
// `Connection: close`, so Node kept the socket open until its idle timeout.
|
|
4
|
+
const HOP_BY_HOP_HEADERS = [
|
|
5
|
+
"connection",
|
|
6
|
+
"keep-alive",
|
|
7
|
+
"proxy-connection",
|
|
8
|
+
"te",
|
|
9
|
+
"trailer",
|
|
10
|
+
"transfer-encoding",
|
|
11
|
+
"upgrade",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
/** Writes the backend's status and end-to-end headers on `response`. */
|
|
15
|
+
export function writeProxyHeaders(response, upstream) {
|
|
16
|
+
const headers = Object.fromEntries(upstream.headers);
|
|
17
|
+
const named = (headers.connection ?? "")
|
|
18
|
+
.split(",")
|
|
19
|
+
.map((name) => name.trim().toLowerCase());
|
|
20
|
+
for (const name of [...HOP_BY_HOP_HEADERS, ...named]) delete headers[name];
|
|
21
|
+
delete headers["set-cookie"];
|
|
22
|
+
const cookies = upstream.headers.getSetCookie?.() ?? [];
|
|
23
|
+
if (cookies.length) headers["set-cookie"] = cookies;
|
|
24
|
+
response.writeHead(upstream.status, headers);
|
|
25
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { writeBadRequest } from "./request-failure.mjs";
|
|
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
|
+
const FRONTEND_ORIGIN = "http://frontend.local";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether `target` is an origin-form request target: a path, optionally with a
|
|
13
|
+
* query, that stays on this server once resolved. Anything else (an absolute
|
|
14
|
+
* or authority form, `*`, a path a URL parser reads as another host) is not a
|
|
15
|
+
* request a browser sends to the frontend.
|
|
16
|
+
*/
|
|
17
|
+
export function isOriginForm(target) {
|
|
18
|
+
if (typeof target !== "string" || !target.startsWith("/")) return false;
|
|
19
|
+
try {
|
|
20
|
+
return new URL(target, FRONTEND_ORIGIN).origin === FRONTEND_ORIGIN;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The backend URL a request target is proxied to: the target's normalized path
|
|
28
|
+
* and query on the backend's origin. The path is set, not resolved, so a path
|
|
29
|
+
* normalizing to `//host` (`/x/..//host`) stays a path on the backend.
|
|
30
|
+
*
|
|
31
|
+
* @param target The request target, origin-form
|
|
32
|
+
* @param backendBaseUrl The backend's base URL
|
|
33
|
+
*/
|
|
34
|
+
export function backendUrl(
|
|
35
|
+
target,
|
|
36
|
+
backendBaseUrl = process.env.DMS_API_BASE_URL,
|
|
37
|
+
) {
|
|
38
|
+
const { pathname, search } = new URL(target, FRONTEND_ORIGIN);
|
|
39
|
+
const url = new URL(new URL(backendBaseUrl).origin);
|
|
40
|
+
url.pathname = pathname;
|
|
41
|
+
url.search = search;
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Answers 400 to a request whose target is not origin-form, before any route
|
|
47
|
+
* reads it. Returns whether it answered.
|
|
48
|
+
*/
|
|
49
|
+
export function refuseForeignTarget(request, response) {
|
|
50
|
+
if (isOriginForm(request.url)) return false;
|
|
51
|
+
writeBadRequest(response);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
package/templates/vue/server.mjs
CHANGED
|
@@ -42,6 +42,8 @@ import { HOMEPAGE } from "./server/homepage.mjs";
|
|
|
42
42
|
import { htmlTag } from "./server/html-tag.mjs";
|
|
43
43
|
import * as icons from "./server/icons.mjs";
|
|
44
44
|
import * as requestFailure from "./server/request-failure.mjs";
|
|
45
|
+
import { writeProxyHeaders } from "./server/proxy-headers.mjs";
|
|
46
|
+
import { backendUrl, refuseForeignTarget } from "./server/request-target.mjs";
|
|
45
47
|
import { handleTester } from "./server/tester.mjs";
|
|
46
48
|
import {
|
|
47
49
|
createInertiaPage,
|
|
@@ -193,14 +195,6 @@ async function pageProps(request) {
|
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
|
|
196
|
-
function writeProxyHeaders(response, upstream) {
|
|
197
|
-
const headers = Object.fromEntries(upstream.headers);
|
|
198
|
-
delete headers["set-cookie"];
|
|
199
|
-
const cookies = upstream.headers.getSetCookie?.() ?? [];
|
|
200
|
-
if (cookies.length) headers["set-cookie"] = cookies;
|
|
201
|
-
response.writeHead(upstream.status, headers);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
198
|
function redirectToHomepage(request, response, pathname) {
|
|
205
199
|
if (pathname !== "/" || HOMEPAGE === "/") return false;
|
|
206
200
|
redirectFrontendVisit(request, response, HOMEPAGE);
|
|
@@ -239,12 +233,11 @@ async function proxy(request, response, fallbackOnNotFound = false) {
|
|
|
239
233
|
refuseCrossOrigin(response);
|
|
240
234
|
return true;
|
|
241
235
|
}
|
|
242
|
-
const target = new URL(request.url, process.env.DMS_API_BASE_URL);
|
|
243
236
|
// Cancelling the body alone misses a client that leaves before the backend
|
|
244
237
|
// answers: its stream, an SSE feed above all, then stayed open for good.
|
|
245
238
|
const abandoned = new AbortController();
|
|
246
239
|
response.once("close", () => abandoned.abort());
|
|
247
|
-
const upstream = await fetch(
|
|
240
|
+
const upstream = await fetch(backendUrl(request.url), {
|
|
248
241
|
signal: abandoned.signal,
|
|
249
242
|
method: request.method,
|
|
250
243
|
headers: backendHeaders(request, publicEndpoint),
|
|
@@ -505,6 +498,7 @@ async function writeRequestFailure(error, request, response) {
|
|
|
505
498
|
|
|
506
499
|
export async function handleRequestSafely(request, response) {
|
|
507
500
|
try {
|
|
501
|
+
if (refuseForeignTarget(request, response)) return;
|
|
508
502
|
await handleRequest(request, response);
|
|
509
503
|
} catch (error) {
|
|
510
504
|
// An error page failing too must not reject: that would end the process.
|