@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.
Files changed (75) hide show
  1. package/README.md +377 -19
  2. package/dist/app-configs.d.ts +9 -0
  3. package/dist/app-configs.js +52 -0
  4. package/dist/asset-roots.d.ts +7 -0
  5. package/dist/asset-roots.js +34 -0
  6. package/dist/commands/build-action.js +9 -0
  7. package/dist/commands/dev-action.js +88 -19
  8. package/dist/commands/start-action.js +2 -0
  9. package/dist/common.d.ts +1 -0
  10. package/dist/common.js +1 -0
  11. package/dist/config.d.ts +2 -0
  12. package/dist/config.js +9 -3
  13. package/dist/derived-outputs.d.ts +2 -0
  14. package/dist/derived-outputs.js +38 -31
  15. package/dist/help.js +10 -3
  16. package/dist/layer-watch.js +123 -131
  17. package/dist/layers.js +2 -0
  18. package/dist/manifest-secrets.js +5 -5
  19. package/dist/manifest.js +7 -1
  20. package/dist/materialize.js +21 -1
  21. package/dist/module-set.d.ts +16 -0
  22. package/dist/module-set.js +80 -0
  23. package/dist/server-process.d.ts +1 -0
  24. package/dist/server-process.js +28 -2
  25. package/dist/verify-source-build.d.ts +11 -0
  26. package/dist/verify-source-build.js +113 -0
  27. package/dist/verify-source-runner.js +14 -57
  28. package/dist/workspace.d.ts +9 -0
  29. package/package.json +2 -1
  30. package/templates/vue/app-runtime.ts +58 -16
  31. package/templates/vue/client-server-guard.mjs +73 -0
  32. package/templates/vue/email-renderer.ts +17 -4
  33. package/templates/vue/email-runtime.ts +1 -1
  34. package/templates/vue/frontend-module.ts +1817 -259
  35. package/templates/vue/globals.d.ts +1 -1
  36. package/templates/vue/icon-hydration.ts +106 -1
  37. package/templates/vue/index.html +0 -1
  38. package/templates/vue/layer-app-config.ts +92 -0
  39. package/templates/vue/main.ts +10 -2
  40. package/templates/vue/package.json +5 -1
  41. package/templates/vue/prepare-types.mjs +34 -0
  42. package/templates/vue/server/auth/backend.mjs +13 -19
  43. package/templates/vue/server/auth/client-ip.mjs +27 -0
  44. package/templates/vue/server/auth/establish-endpoints.mjs +70 -0
  45. package/templates/vue/server/auth/session.mjs +6 -97
  46. package/templates/vue/server/auth-hooks.mjs +84 -0
  47. package/templates/vue/server/backend-proxies.mjs +110 -0
  48. package/templates/vue/server/dev-private-files.mjs +99 -0
  49. package/templates/vue/server/email.mjs +11 -33
  50. package/templates/vue/server/favicon.mjs +20 -0
  51. package/templates/vue/server/inertia.mjs +48 -3
  52. package/templates/vue/server/listen-host.mjs +19 -0
  53. package/templates/vue/server/module-event.mjs +265 -0
  54. package/templates/vue/server/module-routes.mjs +513 -0
  55. package/templates/vue/server/module-server.mjs +225 -0
  56. package/templates/vue/server/module-testing.mjs +65 -0
  57. package/templates/vue/server/page-props.mjs +151 -0
  58. package/templates/vue/server/path-patterns.mjs +38 -0
  59. package/templates/vue/server/proxy-headers.mjs +25 -0
  60. package/templates/vue/server/public-endpoints.mjs +69 -11
  61. package/templates/vue/server/request-routing.mjs +47 -0
  62. package/templates/vue/server/route-rules.mjs +129 -0
  63. package/templates/vue/server-module.ts +294 -0
  64. package/templates/vue/server.mjs +178 -169
  65. package/templates/vue/ssr-renderer.ts +60 -19
  66. package/templates/vue/tsconfig.json +2 -1
  67. package/templates/vue/ui-app-config.ts +18 -6
  68. package/templates/vue/ui-inertia-page.mjs +37 -0
  69. package/templates/vue/vite.config.ts +31 -10
  70. package/templates/vue/vite.email.config.ts +1 -1
  71. package/templates/vue/vite.server.config.ts +57 -0
  72. package/templates/vue/server/auth/oauth.mjs +0 -233
  73. package/templates/vue/server/auth/routes.mjs +0 -444
  74. package/templates/vue/server/render-token.mjs +0 -81
  75. 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 typecheck_output_1 = require("./typecheck-output");
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 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" });
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 buildBundles(context);
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) {
@@ -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.5.5",
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
- serializeDmsAsyncData,
47
+ restoreDmsPayload,
42
48
  trackDmsAsyncComponents,
49
+ updateDmsAppConfig,
43
50
  useDmsAppConfig,
44
51
  useDmsState,
45
- useError,
46
- useUserSession,
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 SSR_ASYNC_DATA_ID = "dms-ssr-async-data";
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>(id: string, empty: T): 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 = JSON.parse(element.textContent) as T;
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 } = useUserSession();
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 = useError();
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"] = useError().value,
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 { serializeDmsAsyncData, trackDmsAsyncComponents };
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 defineAppConfig<T extends Record<string, unknown>>(
19
+ export function defineDmsAppConfig<T extends Record<string, unknown>>(
20
20
  config: T,
21
21
  ): T {
22
22
  return config;