@antelopejs/dms-frontend 0.3.11 → 0.4.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.
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readSourceOptions = readSourceOptions;
4
+ exports.readSources = readSources;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const common_1 = require("./common");
8
+ const output_1 = require("./output");
9
+ const verify_source_result_1 = require("./verify-source-result");
10
+ const DMS_PACKAGE = "@antelopejs/dms";
11
+ const DMS_CORE_LAYER_PACKAGE = "@antelopejs/dms-frontend-vue";
12
+ const CORE_LAYER_DIRECTORY = "frontend-vue";
13
+ const FRONTEND_ENTRY = "dms.frontend.ts";
14
+ const ADD_DEV_DEPENDENCY = {
15
+ pnpm: "pnpm add -D",
16
+ npm: "npm install -D",
17
+ yarn: "yarn add -D",
18
+ bun: "bun add -d",
19
+ };
20
+ const LOCKFILES = [
21
+ ["pnpm-lock.yaml", "pnpm"],
22
+ ["package-lock.json", "npm"],
23
+ ["yarn.lock", "yarn"],
24
+ ["bun.lock", "bun"],
25
+ ["bun.lockb", "bun"],
26
+ ];
27
+ function readSourceOptions() {
28
+ return {
29
+ projectDir: process.cwd(),
30
+ layerSource: process.env.DMS_LAYER_SOURCE || undefined,
31
+ moduleSources: JSON.parse(process.env.DMS_MODULE_SOURCES ?? "[]"),
32
+ localPackages: JSON.parse(process.env.DMS_LOCAL_PACKAGES ?? "{}"),
33
+ };
34
+ }
35
+ function readManifest(packagePath) {
36
+ return JSON.parse((0, node_fs_1.readFileSync)(packagePath, "utf8"));
37
+ }
38
+ function readLayer(root) {
39
+ const packagePath = (0, node_path_1.join)(root, "package.json");
40
+ if (!(0, node_fs_1.existsSync)(packagePath))
41
+ throw new common_1.UsageError({
42
+ title: `No package.json in ${(0, output_1.showPath)(root)}`,
43
+ fixes: [verify_source_result_1.LAYER_PATH_FIX],
44
+ });
45
+ const { name } = readManifest(packagePath);
46
+ return { path: root, sourcePath: root, packageName: name };
47
+ }
48
+ function isCoreLayer(layer) {
49
+ return layer.packageName === DMS_CORE_LAYER_PACKAGE;
50
+ }
51
+ function shellArgument(path) {
52
+ return /^[\w@%+=:,./~-]+$/.test(path) ? path : JSON.stringify(path);
53
+ }
54
+ function defaultModuleRoot(projectDir) {
55
+ const root = [projectDir, (0, node_path_1.join)(projectDir, CORE_LAYER_DIRECTORY)].find((candidate) => (0, node_fs_1.existsSync)((0, node_path_1.join)(candidate, FRONTEND_ENTRY)));
56
+ if (root)
57
+ return root;
58
+ throw new common_1.UsageError({
59
+ title: "No frontend module to verify",
60
+ reason: `Neither ${(0, output_1.showPath)(projectDir)} nor its ${CORE_LAYER_DIRECTORY} directory contains ${FRONTEND_ENTRY}.`,
61
+ fixes: [
62
+ `Run the command from the frontend module, or pass its root with -m`,
63
+ ],
64
+ });
65
+ }
66
+ function packageManager(projectDir) {
67
+ for (let directory = projectDir;; directory = (0, node_path_1.dirname)(directory)) {
68
+ const packagePath = (0, node_path_1.join)(directory, "package.json");
69
+ if ((0, node_fs_1.existsSync)(packagePath)) {
70
+ const [name] = (readManifest(packagePath).packageManager ?? "").split("@");
71
+ if (name && ADD_DEV_DEPENDENCY[name])
72
+ return name;
73
+ const lockfile = LOCKFILES.find(([file]) => (0, node_fs_1.existsSync)((0, node_path_1.join)(directory, file)));
74
+ if (lockfile)
75
+ return lockfile[1];
76
+ }
77
+ if ((0, node_path_1.dirname)(directory) === directory)
78
+ return "pnpm";
79
+ }
80
+ }
81
+ function addDmsCommand(projectDir, version = "") {
82
+ return `${ADD_DEV_DEPENDENCY[packageManager(projectDir)]} ${DMS_PACKAGE}${version}`;
83
+ }
84
+ function describeDirectory(directory) {
85
+ return (0, node_path_1.resolve)(directory) === process.cwd()
86
+ ? "the current directory"
87
+ : (0, output_1.showPath)(directory);
88
+ }
89
+ function resolveDmsPackage(projectDir) {
90
+ try {
91
+ return require.resolve(`${DMS_PACKAGE}/package.json`, {
92
+ paths: [projectDir],
93
+ });
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ }
99
+ function installedCoreLayer(projectDir) {
100
+ const packagePath = resolveDmsPackage(projectDir);
101
+ if (!packagePath)
102
+ throw new common_1.UsageError({
103
+ title: `${DMS_PACKAGE} is not installed in this project`,
104
+ reason: `Frontend modules are verified on top of the DMS core layer that ${DMS_PACKAGE} ships, and Node finds no ${DMS_PACKAGE} from ${describeDirectory(projectDir)}.`,
105
+ fixes: [
106
+ `Add it as a development dependency: ${addDmsCommand(projectDir)}`,
107
+ ],
108
+ });
109
+ const { version } = readManifest(packagePath);
110
+ const root = (0, node_path_1.join)((0, node_path_1.dirname)(packagePath), CORE_LAYER_DIRECTORY);
111
+ const layer = (0, node_fs_1.existsSync)((0, node_path_1.join)(root, "package.json"))
112
+ ? readLayer(root)
113
+ : undefined;
114
+ if (!layer || !isCoreLayer(layer))
115
+ throw new common_1.UsageError({
116
+ title: `${DMS_PACKAGE} ${version} ships no DMS core layer`,
117
+ reason: `${(0, output_1.showPath)((0, node_path_1.dirname)(packagePath))} has no ${CORE_LAYER_DIRECTORY} directory holding ${DMS_CORE_LAYER_PACKAGE}.`,
118
+ fixes: [`Update it: ${addDmsCommand(projectDir, "@latest")}`],
119
+ });
120
+ return {
121
+ layers: [layer],
122
+ coreLayer: `${DMS_PACKAGE} ${version} (installed in this project)`,
123
+ };
124
+ }
125
+ function notCoreLayer(sourceRoot, layers, modules, localPackages) {
126
+ const command = [
127
+ verify_source_result_1.VERIFY_SOURCE_COMMAND,
128
+ ...[...layers, ...modules].flatMap((layer) => [
129
+ "-m",
130
+ shellArgument((0, output_1.showPath)(layer.path)),
131
+ ]),
132
+ ...Object.entries(localPackages).flatMap(([name, path]) => [
133
+ "--local-package",
134
+ shellArgument(`${name}=${(0, output_1.showPath)(path)}`),
135
+ ]),
136
+ ].join(" ");
137
+ const names = layers
138
+ .map((layer) => layer.packageName ?? "a package without a name")
139
+ .join(", ");
140
+ const shown = (0, output_1.showPath)(sourceRoot);
141
+ return {
142
+ title: `${shown} is not the DMS core layer`,
143
+ reason: `--layer is for verifying an unpublished DMS core layer, ${DMS_CORE_LAYER_PACKAGE}, and ${shown} holds ${names}. ` +
144
+ `Without it, frontend modules are verified on the core layer of the ${DMS_PACKAGE} installed in the project.`,
145
+ fixes: [`Drop -l and pass the folder with -m: ${command}`],
146
+ };
147
+ }
148
+ function readLayerSource(layerSource) {
149
+ const sourceRoot = (0, node_path_1.resolve)(layerSource);
150
+ if (!(0, node_fs_1.statSync)(sourceRoot, { throwIfNoEntry: false })?.isDirectory())
151
+ throw new common_1.UsageError({
152
+ title: `Layer path not found: ${(0, output_1.showPath)(sourceRoot)}`,
153
+ fixes: [verify_source_result_1.LAYER_PATH_FIX],
154
+ });
155
+ const roots = (0, node_fs_1.existsSync)((0, node_path_1.join)(sourceRoot, FRONTEND_ENTRY))
156
+ ? [sourceRoot]
157
+ : (0, node_fs_1.readdirSync)(sourceRoot, { withFileTypes: true })
158
+ .filter((entry) => entry.isDirectory())
159
+ .map((entry) => (0, node_path_1.join)(sourceRoot, entry.name));
160
+ if (roots.length === 0)
161
+ throw new common_1.UsageError({
162
+ title: `No frontend package in ${(0, output_1.showPath)(sourceRoot)}`,
163
+ fixes: [verify_source_result_1.LAYER_PATH_FIX],
164
+ });
165
+ return { sourceRoot, layers: roots.map(readLayer) };
166
+ }
167
+ function readSources(options) {
168
+ const { projectDir, layerSource, localPackages } = options;
169
+ const modules = (options.moduleSources.length > 0 || layerSource
170
+ ? options.moduleSources.map((root) => (0, node_path_1.resolve)(root))
171
+ : [defaultModuleRoot(projectDir)]).map(readLayer);
172
+ if (layerSource) {
173
+ const { sourceRoot, layers } = readLayerSource(layerSource);
174
+ if (!layers.some(isCoreLayer))
175
+ throw new common_1.UsageError(notCoreLayer(sourceRoot, layers, modules, localPackages));
176
+ return {
177
+ layers: [...layers, ...modules],
178
+ coreLayer: `the DMS core layer in ${(0, output_1.showPath)(sourceRoot)}`,
179
+ };
180
+ }
181
+ const passedCore = modules.find(isCoreLayer);
182
+ if (passedCore)
183
+ return {
184
+ layers: [passedCore, ...modules.filter((layer) => layer !== passedCore)],
185
+ coreLayer: `the DMS core layer in ${(0, output_1.showPath)(passedCore.path)}`,
186
+ };
187
+ const installed = installedCoreLayer(projectDir);
188
+ return { ...installed, layers: [...installed.layers, ...modules] };
189
+ }
@@ -27,6 +27,7 @@ export interface FrontendManifest {
27
27
  }
28
28
  export interface ResolvedLayer {
29
29
  path: string;
30
+ name?: string;
30
31
  sourcePath?: string;
31
32
  packageName?: string;
32
33
  priority?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.3.11",
3
+ "version": "0.4.0",
4
4
  "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
@@ -37,6 +37,7 @@ interface PageRoute {
37
37
  interface PagePayload {
38
38
  layout?: PageLayout;
39
39
  route?: PageRoute;
40
+ module?: string;
40
41
  }
41
42
 
42
43
  interface DynamicPageProps {
@@ -55,7 +56,8 @@ function resolveDefinition(
55
56
  return {
56
57
  id,
57
58
  component: definition.componentName
58
- ? resolveDmsComponent(definition.componentName) || "div"
59
+ ? resolveDmsComponent(definition.componentName, props.page.module) ||
60
+ "div"
59
61
  : "div",
60
62
  componentName: definition.componentName,
61
63
  options: definition.options,
@@ -6,6 +6,7 @@ import { en as uiEn, fr as uiFr } from "@nuxt/ui/locale";
6
6
  import ui from "@nuxt/ui/vue-plugin";
7
7
  import { useHead } from "@unhead/vue";
8
8
  import type { VueHeadClient } from "@unhead/vue/types";
9
+ import type { IconifyIcon } from "@iconify/vue";
9
10
  import type { FetchOptions } from "ofetch";
10
11
  import {
11
12
  type App,
@@ -23,7 +24,9 @@ import DmsDynamicPage from "./DmsDynamicPage.vue";
23
24
  import {
24
25
  type DmsLocaleLoader,
25
26
  type DmsPageProps,
27
+ type DmsUser,
26
28
  createDmsFrontendRuntime,
29
+ dmsPageSuspenseProps,
27
30
  getDmsErrorPage,
28
31
  getDmsLayout,
29
32
  getDmsLayoutProps,
@@ -39,7 +42,13 @@ import {
39
42
  useDmsAppConfig,
40
43
  useDmsState,
41
44
  useError,
45
+ useUserSession,
42
46
  } from "./frontend-module";
47
+ import {
48
+ SSR_ICONS_ID,
49
+ addDmsIcons,
50
+ drawLoadedIconsAtOnce,
51
+ } from "./icon-hydration";
43
52
  import {
44
53
  loadLocaleMessages,
45
54
  localeMessages,
@@ -94,30 +103,24 @@ function uiLocale(locale: string): UiLocale {
94
103
  return UI_LOCALES[locale as keyof typeof UI_LOCALES] ?? UI_LOCALES.en;
95
104
  }
96
105
 
97
- function pageLocale(props: DmsPageProps): string {
98
- const language = props.user?.language;
99
- if (typeof language !== "string") return DEFAULT_LOCALE;
106
+ /** The supported locale of a user's language, if it has one. */
107
+ function userLocale(user: DmsUser | null | undefined): string | undefined {
108
+ const language = user?.language;
109
+ if (typeof language !== "string") return undefined;
100
110
  const locale = language.slice(0, 2);
101
- return supportedLocales.includes(locale) ? locale : DEFAULT_LOCALE;
111
+ return supportedLocales.includes(locale) ? locale : undefined;
102
112
  }
103
113
 
104
- function readDmsAsyncData(): Record<string, unknown> {
105
- if (typeof document === "undefined") return {};
106
- const element = document.getElementById(SSR_ASYNC_DATA_ID);
107
- if (!element?.textContent) return {};
108
- const data = JSON.parse(element.textContent) as Record<string, unknown>;
114
+ function readServerRendered<T>(id: string, empty: T): T {
115
+ if (typeof document === "undefined") return empty;
116
+ const element = document.getElementById(id);
117
+ if (!element?.textContent) return empty;
118
+ const value = JSON.parse(element.textContent) as T;
109
119
  element.remove();
110
- return data;
120
+ return value;
111
121
  }
112
122
 
113
- function readDmsAsyncComponents(): string[] {
114
- if (typeof document === "undefined") return [];
115
- const element = document.getElementById(SSR_ASYNC_COMPONENTS_ID);
116
- if (!element?.textContent) return [];
117
- const names = JSON.parse(element.textContent) as string[];
118
- element.remove();
119
- return names;
120
- }
123
+ drawLoadedIconsAtOnce();
121
124
 
122
125
  const DmsInertiaPage = defineComponent({
123
126
  name: "DmsInertiaPage",
@@ -148,17 +151,25 @@ const DmsPersistentLayout = defineComponent({
148
151
  setup(_, { attrs, slots }) {
149
152
  const props = computed(() => attrs as unknown as DmsPageProps);
150
153
  const i18n = useI18n();
154
+ const { user } = useUserSession();
151
155
  let localeVersion = 0;
152
156
  useHead({
153
157
  htmlAttrs: { lang: computed(() => i18n.locale.value) },
154
158
  });
155
159
  watch(
156
160
  () => [attrs.path, attrs.page, attrs.user, attrs.session, attrs.error],
157
- async () => {
161
+ () => hydrateDmsPageProps(props.value),
162
+ { immediate: true, flush: "sync" },
163
+ );
164
+ // The session, not the page props: a page restored from history or from
165
+ // the prefetch cache carries the user as it was when it was fetched, and
166
+ // would switch the interface back to a language the user has left. A
167
+ // session without a language keeps the one on screen.
168
+ watch(
169
+ () => userLocale(user.value),
170
+ async (locale) => {
158
171
  const version = ++localeVersion;
159
- hydrateDmsPageProps(props.value);
160
- const locale = pageLocale(props.value);
161
- if (i18n.locale.value === locale) return;
172
+ if (!locale || i18n.locale.value === locale) return;
162
173
  const messages = await loadLocaleMessages(locale);
163
174
  if (version !== localeVersion) return;
164
175
  i18n.setLocaleMessage(locale, messages);
@@ -205,7 +216,7 @@ function renderDmsPage(
205
216
  error,
206
217
  key: props.path,
207
218
  });
208
- return h(Suspense, null, { default: () => pageContent });
219
+ return h(Suspense, dmsPageSuspenseProps(), { default: () => pageContent });
209
220
  }
210
221
 
211
222
  function renderDmsPersistentLayout(
@@ -247,9 +258,9 @@ export async function configureDmsApp(
247
258
  options.runtime ??
248
259
  createDmsFrontendRuntime(
249
260
  options.serverFetch as typeof import("ofetch").ofetch,
250
- readDmsAsyncData(),
261
+ readServerRendered<Record<string, unknown>>(SSR_ASYNC_DATA_ID, {}),
251
262
  );
252
- const locale = pageLocale(options.initialPageProps);
263
+ const locale = userLocale(options.initialPageProps.user) ?? DEFAULT_LOCALE;
253
264
  const messages = await loadLocaleMessages(locale);
254
265
  const i18n = createI18n({
255
266
  legacy: false,
@@ -266,6 +277,9 @@ export async function configureDmsApp(
266
277
  options.app.runWithContext(() =>
267
278
  hydrateDmsPageProps(options.initialPageProps, options.initialPageUrl),
268
279
  );
280
+ addDmsIcons(
281
+ readServerRendered<Record<string, IconifyIcon>>(SSR_ICONS_ID, {}),
282
+ );
269
283
  if (typeof window !== "undefined") {
270
284
  const toast = options.app.runWithContext(() => useToast());
271
285
  showNetworkErrors(toast.add, i18n.global);
@@ -275,7 +289,9 @@ export async function configureDmsApp(
275
289
  // Hydration then adopts the server-rendered markup instead of discarding it.
276
290
  await Promise.all([
277
291
  preloadDmsPage(options.initialPageProps),
278
- resolveDmsAsyncComponents(readDmsAsyncComponents()),
292
+ resolveDmsAsyncComponents(
293
+ readServerRendered<string[]>(SSR_ASYNC_COMPONENTS_ID, []),
294
+ ),
279
295
  ]);
280
296
  const mounted = await installDmsPlugins(
281
297
  options.app,
@@ -0,0 +1,78 @@
1
+ import { isAbsolute, posix, resolve } from "node:path";
2
+ import { normalizePath } from "vite";
3
+ import type { DmsFrontendBuildSetup } from "./frontend-build";
4
+
5
+ /** A module's `dms.frontend.build.ts`, as the generated loader imports it. */
6
+ export interface FrontendBuildDeclaration {
7
+ /** The module's id in the generated module registry. */
8
+ id: string;
9
+ setup: DmsFrontendBuildSetup;
10
+ }
11
+
12
+ interface FrontendModuleRoot {
13
+ id: string;
14
+ root: string;
15
+ }
16
+
17
+ export interface AutoImportDirectory {
18
+ glob: string;
19
+ types: boolean;
20
+ }
21
+
22
+ /** A module's private code: never auto-imported, whatever it declares. */
23
+ const PRIVATE_DIRECTORY = /(?:^|\/)app\/build(?:\/|$)/;
24
+
25
+ function declaredDirectories(setup: DmsFrontendBuildSetup): string[] {
26
+ const directories: string[] = [];
27
+ setup({
28
+ registerAutoImports: (entries) => directories.push(...entries),
29
+ });
30
+ return directories;
31
+ }
32
+
33
+ function rejection(directory: string): string | undefined {
34
+ const path = posix.normalize(normalizePath(directory));
35
+ if (isAbsolute(directory) || path === ".." || path.startsWith("../"))
36
+ return "it is not inside the module";
37
+ if (PRIVATE_DIRECTORY.test(path))
38
+ return "app/build/ holds the module's private code, which is never auto-imported";
39
+ return undefined;
40
+ }
41
+
42
+ /**
43
+ * The directories the modules declared in their `dms.frontend.build.ts`, as
44
+ * the auto-importer's scan globs. Each module's `app/build/` directories are
45
+ * excluded even from a broader declaration; a declaration naming one, or
46
+ * leaving the module, is ignored with a warning.
47
+ *
48
+ * Globs are POSIX on every platform: the scanner reads a backslash as an
49
+ * escape, not as a separator.
50
+ */
51
+ export function autoImportDirectories(
52
+ modules: readonly FrontendModuleRoot[],
53
+ declarations: readonly FrontendBuildDeclaration[],
54
+ warn: (message: string) => void,
55
+ ): AutoImportDirectory[] {
56
+ const roots = new Map(modules.map((module) => [module.id, module.root]));
57
+ return declarations.flatMap(({ id, setup }) => {
58
+ const root = roots.get(id);
59
+ if (root === undefined) return [];
60
+ const scan = (pattern: string) => normalizePath(resolve(root, pattern));
61
+ const accepted = declaredDirectories(setup).filter((directory) => {
62
+ const reason = rejection(directory);
63
+ if (reason)
64
+ warn(
65
+ `Frontend module ${id} declares "${directory}" for auto-import, which is ignored: ${reason}.`,
66
+ );
67
+ return !reason;
68
+ });
69
+ if (accepted.length === 0) return [];
70
+ return [
71
+ ...accepted.map((directory) => ({
72
+ glob: scan(`${directory}/**/*`),
73
+ types: true,
74
+ })),
75
+ { glob: `!${scan("**/app/build/**")}`, types: true },
76
+ ];
77
+ });
78
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * What a frontend module's `dms.frontend.build.ts` declares about how the
3
+ * application is built from it. Read when Vite starts, by `ajs dms dev` and
4
+ * `ajs dms build` alike.
5
+ */
6
+ export interface DmsFrontendBuild {
7
+ /**
8
+ * Makes every export of the files under these directories available to
9
+ * the whole application without an import, in scripts and templates.
10
+ *
11
+ * Each entry is a directory relative to the module root, and may hold glob
12
+ * patterns: `layers/*\/app/composables` covers that directory of every
13
+ * layer. Nothing under an `app/build/` directory is ever auto-imported: it
14
+ * holds the module's private code, which its own files import by path.
15
+ */
16
+ registerAutoImports(directories: readonly string[]): void;
17
+ }
18
+
19
+ export type DmsFrontendBuildSetup = (build: DmsFrontendBuild) => void;
20
+
21
+ /**
22
+ * Declares how the application is built from a frontend module, as the
23
+ * default export of its `dms.frontend.build.ts`:
24
+ *
25
+ * ```ts
26
+ * import { defineDmsFrontendBuild } from "#dms/frontend-build";
27
+ *
28
+ * export default defineDmsFrontendBuild((build) => {
29
+ * build.registerAutoImports(["layers/*\/app/composables"]);
30
+ * });
31
+ * ```
32
+ *
33
+ * A module without this file has nothing auto-imported. The file runs in
34
+ * Node.js when Vite starts, not in the application: it may import Node.js
35
+ * built-ins, never the application's own code.
36
+ */
37
+ export function defineDmsFrontendBuild(
38
+ setup: DmsFrontendBuildSetup,
39
+ ): DmsFrontendBuildSetup {
40
+ return setup;
41
+ }