@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,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InvalidLocaleFileError = void 0;
4
+ exports.readLocaleFile = readLocaleFile;
5
+ const node_fs_1 = require("node:fs");
6
+ const message_compiler_1 = require("@intlify/message-compiler");
7
+ class InvalidLocaleFileError extends Error {
8
+ constructor(path, reason) {
9
+ super(`Invalid locale file ${path}: ${reason}`);
10
+ this.path = path;
11
+ this.reason = reason;
12
+ this.name = "InvalidLocaleFileError";
13
+ }
14
+ }
15
+ exports.InvalidLocaleFileError = InvalidLocaleFileError;
16
+ const LITERAL_ESCAPES = {
17
+ "@": "{'@'}",
18
+ "{": "{'{'}",
19
+ "}": "{'}'}",
20
+ "|": "{'|'}",
21
+ };
22
+ function reservedCharacterAt(message, offset) {
23
+ for (let index = Math.min(offset, message.length - 1); index >= 0; index--) {
24
+ if (message[index] in LITERAL_ESCAPES)
25
+ return message[index];
26
+ }
27
+ return null;
28
+ }
29
+ function describeProblem({ key, message, error }) {
30
+ const where = `${key}: ${error.message} in ${JSON.stringify(message)}`;
31
+ const character = reservedCharacterAt(message, error.location?.start.offset ?? message.length);
32
+ if (!character) {
33
+ return `${where}. Write ${Object.values(LITERAL_ESCAPES).join(", ")} for a literal ${Object.keys(LITERAL_ESCAPES).join(", ")}.`;
34
+ }
35
+ const fixed = message.replaceAll(character, LITERAL_ESCAPES[character]);
36
+ return `${where}. Write a literal ${character} as ${LITERAL_ESCAPES[character]}: ${JSON.stringify(fixed)}.`;
37
+ }
38
+ function firstCompileError(message) {
39
+ let first;
40
+ (0, message_compiler_1.baseCompile)(message, {
41
+ location: true,
42
+ onError: (error) => {
43
+ first ??= error;
44
+ },
45
+ });
46
+ return first;
47
+ }
48
+ function messageProblems(value, key) {
49
+ if (typeof value === "string") {
50
+ const error = firstCompileError(value);
51
+ return error ? [{ key, message: value, error }] : [];
52
+ }
53
+ if (typeof value !== "object" || value === null)
54
+ return [];
55
+ return Object.entries(value).flatMap(([child, nested]) => messageProblems(nested, key ? `${key}.${child}` : child));
56
+ }
57
+ function readLocaleFile(path) {
58
+ let value;
59
+ try {
60
+ value = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
61
+ }
62
+ catch (error) {
63
+ const reason = error instanceof Error ? error.message : String(error);
64
+ throw new InvalidLocaleFileError(path, reason);
65
+ }
66
+ const problems = messageProblems(value, "");
67
+ if (problems.length) {
68
+ const reason = [
69
+ `${problems.length} message(s) vue-i18n cannot compile:`,
70
+ ...problems.map(describeProblem),
71
+ ].join("\n");
72
+ throw new InvalidLocaleFileError(path, reason);
73
+ }
74
+ return value;
75
+ }
package/dist/manifest.js CHANGED
@@ -197,6 +197,8 @@ async function fetchManifest(backendUrl, clientUrl, bootstrapSecret) {
197
197
  const resolved = { modules: manifest.modules };
198
198
  if (manifest.settling)
199
199
  resolved.settling = true;
200
+ if (manifest.originRewrites)
201
+ resolved.originRewrites = manifest.originRewrites;
200
202
  return resolved;
201
203
  }
202
204
  const MANIFEST_CACHE_FILE = ".manifest-cache.json";
@@ -1,5 +1,6 @@
1
1
  import type { FrontendModuleOptions } from "./workspace";
2
2
  export declare function emptiedOptions(options: FrontendModuleOptions): FrontendModuleOptions;
3
+ export declare function filledOptions(shape: FrontendModuleOptions, values: FrontendModuleOptions | undefined): FrontendModuleOptions;
3
4
  export declare const PRIVATE_OPTIONS_VARIABLE = "DMS_PRIVATE_OPTIONS";
4
5
  export interface UnfilledPrivateValue {
5
6
  module: string;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PRIVATE_OPTIONS_VARIABLE = void 0;
4
4
  exports.emptiedOptions = emptiedOptions;
5
+ exports.filledOptions = filledOptions;
5
6
  exports.parsePrivateOptions = parsePrivateOptions;
6
7
  exports.unfilledPrivateValues = unfilledPrivateValues;
7
8
  const config_1 = require("./config");
@@ -17,6 +18,21 @@ function emptied(value) {
17
18
  function emptiedOptions(options) {
18
19
  return Object.fromEntries(Object.entries(options).map(([key, value]) => [key, emptied(value)]));
19
20
  }
21
+ function filled(shape, value) {
22
+ if (typeof shape === "string")
23
+ return typeof value === "string" ? value : "";
24
+ if (Array.isArray(shape))
25
+ return shape.map((item, index) => filled(item, Array.isArray(value) ? value[index] : undefined));
26
+ if (shape !== null && typeof shape === "object")
27
+ return filledOptions(shape, isOptions(value) ? value : undefined);
28
+ return shape;
29
+ }
30
+ function filledOptions(shape, values) {
31
+ return Object.fromEntries(Object.entries(shape).map(([key, value]) => [
32
+ key,
33
+ filled(value, values?.[key]),
34
+ ]));
35
+ }
20
36
  exports.PRIVATE_OPTIONS_VARIABLE = "DMS_PRIVATE_OPTIONS";
21
37
  function isOptions(value) {
22
38
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -0,0 +1,13 @@
1
+ import { CliError, type Ui } from "@antelopejs/core/cli";
2
+ import { type ProjectFrontendModule } from "./project-modules";
3
+ type MessageArgs = Parameters<Ui["message"]>;
4
+ export type ProjectModulesReply = {
5
+ messages: MessageArgs[];
6
+ } & ({
7
+ modules: ProjectFrontendModule[];
8
+ } | {
9
+ problem: CliError["problem"];
10
+ } | {
11
+ failure: string;
12
+ });
13
+ export {};
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const cli_1 = require("@antelopejs/core/cli");
4
+ const project_modules_1 = require("./project-modules");
5
+ process.once("message", async (options) => {
6
+ const messages = [];
7
+ (0, cli_1.getProcessUi)().message = (...args) => {
8
+ messages.push(args);
9
+ };
10
+ let reply;
11
+ try {
12
+ reply = { messages, modules: await (0, project_modules_1.readProjectFrontendModules)(options) };
13
+ }
14
+ catch (error) {
15
+ reply =
16
+ error instanceof cli_1.CliError
17
+ ? { messages, problem: error.problem }
18
+ : {
19
+ messages,
20
+ failure: error instanceof Error
21
+ ? (error.stack ?? error.message)
22
+ : String(error),
23
+ };
24
+ }
25
+ process.send?.(reply, () => process.disconnect());
26
+ });
@@ -26,6 +26,7 @@ interface ModuleSource {
26
26
  interface ProjectModule {
27
27
  source: ModuleSource;
28
28
  config: unknown;
29
+ folder?: string;
29
30
  }
30
31
  interface ProjectConfig {
31
32
  cacheFolder: string;
@@ -48,10 +48,7 @@ const FRONTEND_SOURCE_DIR = "frontend-vue";
48
48
  const RENDERER = { name: "vue", version: "3" };
49
49
  const CONFIG_VAR_PATTERN = /\$\{@([^.}]+)\.([^}]+)\}/g;
50
50
  const PURE_CONFIG_VAR = /^\$\{@([^.}]+)\.([^}]+)\}$/;
51
- const TRAILING_SLASHES = /\/+$/;
52
- const DEFAULT_API_PORT = 80;
53
- const LOOPBACK_HOST = "127.0.0.1";
54
- const WILDCARD_HOSTS = new Set(["0.0.0.0", "::", "[::]"]);
51
+ const MODULE_MANIFEST_FILE = "antelope.module.json";
55
52
  const NODE_FILE_SYSTEM = {
56
53
  readFile: (path) => fsPromises.readFile(path),
57
54
  readFileString: (path, encoding = "utf-8") => fsPromises.readFile(path, encoding),
@@ -67,48 +64,79 @@ const NODE_FILE_SYSTEM = {
67
64
  copyFile: (src, dest) => fsPromises.copyFile(src, dest),
68
65
  rename: (oldPath, newPath) => fsPromises.rename(oldPath, newPath),
69
66
  };
70
- function apiLocalBaseUrl(server) {
71
- const host = !server.host || WILDCARD_HOSTS.has(server.host)
72
- ? LOOPBACK_HOST
73
- : server.host;
74
- const protocol = server.protocol ?? "http";
75
- return `${protocol}://${host}:${server.port ?? DEFAULT_API_PORT}`;
76
- }
77
- const CONFIG_VAR_PROVIDERS = {
78
- api: (config) => {
79
- const api = (config ?? {});
80
- const server = api.servers?.[0];
81
- if (!server)
82
- return {};
83
- const local = apiLocalBaseUrl(server);
84
- return {
85
- API_PORT: server.port ?? DEFAULT_API_PORT,
86
- API_LOCAL_BASE_URL: local,
87
- API_PUBLIC_BASE_URL: api.publicBaseUrl?.trim().replace(TRAILING_SLASHES, "") || local,
88
- };
89
- },
90
- };
91
- function projectConfigVars(config) {
92
- return Object.fromEntries(Object.entries(CONFIG_VAR_PROVIDERS)
93
- .filter(([id]) => config.modules[id])
94
- .map(([id, provide]) => [id, provide(config.modules[id].config)]));
67
+ function readAntelopeManifest(folder) {
68
+ const dedicated = (0, node_path_1.join)(folder, MODULE_MANIFEST_FILE);
69
+ if ((0, node_fs_1.existsSync)(dedicated))
70
+ return JSON.parse((0, node_fs_1.readFileSync)(dedicated, "utf-8"));
71
+ return (0, layers_1.readLayerPackage)(folder)?.antelopeJs;
72
+ }
73
+ function warnUndeclaredConfigVars(id) {
74
+ (0, cli_1.getProcessUi)().message("warn", `${id} declares no antelopeJs.configVarsFile in its package.json: the \${@${id}.<VAR>} references are left unresolved`, { detail: `Upgrade ${id} to a version that declares it` });
75
+ }
76
+ function configVarsProvider(id, folder) {
77
+ const manifest = readAntelopeManifest(folder);
78
+ const file = manifest?.configVarsFile;
79
+ if (typeof file !== "string") {
80
+ warnUndeclaredConfigVars(id);
81
+ return undefined;
82
+ }
83
+ const { configVars } = require((0, node_path_1.join)(folder, file));
84
+ if (typeof configVars !== "function") {
85
+ throw new cli_1.CliError({
86
+ title: `${id}: ${file} exports no configVars function`,
87
+ fixes: [`Export configVars(config) from ${file}`],
88
+ });
89
+ }
90
+ const declared = Array.isArray(manifest?.configVars)
91
+ ? manifest.configVars.filter((name) => typeof name === "string")
92
+ : [];
93
+ return { declared, configVars };
94
+ }
95
+ function computeConfigVars(id, provider, config) {
96
+ let computed;
97
+ try {
98
+ computed = provider.configVars(config) ?? {};
99
+ }
100
+ catch (error) {
101
+ throw new cli_1.CliError({
102
+ title: `${id}: could not compute its config variables`,
103
+ reason: error instanceof Error ? error.message : String(error),
104
+ }, { cause: error });
105
+ }
106
+ return Object.fromEntries(provider.declared
107
+ .filter((name) => name in computed)
108
+ .map((name) => [name, computed[name]]));
109
+ }
110
+ function configVarLookup(modules) {
111
+ const published = new Map();
112
+ const lookup = (id, name) => {
113
+ if (!published.has(id)) {
114
+ published.set(id, undefined);
115
+ const module = modules[id];
116
+ const provider = module?.folder && configVarsProvider(id, module.folder);
117
+ if (provider)
118
+ published.set(id, computeConfigVars(id, provider, substituteConfigVars(module.config, lookup)));
119
+ }
120
+ return published.get(id)?.[name];
121
+ };
122
+ return lookup;
95
123
  }
96
- function substituteConfigVars(value, vars) {
124
+ function substituteConfigVars(value, lookup) {
97
125
  if (typeof value === "string") {
98
126
  const pure = value.match(PURE_CONFIG_VAR);
99
127
  if (pure)
100
- return vars[pure[1]]?.[pure[2]];
128
+ return lookup(pure[1], pure[2]);
101
129
  return value.replace(CONFIG_VAR_PATTERN, (token, module, name) => {
102
- const resolved = vars[module]?.[name];
130
+ const resolved = lookup(module, name);
103
131
  return resolved === undefined ? token : String(resolved);
104
132
  });
105
133
  }
106
134
  if (Array.isArray(value))
107
- return value.map((item) => substituteConfigVars(item, vars));
135
+ return value.map((item) => substituteConfigVars(item, lookup));
108
136
  if (value && typeof value === "object") {
109
137
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [
110
138
  key,
111
- substituteConfigVars(item, vars),
139
+ substituteConfigVars(item, lookup),
112
140
  ]));
113
141
  }
114
142
  return value;
@@ -127,12 +155,19 @@ async function loadProjectConfig(projectDir, environment) {
127
155
  fixes: ["Run from the project root, or pass --project <dir>"],
128
156
  }, { cause: error });
129
157
  }
130
- const vars = projectConfigVars(loaded);
158
+ const located = Object.fromEntries(Object.entries(loaded.modules).map(([id, module]) => [
159
+ id,
160
+ {
161
+ ...module,
162
+ folder: moduleFolder(projectDir, loaded.cacheFolder, module.source),
163
+ },
164
+ ]));
165
+ const lookup = configVarLookup(located);
131
166
  return {
132
167
  cacheFolder: loaded.cacheFolder,
133
- modules: Object.fromEntries(Object.entries(loaded.modules).map(([id, module]) => [
168
+ modules: Object.fromEntries(Object.entries(located).map(([id, module]) => [
134
169
  id,
135
- { ...module, config: substituteConfigVars(module.config, vars) },
170
+ { ...module, config: substituteConfigVars(module.config, lookup) },
136
171
  ])),
137
172
  };
138
173
  }
@@ -214,7 +249,7 @@ async function readProjectFrontendModules(options) {
214
249
  const config = await loadProjectConfig(projectDir, options.environment);
215
250
  const context = { dev: !!options.dev };
216
251
  const registrations = Object.entries(config.modules).flatMap(([id, module]) => {
217
- const folder = moduleFolder(projectDir, config.cacheFolder, module.source);
252
+ const folder = module.folder;
218
253
  if (!folder) {
219
254
  warnMissingModule(id);
220
255
  return [];
@@ -15,6 +15,7 @@ const cli_1 = require("@antelopejs/core/cli");
15
15
  const child_output_1 = require("./child-output");
16
16
  const layers_1 = require("./layers");
17
17
  const manifest_1 = require("./manifest");
18
+ const dev_modules_1 = require("./dev-modules");
18
19
  const project_modules_1 = require("./project-modules");
19
20
  const manifest_secrets_1 = require("./manifest-secrets");
20
21
  const renderer_range_1 = require("./renderer-range");
@@ -23,6 +24,7 @@ const workspace_1 = require("./workspace");
23
24
  const cancellation_1 = require("./cancellation");
24
25
  const output_1 = require("./output");
25
26
  const config_1 = require("./config");
27
+ const locale_files_1 = require("./locale-files");
26
28
  const WORKSPACE_PNPM_CONFIG = "pnpm-workspace.yaml";
27
29
  const WORKSPACE_PATCHES_DIR = "patches";
28
30
  function workspaceInstallInputs(workspaceDir) {
@@ -223,10 +225,33 @@ async function projectLayers(project) {
223
225
  secrets: { published: {}, conflicts: [] },
224
226
  };
225
227
  }
226
- async function servedLayers(opts, backendUrl, workspaceDir) {
228
+ function servedManifest(opts, backendUrl, workspaceDir) {
227
229
  const { offline, clientUrl, bootstrapSecret } = opts;
228
230
  const cacheUse = offline ? "offline" : "fallback";
229
- const { manifest, fromCache, fetchedAt } = await (0, manifest_1.resolveManifest)(workspaceDir, backendUrl, cacheUse, { clientUrl, bootstrapSecret });
231
+ return (0, manifest_1.resolveManifest)(workspaceDir, backendUrl, cacheUse, {
232
+ clientUrl,
233
+ bootstrapSecret,
234
+ });
235
+ }
236
+ async function projectLayersInDev(opts, project, backendUrl, workspaceDir) {
237
+ const { manifest, fromCache, fetchedAt } = await servedManifest(opts, backendUrl, workspaceDir);
238
+ const modules = await (0, dev_modules_1.readProjectModulesFresh)({ ...project, dev: true });
239
+ const layers = (0, dev_modules_1.withBackendValues)(modules, manifest);
240
+ (0, dev_modules_1.warnModuleDrift)(layers, manifest);
241
+ return {
242
+ fromCache,
243
+ fetchedAt,
244
+ layers,
245
+ secrets: (0, manifest_secrets_1.collectManifestSecrets)(layers.map((layer) => ({
246
+ name: layer.name ?? "",
247
+ priority: layer.priority ?? 0,
248
+ privateOptions: layer.privateOptions,
249
+ }))),
250
+ };
251
+ }
252
+ async function servedLayers(opts, backendUrl, workspaceDir) {
253
+ const { bootstrapSecret } = opts;
254
+ const { manifest, fromCache, fetchedAt } = await servedManifest(opts, backendUrl, workspaceDir);
230
255
  (0, layers_1.assertLayerPathsServed)(manifest.modules, backendUrl, {
231
256
  bootstrapSecret,
232
257
  cache: fromCache ? { fetchedAt } : undefined,
@@ -261,6 +286,8 @@ function workspaceSource(opts, workspaceDir) {
261
286
  throw new Error("A build reads a project");
262
287
  return projectLayers(opts.project);
263
288
  }
289
+ if (opts.project)
290
+ return projectLayersInDev(opts, opts.project, requireDevBackend(opts), workspaceDir);
264
291
  return servedLayers(opts, requireDevBackend(opts), workspaceDir);
265
292
  }
266
293
  async function setupWorkspace(opts) {
@@ -270,7 +297,7 @@ async function setupWorkspace(opts) {
270
297
  (0, materialize_1.copyStaticTemplates)(workspaceDir);
271
298
  (0, materialize_1.writeWorkspacePackageJson)(workspaceDir, layers);
272
299
  (0, materialize_1.materializeLayers)(workspaceDir, layers);
273
- (0, materialize_1.writeFrontendModuleRegistry)(workspaceDir, layers);
300
+ writeDerivedOutputs(workspaceDir, layers);
274
301
  (0, materialize_1.writeDmsMainCss)(workspaceDir, layers);
275
302
  const layerPaths = layers.map((layer) => layer.path);
276
303
  const shouldInstall = needsInstall(workspaceDir, layerPaths, opts.force);
@@ -287,3 +314,14 @@ async function setupWorkspace(opts) {
287
314
  manifestSecrets: secrets,
288
315
  };
289
316
  }
317
+ function writeDerivedOutputs(workspaceDir, layers) {
318
+ try {
319
+ (0, materialize_1.writeFrontendModuleRegistry)(workspaceDir, layers);
320
+ }
321
+ catch (error) {
322
+ if (!(error instanceof locale_files_1.InvalidLocaleFileError))
323
+ throw error;
324
+ const showSource = (0, child_output_1.createPathMapper)(workspaceDir, layers);
325
+ throw new locale_files_1.InvalidLocaleFileError(showSource(error.path), error.reason);
326
+ }
327
+ }
@@ -27,14 +27,20 @@ export interface PublicEndpointDeclaration {
27
27
  methods?: string[];
28
28
  headers?: string[];
29
29
  }
30
+ export interface OriginRewrite {
31
+ from: string;
32
+ to: string;
33
+ }
30
34
  export interface Manifest {
31
35
  modules: ManifestModule[];
32
36
  settling?: boolean;
37
+ originRewrites?: OriginRewrite[];
33
38
  }
34
39
  export interface FrontendManifest {
35
40
  version: number;
36
41
  modules: ManifestModule[];
37
42
  settling?: boolean;
43
+ originRewrites?: OriginRewrite[];
38
44
  }
39
45
  export interface ResolvedLayer {
40
46
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
@@ -73,6 +73,7 @@
73
73
  "docs:reference:check": "tsx scripts/docs-reference.ts --check"
74
74
  },
75
75
  "dependencies": {
76
+ "@intlify/message-compiler": "^11.1.12",
76
77
  "chokidar": "^4.0.3",
77
78
  "commander": "^13.1.0",
78
79
  "ignore": "^7.0.5",
@@ -89,6 +90,7 @@
89
90
  "@unhead/vue": "^2.1.4",
90
91
  "@vitejs/plugin-vue": "^6.0.6",
91
92
  "@vue/server-renderer": "^3.5.25",
93
+ "@vueuse/core": "14.4.0",
92
94
  "defu": "^6.1.4",
93
95
  "devalue": "^5.9.4",
94
96
  "knip": "^6.34.0",
@@ -99,7 +101,7 @@
99
101
  "release-it": "^19.2.4",
100
102
  "release-it-changelogen": "^0.1.0",
101
103
  "rimraf": "^6.1.2",
102
- "tsx": "^4.20.6",
104
+ "tsx": "^4.23.15",
103
105
  "typescript": "^6.0.3",
104
106
  "unplugin-auto-import": "^20.2.0",
105
107
  "vite": "^8.3.4",
@@ -120,6 +122,11 @@
120
122
  "node": ">=20.19.0"
121
123
  },
122
124
  "packageManager": "pnpm@10.6.5",
125
+ "pnpm": {
126
+ "patchedDependencies": {
127
+ "@vueuse/core@14.4.0": "templates/vue/patches/@vueuse__core@14.4.0.patch"
128
+ }
129
+ },
123
130
  "antelopeJs": {
124
131
  "skills": [
125
132
  "./skills"
@@ -58,6 +58,7 @@ import {
58
58
  drawLoadedIconsAtOnce,
59
59
  } from "./icon-hydration";
60
60
  import {
61
+ catalogLocale,
61
62
  loadLocaleMessages,
62
63
  localeMessages,
63
64
  supportedLocales,
@@ -85,6 +86,8 @@ export interface DmsAppOptions {
85
86
  }
86
87
 
87
88
  export interface DmsConfiguredApp {
89
+ /** The locale of the catalog the app shows its messages from now. */
90
+ catalogLocale: () => string | undefined;
88
91
  mounted: () => Promise<void>;
89
92
  runtime: ReturnType<typeof createDmsFrontendRuntime>;
90
93
  }
@@ -348,7 +351,11 @@ export async function configureDmsApp(
348
351
  loadLocaleMessages as DmsLocaleLoader,
349
352
  supportedLocales,
350
353
  );
351
- return { mounted, runtime };
354
+ return {
355
+ catalogLocale: () => catalogLocale(i18n.global.locale.value),
356
+ mounted,
357
+ runtime,
358
+ };
352
359
  }
353
360
 
354
361
  export { trackDmsAsyncComponents };
@@ -0,0 +1,132 @@
1
+ // What each frontend module adds to the client entry.
2
+ //
3
+ // Every page loads the entry, and everything it imports statically, before it
4
+ // runs anything: a module's `dms.frontend.ts`, its plugins and whatever they
5
+ // import land there, on every page of every application that loads the
6
+ // module. The client build records, from the bundle's own metadata, how much
7
+ // of the entry each module's sources make up; `ajs dms build` reports it and
8
+ // warns about a module above its threshold.
9
+ //
10
+ // Kept beside vite.config.ts rather than inside it so the plugin can be
11
+ // exercised on its own, the way client-chunking.mjs is.
12
+
13
+ import { mkdirSync, writeFileSync } from "node:fs";
14
+ import { dirname, join, relative, sep } from "node:path";
15
+
16
+ /** Where the client build writes the attribution, under `dist/client`. */
17
+ export const ENTRY_WEIGHT_FILE = ".vite/entry-weight.json";
18
+
19
+ /** Owners of the entry's bytes that are no frontend module. */
20
+ export const SHARED_OWNER = "shared";
21
+ export const TRANSLATIONS_OWNER = "translations";
22
+
23
+ const LOCALE_CATALOG = /[\\/]locales\.generated[\\/]/;
24
+
25
+ function stripQuery(id) {
26
+ const query = id.indexOf("?");
27
+ return query === -1 ? id : id.slice(0, query);
28
+ }
29
+
30
+ function isInside(root, file) {
31
+ const path = relative(root, file);
32
+ return Boolean(path) && !path.startsWith("..") && !path.startsWith(sep);
33
+ }
34
+
35
+ /**
36
+ * The owner of a module id of the bundle: the frontend module whose sources
37
+ * hold the file, by its manifest name; the merged translation catalogs; else
38
+ * what every page shares (the renderer's runtime, the UI and icon libraries,
39
+ * an installed package).
40
+ *
41
+ * @param {{ root: string, name?: string, packageName?: string, id: string }[]} modules
42
+ */
43
+ export function entryOwner(id, modules) {
44
+ if (id.startsWith("\0")) return SHARED_OWNER;
45
+ const file = stripQuery(id);
46
+ if (/[\\/]node_modules[\\/]/.test(file)) return SHARED_OWNER;
47
+ if (LOCALE_CATALOG.test(file)) return TRANSLATIONS_OWNER;
48
+ const module = modules.find(({ root }) => isInside(root, file));
49
+ return module
50
+ ? (module.name ?? module.packageName ?? module.id)
51
+ : SHARED_OWNER;
52
+ }
53
+
54
+ /**
55
+ * The files a page loads before its first line runs: the entry chunks and
56
+ * their static imports, transitively, by file name.
57
+ */
58
+ function entryClosure(bundle) {
59
+ const chunks = Object.values(bundle).filter(
60
+ (output) => output.type === "chunk",
61
+ );
62
+ const byName = new Map(chunks.map((chunk) => [chunk.fileName, chunk]));
63
+ const seen = new Set();
64
+ const visit = (chunk) => {
65
+ if (!chunk || seen.has(chunk.fileName)) return;
66
+ seen.add(chunk.fileName);
67
+ for (const file of chunk.imports ?? []) visit(byName.get(file));
68
+ };
69
+ for (const chunk of chunks) if (chunk.isEntry) visit(chunk);
70
+ return [...seen].map((file) => byName.get(file));
71
+ }
72
+
73
+ /**
74
+ * The bytes of the entry each owner accounts for. Each chunk's bytes are
75
+ * shared out among the modules it holds in proportion to the code Rolldown
76
+ * rendered for each (`renderedLength`, before minification): an estimate that
77
+ * needs nothing the bundle does not already hold.
78
+ *
79
+ * @returns {{ files: string[], bytes: number, owners: Record<string, number> }}
80
+ */
81
+ export function attributeEntry(bundle, modules) {
82
+ const files = entryClosure(bundle);
83
+ const owners = {};
84
+ let bytes = 0;
85
+ for (const chunk of files) {
86
+ const size = Buffer.byteLength(chunk.code);
87
+ bytes += size;
88
+ const rendered = Object.entries(chunk.modules ?? {});
89
+ const total = rendered.reduce(
90
+ (sum, [, info]) => sum + info.renderedLength,
91
+ 0,
92
+ );
93
+ if (!total) {
94
+ owners[SHARED_OWNER] = (owners[SHARED_OWNER] ?? 0) + size;
95
+ continue;
96
+ }
97
+ for (const [id, info] of rendered) {
98
+ const owner = entryOwner(id, modules);
99
+ owners[owner] =
100
+ (owners[owner] ?? 0) + (size * info.renderedLength) / total;
101
+ }
102
+ }
103
+ for (const owner of Object.keys(owners))
104
+ owners[owner] = Math.round(owners[owner]);
105
+ return { files: files.map((chunk) => chunk.fileName), bytes, owners };
106
+ }
107
+
108
+ /**
109
+ * Records, in the client build, what each frontend module adds to the entry
110
+ * (`ENTRY_WEIGHT_FILE`). Server builds are left alone: their bundle is no
111
+ * page's entry.
112
+ */
113
+ export function recordEntryWeight(modules) {
114
+ let ssrBuild = false;
115
+ return {
116
+ name: "dms-entry-weight",
117
+ configResolved(config) {
118
+ ssrBuild = Boolean(config.build.ssr);
119
+ },
120
+ // Once the bundle is written: plugins that generate the bundle after this
121
+ // one still rewrite the chunks' code.
122
+ writeBundle(options, bundle) {
123
+ if (ssrBuild || !options.dir) return;
124
+ const file = join(options.dir, ENTRY_WEIGHT_FILE);
125
+ mkdirSync(dirname(file), { recursive: true });
126
+ writeFileSync(
127
+ file,
128
+ `${JSON.stringify(attributeEntry(bundle, modules), null, 2)}\n`,
129
+ );
130
+ },
131
+ };
132
+ }