@antelopejs/dms-frontend 0.3.1 → 0.3.3

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.
package/README.md CHANGED
@@ -29,7 +29,8 @@ loader's contract with a renderer has three parts:
29
29
  `templates/<renderer>/` — `templates/vue/` today. `TEMPLATE_FILES` in
30
30
  `src/config.ts` lists what is copied verbatim, and `src/materialize.ts`
31
31
  resolves the template root, copies it, materializes the frontend modules
32
- under `frontend-modules/`, and writes `generated-frontend-modules.json` in
32
+ under `frontend-modules/`, and writes the files derived from them
33
+ (`src/derived-outputs.ts`), `generated-frontend-modules.json` first, in
33
34
  manifest-priority order.
34
35
  - **Generated server.** `templates/vue/server.mjs` and `templates/vue/server/`
35
36
  become the Node server that `ajs dms start` runs from the built workspace:
@@ -135,6 +136,27 @@ export default frontendModule;
135
136
 
136
137
  The SDK also exposes `use` for Vue plugins. Entries execute by descending manifest priority, then stable module id. Modules without `dms.frontend.ts` are skipped by the generated loader. Registered page keys match a route's `fullSlug`, request path, or `default`; unregistered pages and components use the generic card renderer.
137
138
 
139
+ Within one setup, the first module to register a name keeps it. `ajs-dms dev` sets every module up again each time a change to a layer reaches the server renderer, into empty registries, so the next server render uses the edited component without a restart. A `setup` may therefore run more than once in a process: it should only register, and keep no state of its own between runs.
140
+
141
+ ### Declaring the loader releases a module supports
142
+
143
+ A frontend module names the `@antelopejs/dms-frontend` releases it runs on in its own `package.json`, the way a package names the Node.js versions it supports:
144
+
145
+ ```json
146
+ {
147
+ "name": "@acme/dms-billing-frontend-vue",
148
+ "engines": {
149
+ "@antelopejs/dms-frontend": ">=0.3.2 <0.4.0"
150
+ }
151
+ }
152
+ ```
153
+
154
+ `prepare`, `dev`, `build` and `verify-source` read it before they generate anything, whether the manifest was just fetched or replayed from the cache, and stop when this release falls outside a module's range: `dev`, `build` and `verify-source` exit 1, and `prepare` warns and exits 0, as it does for any setup it cannot complete. The message names each such module, its range and this release, rather than letting the build, the type-check or a render fail later on an API the release no longer has, or does not have yet. A prerelease is checked as the release it leads to: `0.4.0-next.1` is outside `<0.4.0`. A module that declares no range still loads, and a warning names it once. `start` checks nothing: it runs what `build` produced.
155
+
156
+ Cap the range below the next breaking release, which is the next minor release while this package is 0.x, and widen it once the module has been checked against that release. A range left open to `<1.0.0` admits the very release that removes something the module calls.
157
+
158
+ The range is an engine rather than a peer dependency: the generated workspace installs every module as a workspace package, and pnpm installs a workspace package's peer dependencies, optional ones included, so a peer on the loader would pull a copy of it into every workspace. Package managers ignore an engine they do not know, and so does a loader released before this check.
159
+
138
160
  ### Redirecting typed access refusals
139
161
 
140
162
  A backend can refuse a whole surface with a typed 403 whose body is a machine-readable code, such as a tenant access gate blocking a suspended workspace. A module that owns such a code registers where a refused page visit goes instead of the error page:
@@ -324,6 +346,15 @@ merged catalogs the application imports: the next server render uses them, and
324
346
  the open page applies them in place, without a reload. Adding or removing a
325
347
  locale reloads the page instead.
326
348
 
349
+ The other files the workspace derives from every module follow their sources
350
+ the same way. A file added to, changed in or removed from a module's `public/`
351
+ is served, or no longer served, at once. Adding or removing an
352
+ `app/config/shortcuts-registry.ts` or a `dms.frontend.ts` regenerates the
353
+ aggregated shortcuts or the module loader, and the page reloads. A layer
354
+ directory added to or removed from `layers/` is the exception: Vite reads the
355
+ `#<layer>` aliases and the auto-imported directories at startup only, so the
356
+ dev server says it needs a restart.
357
+
327
358
  ## Options
328
359
 
329
360
  | Option | Environment | Purpose |
@@ -358,6 +389,6 @@ ajs dms verify-source \
358
389
  --local-package @antelopejs/dms=/path/to/dms
359
390
  ```
360
391
 
361
- The verifier prints its generated workspace path, builds client, SSR, and email bundles, runs `vue-tsc`, renders eight DMS email templates, and checks that the email bundle excludes browser-only modules. It leaves the generated workspace in the temporary directory for inspection. It does not start a backend or publish packages. Repository development can invoke the same runner with `DMS_LAYER_SOURCE` through `pnpm test:real-source`.
392
+ A supplied package whose declared range excludes this adapter version is refused before anything is installed (see [Declaring the loader releases a module supports](#declaring-the-loader-releases-a-module-supports)). The verifier prints its generated workspace path, builds client, SSR, and email bundles, runs `vue-tsc`, renders eight DMS email templates, and checks that the email bundle excludes browser-only modules. It leaves the generated workspace in the temporary directory for inspection. It does not start a backend or publish packages. Repository development can invoke the same runner with `DMS_LAYER_SOURCE` through `pnpm test:real-source`.
362
393
 
363
394
  Refresh rotation is single-flight within one frontend process. Horizontally scaled deployments must use sticky routing so a browser reaches the same process, or replace this process-local behavior with an external session adapter. It does not provide a distributed rotation guarantee.
package/dist/common.d.ts CHANGED
@@ -6,5 +6,7 @@ export * from "./fs-sync";
6
6
  export * from "./manifest-secrets";
7
7
  export * from "./layer-watch";
8
8
  export * from "./materialize";
9
+ export * from "./derived-outputs";
10
+ export * from "./renderer-range";
9
11
  export * from "./workspace";
10
12
  export * from "./workspace-setup";
package/dist/common.js CHANGED
@@ -22,5 +22,7 @@ __exportStar(require("./fs-sync"), exports);
22
22
  __exportStar(require("./manifest-secrets"), exports);
23
23
  __exportStar(require("./layer-watch"), exports);
24
24
  __exportStar(require("./materialize"), exports);
25
+ __exportStar(require("./derived-outputs"), exports);
26
+ __exportStar(require("./renderer-range"), exports);
25
27
  __exportStar(require("./workspace"), exports);
26
28
  __exportStar(require("./workspace-setup"), exports);
@@ -0,0 +1,22 @@
1
+ import { FrontendModuleOptions, ResolvedLayer } from "./workspace";
2
+ export interface FrontendModuleRegistryEntry {
3
+ id: string;
4
+ packageName?: string;
5
+ root: string;
6
+ priority: number;
7
+ entry?: string;
8
+ options: FrontendModuleOptions;
9
+ }
10
+ export interface FrontendModuleRegistry {
11
+ modules: FrontendModuleRegistryEntry[];
12
+ }
13
+ export declare function createFrontendModuleRegistry(workspaceDir: string, layers: ResolvedLayer[]): FrontendModuleRegistry;
14
+ export declare function affectsLocaleMessages(relativePath: string): boolean;
15
+ export declare function writeLocaleMessages(workspaceDir: string, registry: FrontendModuleRegistry): boolean;
16
+ export interface DerivedOutput {
17
+ name: string;
18
+ affects(relativePath: string): boolean;
19
+ write(workspaceDir: string, registry: FrontendModuleRegistry): boolean;
20
+ restartNotice?: string;
21
+ }
22
+ export declare const DERIVED_OUTPUTS: readonly DerivedOutput[];
@@ -0,0 +1,296 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DERIVED_OUTPUTS = void 0;
4
+ exports.createFrontendModuleRegistry = createFrontendModuleRegistry;
5
+ exports.affectsLocaleMessages = affectsLocaleMessages;
6
+ exports.writeLocaleMessages = writeLocaleMessages;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ const config_1 = require("./config");
10
+ const layer_aliases_1 = require("./layer-aliases");
11
+ const layers_1 = require("./layers");
12
+ const fs_sync_1 = require("./fs-sync");
13
+ const GENERATED_MODULE_ID = /^[A-Za-z0-9_-]+$/;
14
+ function validateFrontendModuleRegistry(registry, workspaceDir) {
15
+ const ids = new Set();
16
+ const moduleRoot = `${(0, node_path_1.resolve)(workspaceDir, config_1.LAYERS_SUBDIR)}${node_path_1.sep}`;
17
+ for (const module of registry.modules) {
18
+ if (!GENERATED_MODULE_ID.test(module.id) || ids.has(module.id))
19
+ throw new Error(`Invalid or duplicate frontend module id: ${module.id}`);
20
+ if (!Number.isFinite(module.priority))
21
+ throw new Error(`Invalid frontend module priority: ${module.id}`);
22
+ if (module.entry !== undefined && module.entry !== config_1.FRONTEND_MODULE_ENTRY)
23
+ throw new Error(`Invalid frontend module entry: ${module.id}`);
24
+ if (!(0, node_path_1.resolve)(module.root).startsWith(moduleRoot))
25
+ throw new Error(`Frontend module root escapes the workspace: ${module.id}`);
26
+ ids.add(module.id);
27
+ }
28
+ }
29
+ function createFrontendModuleRegistry(workspaceDir, layers) {
30
+ const modules = layers.map((layer) => {
31
+ const sourceEntry = (0, node_path_1.join)(layer.path, config_1.FRONTEND_MODULE_ENTRY);
32
+ const options = layer.options ?? {};
33
+ return {
34
+ id: (0, layers_1.getLayerSafeName)(layer),
35
+ packageName: layer.packageName,
36
+ root: (0, layers_1.getLayerWorkspacePath)(workspaceDir, layer),
37
+ priority: layer.priority ?? 0,
38
+ entry: (0, node_fs_1.existsSync)(sourceEntry) ? config_1.FRONTEND_MODULE_ENTRY : undefined,
39
+ options: layer.configKey ? { [layer.configKey]: options } : options,
40
+ };
41
+ });
42
+ modules.sort((left, right) => right.priority - left.priority || left.id.localeCompare(right.id));
43
+ const registry = { modules };
44
+ validateFrontendModuleRegistry(registry, workspaceDir);
45
+ return registry;
46
+ }
47
+ function writeFrontendTypePaths(workspaceDir, registry) {
48
+ const paths = {
49
+ "#dms/frontend-module": ["./frontend-module.ts"],
50
+ "@frontend/*": ["./frontend-modules/*"],
51
+ ...(0, layer_aliases_1.frontendLayerTypePaths)([...registry.modules].reverse().map((module) => module.root), { relativeTo: workspaceDir }),
52
+ };
53
+ const config = { compilerOptions: { paths } };
54
+ return (0, fs_sync_1.applyContent)(`${JSON.stringify(config, null, 2)}\n`, (0, node_path_1.join)(workspaceDir, "frontend-paths.generated.json"));
55
+ }
56
+ function writeFrontendModuleLoader(workspaceDir, registry) {
57
+ const modules = registry.modules.filter((module) => module.entry);
58
+ const imports = modules
59
+ .map((module, index) => `import module${index} from "@frontend/${module.id}/${module.entry}";`)
60
+ .join("\n");
61
+ const registrations = modules
62
+ .map((module, index) => `{ module: module${index}, options: { public: ${JSON.stringify(module.options)} } }`)
63
+ .join(", ");
64
+ const content = `${imports}\n\nimport type { DmsFrontendModuleRegistration } from "./frontend-module";\n\nexport const frontendModules: DmsFrontendModuleRegistration[] = [${registrations}];\n`;
65
+ return (0, fs_sync_1.applyContent)(content, (0, node_path_1.join)(workspaceDir, "frontend-modules.generated.ts"));
66
+ }
67
+ function writeModuleRegistry(workspaceDir, registry) {
68
+ const wroteRegistry = (0, fs_sync_1.applyContent)(`${JSON.stringify(registry, null, 2)}\n`, (0, node_path_1.join)(workspaceDir, "generated-frontend-modules.json"));
69
+ return writeFrontendModuleLoader(workspaceDir, registry) || wroteRegistry;
70
+ }
71
+ function moduleAssetRoots(moduleRoot) {
72
+ const layersRoot = (0, node_path_1.join)(moduleRoot, "layers");
73
+ if (!(0, node_fs_1.existsSync)(layersRoot)) {
74
+ return [{ absolutePath: moduleRoot, relativePrefix: "" }];
75
+ }
76
+ return (0, node_fs_1.readdirSync)(layersRoot).map((name) => ({
77
+ absolutePath: (0, node_path_1.join)(layersRoot, name),
78
+ relativePrefix: `layers/${name}/`,
79
+ }));
80
+ }
81
+ const PUBLIC_DIRECTORY = "public";
82
+ function writePublicAssets(workspaceDir, registry) {
83
+ const files = new Map();
84
+ for (const module of [...registry.modules].reverse()) {
85
+ for (const root of moduleAssetRoots(module.root)) {
86
+ const source = (0, node_path_1.join)(root.absolutePath, PUBLIC_DIRECTORY);
87
+ for (const file of (0, fs_sync_1.collectFiles)(source)) {
88
+ files.set(file, (0, node_path_1.join)(source, file));
89
+ }
90
+ }
91
+ }
92
+ return (0, fs_sync_1.mirrorFiles)(files, (0, node_path_1.join)(workspaceDir, PUBLIC_DIRECTORY));
93
+ }
94
+ function discoverAssets(registry, directory, extensions) {
95
+ return registry.modules.flatMap((module) => {
96
+ return moduleAssetRoots(module.root).flatMap((assetRoot) => {
97
+ const root = (0, node_path_1.join)(assetRoot.absolutePath, directory);
98
+ return (0, fs_sync_1.collectFiles)(root)
99
+ .filter((file) => extensions.some((extension) => file.endsWith(extension)))
100
+ .sort()
101
+ .map((relativePath) => ({
102
+ moduleId: module.id,
103
+ relativePath: `${assetRoot.relativePrefix}${(0, fs_sync_1.toPosixPath)((0, node_path_1.join)(directory, relativePath))}`,
104
+ }));
105
+ });
106
+ });
107
+ }
108
+ function isRecord(value) {
109
+ return typeof value === "object" && value !== null && !Array.isArray(value);
110
+ }
111
+ function mergeLocaleMessages(source, existing) {
112
+ const merged = { ...source };
113
+ for (const [key, value] of Object.entries(existing)) {
114
+ const sourceValue = merged[key];
115
+ merged[key] =
116
+ isRecord(sourceValue) && isRecord(value)
117
+ ? mergeLocaleMessages(sourceValue, value)
118
+ : value;
119
+ }
120
+ return merged;
121
+ }
122
+ const LOCALE_DIRECTORY = "i18n/locales";
123
+ function readLocaleFile(path) {
124
+ try {
125
+ return JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
126
+ }
127
+ catch (error) {
128
+ const reason = error instanceof Error ? error.message : String(error);
129
+ throw new Error(`Invalid locale file ${path}: ${reason}`);
130
+ }
131
+ }
132
+ function readLocaleMessages(assets, registry) {
133
+ const roots = new Map(registry.modules.map((module) => [module.id, module.root]));
134
+ const messages = {};
135
+ for (const asset of assets) {
136
+ const locale = asset.relativePath.match(/(?:^|[/-])([a-z]{2})-[A-Z]{2}\.json$/)?.[1];
137
+ const root = roots.get(asset.moduleId);
138
+ if (!locale || !root)
139
+ continue;
140
+ const value = readLocaleFile((0, node_path_1.join)(root, asset.relativePath));
141
+ messages[locale] = mergeLocaleMessages(value, messages[locale] ?? {});
142
+ }
143
+ return messages;
144
+ }
145
+ function isLayerDirectory(relativePath) {
146
+ const segments = (0, fs_sync_1.toPosixPath)(relativePath).split("/");
147
+ return segments[0] === layer_aliases_1.MODULE_LAYERS_DIRNAME && segments.length <= 2;
148
+ }
149
+ function affectsAssetDirectory(directory, relativePath) {
150
+ if (isLayerDirectory(relativePath))
151
+ return true;
152
+ const segments = (0, fs_sync_1.toPosixPath)(relativePath).split("/");
153
+ const withinRoot = segments[0] === layer_aliases_1.MODULE_LAYERS_DIRNAME ? segments.slice(2) : segments;
154
+ return directory
155
+ .split("/")
156
+ .every((segment, index) => index >= withinRoot.length || withinRoot[index] === segment);
157
+ }
158
+ function affectsLocaleMessages(relativePath) {
159
+ return affectsAssetDirectory(LOCALE_DIRECTORY, relativePath);
160
+ }
161
+ function localeModuleSource(supportedLocales) {
162
+ const loaders = supportedLocales
163
+ .filter((locale) => locale !== "en")
164
+ .map((locale) => ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(`./locales.generated/${locale}`)}),`);
165
+ return `import defaultMessages from "./locales.generated/en";
166
+
167
+ type LocaleMessages = Record<string, unknown>;
168
+ type LocaleLoader = () => Promise<{ default: LocaleMessages }>;
169
+ interface LocaleMessagesTarget {
170
+ setLocaleMessage(locale: string, messages: LocaleMessages): void;
171
+ }
172
+ interface LocaleState {
173
+ messages: Record<string, LocaleMessages>;
174
+ targets: Set<LocaleMessagesTarget>;
175
+ version: number;
176
+ }
177
+
178
+ export const supportedLocales = ${JSON.stringify(supportedLocales)};
179
+ const localeLoaders: Record<string, LocaleLoader> = {
180
+ "en": async () => ({ default: defaultMessages }),
181
+ ${loaders.join("\n")}
182
+ };
183
+ const shared: LocaleState | undefined = import.meta.hot?.data.state;
184
+ const state: LocaleState = shared ?? {
185
+ messages: { en: defaultMessages },
186
+ targets: new Set(),
187
+ version: 0,
188
+ };
189
+ export const localeMessages = state.messages;
190
+
191
+ async function readLocaleMessages(locale: string): Promise<LocaleMessages> {
192
+ return (await localeLoaders[locale]()).default;
193
+ }
194
+
195
+ export async function loadLocaleMessages(locale: string): Promise<LocaleMessages> {
196
+ const normalized = locale.slice(0, 2);
197
+ if (!localeLoaders[normalized]) return localeMessages.en;
198
+ localeMessages[normalized] ??= await readLocaleMessages(normalized);
199
+ return localeMessages[normalized];
200
+ }
201
+
202
+ /** Development only: applies every regenerated catalog to \`target\`. */
203
+ export function syncLocaleMessages(target: LocaleMessagesTarget): void {
204
+ if (import.meta.hot) state.targets.add(target);
205
+ }
206
+
207
+ async function refreshLocaleMessages(): Promise<void> {
208
+ const version = ++state.version;
209
+ const locales = Object.keys(localeMessages).filter(
210
+ (locale) => locale in localeLoaders,
211
+ );
212
+ const catalogs = await Promise.all(locales.map(readLocaleMessages));
213
+ if (version !== state.version) return;
214
+ locales.forEach((locale, index) => {
215
+ localeMessages[locale] = catalogs[index];
216
+ for (const target of state.targets)
217
+ target.setLocaleMessage(locale, catalogs[index]);
218
+ });
219
+ }
220
+
221
+ if (import.meta.hot) {
222
+ import.meta.hot.data.state = state;
223
+ if (shared) void refreshLocaleMessages();
224
+ import.meta.hot.accept((next) => {
225
+ if (next?.supportedLocales.join() !== supportedLocales.join())
226
+ import.meta.hot?.invalidate("the list of locales changed");
227
+ });
228
+ }
229
+ `;
230
+ }
231
+ function writeLocaleMessages(workspaceDir, registry) {
232
+ const locales = discoverAssets(registry, LOCALE_DIRECTORY, [".json"]);
233
+ const messages = readLocaleMessages(locales, registry);
234
+ const outputDir = (0, node_path_1.join)(workspaceDir, "locales.generated");
235
+ const outputs = new Set();
236
+ let changed = false;
237
+ const apply = (content, path) => {
238
+ changed = (0, fs_sync_1.applyContent)(content, path) || changed;
239
+ };
240
+ (0, node_fs_1.mkdirSync)(outputDir, { recursive: true });
241
+ for (const [locale, value] of Object.entries(messages)) {
242
+ const catalog = JSON.stringify(value);
243
+ apply(catalog, (0, node_path_1.join)(outputDir, `${locale}.json`));
244
+ apply(`const messages = ${catalog};\nexport default messages;\n`, (0, node_path_1.join)(outputDir, `${locale}.ts`));
245
+ outputs.add(`${locale}.json`).add(`${locale}.ts`);
246
+ }
247
+ const supportedLocales = Object.keys(messages);
248
+ apply(JSON.stringify(supportedLocales), (0, node_path_1.join)(workspaceDir, "email-locales.generated.json"));
249
+ apply(localeModuleSource(supportedLocales), (0, node_path_1.join)(workspaceDir, "locales.generated.ts"));
250
+ for (const name of (0, node_fs_1.readdirSync)(outputDir)) {
251
+ if (outputs.has(name))
252
+ continue;
253
+ (0, node_fs_1.rmSync)((0, node_path_1.join)(outputDir, name), { recursive: true, force: true });
254
+ changed = true;
255
+ }
256
+ return changed;
257
+ }
258
+ const SHORTCUTS_DIRECTORY = "app/config";
259
+ function writeShortcutExports(workspaceDir, registry) {
260
+ const registries = discoverAssets(registry, SHORTCUTS_DIRECTORY, [
261
+ "shortcuts-registry.ts",
262
+ "shortcuts-registry.js",
263
+ ]);
264
+ const imports = registries.map((asset, index) => `import registry${index} from "@frontend/${asset.moduleId}/${asset.relativePath}";`);
265
+ const spreads = registries.map((_, index) => `...registry${index}`);
266
+ const content = `${imports.join("\n")}\nconst aggregatedShortcuts = [${spreads.join(", ")}];\nexport { aggregatedShortcuts };\nexport default aggregatedShortcuts;\n`;
267
+ return (0, fs_sync_1.applyContent)(content, (0, node_path_1.join)(workspaceDir, "shortcuts-aggregated.generated.ts"));
268
+ }
269
+ exports.DERIVED_OUTPUTS = [
270
+ {
271
+ name: "module registry",
272
+ affects: (path) => (0, fs_sync_1.toPosixPath)(path) === config_1.FRONTEND_MODULE_ENTRY,
273
+ write: writeModuleRegistry,
274
+ },
275
+ {
276
+ name: "type paths",
277
+ affects: isLayerDirectory,
278
+ write: writeFrontendTypePaths,
279
+ restartNotice: "A layer directory was added or removed: restart 'ajs dms dev' to apply it. Vite reads the '#<layer>' aliases and the auto-imported directories at startup only.",
280
+ },
281
+ {
282
+ name: "locale catalogs",
283
+ affects: affectsLocaleMessages,
284
+ write: writeLocaleMessages,
285
+ },
286
+ {
287
+ name: "aggregated shortcuts",
288
+ affects: (path) => affectsAssetDirectory(SHORTCUTS_DIRECTORY, path),
289
+ write: writeShortcutExports,
290
+ },
291
+ {
292
+ name: "public assets",
293
+ affects: (path) => affectsAssetDirectory(PUBLIC_DIRECTORY, path),
294
+ write: writePublicAssets,
295
+ },
296
+ ];
package/dist/fs-sync.d.ts CHANGED
@@ -3,7 +3,8 @@ export declare function toPosixPath(path: string, separator?: string): string;
3
3
  export declare function isBlocklistedCopyPath(src: string, srcPath: string): boolean;
4
4
  export declare function sanitizedPackageContent(raw: string): string;
5
5
  export declare function filesIdentical(srcPath: string, destPath: string): boolean;
6
- export declare function applyContent(content: string, destPath: string): void;
7
- export declare function applyFile(srcPath: string, destPath: string): void;
6
+ export declare function applyContent(content: string, destPath: string): boolean;
7
+ export declare function applyFile(srcPath: string, destPath: string): boolean;
8
+ export declare function mirrorFiles(files: ReadonlyMap<string, string>, dest: string): boolean;
8
9
  export declare function collectFiles(dir: string, baseDir?: string): string[];
9
10
  export declare function syncDirectories(src: string, dest: string): void;
package/dist/fs-sync.js CHANGED
@@ -10,6 +10,7 @@ exports.sanitizedPackageContent = sanitizedPackageContent;
10
10
  exports.filesIdentical = filesIdentical;
11
11
  exports.applyContent = applyContent;
12
12
  exports.applyFile = applyFile;
13
+ exports.mirrorFiles = mirrorFiles;
13
14
  exports.collectFiles = collectFiles;
14
15
  exports.syncDirectories = syncDirectories;
15
16
  const node_fs_1 = require("node:fs");
@@ -59,7 +60,7 @@ function applyContent(content, destPath) {
59
60
  if ((0, node_fs_1.existsSync)(destPath)) {
60
61
  try {
61
62
  if ((0, node_fs_1.readFileSync)(destPath).equals(buf))
62
- return;
63
+ return false;
63
64
  }
64
65
  catch {
65
66
  }
@@ -74,10 +75,11 @@ function applyContent(content, destPath) {
74
75
  if ((0, node_fs_1.existsSync)(tmp))
75
76
  (0, node_fs_1.rmSync)(tmp, { force: true });
76
77
  }
78
+ return true;
77
79
  }
78
80
  function applyFile(srcPath, destPath) {
79
81
  if (filesIdentical(srcPath, destPath))
80
- return;
82
+ return false;
81
83
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destPath), { recursive: true });
82
84
  const tmp = `${destPath}.ajs-dms-tmp-${process.pid}-${Date.now()}`;
83
85
  try {
@@ -88,6 +90,51 @@ function applyFile(srcPath, destPath) {
88
90
  if ((0, node_fs_1.existsSync)(tmp))
89
91
  (0, node_fs_1.rmSync)(tmp, { force: true });
90
92
  }
93
+ return true;
94
+ }
95
+ function clearWayTo(root, target) {
96
+ let cleared = false;
97
+ for (let dir = (0, node_path_1.dirname)(target); dir.length > root.length; dir = (0, node_path_1.dirname)(dir)) {
98
+ if ((0, node_fs_1.existsSync)(dir) && !(0, node_fs_1.statSync)(dir).isDirectory()) {
99
+ (0, node_fs_1.rmSync)(dir, { force: true });
100
+ cleared = true;
101
+ }
102
+ }
103
+ if ((0, node_fs_1.existsSync)(target) && (0, node_fs_1.statSync)(target).isDirectory()) {
104
+ (0, node_fs_1.rmSync)(target, { recursive: true, force: true });
105
+ cleared = true;
106
+ }
107
+ return cleared;
108
+ }
109
+ function pruneFiles(dir, keep, root = dir) {
110
+ let pruned = false;
111
+ for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
112
+ const path = (0, node_path_1.join)(dir, entry.name);
113
+ if (!entry.isDirectory()) {
114
+ if (keep.has((0, node_path_1.relative)(root, path)))
115
+ continue;
116
+ (0, node_fs_1.rmSync)(path, { force: true });
117
+ pruned = true;
118
+ continue;
119
+ }
120
+ pruned = pruneFiles(path, keep, root) || pruned;
121
+ if ((0, node_fs_1.readdirSync)(path).length === 0) {
122
+ (0, node_fs_1.rmSync)(path, { recursive: true, force: true });
123
+ pruned = true;
124
+ }
125
+ }
126
+ return pruned;
127
+ }
128
+ function mirrorFiles(files, dest) {
129
+ let changed = false;
130
+ for (const [file, source] of files) {
131
+ const target = (0, node_path_1.join)(dest, file);
132
+ changed = clearWayTo(dest, target) || changed;
133
+ changed = applyFile(source, target) || changed;
134
+ }
135
+ if (!(0, node_fs_1.existsSync)(dest))
136
+ return changed;
137
+ return pruneFiles(dest, new Set(files.keys())) || changed;
91
138
  }
92
139
  function loadGitignore(layerPath) {
93
140
  const ig = (0, ignore_1.default)();
@@ -8,10 +8,10 @@ const node_fs_1 = require("node:fs");
8
8
  const node_path_1 = require("node:path");
9
9
  const chokidar_1 = __importDefault(require("chokidar"));
10
10
  const layers_1 = require("./layers");
11
- const materialize_1 = require("./materialize");
11
+ const derived_outputs_1 = require("./derived-outputs");
12
12
  const fs_sync_1 = require("./fs-sync");
13
13
  const WATCH_DEBOUNCE_MS = 80;
14
- const LOCALE_REFRESH_DEBOUNCE_MS = 100;
14
+ const DERIVED_REFRESH_DEBOUNCE_MS = 100;
15
15
  function createLayerSync(src, dest, onFlushed) {
16
16
  const fileUpserts = new Set();
17
17
  const fileDeletes = new Set();
@@ -133,23 +133,43 @@ function createLayerSync(src, dest, onFlushed) {
133
133
  },
134
134
  };
135
135
  }
136
- function createLocaleRefresher(workspaceDir, layers) {
137
- const registry = (0, materialize_1.createFrontendModuleRegistry)(workspaceDir, layers);
136
+ function createDerivedOutputRefresher(workspaceDir, layers) {
137
+ const pending = new Set();
138
138
  let timer = null;
139
+ const regenerate = (output, registry) => {
140
+ try {
141
+ if (output.write(workspaceDir, registry) && output.restartNotice)
142
+ console.warn(`[ajs-dms] ${output.restartNotice}`);
143
+ }
144
+ catch (err) {
145
+ console.warn(`[ajs-dms] ${output.name} not regenerated:`, err);
146
+ }
147
+ };
139
148
  const refresh = () => {
140
149
  timer = null;
150
+ const outputs = derived_outputs_1.DERIVED_OUTPUTS.filter((output) => pending.has(output));
151
+ pending.clear();
152
+ let registry;
141
153
  try {
142
- (0, materialize_1.writeLocaleMessages)(workspaceDir, registry);
154
+ registry = (0, derived_outputs_1.createFrontendModuleRegistry)(workspaceDir, layers);
143
155
  }
144
156
  catch (err) {
145
- console.warn("[ajs-dms] locale catalogs not regenerated:", err);
157
+ console.warn("[ajs-dms] derived files not regenerated:", err);
158
+ return;
146
159
  }
160
+ for (const output of outputs)
161
+ regenerate(output, registry);
147
162
  };
148
163
  return {
149
- schedule() {
164
+ schedule(paths) {
165
+ const affected = derived_outputs_1.DERIVED_OUTPUTS.filter((output) => paths.some((path) => output.affects(path)));
166
+ if (affected.length === 0)
167
+ return;
168
+ for (const output of affected)
169
+ pending.add(output);
150
170
  if (timer)
151
171
  clearTimeout(timer);
152
- timer = setTimeout(refresh, LOCALE_REFRESH_DEBOUNCE_MS);
172
+ timer = setTimeout(refresh, DERIVED_REFRESH_DEBOUNCE_MS);
153
173
  },
154
174
  flushNow() {
155
175
  if (!timer)
@@ -162,16 +182,13 @@ function createLocaleRefresher(workspaceDir, layers) {
162
182
  function startLayerWatchers(workspaceDir, layers) {
163
183
  const watchers = [];
164
184
  const syncs = [];
165
- const localeRefresher = createLocaleRefresher(workspaceDir, layers);
185
+ const refresher = createDerivedOutputRefresher(workspaceDir, layers);
166
186
  for (const layer of layers) {
167
187
  if (!layer.packageName)
168
188
  continue;
169
189
  const dest = (0, layers_1.getLayerWorkspacePath)(workspaceDir, layer);
170
190
  const src = layer.path;
171
- const sync = createLayerSync(src, dest, (paths) => {
172
- if (paths.some(materialize_1.affectsLocaleMessages))
173
- localeRefresher.schedule();
174
- });
191
+ const sync = createLayerSync(src, dest, (paths) => refresher.schedule(paths));
175
192
  syncs.push(sync);
176
193
  const watcher = chokidar_1.default.watch(src, {
177
194
  ignoreInitial: true,
@@ -189,6 +206,6 @@ function startLayerWatchers(workspaceDir, layers) {
189
206
  await Promise.all(watchers.map((w) => w.close().catch(() => { })));
190
207
  for (const sync of syncs)
191
208
  sync.flushNow();
192
- localeRefresher.flushNow();
209
+ refresher.flushNow();
193
210
  };
194
211
  }
package/dist/layers.d.ts CHANGED
@@ -2,6 +2,11 @@ import { ServedWithPath } from "./manifest";
2
2
  import { ManifestModule, ResolvedLayer } from "./workspace";
3
3
  export declare function assertLayerPathsServed(modules: ManifestModule[]): asserts modules is ServedWithPath[];
4
4
  export declare function assertCachedArchivesExist(modules: ManifestModule[], cacheDir: string, fetchedAt?: string): void;
5
+ export interface LayerPackage {
6
+ name?: unknown;
7
+ engines?: Record<string, unknown>;
8
+ }
9
+ export declare function readLayerPackage(layerPath: string): LayerPackage | undefined;
5
10
  export declare function buildLayersFromPaths(modules: ManifestModule[]): ResolvedLayer[];
6
11
  export declare function downloadAndExtractLayers(backendUrl: string, packUrl: string, cacheDir: string, force: boolean, bootstrapSecret?: string): Promise<void>;
7
12
  export declare function buildLayersFromCache(modules: ManifestModule[], cacheDir: string): Promise<ResolvedLayer[]>;
package/dist/layers.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.assertLayerPathsServed = assertLayerPathsServed;
4
4
  exports.assertCachedArchivesExist = assertCachedArchivesExist;
5
+ exports.readLayerPackage = readLayerPackage;
5
6
  exports.buildLayersFromPaths = buildLayersFromPaths;
6
7
  exports.downloadAndExtractLayers = downloadAndExtractLayers;
7
8
  exports.buildLayersFromCache = buildLayersFromCache;
@@ -40,18 +41,21 @@ function assertCachedArchivesExist(modules, cacheDir, fetchedAt) {
40
41
  function sortModulesByPriority(modules) {
41
42
  return [...modules].sort((a, b) => b.priority - a.priority);
42
43
  }
43
- function readLayerPackageName(layerPath) {
44
+ function readLayerPackage(layerPath) {
44
45
  const pkgPath = (0, node_path_1.join)(layerPath, "package.json");
45
46
  if (!(0, node_fs_1.existsSync)(pkgPath))
46
47
  return undefined;
47
48
  try {
48
- const pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgPath, "utf-8"));
49
- return typeof pkg.name === "string" ? pkg.name : undefined;
49
+ return JSON.parse((0, node_fs_1.readFileSync)(pkgPath, "utf-8"));
50
50
  }
51
51
  catch {
52
52
  return undefined;
53
53
  }
54
54
  }
55
+ function readLayerPackageName(layerPath) {
56
+ const name = readLayerPackage(layerPath)?.name;
57
+ return typeof name === "string" ? name : undefined;
58
+ }
55
59
  function resolveLayer(layerPath, mod) {
56
60
  return {
57
61
  path: layerPath,
@@ -1,22 +1,8 @@
1
- import { FrontendModuleOptions, ResolvedLayer } from "./workspace";
1
+ import { ResolvedLayer } from "./workspace";
2
2
  export declare function getPackageRoot(): string;
3
3
  export declare function copyStaticTemplates(workspaceDir: string): void;
4
4
  export declare function writeWorkspacePackageJson(workspaceDir: string, layers: ResolvedLayer[]): void;
5
5
  export declare function materializeLayers(workspaceDir: string, layers: ResolvedLayer[]): void;
6
- export interface FrontendModuleRegistryEntry {
7
- id: string;
8
- packageName?: string;
9
- root: string;
10
- priority: number;
11
- entry?: string;
12
- options: FrontendModuleOptions;
13
- }
14
- export interface FrontendModuleRegistry {
15
- modules: FrontendModuleRegistryEntry[];
16
- }
17
- export declare function createFrontendModuleRegistry(workspaceDir: string, layers: ResolvedLayer[]): FrontendModuleRegistry;
18
6
  export declare function collectAuthEstablishEndpoints(layers: ResolvedLayer[]): string[];
19
7
  export declare function writeFrontendModuleRegistry(workspaceDir: string, layers: ResolvedLayer[]): void;
20
- export declare function affectsLocaleMessages(relativePath: string): boolean;
21
- export declare function writeLocaleMessages(workspaceDir: string, registry: FrontendModuleRegistry): void;
22
8
  export declare function writeDmsMainCss(workspaceDir: string, layers: ResolvedLayer[]): void;