@antelopejs/dms-frontend 0.8.0 → 0.9.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 (47) hide show
  1. package/dist/commands/build-action.js +3 -0
  2. package/dist/commands/dev-action.d.ts +2 -0
  3. package/dist/commands/dev-action.js +12 -5
  4. package/dist/commands/dev.js +3 -1
  5. package/dist/commands/start-action.js +36 -5
  6. package/dist/common.d.ts +1 -0
  7. package/dist/common.js +1 -0
  8. package/dist/config.js +3 -0
  9. package/dist/derived-outputs.d.ts +1 -5
  10. package/dist/derived-outputs.js +46 -33
  11. package/dist/dev-modules.d.ts +5 -0
  12. package/dist/dev-modules.js +88 -0
  13. package/dist/entry-weight.d.ts +24 -0
  14. package/dist/entry-weight.js +86 -0
  15. package/dist/help.js +5 -1
  16. package/dist/layer-watch.js +3 -2
  17. package/dist/layers.d.ts +1 -0
  18. package/dist/locale-files.d.ts +6 -0
  19. package/dist/locale-files.js +75 -0
  20. package/dist/manifest.js +2 -0
  21. package/dist/private-values.d.ts +1 -0
  22. package/dist/private-values.js +16 -0
  23. package/dist/project-modules-child.d.ts +13 -0
  24. package/dist/project-modules-child.js +26 -0
  25. package/dist/project-modules.d.ts +1 -0
  26. package/dist/project-modules.js +73 -38
  27. package/dist/workspace-setup.js +41 -3
  28. package/dist/workspace.d.ts +6 -0
  29. package/package.json +9 -2
  30. package/templates/vue/app-runtime.ts +8 -1
  31. package/templates/vue/entry-weight.mjs +132 -0
  32. package/templates/vue/frontend-module.ts +144 -1
  33. package/templates/vue/gitignore +1 -0
  34. package/templates/vue/main.ts +21 -0
  35. package/templates/vue/network-error.ts +3 -1
  36. package/templates/vue/package.json +1 -1
  37. package/templates/vue/patches/@vueuse__core@14.4.0.patch +24 -0
  38. package/templates/vue/pnpm-workspace.yaml +5 -0
  39. package/templates/vue/server/client-manifest.mjs +12 -0
  40. package/templates/vue/server/in-place-render.mjs +110 -0
  41. package/templates/vue/server/page-props.mjs +10 -0
  42. package/templates/vue/server/proxy-transfers.mjs +35 -9
  43. package/templates/vue/server.mjs +39 -29
  44. package/templates/vue/sfc-global-types.mjs +119 -0
  45. package/templates/vue/ssr-renderer.ts +8 -0
  46. package/templates/vue/ui-kbd.mjs +45 -0
  47. package/templates/vue/vite.config.ts +32 -1
@@ -0,0 +1,119 @@
1
+ // Vue's SFC compiler turns `defineProps<Props>()` into runtime prop types, and
2
+ // it reads a type from the component's own imports only. A type a module
3
+ // auto-imports (its declared `app/types`, `app/composables`…, and Vue's own
4
+ // `Ref`, `Component`…) is visible to TypeScript through auto-imports.d.ts but
5
+ // not to the compiler, which drops it from the prop without a word:
6
+ // `boolean | RowActionConfig` became `{ type: Boolean }` in a production build.
7
+ //
8
+ // The compiler does read ambient declarations from `script.globalTypeFiles`,
9
+ // but not the `export type { … } from` re-exports auto-imports.d.ts is made
10
+ // of. This writes those types as ambient aliases it can follow, from the same
11
+ // directories and through the same scanner as the auto-imports, before Vite
12
+ // compiles anything, and rewrites them when a dev server sees a module's
13
+ // declared directories change.
14
+
15
+ import { readFileSync, renameSync, writeFileSync } from "node:fs";
16
+ import { createRequire } from "node:module";
17
+ import { pathToFileURL } from "node:url";
18
+
19
+ // The very unimport the auto-import plugin scans with, so both read the
20
+ // directories the same way. Imported by URL, not by path: on Windows,
21
+ // `import()` reads `C:\...` as a URL whose scheme is `c:` and refuses it.
22
+ const unimportUrl = pathToFileURL(
23
+ createRequire(
24
+ createRequire(import.meta.url).resolve("unplugin-auto-import/package.json"),
25
+ ).resolve("unimport"),
26
+ );
27
+ const { builtinPresets, resolvePreset, scanDirExports } = await import(
28
+ unimportUrl.href
29
+ );
30
+
31
+ const HEADER =
32
+ "// Generated by @antelopejs/dms-frontend from the auto-imported types, for\n" +
33
+ "// Vue's SFC compiler (script.globalTypeFiles). Not read by TypeScript.\n";
34
+
35
+ const withoutExtension = (path) => path.replace(/\.[cm]?[jt]sx?$/, "");
36
+
37
+ /**
38
+ * The ambient aliases of the auto-imported types: one per name, in the order
39
+ * the auto-import plugin reads them (Vue's preset, then the scanned
40
+ * directories), the last one winning, as unimport's own deduplication keeps
41
+ * the last of two imports of the same name. A module that exports its own
42
+ * `Component` thus shadows Vue's, for the compiler as for TypeScript.
43
+ *
44
+ * @param {(string | { glob: string, types?: boolean })[]} dirs the auto-import
45
+ * directories, as handed to the auto-import plugin
46
+ * @param {string} cwd the workspace
47
+ */
48
+ export async function sfcGlobalTypes(dirs, cwd) {
49
+ const vue = await resolvePreset(builtinPresets.vue);
50
+ const scanned = await scanDirExports(dirs, { cwd, types: true });
51
+ const aliases = new Map();
52
+ for (const entry of [...vue, ...scanned]) {
53
+ if (!entry.type) continue;
54
+ const name = entry.as ?? entry.name;
55
+ const source = JSON.stringify(withoutExtension(entry.from));
56
+ aliases.set(
57
+ name,
58
+ `declare type ${name} = import(${source}).${entry.name};`,
59
+ );
60
+ }
61
+ return `${HEADER}${[...aliases.values()].join("\n")}\n`;
62
+ }
63
+
64
+ function writeIfChanged(file, content) {
65
+ let current;
66
+ try {
67
+ current = readFileSync(file, "utf8");
68
+ } catch {
69
+ current = undefined;
70
+ }
71
+ if (current === content) return false;
72
+ // Through a temporary file: a build starting meanwhile reads the previous
73
+ // declarations or the new ones, never a half-written file the compiler
74
+ // would then keep in its cache.
75
+ const temporary = `${file}.${process.pid}.tmp`;
76
+ writeFileSync(temporary, content);
77
+ renameSync(temporary, file);
78
+ return true;
79
+ }
80
+
81
+ /** Writes the declarations once, before Vite's config is returned. */
82
+ export async function writeSfcGlobalTypes(dirs, cwd, file) {
83
+ writeIfChanged(file, await sfcGlobalTypes(dirs, cwd));
84
+ }
85
+
86
+ /**
87
+ * Keeps the declarations in step on a dev server: a type file added, renamed
88
+ * or removed rewrites them, and the Vue plugin reloads the components whose
89
+ * props read the file.
90
+ */
91
+ export function sfcGlobalTypesWatcher(dirs, cwd, file) {
92
+ return {
93
+ name: "dms-sfc-global-types",
94
+ apply: "serve",
95
+ configureServer(server) {
96
+ let pending;
97
+ const refresh = (path) => {
98
+ if (!/\.[cm]?[jt]sx?$/.test(path) || path === file) return;
99
+ clearTimeout(pending);
100
+ pending = setTimeout(() => {
101
+ // The file is under the workspace, which Vite watches: rewriting it
102
+ // is the change the Vue plugin reloads the dependent components on.
103
+ // A failed scan or write keeps the previous declarations: it must
104
+ // not end the dev server.
105
+ sfcGlobalTypes(dirs, cwd)
106
+ .then((content) => writeIfChanged(file, content))
107
+ .catch((error) =>
108
+ server.config.logger.warn(
109
+ `Could not update ${file}, the types Vue's SFC compiler ` +
110
+ `reads: ${error instanceof Error ? error.message : error}`,
111
+ ),
112
+ );
113
+ }, 100);
114
+ };
115
+ for (const event of ["add", "change", "unlink"])
116
+ server.watcher.on(event, refresh);
117
+ },
118
+ };
119
+ }
@@ -63,6 +63,11 @@ export interface DmsSsrResult {
63
63
  modules: string[];
64
64
  error?: DmsErrorData;
65
65
  redirect?: string;
66
+ /**
67
+ * The locale of the catalog the page was rendered with: the server preloads
68
+ * that catalog's file, which the browser loads before it hydrates.
69
+ */
70
+ catalogLocale?: string;
66
71
  }
67
72
 
68
73
  export interface DmsSsrHead {
@@ -189,6 +194,7 @@ async function renderDmsPageWithRuntime(
189
194
  let icons = new Set<string>();
190
195
  let iconSymbols = new Map<string, string>();
191
196
  let payload: DmsPayload = { data: {}, errors: {}, state: {} };
197
+ let catalogLocale: (() => string | undefined) | undefined;
192
198
  const inertiaResult = await createInertiaApp({
193
199
  page,
194
200
  // Read as the HTML is complete: data a run settles afterwards was never
@@ -214,6 +220,7 @@ async function renderDmsPageWithRuntime(
214
220
  runtime,
215
221
  serverFetch,
216
222
  });
223
+ catalogLocale = configured.catalogLocale;
217
224
  return app;
218
225
  },
219
226
  });
@@ -256,6 +263,7 @@ async function renderDmsPageWithRuntime(
256
263
  },
257
264
  overlays: ssrContext.teleports?.["#dms-overlays"] ?? "",
258
265
  modules: [...(ssrContext.modules ?? [])],
266
+ catalogLocale: catalogLocale?.(),
259
267
  };
260
268
  } finally {
261
269
  // Only once the head and the async data are serialized: both still read
@@ -0,0 +1,45 @@
1
+ // Nuxt UI prints the `meta`, `ctrl` and `alt` of a key hint (`UKbd`, the
2
+ // `kbds` of its menus) from a map every hint shares, blank until the first
3
+ // hint mounts: a hint hydrating after that, in an async component, printed
4
+ // "⌘" or "Ctrl" over the server's blank, a hydration mismatch on every
5
+ // platform. Its `useKbd` is routed to the DMS runtime's `useDmsKbd`, which
6
+ // keeps each hint on the server's value until it mounts and lets the
7
+ // application give the platform to the server render
8
+ // (`provideDmsKbdPlatform`). The build fails if Nuxt UI changes the form
9
+ // rewritten here, rather than letting the mismatch come back unnoticed.
10
+
11
+ /** Nuxt UI's key hint composable. */
12
+ const USE_KBD = /\/@nuxt\/ui\/dist\/runtime\/composables\/useKbd\.js$/;
13
+ /** Its export as Nuxt UI 4 writes it: one composable shared by every hint. */
14
+ const SHARED_EXPORT =
15
+ /export const useKbd = \/\* @__PURE__ \*\/ createSharedComposable\(_useKbd\);/;
16
+ const ROUTED_EXPORT =
17
+ 'import { useDmsKbd } from "#dms/frontend-module";\n' +
18
+ "const useSharedKbd = /* @__PURE__ */ createSharedComposable(_useKbd);\n" +
19
+ "export const useKbd = () => useDmsKbd(useSharedKbd, kbdKeysMap);";
20
+
21
+ /**
22
+ * The rewrite, as a Vite plugin.
23
+ *
24
+ * @param isNuxtUiFile `(id) => boolean`, whether a module is Nuxt UI's
25
+ */
26
+ export function uiKbd(isNuxtUiFile) {
27
+ return {
28
+ name: "dms-ui-kbd",
29
+ enforce: "pre",
30
+ transform(code, id) {
31
+ const path = id.replaceAll("\\", "/").split("?")[0];
32
+ if (!isNuxtUiFile(id) || !USE_KBD.test(path)) return null;
33
+ if (
34
+ !SHARED_EXPORT.test(code) ||
35
+ !code.includes("export const kbdKeysMap")
36
+ )
37
+ this.error(
38
+ `${id} defines useKbd in a form the DMS does not rewrite: its key ` +
39
+ "hints would not hydrate cleanly. Update ui-kbd.mjs for this " +
40
+ "Nuxt UI release.",
41
+ );
42
+ return { code: code.replace(SHARED_EXPORT, ROUTED_EXPORT), map: null };
43
+ },
44
+ };
45
+ }
@@ -10,16 +10,24 @@ import {
10
10
  type Plugin,
11
11
  } from "vite";
12
12
  import { clientCodeSplitting, onBuildLog } from "./client-chunking.mjs";
13
+ import { recordEntryWeight } from "./entry-weight.mjs";
13
14
  import { rejectServerSourcesInClient } from "./client-server-guard.mjs";
14
15
  import { uiInertiaPage } from "./ui-inertia-page.mjs";
16
+ import { uiKbd } from "./ui-kbd.mjs";
15
17
  import { uiRequestState } from "./ui-request-state.mjs";
16
18
  import { autoImportDirectories } from "./frontend-build-loader.ts";
17
19
  import { frontendBuilds } from "./frontend-builds.generated.ts";
18
20
  import { orderHeadForFirstPaint } from "./head-order.mjs";
19
21
  import { iconScanGlobs } from "./icon-scan.mjs";
22
+ import {
23
+ sfcGlobalTypesWatcher,
24
+ writeSfcGlobalTypes,
25
+ } from "./sfc-global-types.mjs";
20
26
 
21
27
  interface FrontendModuleRegistryEntry {
22
28
  id: string;
29
+ name?: string;
30
+ packageName?: string;
23
31
  root: string;
24
32
  priority: number;
25
33
  }
@@ -69,6 +77,19 @@ const importDirectories = autoImportDirectories(
69
77
  frontendBuilds,
70
78
  (message) => logger.warn(message),
71
79
  );
80
+ /**
81
+ * The auto-imported types, as declarations Vue's SFC compiler reads: without
82
+ * them a prop typed with one compiles to the wrong runtime type.
83
+ */
84
+ const sfcGlobalTypesFile = resolve(
85
+ import.meta.dirname,
86
+ "sfc-global-types.generated.d.ts",
87
+ );
88
+ await writeSfcGlobalTypes(
89
+ importDirectories,
90
+ import.meta.dirname,
91
+ sfcGlobalTypesFile,
92
+ );
72
93
  /**
73
94
  * Dependencies pre-bundled by name. A module's own dependencies need no entry:
74
95
  * the startup crawl below (`optimizerEntries`) finds them in its sources. A
@@ -153,6 +174,8 @@ export default defineConfig({
153
174
  // Fails a client build that imports a dms.server.ts or a file under a
154
175
  // module's server/ directory, however it is imported.
155
176
  rejectServerSourcesInClient(moduleRoots),
177
+ // What each module adds to the client entry, which `ajs dms build` reports.
178
+ recordEntryWeight(registry.modules),
156
179
  {
157
180
  name: "dms-ui-inertia-link",
158
181
  enforce: "pre",
@@ -160,9 +183,15 @@ export default defineConfig({
160
183
  return id === uiLinkImport ? uiInertiaLinkImport : null;
161
184
  },
162
185
  },
186
+ uiKbd(isNuxtUiFile),
163
187
  uiInertiaPage(isNuxtUiFile),
164
188
  uiRequestState(isNuxtUiFile),
165
- vue(),
189
+ vue({ script: { globalTypeFiles: [sfcGlobalTypesFile] } }),
190
+ sfcGlobalTypesWatcher(
191
+ importDirectories,
192
+ import.meta.dirname,
193
+ sfcGlobalTypesFile,
194
+ ),
166
195
  ui({
167
196
  router: "inertia",
168
197
  icon: {
@@ -214,6 +243,7 @@ export default defineConfig({
214
243
  "useDmsError",
215
244
  "useDmsPagedData",
216
245
  "useDmsPageLoading",
246
+ "isDmsHydrating",
217
247
  "useDmsApp",
218
248
  "useDmsRoute",
219
249
  "useDmsRouter",
@@ -224,6 +254,7 @@ export default defineConfig({
224
254
  "DmsClientOnly",
225
255
  "DmsLink",
226
256
  "useDmsUserSession",
257
+ "provideDmsKbdPlatform",
227
258
  ],
228
259
  },
229
260
  ],