@antelopejs/dms-frontend 0.5.5 → 0.6.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 +377 -19
- package/dist/app-configs.d.ts +9 -0
- package/dist/app-configs.js +52 -0
- package/dist/asset-roots.d.ts +7 -0
- package/dist/asset-roots.js +34 -0
- package/dist/commands/build-action.js +9 -0
- package/dist/commands/dev-action.js +88 -19
- package/dist/commands/start-action.js +2 -0
- 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 -3
- package/dist/derived-outputs.d.ts +2 -0
- package/dist/derived-outputs.js +38 -31
- package/dist/help.js +10 -3
- package/dist/layer-watch.js +123 -131
- package/dist/layers.js +2 -0
- package/dist/manifest-secrets.js +5 -5
- package/dist/manifest.js +7 -1
- package/dist/materialize.js +21 -1
- package/dist/module-set.d.ts +16 -0
- package/dist/module-set.js +80 -0
- package/dist/server-process.d.ts +1 -0
- package/dist/server-process.js +28 -2
- package/dist/verify-source-build.d.ts +11 -0
- package/dist/verify-source-build.js +113 -0
- package/dist/verify-source-runner.js +14 -57
- package/dist/workspace.d.ts +9 -0
- package/package.json +2 -1
- package/templates/vue/app-runtime.ts +58 -16
- package/templates/vue/client-server-guard.mjs +73 -0
- package/templates/vue/email-renderer.ts +17 -4
- package/templates/vue/email-runtime.ts +1 -1
- package/templates/vue/frontend-module.ts +1817 -259
- package/templates/vue/globals.d.ts +1 -1
- package/templates/vue/icon-hydration.ts +106 -1
- package/templates/vue/index.html +0 -1
- package/templates/vue/layer-app-config.ts +92 -0
- package/templates/vue/main.ts +10 -2
- package/templates/vue/package.json +5 -1
- package/templates/vue/prepare-types.mjs +34 -0
- package/templates/vue/server/auth/backend.mjs +13 -19
- package/templates/vue/server/auth/client-ip.mjs +27 -0
- package/templates/vue/server/auth/establish-endpoints.mjs +70 -0
- package/templates/vue/server/auth/session.mjs +6 -97
- package/templates/vue/server/auth-hooks.mjs +84 -0
- package/templates/vue/server/backend-proxies.mjs +110 -0
- package/templates/vue/server/dev-private-files.mjs +99 -0
- package/templates/vue/server/email.mjs +11 -33
- package/templates/vue/server/favicon.mjs +20 -0
- package/templates/vue/server/inertia.mjs +48 -3
- package/templates/vue/server/listen-host.mjs +19 -0
- package/templates/vue/server/module-event.mjs +265 -0
- package/templates/vue/server/module-routes.mjs +513 -0
- package/templates/vue/server/module-server.mjs +225 -0
- package/templates/vue/server/module-testing.mjs +65 -0
- package/templates/vue/server/page-props.mjs +151 -0
- package/templates/vue/server/path-patterns.mjs +38 -0
- package/templates/vue/server/proxy-headers.mjs +25 -0
- package/templates/vue/server/public-endpoints.mjs +69 -11
- package/templates/vue/server/request-routing.mjs +47 -0
- package/templates/vue/server/route-rules.mjs +129 -0
- package/templates/vue/server-module.ts +294 -0
- package/templates/vue/server.mjs +178 -169
- package/templates/vue/ssr-renderer.ts +60 -19
- package/templates/vue/tsconfig.json +2 -1
- package/templates/vue/ui-app-config.ts +18 -6
- package/templates/vue/ui-inertia-page.mjs +37 -0
- package/templates/vue/vite.config.ts +31 -10
- package/templates/vue/vite.email.config.ts +1 -1
- package/templates/vue/vite.server.config.ts +57 -0
- package/templates/vue/server/auth/oauth.mjs +0 -233
- package/templates/vue/server/auth/routes.mjs +0 -444
- package/templates/vue/server/render-token.mjs +0 -81
- package/templates/vue/server/tester.mjs +0 -227
|
@@ -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,39 +112,16 @@ 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
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
"
|
|
140
|
-
|
|
141
|
-
"
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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" });
|
|
115
|
+
async function checkServerModulesBundle(workspace) {
|
|
116
|
+
const root = (0, node_path_1.join)(workspace, "dist", "server-modules");
|
|
117
|
+
const javascript = (0, node_fs_1.readdirSync)(root, { recursive: true, encoding: "utf8" })
|
|
118
|
+
.filter((file) => file.endsWith(".js"))
|
|
119
|
+
.map((file) => (0, node_fs_1.readFileSync)((0, node_path_1.join)(root, file), "utf8"))
|
|
120
|
+
.join("\n");
|
|
121
|
+
ensure(!/\.vue["'?]|createApp\(|DefaultLayout/.test(javascript), "The server modules bundle loads application code", "A dms.server.ts imports server code only, never .vue files or dms.frontend.ts.");
|
|
122
|
+
const { serverModules } = (await Promise.resolve(`${(0, node_path_1.join)(root, "server-modules.generated.js")}`).then(s => __importStar(require(s))));
|
|
123
|
+
ensure(Array.isArray(serverModules), "The server modules bundle exports none");
|
|
124
|
+
return `${(0, cli_1.pluralize)(serverModules.length, "server module")}, ${(0, output_1.formatSize)(Buffer.byteLength(javascript))} JS`;
|
|
169
125
|
}
|
|
170
126
|
const EMAIL_CASES = [
|
|
171
127
|
{
|
|
@@ -293,10 +249,11 @@ async function verifySources() {
|
|
|
293
249
|
const tasks = (0, cli_1.getProcessTasks)();
|
|
294
250
|
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
251
|
await (0, common_1.installDeps)(workspace, context.mapLine, ["--ignore-scripts"]);
|
|
296
|
-
await
|
|
252
|
+
await (0, verify_source_build_1.prepareTypes)(context);
|
|
253
|
+
await (0, verify_source_build_1.buildAndTypeCheck)(context);
|
|
297
254
|
await tasks.run("Checking the client bundle", async () => checkClientBundle(workspace), { done: "Client bundle checked", failed: "Client bundle check failed" });
|
|
298
|
-
await typeCheck(context);
|
|
299
255
|
await tasks.run("Checking the e-mail contracts", async () => checkEmailContracts(workspace), { done: (line) => line, failed: "E-mail contracts failed" });
|
|
256
|
+
await tasks.run("Checking the server modules", async () => checkServerModulesBundle(workspace), { done: (line) => line, failed: "Server modules check failed" });
|
|
300
257
|
return { ok: true, modules: layers.length };
|
|
301
258
|
}
|
|
302
259
|
function sendResult(result) {
|
package/dist/workspace.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface ManifestModule {
|
|
|
16
16
|
configKey?: string;
|
|
17
17
|
authEstablishEndpoints?: string[];
|
|
18
18
|
publicEndpoints?: PublicEndpointDeclaration[];
|
|
19
|
+
backendProxies?: BackendProxyDeclaration[];
|
|
20
|
+
}
|
|
21
|
+
export interface BackendProxyDeclaration {
|
|
22
|
+
path: string;
|
|
23
|
+
auth?: "session" | "none";
|
|
19
24
|
}
|
|
20
25
|
export interface PublicEndpointDeclaration {
|
|
21
26
|
path: string;
|
|
@@ -25,11 +30,13 @@ export interface PublicEndpointDeclaration {
|
|
|
25
30
|
export interface Manifest {
|
|
26
31
|
pack: string;
|
|
27
32
|
modules: ManifestModule[];
|
|
33
|
+
settling?: boolean;
|
|
28
34
|
}
|
|
29
35
|
export interface FrontendManifest {
|
|
30
36
|
version: number;
|
|
31
37
|
archive: string;
|
|
32
38
|
modules: ManifestModule[];
|
|
39
|
+
settling?: boolean;
|
|
33
40
|
}
|
|
34
41
|
export interface ResolvedLayer {
|
|
35
42
|
path: string;
|
|
@@ -39,8 +46,10 @@ export interface ResolvedLayer {
|
|
|
39
46
|
priority?: number;
|
|
40
47
|
configKey?: string;
|
|
41
48
|
options?: FrontendModuleOptions;
|
|
49
|
+
privateOptions?: FrontendModuleOptions;
|
|
42
50
|
authEstablishEndpoints?: string[];
|
|
43
51
|
publicEndpoints?: PublicEndpointDeclaration[];
|
|
52
|
+
backendProxies?: BackendProxyDeclaration[];
|
|
44
53
|
}
|
|
45
54
|
export declare function getWorkspaceDirForKey(key: string): string;
|
|
46
55
|
export declare function projectWorkspaceKey(projectDir: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelope",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"@vitejs/plugin-vue": "^6.0.1",
|
|
86
86
|
"@vue/server-renderer": "^3.5.25",
|
|
87
87
|
"defu": "^6.1.4",
|
|
88
|
+
"devalue": "^5.9.4",
|
|
88
89
|
"knip": "^6.34.0",
|
|
89
90
|
"ofetch": "^1.5.1",
|
|
90
91
|
"oxfmt": "^0.66.0",
|
|
@@ -7,6 +7,7 @@ import ui from "@nuxt/ui/vue-plugin";
|
|
|
7
7
|
import { useHead } from "@unhead/vue";
|
|
8
8
|
import type { VueHeadClient } from "@unhead/vue/types";
|
|
9
9
|
import type { IconifyIcon } from "@iconify/vue";
|
|
10
|
+
import { parse } from "devalue";
|
|
10
11
|
import type { FetchOptions } from "ofetch";
|
|
11
12
|
import {
|
|
12
13
|
type App,
|
|
@@ -22,8 +23,10 @@ import {
|
|
|
22
23
|
import { createI18n, useI18n } from "vue-i18n";
|
|
23
24
|
import DmsDynamicPage from "./DmsDynamicPage.vue";
|
|
24
25
|
import {
|
|
26
|
+
type DmsLayerAppConfig,
|
|
25
27
|
type DmsLocaleLoader,
|
|
26
28
|
type DmsPageProps,
|
|
29
|
+
type DmsPayload,
|
|
27
30
|
type DmsUser,
|
|
28
31
|
createDmsFrontendRuntime,
|
|
29
32
|
dmsPageSuspenseProps,
|
|
@@ -32,18 +35,22 @@ import {
|
|
|
32
35
|
getDmsLayoutProps,
|
|
33
36
|
getDmsPage,
|
|
34
37
|
hydrateDmsPageProps,
|
|
38
|
+
hydrateDmsSharedProps,
|
|
35
39
|
installDmsPlugins,
|
|
40
|
+
keepSharedOverHistory,
|
|
36
41
|
preloadDmsPage,
|
|
42
|
+
preloadPrefetchedDmsPages,
|
|
37
43
|
provideDmsFrontendRuntime,
|
|
38
44
|
readDmsLocaleCookie,
|
|
39
45
|
resolveDmsAsyncComponents,
|
|
40
46
|
resolveDmsComponent,
|
|
41
|
-
|
|
47
|
+
restoreDmsPayload,
|
|
42
48
|
trackDmsAsyncComponents,
|
|
49
|
+
updateDmsAppConfig,
|
|
43
50
|
useDmsAppConfig,
|
|
44
51
|
useDmsState,
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
useDmsError,
|
|
53
|
+
useDmsUserSession,
|
|
47
54
|
} from "./frontend-module";
|
|
48
55
|
import {
|
|
49
56
|
SSR_ICONS_ID,
|
|
@@ -87,7 +94,7 @@ export type DmsServerFetch = (
|
|
|
87
94
|
options?: FetchOptions,
|
|
88
95
|
) => Promise<unknown>;
|
|
89
96
|
|
|
90
|
-
const
|
|
97
|
+
export const DMS_PAYLOAD_ID = "dms-payload";
|
|
91
98
|
export const SSR_ASYNC_COMPONENTS_ID = "dms-ssr-async-components";
|
|
92
99
|
const DEFAULT_LOCALE = "en";
|
|
93
100
|
|
|
@@ -112,17 +119,38 @@ function userLocale(user: DmsUser | null | undefined): string | undefined {
|
|
|
112
119
|
return supportedLocales.includes(locale) ? locale : undefined;
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
function readServerRendered<T>(
|
|
122
|
+
function readServerRendered<T>(
|
|
123
|
+
id: string,
|
|
124
|
+
empty: T,
|
|
125
|
+
parseText: (text: string) => unknown = JSON.parse,
|
|
126
|
+
): T {
|
|
116
127
|
if (typeof document === "undefined") return empty;
|
|
117
128
|
const element = document.getElementById(id);
|
|
118
129
|
if (!element?.textContent) return empty;
|
|
119
|
-
const value =
|
|
130
|
+
const value = parseText(element.textContent) as T;
|
|
120
131
|
element.remove();
|
|
121
132
|
return value;
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
drawLoadedIconsAtOnce();
|
|
125
136
|
|
|
137
|
+
/** A browser runtime, started from what the server rendered, if it did. */
|
|
138
|
+
function createBrowserRuntime(
|
|
139
|
+
serverFetch?: DmsServerFetch,
|
|
140
|
+
): DmsConfiguredApp["runtime"] {
|
|
141
|
+
const runtime = createDmsFrontendRuntime(
|
|
142
|
+
serverFetch as typeof import("ofetch").ofetch,
|
|
143
|
+
);
|
|
144
|
+
// Nothing when the server did not render the page: no hydration then.
|
|
145
|
+
const payload = readServerRendered<DmsPayload | undefined>(
|
|
146
|
+
DMS_PAYLOAD_ID,
|
|
147
|
+
undefined,
|
|
148
|
+
parse,
|
|
149
|
+
);
|
|
150
|
+
if (payload) restoreDmsPayload(runtime, payload);
|
|
151
|
+
return runtime;
|
|
152
|
+
}
|
|
153
|
+
|
|
126
154
|
const DmsInertiaPage = defineComponent({
|
|
127
155
|
name: "DmsInertiaPage",
|
|
128
156
|
inheritAttrs: false,
|
|
@@ -152,7 +180,7 @@ const DmsPersistentLayout = defineComponent({
|
|
|
152
180
|
setup(_, { attrs, slots }) {
|
|
153
181
|
const props = computed(() => attrs as unknown as DmsPageProps);
|
|
154
182
|
const i18n = useI18n();
|
|
155
|
-
const { user } =
|
|
183
|
+
const { user } = useDmsUserSession();
|
|
156
184
|
let localeVersion = 0;
|
|
157
185
|
useHead({
|
|
158
186
|
htmlAttrs: { lang: computed(() => i18n.locale.value) },
|
|
@@ -162,6 +190,13 @@ const DmsPersistentLayout = defineComponent({
|
|
|
162
190
|
() => hydrateDmsPageProps(props.value),
|
|
163
191
|
{ immediate: true, flush: "sync" },
|
|
164
192
|
);
|
|
193
|
+
// Apart from the page: a reload of the site layout alone (`router.reload({
|
|
194
|
+
// only: ["shared"] })`) is not a navigation.
|
|
195
|
+
watch(
|
|
196
|
+
() => attrs.shared,
|
|
197
|
+
() => hydrateDmsSharedProps(props.value.shared),
|
|
198
|
+
{ flush: "sync" },
|
|
199
|
+
);
|
|
165
200
|
// The session, not the page props: a page restored from history or from
|
|
166
201
|
// the prefetch cache carries the user as it was when it was fetched, and
|
|
167
202
|
// would switch the interface back to a language the user has left. A
|
|
@@ -178,7 +213,7 @@ const DmsPersistentLayout = defineComponent({
|
|
|
178
213
|
},
|
|
179
214
|
{ immediate: true, flush: "sync" },
|
|
180
215
|
);
|
|
181
|
-
const currentError =
|
|
216
|
+
const currentError = useDmsError();
|
|
182
217
|
const overlays = useDmsState<string[]>("dms-app-overlays", () => []);
|
|
183
218
|
return () => {
|
|
184
219
|
const error = currentError.value ?? props.value.error;
|
|
@@ -206,7 +241,7 @@ export function resolveDmsInertiaPage(): Component {
|
|
|
206
241
|
|
|
207
242
|
function renderDmsPage(
|
|
208
243
|
props: DmsPageProps,
|
|
209
|
-
capturedError: DmsPageProps["error"] =
|
|
244
|
+
capturedError: DmsPageProps["error"] = useDmsError().value,
|
|
210
245
|
) {
|
|
211
246
|
const error = capturedError ?? props.error;
|
|
212
247
|
const page = error
|
|
@@ -251,16 +286,21 @@ function mergeDmsAppConfig(): void {
|
|
|
251
286
|
mergeUiAppConfig(useAppConfig(), useDmsAppConfig());
|
|
252
287
|
}
|
|
253
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Development only: applies the layers' app configs, edited while the page is
|
|
291
|
+
* open, to the DMS app config and to Nuxt UI's, both in place, so the
|
|
292
|
+
* components reading them and Nuxt UI's colors follow without a reload.
|
|
293
|
+
*/
|
|
294
|
+
export function applyLayerAppConfigs(appConfigs: DmsLayerAppConfig[]): void {
|
|
295
|
+
updateDmsAppConfig(appConfigs);
|
|
296
|
+
mergeUiAppConfig(useAppConfig(), useDmsAppConfig(), { isChanged: true });
|
|
297
|
+
}
|
|
298
|
+
|
|
254
299
|
export async function configureDmsApp(
|
|
255
300
|
options: DmsAppOptions,
|
|
256
301
|
): Promise<DmsConfiguredApp> {
|
|
257
302
|
mergeDmsAppConfig();
|
|
258
|
-
const runtime =
|
|
259
|
-
options.runtime ??
|
|
260
|
-
createDmsFrontendRuntime(
|
|
261
|
-
options.serverFetch as typeof import("ofetch").ofetch,
|
|
262
|
-
readServerRendered<Record<string, unknown>>(SSR_ASYNC_DATA_ID, {}),
|
|
263
|
-
);
|
|
303
|
+
const runtime = options.runtime ?? createBrowserRuntime(options.serverFetch);
|
|
264
304
|
// The signed-in user's language, else the one this browser picked, which is
|
|
265
305
|
// how a visitor's choice on a public page survives a reload.
|
|
266
306
|
const locale =
|
|
@@ -289,6 +329,8 @@ export async function configureDmsApp(
|
|
|
289
329
|
if (typeof window !== "undefined") {
|
|
290
330
|
const toast = options.app.runWithContext(() => useToast());
|
|
291
331
|
showNetworkErrors(toast.add, i18n.global);
|
|
332
|
+
preloadPrefetchedDmsPages();
|
|
333
|
+
options.app.runWithContext(keepSharedOverHistory);
|
|
292
334
|
}
|
|
293
335
|
// Resolve, before the app mounts, every async component the server
|
|
294
336
|
// rendered: the page, its layout, and the components the render reached.
|
|
@@ -309,4 +351,4 @@ export async function configureDmsApp(
|
|
|
309
351
|
return { mounted, runtime };
|
|
310
352
|
}
|
|
311
353
|
|
|
312
|
-
export {
|
|
354
|
+
export { trackDmsAsyncComponents };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Keeping server code out of the browser.
|
|
2
|
+
//
|
|
3
|
+
// A module's `dms.server.ts` and the files under its `server/` directories run
|
|
4
|
+
// in the generated server only: they read private options, sign tokens, talk
|
|
5
|
+
// to the backend with its secrets. A client file that imports one of them,
|
|
6
|
+
// even statically and through an alias, would ship that code in the client
|
|
7
|
+
// bundle. Rollup's manifest only lists entries and dynamic imports, so a check
|
|
8
|
+
// on the built output misses a static import; the guard sits in the module
|
|
9
|
+
// graph instead, where every resolved file passes.
|
|
10
|
+
//
|
|
11
|
+
// Kept beside vite.config.ts rather than inside it so the plugin can be
|
|
12
|
+
// exercised on its own, the way head-order.mjs is.
|
|
13
|
+
|
|
14
|
+
import { relative, sep } from "node:path";
|
|
15
|
+
|
|
16
|
+
const SERVER_ENTRY = /(?:^|[\\/])dms\.server\.[cm]?[jt]sx?$/;
|
|
17
|
+
|
|
18
|
+
function stripQuery(id) {
|
|
19
|
+
const query = id.indexOf("?");
|
|
20
|
+
return query === -1 ? id : id.slice(0, query);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function insideModuleServerDirectory(file, moduleRoots) {
|
|
24
|
+
return moduleRoots.some((root) => {
|
|
25
|
+
const path = relative(root, file);
|
|
26
|
+
if (!path || path.startsWith("..") || path.startsWith(sep)) return false;
|
|
27
|
+
const segments = path.split(/[\\/]/);
|
|
28
|
+
return segments.slice(0, -1).includes("server");
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether `id`, a resolved module id, is server code: a module's server entry
|
|
34
|
+
* (`dms.server.*`) or a file under a `server/` directory of a module. Installed
|
|
35
|
+
* packages never are: `node_modules` has its own `server` directories.
|
|
36
|
+
*/
|
|
37
|
+
export function isServerSource(id, moduleRoots) {
|
|
38
|
+
if (id.startsWith("\0")) return false;
|
|
39
|
+
const file = stripQuery(id);
|
|
40
|
+
if (/[\\/]node_modules[\\/]/.test(file)) return false;
|
|
41
|
+
return (
|
|
42
|
+
SERVER_ENTRY.test(file) || insideModuleServerDirectory(file, moduleRoots)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fail a client build, and a client request in development, that loads server
|
|
48
|
+
* code. Server-side rendering and the server modules build load it through
|
|
49
|
+
* SSR, which the guard leaves alone.
|
|
50
|
+
*/
|
|
51
|
+
export function rejectServerSourcesInClient(moduleRoots) {
|
|
52
|
+
let ssrBuild = false;
|
|
53
|
+
return {
|
|
54
|
+
name: "dms-reject-server-sources-in-client",
|
|
55
|
+
enforce: "pre",
|
|
56
|
+
configResolved(config) {
|
|
57
|
+
ssrBuild = Boolean(config.build.ssr);
|
|
58
|
+
},
|
|
59
|
+
load(id, options) {
|
|
60
|
+
if (ssrBuild || options?.ssr) return null;
|
|
61
|
+
if (!isServerSource(id, moduleRoots)) return null;
|
|
62
|
+
const importers = this.getModuleInfo?.(id)?.importers ?? [];
|
|
63
|
+
const from = importers.length
|
|
64
|
+
? ` (imported by ${importers.join(", ")})`
|
|
65
|
+
: "";
|
|
66
|
+
this.error(
|
|
67
|
+
`Server code in the client bundle: ${stripQuery(id)}${from}. ` +
|
|
68
|
+
"A dms.server.ts and the files under a module's server/ directory " +
|
|
69
|
+
"run in the server only; import them from a dms.server.ts, never from client code.",
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -2,6 +2,7 @@ import { renderToString } from "@vue/server-renderer";
|
|
|
2
2
|
import { defu } from "defu";
|
|
3
3
|
import { type Component, createSSRApp, h } from "vue";
|
|
4
4
|
import { createI18n } from "vue-i18n";
|
|
5
|
+
import { appConfigs } from "./app-configs.generated";
|
|
5
6
|
import moduleRegistry from "./generated-frontend-modules.json";
|
|
6
7
|
import { loadEmailLocaleMessages } from "./email-locales";
|
|
7
8
|
import { stripSsrMarkers } from "./email-markup";
|
|
@@ -28,6 +29,21 @@ const orderedRegistries = moduleRegistry.modules
|
|
|
28
29
|
.map((module) => registries[`/frontend-modules/${module.id}/dms.email.ts`])
|
|
29
30
|
.filter((registry): registry is EmailRegistry => Boolean(registry));
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Every app config an email reads, highest precedence first: module by module,
|
|
34
|
+
* by descending priority, what its `dms.email.ts` declares for emails, then
|
|
35
|
+
* its layers' `app/app.config.ts`. A higher-priority module's layers still
|
|
36
|
+
* override a lower one's email config, and a module's own email config, the
|
|
37
|
+
* more specific of its two, keeps the values it gave emails before the layers'
|
|
38
|
+
* configs applied to them.
|
|
39
|
+
*/
|
|
40
|
+
const emailAppConfigs = moduleRegistry.modules.flatMap((module) => [
|
|
41
|
+
registries[`/frontend-modules/${module.id}/dms.email.ts`]?.appConfig ?? {},
|
|
42
|
+
...appConfigs
|
|
43
|
+
.filter((layer) => layer.module === module.id)
|
|
44
|
+
.map((layer) => layer.config),
|
|
45
|
+
]);
|
|
46
|
+
|
|
31
47
|
function templateEntries(): Array<[string, () => Promise<EmailModule>]> {
|
|
32
48
|
return orderedRegistries.flatMap((registry) =>
|
|
33
49
|
Object.entries(registry.serverEmailTemplates),
|
|
@@ -138,10 +154,7 @@ export async function renderEmail(
|
|
|
138
154
|
}),
|
|
139
155
|
);
|
|
140
156
|
app.provide("dmsEmailLocale", locale);
|
|
141
|
-
app.provide(
|
|
142
|
-
"dmsEmailAppConfig",
|
|
143
|
-
defu({}, ...orderedRegistries.map((registry) => registry.appConfig ?? {})),
|
|
144
|
-
);
|
|
157
|
+
app.provide("dmsEmailAppConfig", defu({}, ...emailAppConfigs));
|
|
145
158
|
const publicConfig = defu(
|
|
146
159
|
{},
|
|
147
160
|
...moduleRegistry.modules.map((module) => module.options),
|
|
@@ -16,7 +16,7 @@ export function useDmsRuntimeConfig(): EmailRuntimeConfig {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function
|
|
19
|
+
export function defineDmsAppConfig<T extends Record<string, unknown>>(
|
|
20
20
|
config: T,
|
|
21
21
|
): T {
|
|
22
22
|
return config;
|