@antelopejs/dms-frontend 0.5.6 → 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 (71) hide show
  1. package/README.md +376 -18
  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 +8 -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-runner.js +12 -0
  26. package/dist/workspace.d.ts +9 -0
  27. package/package.json +2 -1
  28. package/templates/vue/app-runtime.ts +58 -16
  29. package/templates/vue/client-server-guard.mjs +73 -0
  30. package/templates/vue/email-renderer.ts +17 -4
  31. package/templates/vue/email-runtime.ts +1 -1
  32. package/templates/vue/frontend-module.ts +1817 -259
  33. package/templates/vue/globals.d.ts +1 -1
  34. package/templates/vue/icon-hydration.ts +106 -1
  35. package/templates/vue/index.html +0 -1
  36. package/templates/vue/layer-app-config.ts +92 -0
  37. package/templates/vue/main.ts +10 -2
  38. package/templates/vue/package.json +4 -1
  39. package/templates/vue/server/auth/backend.mjs +13 -19
  40. package/templates/vue/server/auth/client-ip.mjs +27 -0
  41. package/templates/vue/server/auth/establish-endpoints.mjs +70 -0
  42. package/templates/vue/server/auth/session.mjs +6 -97
  43. package/templates/vue/server/auth-hooks.mjs +84 -0
  44. package/templates/vue/server/backend-proxies.mjs +110 -0
  45. package/templates/vue/server/dev-private-files.mjs +99 -0
  46. package/templates/vue/server/email.mjs +11 -33
  47. package/templates/vue/server/favicon.mjs +20 -0
  48. package/templates/vue/server/inertia.mjs +48 -3
  49. package/templates/vue/server/listen-host.mjs +19 -0
  50. package/templates/vue/server/module-event.mjs +265 -0
  51. package/templates/vue/server/module-routes.mjs +513 -0
  52. package/templates/vue/server/module-server.mjs +225 -0
  53. package/templates/vue/server/module-testing.mjs +65 -0
  54. package/templates/vue/server/page-props.mjs +151 -0
  55. package/templates/vue/server/path-patterns.mjs +38 -0
  56. package/templates/vue/server/public-endpoints.mjs +69 -11
  57. package/templates/vue/server/request-routing.mjs +47 -0
  58. package/templates/vue/server/route-rules.mjs +129 -0
  59. package/templates/vue/server-module.ts +294 -0
  60. package/templates/vue/server.mjs +177 -161
  61. package/templates/vue/ssr-renderer.ts +60 -19
  62. package/templates/vue/tsconfig.json +2 -1
  63. package/templates/vue/ui-app-config.ts +18 -6
  64. package/templates/vue/ui-inertia-page.mjs +37 -0
  65. package/templates/vue/vite.config.ts +31 -10
  66. package/templates/vue/vite.email.config.ts +1 -1
  67. package/templates/vue/vite.server.config.ts +57 -0
  68. package/templates/vue/server/auth/oauth.mjs +0 -233
  69. package/templates/vue/server/auth/routes.mjs +0 -444
  70. package/templates/vue/server/render-token.mjs +0 -81
  71. package/templates/vue/server/tester.mjs +0 -227
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.5.6",
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;