@antelopejs/dms-frontend 0.7.0 → 0.8.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 +4 -4
  2. package/dist/commands/build-action.d.ts +8 -5
  3. package/dist/commands/build-action.js +13 -41
  4. package/dist/commands/build.js +9 -15
  5. package/dist/commands/dev-action.js +5 -1
  6. package/dist/commands/start-action.d.ts +2 -0
  7. package/dist/commands/start-action.js +36 -49
  8. package/dist/commands/start.js +8 -6
  9. package/dist/config.d.ts +2 -0
  10. package/dist/config.js +6 -0
  11. package/dist/derived-outputs.js +2 -2
  12. package/dist/fs-sync.d.ts +1 -0
  13. package/dist/fs-sync.js +6 -0
  14. package/dist/help.js +8 -4
  15. package/dist/index.js +4 -6
  16. package/dist/layer-watch.js +4 -0
  17. package/dist/layers.d.ts +1 -3
  18. package/dist/layers.js +1 -95
  19. package/dist/manifest.d.ts +8 -8
  20. package/dist/manifest.js +15 -39
  21. package/dist/materialize.js +6 -0
  22. package/dist/module-set.d.ts +14 -12
  23. package/dist/module-set.js +39 -31
  24. package/dist/private-values.d.ts +2 -2
  25. package/dist/private-values.js +1 -6
  26. package/dist/project-modules.d.ts +46 -0
  27. package/dist/project-modules.js +233 -0
  28. package/dist/workspace-setup.d.ts +3 -7
  29. package/dist/workspace-setup.js +41 -62
  30. package/dist/workspace.d.ts +2 -4
  31. package/dist/workspace.js +1 -1
  32. package/package.json +5 -7
  33. package/skills/dms-frontend/SKILL.md +2 -2
  34. package/templates/vue/client-chunking.mjs +61 -0
  35. package/templates/vue/email-renderer.ts +6 -1
  36. package/templates/vue/frontend-build-loader.ts +1 -1
  37. package/templates/vue/frontend-module.ts +24 -0
  38. package/templates/vue/main.ts +11 -2
  39. package/templates/vue/package.json +2 -2
  40. package/templates/vue/prepare-types.mjs +2 -7
  41. package/templates/vue/public-options.ts +19 -0
  42. package/templates/vue/server/auth/backend.mjs +20 -3
  43. package/templates/vue/server/auth/client-ip.mjs +64 -22
  44. package/templates/vue/server/auth/session.mjs +4 -11
  45. package/templates/vue/server/backend-proxies.mjs +13 -2
  46. package/templates/vue/server/backend-response.mjs +2 -13
  47. package/templates/vue/server/cli-report.mjs +42 -18
  48. package/templates/vue/server/client-manifest.mjs +22 -58
  49. package/templates/vue/server/dev-private-files.mjs +1 -1
  50. package/templates/vue/server/dev-styles.mjs +16 -5
  51. package/templates/vue/server/homepage.mjs +4 -1
  52. package/templates/vue/server/module-event.mjs +23 -8
  53. package/templates/vue/server/module-server.mjs +4 -4
  54. package/templates/vue/server/module-testing.mjs +5 -3
  55. package/templates/vue/server/proxy-compression.mjs +15 -8
  56. package/templates/vue/server/proxy-headers.mjs +12 -1
  57. package/templates/vue/server/proxy-redirects.mjs +66 -0
  58. package/templates/vue/server/proxy-transfers.mjs +84 -0
  59. package/templates/vue/server/public-endpoints.mjs +2 -2
  60. package/templates/vue/server/public-options.mjs +49 -0
  61. package/templates/vue/server/request-routing.mjs +20 -0
  62. package/templates/vue/server/web-body.mjs +24 -0
  63. package/templates/vue/server-module.ts +19 -7
  64. package/templates/vue/server.mjs +59 -49
  65. package/templates/vue/ssr-renderer.ts +19 -1
  66. package/templates/vue/vite.config.ts +72 -46
  67. package/templates/vue/vite.email.config.ts +6 -3
  68. package/templates/vue/vite.server.config.ts +5 -2
  69. package/dist/commands/snapshot-action.d.ts +0 -6
  70. package/dist/commands/snapshot-action.js +0 -53
  71. package/dist/commands/snapshot.d.ts +0 -2
  72. package/dist/commands/snapshot.js +0 -55
  73. package/dist/snapshot.d.ts +0 -25
  74. package/dist/snapshot.js +0 -90
  75. package/templates/vue/server/backend-wake.mjs +0 -129
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FRONTEND_DECLARATION_FIELD = void 0;
37
+ exports.loadProjectConfig = loadProjectConfig;
38
+ exports.readShippingVersion = readShippingVersion;
39
+ exports.readProjectFrontendModules = readProjectFrontendModules;
40
+ const node_fs_1 = require("node:fs");
41
+ const fsPromises = __importStar(require("node:fs/promises"));
42
+ const node_path_1 = require("node:path");
43
+ const cli_1 = require("@antelopejs/core/cli");
44
+ const layers_1 = require("./layers");
45
+ const private_values_1 = require("./private-values");
46
+ exports.FRONTEND_DECLARATION_FIELD = "dmsFrontend";
47
+ const FRONTEND_SOURCE_DIR = "frontend-vue";
48
+ const RENDERER = { name: "vue", version: "3" };
49
+ const CONFIG_VAR_PATTERN = /\$\{@([^.}]+)\.([^}]+)\}/g;
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", "::", "[::]"]);
55
+ const NODE_FILE_SYSTEM = {
56
+ readFile: (path) => fsPromises.readFile(path),
57
+ readFileString: (path, encoding = "utf-8") => fsPromises.readFile(path, encoding),
58
+ writeFile: (path, data) => fsPromises.writeFile(path, data),
59
+ readdir: (path) => fsPromises.readdir(path),
60
+ stat: (path) => fsPromises.stat(path),
61
+ mkdir: async (path, options) => {
62
+ await fsPromises.mkdir(path, options);
63
+ },
64
+ rm: (path, options) => fsPromises.rm(path, options),
65
+ exists: async (path) => (0, node_fs_1.existsSync)(path),
66
+ access: (path) => fsPromises.access(path),
67
+ copyFile: (src, dest) => fsPromises.copyFile(src, dest),
68
+ rename: (oldPath, newPath) => fsPromises.rename(oldPath, newPath),
69
+ };
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)]));
95
+ }
96
+ function substituteConfigVars(value, vars) {
97
+ if (typeof value === "string") {
98
+ const pure = value.match(PURE_CONFIG_VAR);
99
+ if (pure)
100
+ return vars[pure[1]]?.[pure[2]];
101
+ return value.replace(CONFIG_VAR_PATTERN, (token, module, name) => {
102
+ const resolved = vars[module]?.[name];
103
+ return resolved === undefined ? token : String(resolved);
104
+ });
105
+ }
106
+ if (Array.isArray(value))
107
+ return value.map((item) => substituteConfigVars(item, vars));
108
+ if (value && typeof value === "object") {
109
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
110
+ key,
111
+ substituteConfigVars(item, vars),
112
+ ]));
113
+ }
114
+ return value;
115
+ }
116
+ async function loadProjectConfig(projectDir, environment) {
117
+ const { ConfigLoader } = await Promise.resolve().then(() => __importStar(require("@antelopejs/core")));
118
+ const loader = new ConfigLoader(NODE_FILE_SYSTEM);
119
+ let loaded;
120
+ try {
121
+ loaded = (await loader.load(projectDir, environment));
122
+ }
123
+ catch (error) {
124
+ throw new cli_1.CliError({
125
+ title: `Could not evaluate the project's configuration in ${projectDir}`,
126
+ reason: error instanceof Error ? error.message : String(error),
127
+ fixes: ["Run from the project root, or pass --project <dir>"],
128
+ }, { cause: error });
129
+ }
130
+ const vars = projectConfigVars(loaded);
131
+ return {
132
+ cacheFolder: loaded.cacheFolder,
133
+ modules: Object.fromEntries(Object.entries(loaded.modules).map(([id, module]) => [
134
+ id,
135
+ { ...module, config: substituteConfigVars(module.config, vars) },
136
+ ])),
137
+ };
138
+ }
139
+ function moduleFolder(projectDir, cacheFolder, source) {
140
+ const candidates = source.type === "local" && source.path
141
+ ? [(0, node_path_1.resolve)(projectDir, source.path)]
142
+ : source.package
143
+ ? [
144
+ (0, node_path_1.join)(projectDir, "node_modules", source.package),
145
+ (0, node_path_1.resolve)(projectDir, cacheFolder, source.package),
146
+ ]
147
+ : [];
148
+ return candidates.find((folder) => (0, node_fs_1.existsSync)((0, node_path_1.join)(folder, "package.json")));
149
+ }
150
+ function readShippingVersion(sourcePath) {
151
+ try {
152
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)((0, node_path_1.dirname)(sourcePath), "package.json"), "utf-8"));
153
+ return typeof pkg.version === "string" ? pkg.version : undefined;
154
+ }
155
+ catch {
156
+ return undefined;
157
+ }
158
+ }
159
+ function toLayer(registration) {
160
+ const name = (0, layers_1.readLayerPackage)(registration.sourcePath)?.name;
161
+ return {
162
+ path: registration.sourcePath,
163
+ name: registration.name,
164
+ sourcePath: registration.sourcePath,
165
+ packageName: typeof name === "string" ? name : undefined,
166
+ priority: registration.priority ?? 0,
167
+ configKey: registration.configKey,
168
+ options: registration.options ?? {},
169
+ privateOptions: registration.privateOptions &&
170
+ (0, private_values_1.emptiedOptions)(registration.privateOptions),
171
+ authEstablishEndpoints: registration.authEstablishEndpoints ?? [],
172
+ publicEndpoints: registration.publicEndpoints ?? [],
173
+ backendProxies: registration.backendProxies ?? [],
174
+ };
175
+ }
176
+ function warnMissingModule(id) {
177
+ (0, cli_1.getProcessUi)().message("warn", `${id} is not installed in the project: its pages, if it has any, are left out`, { detail: "Install the project's modules first: ajs project build" });
178
+ }
179
+ function warnUndeclaredFrontend(id, folder) {
180
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(folder, FRONTEND_SOURCE_DIR)))
181
+ return;
182
+ (0, cli_1.getProcessUi)().message("warn", `${id} ships ${FRONTEND_SOURCE_DIR}/ but declares no ${exports.FRONTEND_DECLARATION_FIELD} in its package.json: its pages are left out`);
183
+ }
184
+ function registrationOf(id, folder, config, context) {
185
+ const declaration = (0, layers_1.readLayerPackage)(folder)?.[exports.FRONTEND_DECLARATION_FIELD];
186
+ if (typeof declaration !== "string") {
187
+ warnUndeclaredFrontend(id, folder);
188
+ return undefined;
189
+ }
190
+ const { frontendModule } = require((0, node_path_1.join)(folder, declaration));
191
+ if (typeof frontendModule !== "function") {
192
+ throw new cli_1.CliError({
193
+ title: `${id}: ${declaration} exports no frontendModule function`,
194
+ fixes: [`Export frontendModule(config, context) from ${declaration}`],
195
+ });
196
+ }
197
+ return frontendModule(config, context);
198
+ }
199
+ function isServedRenderer(registration) {
200
+ return (registration.renderer.name === RENDERER.name &&
201
+ registration.renderer.version === RENDERER.version);
202
+ }
203
+ function keepHighestPriority(registrations) {
204
+ const byName = new Map();
205
+ for (const registration of registrations) {
206
+ const kept = byName.get(registration.name);
207
+ if (!kept || (kept.priority ?? 0) <= (registration.priority ?? 0))
208
+ byName.set(registration.name, registration);
209
+ }
210
+ return [...byName.values()];
211
+ }
212
+ async function readProjectFrontendModules(options) {
213
+ const projectDir = (0, node_path_1.resolve)(options.projectDir);
214
+ const config = await loadProjectConfig(projectDir, options.environment);
215
+ const context = { dev: !!options.dev };
216
+ const registrations = Object.entries(config.modules).flatMap(([id, module]) => {
217
+ const folder = moduleFolder(projectDir, config.cacheFolder, module.source);
218
+ if (!folder) {
219
+ warnMissingModule(id);
220
+ return [];
221
+ }
222
+ const registration = registrationOf(id, folder, module.config, context);
223
+ return registration && isServedRenderer(registration)
224
+ ? [registration]
225
+ : [];
226
+ });
227
+ return keepHighestPriority(registrations)
228
+ .sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0))
229
+ .map((registration) => ({
230
+ layer: toLayer(registration),
231
+ version: readShippingVersion(registration.sourcePath),
232
+ }));
233
+ }
@@ -1,8 +1,8 @@
1
1
  import { type ChildProcess, type SpawnOptions } from "node:child_process";
2
2
  import { type PathMapper } from "./child-output";
3
+ import { type ProjectModulesOptions } from "./project-modules";
3
4
  import { type ManifestSecrets } from "./manifest-secrets";
4
5
  import { ResolvedLayer } from "./workspace";
5
- import { type BuildOrigin, type FrontendSnapshot } from "./snapshot";
6
6
  export declare function computeDepsHash(layerPaths: string[], workspacePackage?: string, installInputs?: readonly string[]): string;
7
7
  export declare function needsInstall(workspaceDir: string, layerPaths: string[], force: boolean): boolean;
8
8
  export declare function saveDepsHash(workspaceDir: string, layerPaths: string[]): void;
@@ -21,18 +21,15 @@ export interface FramedCommandResult {
21
21
  }
22
22
  export declare function runFramedCommand(command: string, args: string[], options: FramedCommandOptions): Promise<FramedCommandResult>;
23
23
  export interface SetupWorkspaceOptions {
24
- backendUrl: string;
25
24
  force: boolean;
26
25
  mode: "dev" | "build";
26
+ backendUrl?: string;
27
27
  offline?: boolean;
28
28
  clientUrl?: string;
29
29
  workspaceKey?: string;
30
30
  bootstrapSecret?: string;
31
31
  workspaceDir?: string;
32
- snapshot?: {
33
- dir: string;
34
- snapshot: FrontendSnapshot;
35
- };
32
+ project?: ProjectModulesOptions;
36
33
  beforeInstall?: () => Promise<void>;
37
34
  }
38
35
  export interface SetupWorkspaceResult {
@@ -41,6 +38,5 @@ export interface SetupWorkspaceResult {
41
38
  manifestFromCache: boolean;
42
39
  manifestFetchedAt?: string;
43
40
  manifestSecrets: ManifestSecrets;
44
- buildOrigin: BuildOrigin;
45
41
  }
46
42
  export declare function setupWorkspace(opts: SetupWorkspaceOptions): Promise<SetupWorkspaceResult>;
@@ -15,11 +15,11 @@ 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 project_modules_1 = require("./project-modules");
18
19
  const manifest_secrets_1 = require("./manifest-secrets");
19
20
  const renderer_range_1 = require("./renderer-range");
20
21
  const materialize_1 = require("./materialize");
21
22
  const workspace_1 = require("./workspace");
22
- const snapshot_1 = require("./snapshot");
23
23
  const cancellation_1 = require("./cancellation");
24
24
  const output_1 = require("./output");
25
25
  const config_1 = require("./config");
@@ -215,77 +215,57 @@ async function runFramedCommand(command, args, options) {
215
215
  await Promise.race([ended, drainTimeout()]);
216
216
  return { code, lines: tails.flatMap((tail) => tail.lines()) };
217
217
  }
218
- function manifestCacheUse(mode, offline) {
219
- if (offline)
220
- return "offline";
221
- return mode === "build" ? "never" : "fallback";
222
- }
223
- async function snapshotLayers({ dir, snapshot }, workspaceDir) {
224
- const { manifest, takenAt } = snapshot;
225
- const cacheDir = (0, node_path_1.join)(dir, snapshot_1.SNAPSHOT_LAYERS_DIR);
226
- (0, layers_1.assertCachedArchivesExist)(manifest.modules, cacheDir, takenAt);
227
- (0, manifest_1.writeCachedManifest)(workspaceDir, manifest, "drop", takenAt);
218
+ async function projectLayers(project) {
219
+ const modules = await (0, project_modules_1.readProjectFrontendModules)(project);
228
220
  return {
229
- manifest,
230
221
  fromCache: false,
231
- layers: await (0, layers_1.buildLayersFromCache)(manifest.modules, cacheDir),
222
+ layers: modules.map((module) => module.layer),
223
+ secrets: { published: {}, conflicts: [] },
232
224
  };
233
225
  }
234
- async function servedLayers(opts, workspaceDir) {
235
- const { backendUrl, force, mode, offline, clientUrl, bootstrapSecret } = opts;
236
- const resolved = await (0, manifest_1.resolveManifest)(workspaceDir, backendUrl, manifestCacheUse(mode, offline), {
237
- clientUrl,
226
+ async function servedLayers(opts, backendUrl, workspaceDir) {
227
+ const { offline, clientUrl, bootstrapSecret } = opts;
228
+ const cacheUse = offline ? "offline" : "fallback";
229
+ const { manifest, fromCache, fetchedAt } = await (0, manifest_1.resolveManifest)(workspaceDir, backendUrl, cacheUse, { clientUrl, bootstrapSecret });
230
+ (0, layers_1.assertLayerPathsServed)(manifest.modules, backendUrl, {
238
231
  bootstrapSecret,
239
- privateValues: mode === "build" ? "drop" : "keep",
232
+ cache: fromCache ? { fetchedAt } : undefined,
240
233
  });
241
- const { manifest, fromCache, fetchedAt } = resolved;
242
- if (mode === "dev") {
243
- (0, layers_1.assertLayerPathsServed)(manifest.modules, backendUrl, {
244
- bootstrapSecret,
245
- cache: fromCache ? { fetchedAt } : undefined,
246
- });
247
- if (fromCache) {
248
- (0, manifest_1.assertCachedLayerPathsExist)(manifest.modules, fetchedAt);
249
- }
250
- return { ...resolved, layers: (0, layers_1.buildLayersFromPaths)(manifest.modules) };
251
- }
252
- const cacheDir = (0, node_path_1.join)(workspaceDir, ".layers-cache");
253
234
  if (fromCache) {
254
- if (!(0, node_fs_1.existsSync)(cacheDir)) {
255
- throw new cli_1.CliError({
256
- title: "No layers archive was downloaded for this workspace",
257
- fixes: [
258
- "Run once without --offline, with the backend reachable, first",
259
- ],
260
- });
261
- }
262
- (0, layers_1.assertCachedArchivesExist)(manifest.modules, cacheDir, fetchedAt);
263
- }
264
- else {
265
- await (0, layers_1.downloadAndExtractLayers)(backendUrl, manifest.pack, cacheDir, force, bootstrapSecret);
235
+ (0, manifest_1.assertCachedLayerPathsExist)(manifest.modules, fetchedAt);
266
236
  }
267
237
  return {
268
- ...resolved,
269
- layers: await (0, layers_1.buildLayersFromCache)(manifest.modules, cacheDir),
238
+ fromCache,
239
+ fetchedAt,
240
+ layers: (0, layers_1.buildLayersFromPaths)(manifest.modules),
241
+ secrets: (0, manifest_secrets_1.collectManifestSecrets)(manifest.modules),
270
242
  };
271
243
  }
272
- async function setupWorkspace(opts) {
273
- const { backendUrl, force } = opts;
244
+ function workspaceDirFor(opts) {
245
+ if (opts.workspaceDir)
246
+ return (0, workspace_1.ensureExplicitWorkspace)(opts.workspaceDir);
247
+ const backendUrl = requireDevBackend(opts);
274
248
  const workspaceKey = opts.workspaceKey ?? (0, config_1.canonicalizeBackendUrl)(backendUrl);
275
- const workspaceDir = opts.workspaceDir
276
- ? (0, workspace_1.ensureExplicitWorkspace)(opts.workspaceDir)
277
- : (0, workspace_1.ensureWorkspace)(workspaceKey);
249
+ const workspaceDir = (0, workspace_1.ensureWorkspace)(workspaceKey);
278
250
  (0, workspace_1.writeWorkspaceMeta)(workspaceDir, backendUrl, workspaceKey);
279
- const { manifest, fromCache, fetchedAt, layers } = opts.snapshot
280
- ? await snapshotLayers(opts.snapshot, workspaceDir)
281
- : await servedLayers(opts, workspaceDir);
282
- const buildOrigin = {
283
- source: opts.snapshot ? "snapshot" : "backend",
284
- backendUrl: (0, config_1.canonicalizeBackendUrl)(backendUrl),
285
- takenAt: opts.snapshot?.snapshot.takenAt ?? fetchedAt ?? new Date().toISOString(),
286
- digest: (0, snapshot_1.manifestDigest)(manifest.modules),
287
- authenticated: !!opts.snapshot || (!!opts.bootstrapSecret && !fromCache),
288
- };
251
+ return workspaceDir;
252
+ }
253
+ function requireDevBackend(opts) {
254
+ if (!opts.backendUrl)
255
+ throw new Error("A dev workspace is set up from a backend URL");
256
+ return opts.backendUrl;
257
+ }
258
+ function workspaceSource(opts, workspaceDir) {
259
+ if (opts.mode === "build") {
260
+ if (!opts.project)
261
+ throw new Error("A build reads a project");
262
+ return projectLayers(opts.project);
263
+ }
264
+ return servedLayers(opts, requireDevBackend(opts), workspaceDir);
265
+ }
266
+ async function setupWorkspace(opts) {
267
+ const workspaceDir = workspaceDirFor(opts);
268
+ const { fromCache, fetchedAt, layers, secrets } = await workspaceSource(opts, workspaceDir);
289
269
  (0, renderer_range_1.assertLayersSupportRenderer)(layers, { workspaceDir });
290
270
  (0, materialize_1.copyStaticTemplates)(workspaceDir);
291
271
  (0, materialize_1.writeWorkspacePackageJson)(workspaceDir, layers);
@@ -293,7 +273,7 @@ async function setupWorkspace(opts) {
293
273
  (0, materialize_1.writeFrontendModuleRegistry)(workspaceDir, layers);
294
274
  (0, materialize_1.writeDmsMainCss)(workspaceDir, layers);
295
275
  const layerPaths = layers.map((layer) => layer.path);
296
- const shouldInstall = needsInstall(workspaceDir, layerPaths, force);
276
+ const shouldInstall = needsInstall(workspaceDir, layerPaths, opts.force);
297
277
  if (shouldInstall) {
298
278
  await opts.beforeInstall?.();
299
279
  await installDeps(workspaceDir, (0, child_output_1.createPathMapper)(workspaceDir, layers));
@@ -304,7 +284,6 @@ async function setupWorkspace(opts) {
304
284
  layers,
305
285
  manifestFromCache: fromCache,
306
286
  manifestFetchedAt: fetchedAt,
307
- manifestSecrets: (0, manifest_secrets_1.collectManifestSecrets)(manifest.modules),
308
- buildOrigin,
287
+ manifestSecrets: secrets,
309
288
  };
310
289
  }
@@ -4,7 +4,6 @@ export interface FrontendModuleOptions {
4
4
  }
5
5
  export interface ManifestModule {
6
6
  name: string;
7
- archiveName: string;
8
7
  renderer?: {
9
8
  name: string;
10
9
  version: string;
@@ -21,6 +20,7 @@ export interface ManifestModule {
21
20
  export interface BackendProxyDeclaration {
22
21
  path: string;
23
22
  auth?: "session" | "none";
23
+ maxBodyBytes?: number;
24
24
  }
25
25
  export interface PublicEndpointDeclaration {
26
26
  path: string;
@@ -28,13 +28,11 @@ export interface PublicEndpointDeclaration {
28
28
  headers?: string[];
29
29
  }
30
30
  export interface Manifest {
31
- pack: string;
32
31
  modules: ManifestModule[];
33
32
  settling?: boolean;
34
33
  }
35
34
  export interface FrontendManifest {
36
35
  version: number;
37
- archive: string;
38
36
  modules: ManifestModule[];
39
37
  settling?: boolean;
40
38
  }
@@ -70,6 +68,6 @@ export interface WorkspaceListing {
70
68
  export declare function listWorkspaces(): WorkspaceListing;
71
69
  export declare function directorySize(dir: string): number;
72
70
  export declare function writeWorkspaceMeta(workspaceDir: string, backendUrl: string, workspaceKey: string): void;
73
- export declare const FRONTEND_MANIFEST_VERSION = 1;
71
+ export declare const FRONTEND_MANIFEST_VERSION = 2;
74
72
  export declare function bootstrapHeaders(secret?: string): Record<string, string>;
75
73
  export declare function isUnauthorized(status: number): boolean;
package/dist/workspace.js CHANGED
@@ -131,7 +131,7 @@ function writeWorkspaceMeta(workspaceDir, backendUrl, workspaceKey) {
131
131
  }
132
132
  const BOOTSTRAP_HEADER = "x-dms-bootstrap";
133
133
  const UNAUTHORIZED_STATUSES = [401, 403];
134
- exports.FRONTEND_MANIFEST_VERSION = 1;
134
+ exports.FRONTEND_MANIFEST_VERSION = 2;
135
135
  function bootstrapHeaders(secret) {
136
136
  return secret ? { [BOOTSTRAP_HEADER]: secret } : {};
137
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
@@ -76,8 +76,7 @@
76
76
  "chokidar": "^4.0.3",
77
77
  "commander": "^13.1.0",
78
78
  "ignore": "^7.0.5",
79
- "semver": "^7.7.4",
80
- "unzipper": "^0.12.3"
79
+ "semver": "^7.7.4"
81
80
  },
82
81
  "devDependencies": {
83
82
  "@antelopejs/core": "^1.16.0",
@@ -87,9 +86,8 @@
87
86
  "@inertiajs/vue3": "^3.7.1",
88
87
  "@types/node": "^24.8.1",
89
88
  "@types/semver": "^7.7.1",
90
- "@types/unzipper": "^0.10.11",
91
89
  "@unhead/vue": "^2.1.4",
92
- "@vitejs/plugin-vue": "^6.0.1",
90
+ "@vitejs/plugin-vue": "^6.0.6",
93
91
  "@vue/server-renderer": "^3.5.25",
94
92
  "defu": "^6.1.4",
95
93
  "devalue": "^5.9.4",
@@ -104,7 +102,7 @@
104
102
  "tsx": "^4.20.6",
105
103
  "typescript": "^6.0.3",
106
104
  "unplugin-auto-import": "^20.2.0",
107
- "vite": "^7.2.4",
105
+ "vite": "^8.3.4",
108
106
  "vue": "^3.5.25",
109
107
  "vue-i18n": "^11.2.7"
110
108
  },
@@ -119,7 +117,7 @@
119
117
  "@oxc-parser/binding-win32-x64-msvc": "^0.95.0"
120
118
  },
121
119
  "engines": {
122
- "node": ">=20.12.0"
120
+ "node": ">=20.19.0"
123
121
  },
124
122
  "packageManager": "pnpm@10.6.5",
125
123
  "antelopeJs": {
@@ -18,8 +18,8 @@ tags: [frontend, cli, renderer, vue, inertia, ssr, deployment, dms-module]
18
18
 
19
19
  ## Rules that bite
20
20
 
21
- - **One workspace per backend URL**, under `~/.antelopejs/dms-frontend/<sha256>/`. `dev` without `-b` keys its own on the project directory instead, so `dev` then `build -b` builds a second workspace. `start` serves the workspace `build` made for the same URL, user and home directory.
22
- - **Secrets.** `build` and `start` need the same `DMS_SESSION_SECRET` (32+ characters); `dev` generates one only when it is unset. `build` takes `DMS_BOOTSTRAP_SECRET` explicitly, never from `.antelope/dms-dev.json`. A build without it still succeeds, but loses the modules' private options, the manifest's backend proxies and the e-mail and OAuth secrets: `start` then warns that e-mail renders and OAuth sign-in will be refused.
21
+ - **One dev workspace per backend URL**, under `~/.antelopejs/dms-frontend/<sha256>/`. `dev` without `-b` keys its own on the project directory instead. A production build is different: `build` reads the project itself (`antelope.config.ts` with `-e production`, each installed module's `dmsFrontend` function), needs no backend, and writes `<project>/.dms-frontend`, which `start` serves from the project root.
22
+ - **Secrets.** `start` needs `DMS_SESSION_SECRET` (32+ characters); `dev` generates one only when it is unset. `build` needs no secret. `start` reads `DMS_HTML_RENDER_SECRET` and `DMS_OAUTH_RELAY_SECRET` from its environment and warns when they are unset, and computes the modules' public options from the project's configuration with its own variables, which must match the backend's.
23
23
  - **`.env`** is read from the current directory only (shell, then `.env.local`, then `.env`). A `DMS_API_BASE_URL` in `.env` turns off backend discovery for `dev`. `clean` never reads it.
24
24
  - **`prepare` never fails** without `--strict`: it warns and exits 0. Use `--strict` where the types are required.
25
25
  - **Exit codes:** 0 success, 1 failure, 2 usage error, 130 Ctrl+C.
@@ -0,0 +1,61 @@
1
+ // How the client bundle is cut into files, and what the build says about it.
2
+ //
3
+ // Kept beside vite.config.ts rather than inside it so the options can be
4
+ // exercised on their own, the way head-order.mjs and client-server-guard.mjs are.
5
+
6
+ /**
7
+ * Rolldown's `output.codeSplitting` for the client build.
8
+ *
9
+ * By default Rolldown gives every set of dynamic imports that shares a module
10
+ * its own common chunk. The DMS's pages are hundreds of dynamic imports (a
11
+ * module's components and pages are `import.meta.glob`ed), so the code the
12
+ * entry also needs lands in about sixty small common chunks the entry imports
13
+ * before anything runs: one request each, 55 to 60 more files on every page
14
+ * than the single entry file Rollup made, for no byte saved.
15
+ *
16
+ * The `$initial` tag is every module the entry imports statically, directly or
17
+ * not. One group over it puts them in the entry file again, and leaves every
18
+ * lazy chunk to Rolldown's own splitting, which a page only loads when it needs
19
+ * it: what an anonymous page loads is still what that page's own routes pull.
20
+ *
21
+ * Deliberately not here: groups over what the lazy chunks share (the UI
22
+ * libraries, a chunk per module). They cut a signed-in page's files by a
23
+ * quarter to four fifths, but every group is loaded whole by each page that
24
+ * uses any of it, and the anonymous pages pay 70 to 115 KB of Brotli for code
25
+ * only the signed-in shell uses.
26
+ */
27
+ export const clientCodeSplitting = {
28
+ groups: [{ name: "entry", tags: ["$initial"] }],
29
+ };
30
+
31
+ const MODULE_ENTRY = /[\\/]dms\.frontend\.[cm]?[jt]sx?$/;
32
+
33
+ /**
34
+ * Rolldown's `onLog` for every build: drops the
35
+ * INEFFECTIVE_DYNAMIC_IMPORT notices whose dynamic importer is a module's
36
+ * `dms.frontend.ts`.
37
+ *
38
+ * That file `import.meta.glob`s every component of the module so a page can
39
+ * resolve one by name. A few of them (StatusPill, MonoCell, IdentityCell: the
40
+ * table cell renderers) are also imported statically by the entry, which is
41
+ * what a cell renderer needs at hydration. The glob entry then resolves to the
42
+ * chunk already loaded: nothing is duplicated and nothing was meant to be
43
+ * lazy, and nobody can act on the notice. A `import()` a module author
44
+ * writes by hand for a file that is also imported statically is the same
45
+ * mistake with a different cause (the lazy load never happens), and still
46
+ * warns.
47
+ *
48
+ * @param {string} level
49
+ * @param {{ code?: string, ids?: string[] }} log
50
+ * @param {(level: string, log: object) => void} defaultHandler
51
+ */
52
+ export function onBuildLog(level, log, defaultHandler) {
53
+ if (
54
+ log.code === "INEFFECTIVE_DYNAMIC_IMPORT" &&
55
+ log.ids?.length &&
56
+ MODULE_ENTRY.test(log.ids[0])
57
+ ) {
58
+ return;
59
+ }
60
+ defaultHandler(level, log);
61
+ }
@@ -6,6 +6,7 @@ import { appConfigs } from "./app-configs.generated";
6
6
  import moduleRegistry from "./generated-frontend-modules.json";
7
7
  import { loadEmailLocaleMessages } from "./email-locales";
8
8
  import { stripSsrMarkers } from "./email-markup";
9
+ import { parsePublicOptions } from "./public-options";
9
10
 
10
11
  interface EmailModule {
11
12
  default: Component;
@@ -20,6 +21,8 @@ interface EmailRenderOptions {
20
21
  locale?: string;
21
22
  }
22
23
 
24
+ const runtimeOptions = parsePublicOptions(process.env.DMS_PUBLIC_OPTIONS);
25
+
23
26
  const registries = import.meta.glob<EmailRegistry>(
24
27
  "/frontend-modules/*/dms.email.ts",
25
28
  { eager: true },
@@ -157,7 +160,9 @@ export async function renderEmail(
157
160
  app.provide("dmsEmailAppConfig", defu({}, ...emailAppConfigs));
158
161
  const publicConfig = defu(
159
162
  {},
160
- ...moduleRegistry.modules.map((module) => module.options),
163
+ ...moduleRegistry.modules.map(
164
+ (module) => runtimeOptions?.[module.id] ?? module.options,
165
+ ),
161
166
  );
162
167
  app.provide("dmsEmailRuntimeConfig", {
163
168
  public: defu(
@@ -1,6 +1,6 @@
1
1
  import { isAbsolute, posix, resolve } from "node:path";
2
2
  import { normalizePath } from "vite";
3
- import type { DmsFrontendBuildSetup } from "./frontend-build";
3
+ import type { DmsFrontendBuildSetup } from "./frontend-build.ts";
4
4
 
5
5
  /** A module's `dms.frontend.build.ts`, as the generated loader imports it. */
6
6
  export interface FrontendBuildDeclaration {
@@ -55,6 +55,7 @@ import {
55
55
  mergeLayerAppConfigs,
56
56
  withAppConfigChanges,
57
57
  } from "./layer-app-config";
58
+ import type { DmsPublicOptionsById } from "./public-options";
58
59
 
59
60
  export interface DmsPageRoute {
60
61
  displayName?: string;
@@ -457,11 +458,34 @@ export interface DmsFrontendModule {
457
458
  setup(sdk: DmsFrontendSdk): void | Promise<void>;
458
459
  }
459
460
  export interface DmsFrontendModuleRegistration {
461
+ /** The module's id in the generated module registry. */
462
+ id?: string;
460
463
  /** The module's manifest name, which collision warnings name it by. */
461
464
  name?: string;
462
465
  module: DmsFrontendModule;
463
466
  options: DmsModuleOptions;
464
467
  }
468
+
469
+ /**
470
+ * The registrations with the public options the server runs with in place of
471
+ * the build's: `ajs dms start` computes them from the project's configuration
472
+ * and the deployment's variables. Unset, the build's options stand.
473
+ */
474
+ export function withPublicOptions(
475
+ registrations: DmsFrontendModuleRegistration[],
476
+ byId: DmsPublicOptionsById | undefined,
477
+ ): DmsFrontendModuleRegistration[] {
478
+ if (!byId) return registrations;
479
+ return registrations.map((registration) => {
480
+ const options = registration.id ? byId[registration.id] : undefined;
481
+ return options
482
+ ? {
483
+ ...registration,
484
+ options: { ...registration.options, public: options },
485
+ }
486
+ : registration;
487
+ });
488
+ }
465
489
  /** A layer's `app/app.config.ts`, as `app-configs.generated.ts` lists them. */
466
490
  export interface DmsLayerAppConfig {
467
491
  /** The id of the frontend module the layer belongs to. */