@antelopejs/dms-frontend 0.3.2 → 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 +33 -2
- package/dist/common.d.ts +2 -0
- package/dist/common.js +2 -0
- package/dist/derived-outputs.d.ts +22 -0
- package/dist/derived-outputs.js +296 -0
- package/dist/fs-sync.d.ts +3 -2
- package/dist/fs-sync.js +49 -2
- package/dist/layer-watch.js +31 -14
- package/dist/layers.d.ts +5 -0
- package/dist/layers.js +7 -3
- package/dist/materialize.d.ts +1 -15
- package/dist/materialize.js +4 -245
- package/dist/renderer-range.d.ts +10 -0
- package/dist/renderer-range.js +55 -0
- package/dist/verify-source-runner.js +1 -0
- package/dist/workspace-setup.js +2 -0
- package/package.json +1 -1
- package/templates/vue/frontend-module.ts +116 -51
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
|
|
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):
|
|
7
|
-
export declare function applyFile(srcPath: string, destPath: string):
|
|
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)();
|
package/dist/layer-watch.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
137
|
-
const
|
|
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,
|
|
154
|
+
registry = (0, derived_outputs_1.createFrontendModuleRegistry)(workspaceDir, layers);
|
|
143
155
|
}
|
|
144
156
|
catch (err) {
|
|
145
|
-
console.warn("[ajs-dms]
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
package/dist/materialize.d.ts
CHANGED
|
@@ -1,22 +1,8 @@
|
|
|
1
|
-
import {
|
|
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;
|