@antelopejs/dms-frontend 0.3.0 → 0.3.2
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 +24 -0
- package/dist/commands/dev.js +4 -1
- package/dist/commands/start.js +3 -0
- package/dist/common.d.ts +1 -0
- package/dist/common.js +1 -0
- package/dist/config.js +1 -0
- package/dist/layer-watch.js +42 -2
- package/dist/manifest-secrets.d.ts +31 -0
- package/dist/manifest-secrets.js +101 -0
- package/dist/materialize.d.ts +2 -0
- package/dist/materialize.js +102 -10
- package/dist/ports.js +1 -1
- package/dist/workspace-setup.d.ts +2 -0
- package/dist/workspace-setup.js +2 -0
- package/package.json +1 -1
- package/templates/vue/app-runtime.ts +27 -2
- package/templates/vue/frontend-module.ts +61 -5
- package/templates/vue/gitignore +9 -0
- package/templates/vue/network-error.ts +70 -0
- package/templates/vue/server/auth/backend.mjs +9 -2
- package/templates/vue/server/backend-response.mjs +85 -0
- package/templates/vue/server/email-bundle-watch.mjs +31 -0
- package/templates/vue/server/email.mjs +76 -1
- package/templates/vue/server/inertia.mjs +9 -0
- package/templates/vue/server/request-failure.mjs +54 -0
- package/templates/vue/server.mjs +47 -41
- package/templates/vue/ssr-renderer.ts +93 -61
- package/templates/vue/vite.email.config.ts +5 -1
package/README.md
CHANGED
|
@@ -112,6 +112,8 @@ An email entry exports `serverEmailTemplates` and may export a plain `appConfig`
|
|
|
112
112
|
|
|
113
113
|
Email builds ship complete merged translation catalogs as JSON under `dist/server/locales/`, separate from executable JavaScript. Deploy the entire `dist/server` directory. Each render loads only its requested language and the English fallback; arbitrary translation keys and module overrides remain available. Unknown languages fall back to English, while missing or corrupt files for a supported language fail rendering rather than silently dropping translations. The source harness keeps the 256 KiB JavaScript ceiling and reports locale-data bytes separately.
|
|
114
114
|
|
|
115
|
+
`ajs-dms dev` builds the email bundle too, once the dev server is ready, and rebuilds it whenever an email template changes. The build runs in a separate low-priority process, so page reloads and HMR never wait on it; a render requested while a build is running waits for that build to finish.
|
|
116
|
+
|
|
115
117
|
## Frontend modules
|
|
116
118
|
|
|
117
119
|
A materialized module opts into the Vue adapter with a root `dms.frontend.ts`:
|
|
@@ -133,6 +135,19 @@ export default frontendModule;
|
|
|
133
135
|
|
|
134
136
|
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.
|
|
135
137
|
|
|
138
|
+
### Redirecting typed access refusals
|
|
139
|
+
|
|
140
|
+
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:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
sdk.registerAccessRedirect(
|
|
144
|
+
"saas.errors.workspace.access_blocked",
|
|
145
|
+
"/workspace-suspended",
|
|
146
|
+
);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The server answers the refused visit, document or Inertia, with a redirect to that path before anything renders, so a reload lands there directly. The body may be the bare code or JSON (`"code"` or `{ "message": "code" }`). A destination refused in turn keeps the error page instead of looping. The first registration of a code wins, in manifest-priority order.
|
|
150
|
+
|
|
136
151
|
### Components rendered inside `<svg>`
|
|
137
152
|
|
|
138
153
|
Modules usually register their components lazily, with `defineAsyncComponent`, and the page is rendered under a `<Suspense>`. Vue mounts an async component that resolves under a `<Suspense>` with the `<Suspense>`'s element namespace rather than the namespace of the place it is rendered: a component resolved by name (`resolveComponent` or a template tag) whose root is an SVG element (`<path>`, `<g>`, …) is created as an HTML element inside the `<svg>`, and the browser draws nothing ([vuejs/core#15639](https://github.com/vuejs/core/issues/15639)). Import such components directly instead, for example the node and edge types passed to Vue Flow:
|
|
@@ -304,6 +319,11 @@ the matching Inertia page object to the browser for hydration. Subsequent
|
|
|
304
319
|
plugins and `DmsClientOnly` defer browser-only work while the same generated
|
|
305
320
|
frontend-module registry drives server and client entries.
|
|
306
321
|
|
|
322
|
+
Under `ajs dms dev`, an edit to a module's `i18n/locales/*.json` regenerates the
|
|
323
|
+
merged catalogs the application imports: the next server render uses them, and
|
|
324
|
+
the open page applies them in place, without a reload. Adding or removing a
|
|
325
|
+
locale reloads the page instead.
|
|
326
|
+
|
|
307
327
|
## Options
|
|
308
328
|
|
|
309
329
|
| Option | Environment | Purpose |
|
|
@@ -316,11 +336,15 @@ frontend-module registry drives server and client entries.
|
|
|
316
336
|
| | `DMS_COOKIE_SECURE` | Secure cookies (`true` by default; `ajs dms dev` defaults to `false`) |
|
|
317
337
|
| | `DMS_TRUSTED_PROXY_HOPS` | Number of trusted, rightmost reverse-proxy hops (default `0`) |
|
|
318
338
|
| | `DMS_SESSION_SECRET` | Session cookie encryption key, 32 characters or more (required for login) |
|
|
339
|
+
| | `DMS_HTML_RENDER_SECRET` | Secret that verifies the backend's signed HTML/email render requests; without it, e-mail renders are refused. Defaults to the `htmlRender.serviceSecret` the backend publishes in the frontend manifest |
|
|
340
|
+
| | `DMS_OAUTH_RELAY_SECRET` | Secret the server presents to the backend's OAuth endpoints; without it, the backend refuses OAuth sign-in. Defaults to the `oauth.relaySecret` the backend publishes in the frontend manifest |
|
|
319
341
|
| | `DMS_AUTH_ESTABLISH_ENDPOINTS` | Extra backend endpoints `/auth/establish` may open a session from, on top of those the backend's modules declare (comma-separated, empty by default) |
|
|
320
342
|
| | `DMS_CLIENT_BASE_URL` | Public frontend URL used in generated links and emails |
|
|
321
343
|
|
|
322
344
|
All of these can be set in the project's `.env` instead of the environment; see [Configuration](#configuration).
|
|
323
345
|
|
|
346
|
+
`ajs dms dev` takes the two manifest secrets from the manifest it fetches, and `ajs dms start` from the manifest cached by `ajs dms build`. An explicitly set variable always wins over the manifest value. Before the server starts, both commands log where each secret comes from (`env`, `manifest`, `build-time manifest` or `not set`), never its value. When several modules publish the same secret, the first one in manifest-priority order wins, and a module publishing a different value is named in a warning.
|
|
347
|
+
|
|
324
348
|
Use pnpm for all repository and workspace operations.
|
|
325
349
|
|
|
326
350
|
## Verify local frontend modules
|
package/dist/commands/dev.js
CHANGED
|
@@ -66,7 +66,7 @@ function cmdDev() {
|
|
|
66
66
|
const spinner = new cli_ui_1.Spinner("Setting up workspace...");
|
|
67
67
|
await spinner.start();
|
|
68
68
|
try {
|
|
69
|
-
const { workspaceDir, layers, manifestFromCache, manifestFetchedAt } = await (0, common_1.setupWorkspace)({
|
|
69
|
+
const { workspaceDir, layers, manifestFromCache, manifestFetchedAt, manifestSecrets, } = await (0, common_1.setupWorkspace)({
|
|
70
70
|
backendUrl,
|
|
71
71
|
force: !!options.force,
|
|
72
72
|
mode: "dev",
|
|
@@ -90,6 +90,8 @@ function cmdDev() {
|
|
|
90
90
|
console.log(chalk_1.default.dim(` Workspace: ${workspaceDir}`));
|
|
91
91
|
console.log(chalk_1.default.dim(` Backend: ${backendUrl}`));
|
|
92
92
|
console.log(chalk_1.default.dim(` Watching: ${layers.length} layer source tree(s)`));
|
|
93
|
+
const secrets = (0, common_1.resolveManifestSecrets)(manifestSecrets);
|
|
94
|
+
(0, common_1.reportManifestSecrets)(secrets);
|
|
93
95
|
console.log("");
|
|
94
96
|
const nodeModulesDir = (0, node_path_1.join)(workspaceDir, "node_modules");
|
|
95
97
|
await reserved.release();
|
|
@@ -103,6 +105,7 @@ function cmdDev() {
|
|
|
103
105
|
DMS_API_BASE_URL: backendUrl,
|
|
104
106
|
DMS_BOOTSTRAP_SECRET: bootstrapSecret,
|
|
105
107
|
DMS_SESSION_SECRET: sessionSecret,
|
|
108
|
+
...secrets.env,
|
|
106
109
|
NODE_OPTIONS: "--max-old-space-size=4096",
|
|
107
110
|
NODE_PATH: nodeModulesDir,
|
|
108
111
|
},
|
package/dist/commands/start.js
CHANGED
|
@@ -35,6 +35,8 @@ function cmdStart() {
|
|
|
35
35
|
console.log("");
|
|
36
36
|
(0, cli_ui_1.info)(`Starting production server on port ${chalk_1.default.cyan(options.port)}...`);
|
|
37
37
|
console.log(chalk_1.default.dim(` Workspace: ${workspaceDir}`));
|
|
38
|
+
const secrets = (0, common_1.resolveManifestSecrets)((0, common_1.collectManifestSecrets)((0, common_1.readCachedManifest)(workspaceDir)?.manifest.modules ?? []));
|
|
39
|
+
(0, common_1.reportManifestSecrets)(secrets, "build-time manifest");
|
|
38
40
|
console.log("");
|
|
39
41
|
const code = await (0, common_1.runCommand)("node", [serverPath], {
|
|
40
42
|
cwd: workspaceDir,
|
|
@@ -43,6 +45,7 @@ function cmdStart() {
|
|
|
43
45
|
PORT: options.port,
|
|
44
46
|
DMS_API_BASE_URL: options.backendUrl,
|
|
45
47
|
DMS_SESSION_SECRET: sessionSecret,
|
|
48
|
+
...secrets.env,
|
|
46
49
|
DMS_COOKIE_SECURE: process.env.DMS_COOKIE_SECURE ?? "true",
|
|
47
50
|
},
|
|
48
51
|
});
|
package/dist/common.d.ts
CHANGED
package/dist/common.js
CHANGED
|
@@ -19,6 +19,7 @@ __exportStar(require("./layers"), exports);
|
|
|
19
19
|
__exportStar(require("./layer-aliases"), exports);
|
|
20
20
|
__exportStar(require("./manifest"), exports);
|
|
21
21
|
__exportStar(require("./fs-sync"), exports);
|
|
22
|
+
__exportStar(require("./manifest-secrets"), exports);
|
|
22
23
|
__exportStar(require("./layer-watch"), exports);
|
|
23
24
|
__exportStar(require("./materialize"), exports);
|
|
24
25
|
__exportStar(require("./workspace"), exports);
|
package/dist/config.js
CHANGED
|
@@ -35,6 +35,7 @@ exports.TEMPLATE_FILES = [
|
|
|
35
35
|
["index.html", "index.html"],
|
|
36
36
|
["main.ts", "main.ts"],
|
|
37
37
|
["app-runtime.ts", "app-runtime.ts"],
|
|
38
|
+
["network-error.ts", "network-error.ts"],
|
|
38
39
|
["ssr-renderer.ts", "ssr-renderer.ts"],
|
|
39
40
|
["frontend-module.ts", "frontend-module.ts"],
|
|
40
41
|
["globals.d.ts", "globals.d.ts"],
|
package/dist/layer-watch.js
CHANGED
|
@@ -8,9 +8,11 @@ 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
12
|
const fs_sync_1 = require("./fs-sync");
|
|
12
13
|
const WATCH_DEBOUNCE_MS = 80;
|
|
13
|
-
|
|
14
|
+
const LOCALE_REFRESH_DEBOUNCE_MS = 100;
|
|
15
|
+
function createLayerSync(src, dest, onFlushed) {
|
|
14
16
|
const fileUpserts = new Set();
|
|
15
17
|
const fileDeletes = new Set();
|
|
16
18
|
const dirCreates = new Set();
|
|
@@ -25,6 +27,12 @@ function createLayerSync(src, dest) {
|
|
|
25
27
|
};
|
|
26
28
|
const flush = () => {
|
|
27
29
|
timer = null;
|
|
30
|
+
const touched = [
|
|
31
|
+
...dirCreates,
|
|
32
|
+
...fileUpserts,
|
|
33
|
+
...fileDeletes,
|
|
34
|
+
...dirDeletes,
|
|
35
|
+
];
|
|
28
36
|
for (const rel of [...dirCreates].sort((a, b) => a.length - b.length)) {
|
|
29
37
|
try {
|
|
30
38
|
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(dest, rel), { recursive: true });
|
|
@@ -78,6 +86,7 @@ function createLayerSync(src, dest) {
|
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
88
|
dirDeletes.clear();
|
|
89
|
+
onFlushed(touched);
|
|
81
90
|
};
|
|
82
91
|
const arm = () => {
|
|
83
92
|
if (timer)
|
|
@@ -124,15 +133,45 @@ function createLayerSync(src, dest) {
|
|
|
124
133
|
},
|
|
125
134
|
};
|
|
126
135
|
}
|
|
136
|
+
function createLocaleRefresher(workspaceDir, layers) {
|
|
137
|
+
const registry = (0, materialize_1.createFrontendModuleRegistry)(workspaceDir, layers);
|
|
138
|
+
let timer = null;
|
|
139
|
+
const refresh = () => {
|
|
140
|
+
timer = null;
|
|
141
|
+
try {
|
|
142
|
+
(0, materialize_1.writeLocaleMessages)(workspaceDir, registry);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
console.warn("[ajs-dms] locale catalogs not regenerated:", err);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
schedule() {
|
|
150
|
+
if (timer)
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
timer = setTimeout(refresh, LOCALE_REFRESH_DEBOUNCE_MS);
|
|
153
|
+
},
|
|
154
|
+
flushNow() {
|
|
155
|
+
if (!timer)
|
|
156
|
+
return;
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
refresh();
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
127
162
|
function startLayerWatchers(workspaceDir, layers) {
|
|
128
163
|
const watchers = [];
|
|
129
164
|
const syncs = [];
|
|
165
|
+
const localeRefresher = createLocaleRefresher(workspaceDir, layers);
|
|
130
166
|
for (const layer of layers) {
|
|
131
167
|
if (!layer.packageName)
|
|
132
168
|
continue;
|
|
133
169
|
const dest = (0, layers_1.getLayerWorkspacePath)(workspaceDir, layer);
|
|
134
170
|
const src = layer.path;
|
|
135
|
-
const sync = createLayerSync(src, dest)
|
|
171
|
+
const sync = createLayerSync(src, dest, (paths) => {
|
|
172
|
+
if (paths.some(materialize_1.affectsLocaleMessages))
|
|
173
|
+
localeRefresher.schedule();
|
|
174
|
+
});
|
|
136
175
|
syncs.push(sync);
|
|
137
176
|
const watcher = chokidar_1.default.watch(src, {
|
|
138
177
|
ignoreInitial: true,
|
|
@@ -150,5 +189,6 @@ function startLayerWatchers(workspaceDir, layers) {
|
|
|
150
189
|
await Promise.all(watchers.map((w) => w.close().catch(() => { })));
|
|
151
190
|
for (const sync of syncs)
|
|
152
191
|
sync.flushNow();
|
|
192
|
+
localeRefresher.flushNow();
|
|
153
193
|
};
|
|
154
194
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ManifestModule } from "./workspace";
|
|
2
|
+
export declare const MANIFEST_SECRET_ENV: {
|
|
3
|
+
readonly DMS_HTML_RENDER_SECRET: readonly ["htmlRender", "serviceSecret"];
|
|
4
|
+
readonly DMS_OAUTH_RELAY_SECRET: readonly ["oauth", "relaySecret"];
|
|
5
|
+
};
|
|
6
|
+
export type ManifestSecretName = keyof typeof MANIFEST_SECRET_ENV;
|
|
7
|
+
interface PublishedSecret {
|
|
8
|
+
value: string;
|
|
9
|
+
module: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ManifestSecretConflict {
|
|
12
|
+
name: ManifestSecretName;
|
|
13
|
+
used: string;
|
|
14
|
+
ignored: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface ManifestSecrets {
|
|
17
|
+
published: Partial<Record<ManifestSecretName, PublishedSecret>>;
|
|
18
|
+
conflicts: ManifestSecretConflict[];
|
|
19
|
+
}
|
|
20
|
+
export type ManifestSecretSource = "env" | "manifest" | "not set";
|
|
21
|
+
export interface ResolvedManifestSecrets {
|
|
22
|
+
env: Partial<Record<ManifestSecretName, string>>;
|
|
23
|
+
sources: Record<ManifestSecretName, ManifestSecretSource>;
|
|
24
|
+
conflicts: ManifestSecretConflict[];
|
|
25
|
+
}
|
|
26
|
+
export declare function collectManifestSecrets(modules: readonly ManifestModule[]): ManifestSecrets;
|
|
27
|
+
export declare function resolveManifestSecrets(secrets: ManifestSecrets, env?: NodeJS.ProcessEnv): ResolvedManifestSecrets;
|
|
28
|
+
export declare function formatSecretConflicts(conflicts: readonly ManifestSecretConflict[]): string[];
|
|
29
|
+
export declare function formatSecretSources(sources: ResolvedManifestSecrets["sources"], manifestLabel?: string): string[];
|
|
30
|
+
export declare function reportManifestSecrets(resolved: ResolvedManifestSecrets, manifestLabel?: string, log?: (line: string) => void): void;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MANIFEST_SECRET_ENV = void 0;
|
|
7
|
+
exports.collectManifestSecrets = collectManifestSecrets;
|
|
8
|
+
exports.resolveManifestSecrets = resolveManifestSecrets;
|
|
9
|
+
exports.formatSecretConflicts = formatSecretConflicts;
|
|
10
|
+
exports.formatSecretSources = formatSecretSources;
|
|
11
|
+
exports.reportManifestSecrets = reportManifestSecrets;
|
|
12
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
13
|
+
exports.MANIFEST_SECRET_ENV = {
|
|
14
|
+
DMS_HTML_RENDER_SECRET: ["htmlRender", "serviceSecret"],
|
|
15
|
+
DMS_OAUTH_RELAY_SECRET: ["oauth", "relaySecret"],
|
|
16
|
+
};
|
|
17
|
+
const SECRET_NAMES = Object.keys(exports.MANIFEST_SECRET_ENV);
|
|
18
|
+
const UNSET_CONSEQUENCE = {
|
|
19
|
+
DMS_HTML_RENDER_SECRET: "e-mail renders will be refused",
|
|
20
|
+
DMS_OAUTH_RELAY_SECRET: "OAuth sign-in will be refused by the backend",
|
|
21
|
+
};
|
|
22
|
+
function readPath(options, path) {
|
|
23
|
+
let value = options;
|
|
24
|
+
for (const key of path) {
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
26
|
+
return undefined;
|
|
27
|
+
value = value[key];
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function byPriority(modules) {
|
|
32
|
+
return [...modules].sort((a, b) => b.priority - a.priority);
|
|
33
|
+
}
|
|
34
|
+
function collectManifestSecrets(modules) {
|
|
35
|
+
const published = {};
|
|
36
|
+
const conflicts = [];
|
|
37
|
+
const ordered = byPriority(modules);
|
|
38
|
+
for (const name of SECRET_NAMES) {
|
|
39
|
+
const ignored = [];
|
|
40
|
+
for (const module of ordered) {
|
|
41
|
+
const value = readPath(module.privateOptions, exports.MANIFEST_SECRET_ENV[name]);
|
|
42
|
+
if (typeof value !== "string" || value === "")
|
|
43
|
+
continue;
|
|
44
|
+
const first = published[name];
|
|
45
|
+
if (!first)
|
|
46
|
+
published[name] = { value, module: module.name };
|
|
47
|
+
else if (first.value !== value)
|
|
48
|
+
ignored.push(module.name);
|
|
49
|
+
}
|
|
50
|
+
const used = published[name];
|
|
51
|
+
if (used && ignored.length)
|
|
52
|
+
conflicts.push({ name, used: used.module, ignored });
|
|
53
|
+
}
|
|
54
|
+
return { published, conflicts };
|
|
55
|
+
}
|
|
56
|
+
function resolveManifestSecrets(secrets, env = process.env) {
|
|
57
|
+
const resolved = {
|
|
58
|
+
env: {},
|
|
59
|
+
sources: {},
|
|
60
|
+
conflicts: [],
|
|
61
|
+
};
|
|
62
|
+
for (const name of SECRET_NAMES) {
|
|
63
|
+
const explicit = env[name];
|
|
64
|
+
const published = secrets.published[name];
|
|
65
|
+
if (explicit) {
|
|
66
|
+
resolved.env[name] = explicit;
|
|
67
|
+
resolved.sources[name] = "env";
|
|
68
|
+
}
|
|
69
|
+
else if (published) {
|
|
70
|
+
resolved.env[name] = published.value;
|
|
71
|
+
resolved.sources[name] = "manifest";
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
resolved.sources[name] = "not set";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
resolved.conflicts = secrets.conflicts.filter((conflict) => resolved.sources[conflict.name] === "manifest");
|
|
78
|
+
return resolved;
|
|
79
|
+
}
|
|
80
|
+
function formatSecretConflicts(conflicts) {
|
|
81
|
+
return conflicts.map(({ name, used, ignored }) => `${name}: ${ignored.join(", ")} publish${ignored.length === 1 ? "es" : ""} a different value than ${used}; using ${used}'s`);
|
|
82
|
+
}
|
|
83
|
+
function formatSecretSources(sources, manifestLabel = "manifest") {
|
|
84
|
+
const width = Math.max(...SECRET_NAMES.map((name) => name.length));
|
|
85
|
+
return SECRET_NAMES.map((name) => {
|
|
86
|
+
const source = sources[name];
|
|
87
|
+
const label = source === "manifest"
|
|
88
|
+
? manifestLabel
|
|
89
|
+
: source === "not set"
|
|
90
|
+
? `not set (${UNSET_CONSEQUENCE[name]})`
|
|
91
|
+
: source;
|
|
92
|
+
return `${name.padEnd(width)} ${label}`;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function reportManifestSecrets(resolved, manifestLabel = "manifest", log = console.log) {
|
|
96
|
+
for (const line of formatSecretConflicts(resolved.conflicts))
|
|
97
|
+
log(`${chalk_1.default.yellow.bold("⚠")} ${chalk_1.default.yellow(line)}`);
|
|
98
|
+
log(chalk_1.default.dim(" Server secrets:"));
|
|
99
|
+
for (const line of formatSecretSources(resolved.sources, manifestLabel))
|
|
100
|
+
log(chalk_1.default.dim(` ${line}`));
|
|
101
|
+
}
|
package/dist/materialize.d.ts
CHANGED
|
@@ -17,4 +17,6 @@ export interface FrontendModuleRegistry {
|
|
|
17
17
|
export declare function createFrontendModuleRegistry(workspaceDir: string, layers: ResolvedLayer[]): FrontendModuleRegistry;
|
|
18
18
|
export declare function collectAuthEstablishEndpoints(layers: ResolvedLayer[]): string[];
|
|
19
19
|
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;
|
|
20
22
|
export declare function writeDmsMainCss(workspaceDir: string, layers: ResolvedLayer[]): void;
|
package/dist/materialize.js
CHANGED
|
@@ -7,6 +7,8 @@ exports.materializeLayers = materializeLayers;
|
|
|
7
7
|
exports.createFrontendModuleRegistry = createFrontendModuleRegistry;
|
|
8
8
|
exports.collectAuthEstablishEndpoints = collectAuthEstablishEndpoints;
|
|
9
9
|
exports.writeFrontendModuleRegistry = writeFrontendModuleRegistry;
|
|
10
|
+
exports.affectsLocaleMessages = affectsLocaleMessages;
|
|
11
|
+
exports.writeLocaleMessages = writeLocaleMessages;
|
|
10
12
|
exports.writeDmsMainCss = writeDmsMainCss;
|
|
11
13
|
const node_fs_1 = require("node:fs");
|
|
12
14
|
const node_path_1 = require("node:path");
|
|
@@ -209,6 +211,16 @@ function mergeLocaleMessages(source, existing) {
|
|
|
209
211
|
}
|
|
210
212
|
return merged;
|
|
211
213
|
}
|
|
214
|
+
const LOCALE_DIRECTORY = "i18n/locales";
|
|
215
|
+
function readLocaleFile(path) {
|
|
216
|
+
try {
|
|
217
|
+
return JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
221
|
+
throw new Error(`Invalid locale file ${path}: ${reason}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
212
224
|
function readLocaleMessages(assets, registry) {
|
|
213
225
|
const roots = new Map(registry.modules.map((module) => [module.id, module.root]));
|
|
214
226
|
const messages = {};
|
|
@@ -217,27 +229,107 @@ function readLocaleMessages(assets, registry) {
|
|
|
217
229
|
const root = roots.get(asset.moduleId);
|
|
218
230
|
if (!locale || !root)
|
|
219
231
|
continue;
|
|
220
|
-
const value =
|
|
232
|
+
const value = readLocaleFile((0, node_path_1.join)(root, asset.relativePath));
|
|
221
233
|
messages[locale] = mergeLocaleMessages(value, messages[locale] ?? {});
|
|
222
234
|
}
|
|
223
235
|
return messages;
|
|
224
236
|
}
|
|
237
|
+
function affectsLocaleMessages(relativePath) {
|
|
238
|
+
const segments = (0, fs_sync_1.toPosixPath)(relativePath).split("/");
|
|
239
|
+
if (segments[0] === "layers" && segments.length <= 2)
|
|
240
|
+
return true;
|
|
241
|
+
const withinRoot = segments[0] === "layers" ? segments.slice(2) : segments;
|
|
242
|
+
return LOCALE_DIRECTORY.split("/").every((segment, index) => index >= withinRoot.length || withinRoot[index] === segment);
|
|
243
|
+
}
|
|
244
|
+
function localeModuleSource(supportedLocales) {
|
|
245
|
+
const loaders = supportedLocales
|
|
246
|
+
.filter((locale) => locale !== "en")
|
|
247
|
+
.map((locale) => ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(`./locales.generated/${locale}`)}),`);
|
|
248
|
+
return `import defaultMessages from "./locales.generated/en";
|
|
249
|
+
|
|
250
|
+
type LocaleMessages = Record<string, unknown>;
|
|
251
|
+
type LocaleLoader = () => Promise<{ default: LocaleMessages }>;
|
|
252
|
+
interface LocaleMessagesTarget {
|
|
253
|
+
setLocaleMessage(locale: string, messages: LocaleMessages): void;
|
|
254
|
+
}
|
|
255
|
+
interface LocaleState {
|
|
256
|
+
messages: Record<string, LocaleMessages>;
|
|
257
|
+
targets: Set<LocaleMessagesTarget>;
|
|
258
|
+
version: number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const supportedLocales = ${JSON.stringify(supportedLocales)};
|
|
262
|
+
const localeLoaders: Record<string, LocaleLoader> = {
|
|
263
|
+
"en": async () => ({ default: defaultMessages }),
|
|
264
|
+
${loaders.join("\n")}
|
|
265
|
+
};
|
|
266
|
+
const shared: LocaleState | undefined = import.meta.hot?.data.state;
|
|
267
|
+
const state: LocaleState = shared ?? {
|
|
268
|
+
messages: { en: defaultMessages },
|
|
269
|
+
targets: new Set(),
|
|
270
|
+
version: 0,
|
|
271
|
+
};
|
|
272
|
+
export const localeMessages = state.messages;
|
|
273
|
+
|
|
274
|
+
async function readLocaleMessages(locale: string): Promise<LocaleMessages> {
|
|
275
|
+
return (await localeLoaders[locale]()).default;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function loadLocaleMessages(locale: string): Promise<LocaleMessages> {
|
|
279
|
+
const normalized = locale.slice(0, 2);
|
|
280
|
+
if (!localeLoaders[normalized]) return localeMessages.en;
|
|
281
|
+
localeMessages[normalized] ??= await readLocaleMessages(normalized);
|
|
282
|
+
return localeMessages[normalized];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Development only: applies every regenerated catalog to \`target\`. */
|
|
286
|
+
export function syncLocaleMessages(target: LocaleMessagesTarget): void {
|
|
287
|
+
if (import.meta.hot) state.targets.add(target);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function refreshLocaleMessages(): Promise<void> {
|
|
291
|
+
const version = ++state.version;
|
|
292
|
+
const locales = Object.keys(localeMessages).filter(
|
|
293
|
+
(locale) => locale in localeLoaders,
|
|
294
|
+
);
|
|
295
|
+
const catalogs = await Promise.all(locales.map(readLocaleMessages));
|
|
296
|
+
if (version !== state.version) return;
|
|
297
|
+
locales.forEach((locale, index) => {
|
|
298
|
+
localeMessages[locale] = catalogs[index];
|
|
299
|
+
for (const target of state.targets)
|
|
300
|
+
target.setLocaleMessage(locale, catalogs[index]);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (import.meta.hot) {
|
|
305
|
+
import.meta.hot.data.state = state;
|
|
306
|
+
if (shared) void refreshLocaleMessages();
|
|
307
|
+
import.meta.hot.accept((next) => {
|
|
308
|
+
if (next?.supportedLocales.join() !== supportedLocales.join())
|
|
309
|
+
import.meta.hot?.invalidate("the list of locales changed");
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
225
314
|
function writeLocaleMessages(workspaceDir, registry) {
|
|
226
|
-
const locales = discoverAssets(registry,
|
|
315
|
+
const locales = discoverAssets(registry, LOCALE_DIRECTORY, [".json"]);
|
|
227
316
|
const messages = readLocaleMessages(locales, registry);
|
|
228
317
|
const outputDir = (0, node_path_1.join)(workspaceDir, "locales.generated");
|
|
229
|
-
|
|
318
|
+
const outputs = new Set();
|
|
230
319
|
(0, node_fs_1.mkdirSync)(outputDir, { recursive: true });
|
|
231
320
|
for (const [locale, value] of Object.entries(messages)) {
|
|
232
|
-
|
|
233
|
-
(0,
|
|
321
|
+
const catalog = JSON.stringify(value);
|
|
322
|
+
(0, fs_sync_1.applyContent)(catalog, (0, node_path_1.join)(outputDir, `${locale}.json`));
|
|
323
|
+
(0, fs_sync_1.applyContent)(`const messages = ${catalog};\nexport default messages;\n`, (0, node_path_1.join)(outputDir, `${locale}.ts`));
|
|
324
|
+
outputs.add(`${locale}.json`).add(`${locale}.ts`);
|
|
234
325
|
}
|
|
235
326
|
const supportedLocales = Object.keys(messages);
|
|
236
|
-
(0,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
327
|
+
(0, fs_sync_1.applyContent)(JSON.stringify(supportedLocales), (0, node_path_1.join)(workspaceDir, "email-locales.generated.json"));
|
|
328
|
+
(0, fs_sync_1.applyContent)(localeModuleSource(supportedLocales), (0, node_path_1.join)(workspaceDir, "locales.generated.ts"));
|
|
329
|
+
for (const name of (0, node_fs_1.readdirSync)(outputDir)) {
|
|
330
|
+
if (!outputs.has(name))
|
|
331
|
+
(0, node_fs_1.rmSync)((0, node_path_1.join)(outputDir, name), { recursive: true, force: true });
|
|
332
|
+
}
|
|
241
333
|
}
|
|
242
334
|
function writeShortcutExports(workspaceDir, registry) {
|
|
243
335
|
const registries = discoverAssets(registry, "app/config", [
|
package/dist/ports.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.PORT_FALLBACK_RANGE = 20;
|
|
|
7
7
|
const MAX_TCP_PORT = 65535;
|
|
8
8
|
function tryListen(port) {
|
|
9
9
|
return new Promise((resolve) => {
|
|
10
|
-
const server = (0, node_net_1.createServer)();
|
|
10
|
+
const server = (0, node_net_1.createServer)((socket) => socket.destroy());
|
|
11
11
|
server.unref();
|
|
12
12
|
server.once("error", () => resolve(undefined));
|
|
13
13
|
server.listen({ port }, () => resolve(server));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SpawnOptions } from "node:child_process";
|
|
2
|
+
import { type ManifestSecrets } from "./manifest-secrets";
|
|
2
3
|
import { ResolvedLayer } from "./workspace";
|
|
3
4
|
export declare function computeDepsHash(layerPaths: string[], workspacePackage?: string, installInputs?: readonly string[]): string;
|
|
4
5
|
export declare function needsInstall(workspaceDir: string, layerPaths: string[], force: boolean): boolean;
|
|
@@ -19,5 +20,6 @@ export interface SetupWorkspaceResult {
|
|
|
19
20
|
layers: ResolvedLayer[];
|
|
20
21
|
manifestFromCache: boolean;
|
|
21
22
|
manifestFetchedAt?: string;
|
|
23
|
+
manifestSecrets: ManifestSecrets;
|
|
22
24
|
}
|
|
23
25
|
export declare function setupWorkspace(opts: SetupWorkspaceOptions): Promise<SetupWorkspaceResult>;
|
package/dist/workspace-setup.js
CHANGED
|
@@ -13,6 +13,7 @@ const node_os_1 = require("node:os");
|
|
|
13
13
|
const node_path_1 = require("node:path");
|
|
14
14
|
const layers_1 = require("./layers");
|
|
15
15
|
const manifest_1 = require("./manifest");
|
|
16
|
+
const manifest_secrets_1 = require("./manifest-secrets");
|
|
16
17
|
const materialize_1 = require("./materialize");
|
|
17
18
|
const workspace_1 = require("./workspace");
|
|
18
19
|
const config_1 = require("./config");
|
|
@@ -183,5 +184,6 @@ async function setupWorkspace(opts) {
|
|
|
183
184
|
layers,
|
|
184
185
|
manifestFromCache: fromCache,
|
|
185
186
|
manifestFetchedAt: fetchedAt,
|
|
187
|
+
manifestSecrets: (0, manifest_secrets_1.collectManifestSecrets)(manifest.modules),
|
|
186
188
|
};
|
|
187
189
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import UApp from "@nuxt/ui/components/App.vue";
|
|
2
2
|
import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
|
|
3
|
+
import { useToast } from "@nuxt/ui/composables/useToast";
|
|
3
4
|
import { useAppConfig } from "@nuxt/ui/runtime/vue/composables/useAppConfig";
|
|
4
5
|
import ui from "@nuxt/ui/vue-plugin";
|
|
5
6
|
import { useHead } from "@unhead/vue";
|
|
@@ -43,7 +44,9 @@ import {
|
|
|
43
44
|
loadLocaleMessages,
|
|
44
45
|
localeMessages,
|
|
45
46
|
supportedLocales,
|
|
47
|
+
syncLocaleMessages,
|
|
46
48
|
} from "./locales.generated";
|
|
49
|
+
import { showNetworkErrors } from "./network-error";
|
|
47
50
|
|
|
48
51
|
export interface DmsInertiaSetupProps {
|
|
49
52
|
initialPage: unknown;
|
|
@@ -214,11 +217,26 @@ function renderDmsPersistentLayout(
|
|
|
214
217
|
);
|
|
215
218
|
}
|
|
216
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Nuxt UI keeps one app config for the whole process, and defu concatenates
|
|
222
|
+
* arrays: merged into it on every server render, the compound variants the DMS
|
|
223
|
+
* declares piled up, and every class list built from them grew with each
|
|
224
|
+
* request. Each config is merged once; a new one, after a reload, is merged
|
|
225
|
+
* again.
|
|
226
|
+
*/
|
|
227
|
+
const mergedUiAppConfigs = new WeakSet<object>();
|
|
228
|
+
|
|
229
|
+
function mergeDmsAppConfig(): void {
|
|
230
|
+
const uiAppConfig = useAppConfig();
|
|
231
|
+
if (mergedUiAppConfigs.has(uiAppConfig)) return;
|
|
232
|
+
Object.assign(uiAppConfig, defu(useDmsAppConfig(), uiAppConfig));
|
|
233
|
+
mergedUiAppConfigs.add(uiAppConfig);
|
|
234
|
+
}
|
|
235
|
+
|
|
217
236
|
export async function configureDmsApp(
|
|
218
237
|
options: DmsAppOptions,
|
|
219
238
|
): Promise<DmsConfiguredApp> {
|
|
220
|
-
|
|
221
|
-
Object.assign(uiAppConfig, defu(useDmsAppConfig(), uiAppConfig));
|
|
239
|
+
mergeDmsAppConfig();
|
|
222
240
|
const runtime =
|
|
223
241
|
options.runtime ??
|
|
224
242
|
createDmsFrontendRuntime(
|
|
@@ -233,12 +251,19 @@ export async function configureDmsApp(
|
|
|
233
251
|
fallbackLocale: DEFAULT_LOCALE,
|
|
234
252
|
messages: { ...localeMessages, [locale]: messages },
|
|
235
253
|
});
|
|
254
|
+
// A translation changed in a layer during development reaches the page
|
|
255
|
+
// without a reload. A server render imports the new catalogs instead.
|
|
256
|
+
if (typeof window !== "undefined") syncLocaleMessages(i18n.global);
|
|
236
257
|
provideDmsFrontendRuntime(options.app, runtime);
|
|
237
258
|
options.app.component("Icon", UIcon);
|
|
238
259
|
options.app.use(options.inertiaPlugin).use(options.head).use(ui).use(i18n);
|
|
239
260
|
options.app.runWithContext(() =>
|
|
240
261
|
hydrateDmsPageProps(options.initialPageProps, options.initialPageUrl),
|
|
241
262
|
);
|
|
263
|
+
if (typeof window !== "undefined") {
|
|
264
|
+
const toast = options.app.runWithContext(() => useToast());
|
|
265
|
+
showNetworkErrors(toast.add, i18n.global);
|
|
266
|
+
}
|
|
242
267
|
// Resolve, before the app mounts, every async component the server
|
|
243
268
|
// rendered: the page, its layout, and the components the render reached.
|
|
244
269
|
// Hydration then adopts the server-rendered markup instead of discarding it.
|