@antelopejs/dms-frontend 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +377 -19
- package/dist/app-configs.d.ts +9 -0
- package/dist/app-configs.js +52 -0
- package/dist/asset-roots.d.ts +7 -0
- package/dist/asset-roots.js +34 -0
- package/dist/commands/build-action.js +9 -0
- package/dist/commands/dev-action.js +88 -19
- package/dist/commands/start-action.js +2 -0
- package/dist/common.d.ts +1 -0
- package/dist/common.js +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -3
- package/dist/derived-outputs.d.ts +2 -0
- package/dist/derived-outputs.js +38 -31
- package/dist/help.js +10 -3
- package/dist/layer-watch.js +123 -131
- package/dist/layers.js +2 -0
- package/dist/manifest-secrets.js +5 -5
- package/dist/manifest.js +7 -1
- package/dist/materialize.js +21 -1
- package/dist/module-set.d.ts +16 -0
- package/dist/module-set.js +80 -0
- package/dist/server-process.d.ts +1 -0
- package/dist/server-process.js +28 -2
- package/dist/verify-source-build.d.ts +11 -0
- package/dist/verify-source-build.js +113 -0
- package/dist/verify-source-runner.js +14 -57
- package/dist/workspace.d.ts +9 -0
- package/package.json +2 -1
- package/templates/vue/app-runtime.ts +58 -16
- package/templates/vue/client-server-guard.mjs +73 -0
- package/templates/vue/email-renderer.ts +17 -4
- package/templates/vue/email-runtime.ts +1 -1
- package/templates/vue/frontend-module.ts +1817 -259
- package/templates/vue/globals.d.ts +1 -1
- package/templates/vue/icon-hydration.ts +106 -1
- package/templates/vue/index.html +0 -1
- package/templates/vue/layer-app-config.ts +92 -0
- package/templates/vue/main.ts +10 -2
- package/templates/vue/package.json +5 -1
- package/templates/vue/prepare-types.mjs +34 -0
- package/templates/vue/server/auth/backend.mjs +13 -19
- package/templates/vue/server/auth/client-ip.mjs +27 -0
- package/templates/vue/server/auth/establish-endpoints.mjs +70 -0
- package/templates/vue/server/auth/session.mjs +6 -97
- package/templates/vue/server/auth-hooks.mjs +84 -0
- package/templates/vue/server/backend-proxies.mjs +110 -0
- package/templates/vue/server/dev-private-files.mjs +99 -0
- package/templates/vue/server/email.mjs +11 -33
- package/templates/vue/server/favicon.mjs +20 -0
- package/templates/vue/server/inertia.mjs +48 -3
- package/templates/vue/server/listen-host.mjs +19 -0
- package/templates/vue/server/module-event.mjs +265 -0
- package/templates/vue/server/module-routes.mjs +513 -0
- package/templates/vue/server/module-server.mjs +225 -0
- package/templates/vue/server/module-testing.mjs +65 -0
- package/templates/vue/server/page-props.mjs +151 -0
- package/templates/vue/server/path-patterns.mjs +38 -0
- package/templates/vue/server/proxy-headers.mjs +25 -0
- package/templates/vue/server/public-endpoints.mjs +69 -11
- package/templates/vue/server/request-routing.mjs +47 -0
- package/templates/vue/server/route-rules.mjs +129 -0
- package/templates/vue/server-module.ts +294 -0
- package/templates/vue/server.mjs +178 -169
- package/templates/vue/ssr-renderer.ts +60 -19
- package/templates/vue/tsconfig.json +2 -1
- package/templates/vue/ui-app-config.ts +18 -6
- package/templates/vue/ui-inertia-page.mjs +37 -0
- package/templates/vue/vite.config.ts +31 -10
- package/templates/vue/vite.email.config.ts +1 -1
- package/templates/vue/vite.server.config.ts +57 -0
- package/templates/vue/server/auth/oauth.mjs +0 -233
- package/templates/vue/server/auth/routes.mjs +0 -444
- package/templates/vue/server/render-token.mjs +0 -81
- package/templates/vue/server/tester.mjs +0 -227
package/README.md
CHANGED
|
@@ -155,11 +155,11 @@ attempt. The throttle stamp lives at
|
|
|
155
155
|
|
|
156
156
|
Manifest negotiation and module materialization are the renderer contract described in [Renderers](#renderers).
|
|
157
157
|
|
|
158
|
-
The generated Vue application uses `@inertiajs/vue3`, `@nuxt/ui/vite` with `{ router: "inertia" }`, and `@nuxt/ui/vue-plugin`. The Node server resolves each Inertia visit through `/dms/page?path
|
|
158
|
+
The generated Vue application uses `@inertiajs/vue3`, `@nuxt/ui/vite` with `{ router: "inertia" }`, and `@nuxt/ui/vue-plugin`. The Node server resolves each Inertia visit through `/dms/page?path=…`; the site layout it returns comes as a once prop the browser reuses for five minutes per account and workspace (see [Navigation and the site layout](#navigation-and-the-site-layout)). It proxies backend routes and manages authentication through server-side sessions. `DMS_BOOTSTRAP_SECRET` is used only by the CLI's server-to-server frontend manifest and module archive requests and is never sent by, or exposed to, browser traffic.
|
|
159
159
|
|
|
160
160
|
Vue modules register through `dms.frontend.ts` and import the SDK from the `#dms/frontend-module` alias, which the loader resolves to the generated `frontend-module.ts`: `defineDmsPlugin`, `useDmsRouter`, the page and session types, and everything else a module needs from the host. Email templates register separately through `dms.email.ts`.
|
|
161
161
|
|
|
162
|
-
An email entry exports `serverEmailTemplates` and may export a plain `appConfig` object, such as shared branding defaults. Email rendering merges these configurations
|
|
162
|
+
An email entry exports `serverEmailTemplates` and may export a plain `appConfig` object, such as shared branding defaults. Email rendering merges these configurations with the layers' [app configs](#app-config-appappconfigts) and provides the result to `useDmsAppConfig` per render: module by module, in manifest-priority order, a module's email `appConfig` first, then its layers' `app/app.config.ts`. A higher-priority module's layers thus override a lower one's email `appConfig`, and a module's email `appConfig`, the more specific of its two, keeps the values it gives emails over its own layers. Public runtime options come from the module manifest; `DMS_CLIENT_BASE_URL` overrides `public.dms.clientBaseUrl`. Email entries must not import the browser frontend module. The rendered HTML carries no Vue hydration comments (`<!--[-->`, `<!---->`, teleport anchors): an email is never hydrated, so the renderer strips them, and keeps every other comment, Outlook's conditional comments included.
|
|
163
163
|
|
|
164
164
|
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.
|
|
165
165
|
|
|
@@ -167,6 +167,9 @@ Email builds ship complete merged translation catalogs as JSON under `dist/serve
|
|
|
167
167
|
|
|
168
168
|
## Frontend modules
|
|
169
169
|
|
|
170
|
+
Moving a module, or the DMS, from 0.5 to 0.6: see
|
|
171
|
+
[Migrating to 0.6.0](docs/migration-0.6.md).
|
|
172
|
+
|
|
170
173
|
A materialized module opts into the Vue adapter with a root `dms.frontend.ts`:
|
|
171
174
|
|
|
172
175
|
```ts
|
|
@@ -188,6 +191,23 @@ The SDK also exposes `use` for Vue plugins. Entries execute by descending manife
|
|
|
188
191
|
|
|
189
192
|
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.
|
|
190
193
|
|
|
194
|
+
### App config (`app/app.config.ts`)
|
|
195
|
+
|
|
196
|
+
Each layer of a module, or the module root when it has no `layers/` directory, may hold an `app/app.config.ts` whose default export is a plain object: branding, Nuxt UI's `ui` (colors, icons, component themes), anything the modules read through `useDmsAppConfig()`.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
export default defineDmsAppConfig({
|
|
200
|
+
branding: { logo: { light: "/branding/logo.svg" } },
|
|
201
|
+
ui: { colors: { primary: "emerald" } },
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The loader merges every one of them with `defu` (records key by key, arrays concatenated) before the first module is set up, so `useDmsAppConfig()` already holds the result when a `setup` runs: modules in manifest-priority order, and within a module its layers from the last by name to the first, the order in which a contested `public/` file is resolved. A project's layer therefore overrides the DMS's, and the DMS fills in everything the project leaves out. A `setup` may still read and extend the config. Nuxt UI's own app config is merged from the result, and so are emails' (see above). The file is TypeScript only, and is imported in Node.js by the server renderer and the email bundle as well as in the browser: it must not touch browser globals when it loads.
|
|
206
|
+
|
|
207
|
+
`ajs-dms dev` applies an edit at once: the next server render uses it, and the open page updates `useDmsAppConfig()` and Nuxt UI's app config in place, colors included, without a reload. What the modules' `setup` changed in the config stays on top of the edited values; adding or removing a config file is applied the same way.
|
|
208
|
+
|
|
209
|
+
Up to 0.5.6, the loader applied none of these files: an entry merged its own into `useDmsAppConfig()` in its `setup`, as the DMS up to 0.8 does (`Object.assign(useDmsAppConfig(), defu(useDmsAppConfig(), appConfig))`). Such an entry keeps working, its values unchanged, but its config is now merged twice, and `defu` concatenates arrays: every array it declares, Nuxt UI's `compoundVariants` among them, holds its entries twice. Nuxt UI resolves the repeated classes through `tailwind-merge` to the same ones, so nothing changes on screen, but an entry should stop merging its own config once it requires a release of the loader that applies it. The DMS does so in a release coordinated with this one. Until then, an edit to a config its entry imports reloads the page instead of updating it in place, since the entry imports it too.
|
|
210
|
+
|
|
191
211
|
### Component prefix
|
|
192
212
|
|
|
193
213
|
Every registered component joins the application's global components and resolves by name anywhere, in templates and in backend page trees. To keep the components of two modules from colliding, a module declares a `componentPrefix`, put in front of the name of every component it registers:
|
|
@@ -233,6 +253,95 @@ The file runs in Node.js when Vite starts, under `ajs dms dev` and `ajs dms buil
|
|
|
233
253
|
|
|
234
254
|
Up to 0.3, every module's `app/composables`, `app/types`, `app/utils`, `app/build/composables` and `app/build/types` were auto-imported. A module that relies on auto-imports declares its public directories in `dms.frontend.build.ts`, and imports its `app/build/` code by path.
|
|
235
255
|
|
|
256
|
+
### Server routes (`dms.server.ts`)
|
|
257
|
+
|
|
258
|
+
A module serves routes from the frontend server, next to the session, in an optional `dms.server.ts` beside its `dms.frontend.ts`: a sign-in flow that writes cookies, a provider redirect, a render a backend asks for.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { defineDmsServer } from "#dms/server";
|
|
262
|
+
import { ping, startExport } from "./server/routes";
|
|
263
|
+
|
|
264
|
+
export default defineDmsServer((server) => {
|
|
265
|
+
server.registerRoute("GET", "/api/_acme/ping", ping);
|
|
266
|
+
server.registerRoute("POST", "/api/_acme/exports/:kind", startExport);
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
// server/routes.ts
|
|
272
|
+
import type { DmsServerEvent } from "#dms/server";
|
|
273
|
+
|
|
274
|
+
export function ping() {
|
|
275
|
+
return { ok: true };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function startExport(event: DmsServerEvent) {
|
|
279
|
+
const input = await event.readJson<{ from: string }>();
|
|
280
|
+
return event.backend(`/api/acme/exports/${event.params.kind}`, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
body: input,
|
|
283
|
+
token: event.session.read()?.accessToken,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
A handler takes the request and answers with a Web-standard `Response`, or with a value sent as JSON (`null` or nothing is an empty `204`). The event it receives carries:
|
|
289
|
+
|
|
290
|
+
- `request`, `url`, `params` (the `:name` segments, decoded) and `readJson()`;
|
|
291
|
+
- `session`: `read()`, `write()` (stamped with `updatedAt`, ends with the browser unless `persistent`), `clear()`, `useForRequest()`;
|
|
292
|
+
- `cookies`: sealed cookies by logical name (`readSealed`, `writeSealed` with an optional `maxAge` and `path`, `clear`). The server scopes the name to its backend and sets `HttpOnly`, `SameSite=Lax` and `Secure`, so a module never depends on how cookies are named; the session cookie is only reachable through `session`. The `dms_*` names belong to the module that owns sign-in (the one whose `registerAuth` is used): another module naming one gets an error, and its own cookies are stored as `<module id>.<name>`, so no module reads or overwrites another's;
|
|
293
|
+
- `backend(path, init)`: a JSON call to the backend that always carries the client's address and the headers the proxy relays; a refusal throws an error with the status and the backend's public `error.*` message, which reaches the client as is when the handler lets it through;
|
|
294
|
+
- `backendUrl(path)`: the backend URL of a path, always on the backend's origin, for a call `backend` cannot make (a raw response);
|
|
295
|
+
- `clientIp()`, `renderEmail(template, props, { locale })`, and `module`: this module's id, its options and its own `privateOptions`, never another module's.
|
|
296
|
+
|
|
297
|
+
Every route gets the same defaults: a request with a state-changing method must carry the frontend's own `Origin` (`{ sameOrigin: false }` only for a route authenticated otherwise, such as by a service token), its body stops at 64 KiB (`bodyLimit`) with a `413`, and the answer is `Cache-Control: no-store` unless the handler sets its own. An error the handler lets through is answered like a backend failure: its status, and only a public `error.*` message.
|
|
298
|
+
|
|
299
|
+
Paths are literal segments and `:name` parameters. Module routes are tried after the renderer's own (`/api/_dms/`, `/@…`, `/assets/`, which no module can declare) and before the backend proxy, literal paths before parameters. A route under `/api/` or `/dms/` outside a module namespace (`/api/_<module>/`) hides what the backend serves there, so the server warns unless the route passes `{ overridesBackend: true }`. When two modules declare the same method and path, the module of higher priority keeps it and a warning names both, the module id settling a tie; a module route answering a [public endpoint](#public-endpoints) is reported too. A malformed declaration is ignored with a warning naming its module.
|
|
300
|
+
|
|
301
|
+
`dms.server.ts` runs in the frontend server's Node.js process, next to the session secret: it is trusted code, as the module's backend half is, not a sandbox. Every route reads the session (`event.session.read()`, access and refresh tokens included) and may write it; only install a module whose `dms.server.ts` you trust as much as the DMS's own. It may import `#dms/server`, Node.js built-ins, npm dependencies and the module's own server files (a `server/` directory is copied into the workspace for that), never the application's code (`.vue` files, `dms.frontend.ts`); nothing it imports reaches the browser bundle, and `verify-source` checks both. Under `ajs dms dev` the server loads it through Vite, so an edited handler answers on the next request; `ajs dms build` bundles it into `dist/server-modules`, and a workspace built without that bundle simply serves no module route. Import what a handler uses: auto-imports do not apply to server code.
|
|
302
|
+
|
|
303
|
+
A module tests its routes through the same router, event and cookie jar with `serveModuleRoutes` from `@antelopejs/dms-frontend/templates/vue/server/module-testing.mjs`, which serves the default export of its `dms.server.ts` on a local port.
|
|
304
|
+
|
|
305
|
+
#### Session hooks
|
|
306
|
+
|
|
307
|
+
The server also runs session logic outside any route: it renews a session the backend refused on a page visit, puts the `user` and `session` page props together, and sends a signed-out visit to sign in. The module that owns sign-in provides it:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
server.registerAuth({
|
|
311
|
+
signInPath: "/auth",
|
|
312
|
+
signInExempt: ["/auth/**", "/onboarding"],
|
|
313
|
+
refreshSession: renewSession, // (event, session) => the new session, or undefined
|
|
314
|
+
publicSession: (session) => ({
|
|
315
|
+
user: session.user,
|
|
316
|
+
session: { accountId: session.accountId, activeTenantId: session.activeTenantId },
|
|
317
|
+
}),
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
`refreshSession` is called once when the backend answers `401` to a signed-in page visit, which is then retried with the session it wrote (`event.session.write`). `publicSession` gives the page props of a signed-in session; the server adds `session.updatedAt`, the stamp of its last write, which the browser compares to drop a page restored from its cache (see [Interface language](#interface-language)), and `event.session.view()` returns the same props to a route. `signInExempt` patterns are `/x`, `/x/*` (one segment below) or `/x/**` (the page and everything below); left out, they are `<signInPath>/**` and `/onboarding`, so a custom `signInPath` is exempt on its own. The highest-priority module that registers them provides all of them; another one is reported and ignored.
|
|
322
|
+
|
|
323
|
+
#### Route rules
|
|
324
|
+
|
|
325
|
+
A module also declares how its pages are rendered and cached, by page pattern (`/x`, `/x/*` one segment below, `/x/**` the page and everything below):
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
server.registerRouteRules({
|
|
329
|
+
"/terms-of-use": { cache: { public: true, maxAge: 300, staleWhileRevalidate: 3600 } },
|
|
330
|
+
"/builder/**": { ssr: false },
|
|
331
|
+
"/reports/**": { headers: { "x-robots-tag": "noindex" } },
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
- `cache`: page documents and Inertia answers default to `private, no-cache`, since they carry the visitor's session. A public `cache` replaces it, with `Vary: Cookie`, only for a `GET` of a page the backend serves to anyone (`publicAccess`), answered `200` to a request without any cookie and without `x-content-language`: such an answer holds nobody's data. Any other request keeps the private default. The frontend server itself stores nothing; the rule is for a shared cache or a CDN in front of it. `cache: false` keeps the default under a broader rule.
|
|
336
|
+
- `ssr: false`: a document request gets the client-rendered shell instead of a server render; Inertia visits are unchanged.
|
|
337
|
+
- `headers`: added to the page's document and Inertia answers; `cache-control`, `vary`, `set-cookie` and the content and Inertia headers stay the server's.
|
|
338
|
+
|
|
339
|
+
The rules matching a page merge from the least to the most specific pattern; for one pattern, the highest-priority module's rule wins, with a warning. A rule never changes who may see a page.
|
|
340
|
+
|
|
341
|
+
The renderer serves no route of its own besides its files and icons: the session, sign-in, OAuth and e-mail render routes are the DMS's (`dms.server.ts`), the API tester is the DMS API's. Without a module that registers auth hooks, pages carry no `user` and `session` and a refused session is not renewed.
|
|
342
|
+
|
|
343
|
+
A module that ships `dms.server.ts` raises its [loader range](#declaring-the-loader-releases-a-module-supports) floor to the release that serves it: an older one never reads the file, so the routes would silently be missing.
|
|
344
|
+
|
|
236
345
|
### Page loading state
|
|
237
346
|
|
|
238
347
|
After a client navigation, the page renders under a `<Suspense>` until its chunk and any async `setup` resolve. `useDmsPageLoading()` (auto-imported) is `true` meanwhile, so a layout can show its own placeholder instead of an empty page body:
|
|
@@ -252,7 +361,7 @@ It stays `false` on the server and during hydration, which render the page resol
|
|
|
252
361
|
|
|
253
362
|
### Interface language
|
|
254
363
|
|
|
255
|
-
The interface language follows the session (`
|
|
364
|
+
The interface language follows the session (`useDmsUserSession().user.language`), not the props of the page on screen: the first render takes it from the session the page arrived with, and it changes when the session's language does, after `useDmsUserSession().fetch()` or a visit with a newer session. A page Inertia restores from history or serves from its prefetch cache carries the session it was fetched with: the frontend server stamps every session it writes (`session.updatedAt`), and a page carrying an older stamp of the same account updates neither the session nor the language. A session without a language, such as a signed-out one, keeps the language on screen; `$i18n.setLocale()` still switches it at any time. It also remembers a language the application offers in the `dms_locale` cookie (one year, `SameSite=Lax`), and a page without a signed-in user's language, such as a public sign-in page, first renders in that one: a visitor's choice survives a reload, while a signed-in user's own language still comes first.
|
|
256
365
|
|
|
257
366
|
### Declaring the loader releases a module supports
|
|
258
367
|
|
|
@@ -262,7 +371,7 @@ A frontend module names the `@antelopejs/dms-frontend` releases it runs on in it
|
|
|
262
371
|
{
|
|
263
372
|
"name": "@acme/dms-billing-frontend-vue",
|
|
264
373
|
"engines": {
|
|
265
|
-
"@antelopejs/dms-frontend": ">=0.
|
|
374
|
+
"@antelopejs/dms-frontend": ">=0.6.0 <0.7.0"
|
|
266
375
|
}
|
|
267
376
|
}
|
|
268
377
|
```
|
|
@@ -426,11 +535,61 @@ pair can be turned into a login by a request from the browser. An undeclared
|
|
|
426
535
|
endpoint is answered `403` and never called. The route is `POST`-only and
|
|
427
536
|
same-origin, like every other auth action.
|
|
428
537
|
|
|
538
|
+
### Routing and the backend proxy
|
|
539
|
+
|
|
540
|
+
The frontend server answers each path with one owner, whoever asks and
|
|
541
|
+
whatever the method, in this order:
|
|
542
|
+
|
|
543
|
+
1. **The renderer**: its icon endpoint (`/api/_dms/icons`), Vite's URLs, the
|
|
544
|
+
built assets, and any file it has (a development source, a built asset).
|
|
545
|
+
2. **The module routes** of the modules' `dms.server.ts`, for every method: a
|
|
546
|
+
method the module does not declare on its path answers `405`, and a `HEAD`
|
|
547
|
+
reaches the `GET` route.
|
|
548
|
+
3. **The backend proxies** the modules declare. The renderer proxies no path on
|
|
549
|
+
its own: the DMS declares `/api/**` and `/dms/**` in its `dms.server.ts`.
|
|
550
|
+
4. **The pages**, for everything else; a method other than `GET` and `HEAD`
|
|
551
|
+
there is a `404`. A backend route outside every declared prefix (such as a
|
|
552
|
+
page's `<page>/pagelayout`) is never reached from the browser.
|
|
553
|
+
|
|
554
|
+
A frontend module declares a prefix in its `dms.server.ts`:
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
server.registerBackendProxy("/api/**"); // auth: "session", the default
|
|
558
|
+
server.registerBackendProxy("/media/**", { auth: "none" });
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
A backend module declares one in the manifest, next to its `publicEndpoints`:
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
await AddFrontendModule({
|
|
565
|
+
// …
|
|
566
|
+
backendProxies: [{ path: "/webhooks/acme/**", auth: "none" }],
|
|
567
|
+
});
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
- `auth: "session"` adds the session's access token to the request
|
|
571
|
+
(`Authorization: Bearer`), server-side: the token never reaches the browser,
|
|
572
|
+
which only holds the sealed, `HttpOnly` session cookie. A request with a
|
|
573
|
+
state-changing method must carry the frontend's own `Origin`, except a
|
|
574
|
+
public endpoint (below).
|
|
575
|
+
- `auth: "none"` never sends the session, so it needs no `Origin`: for media,
|
|
576
|
+
or a webhook that authenticates its caller itself.
|
|
577
|
+
|
|
578
|
+
A pattern is `/x`, `/x/*` (one segment below) or `/x/**` (the path and
|
|
579
|
+
everything below), its first segment literal, outside the renderer's own paths.
|
|
580
|
+
Of two patterns covering a path, the more specific wins; for one pattern, the
|
|
581
|
+
highest-priority frontend module's, then the manifest's, with a warning.
|
|
582
|
+
`DMS_BACKEND_PROXIES` adds entries of the manifest's shape, as a JSON array,
|
|
583
|
+
for an operator. The server lists the backend proxies, their owner and their
|
|
584
|
+
auth when it starts, and the development server names the owner of each answer
|
|
585
|
+
in an `x-dms-route-owner` header (`renderer`, `module:<id>`,
|
|
586
|
+
`backend:<pattern>`, `page` or `not-found`).
|
|
587
|
+
|
|
429
588
|
### Public endpoints
|
|
430
589
|
|
|
431
|
-
Every state-changing request the server proxies
|
|
432
|
-
`Origin` matching the frontend's own, and goes out with the signed-in
|
|
433
|
-
session. A route meant for callers outside the dashboard — a mail provider's
|
|
590
|
+
Every state-changing request the server proxies with the session must carry
|
|
591
|
+
an `Origin` matching the frontend's own, and goes out with the signed-in
|
|
592
|
+
user's session. A route meant for callers outside the dashboard — a mail provider's
|
|
434
593
|
delivery webhook, a payment provider's callback — fails both ways: the caller
|
|
435
594
|
sends no `Origin`, and its secret header is not among the few the proxy
|
|
436
595
|
relays.
|
|
@@ -475,7 +634,12 @@ widened. `DMS_PUBLIC_ENDPOINTS` adds entries of the same shape, as a JSON
|
|
|
475
634
|
array, for a route no module declares.
|
|
476
635
|
|
|
477
636
|
Every proxied request also relays `If-Match`, so a backend route can refuse a
|
|
478
|
-
write made from a stale copy (`412` or `409`)
|
|
637
|
+
write made from a stale copy (`412` or `409`), and the client's `Origin` and
|
|
638
|
+
`Referer` as received, so a public route can check where a beacon comes from.
|
|
639
|
+
The proxy sets `X-Forwarded-For`, `X-Forwarded-Proto` and `X-Forwarded-Host`
|
|
640
|
+
itself, so a route can limit each client: the client's own `X-Forwarded-For`
|
|
641
|
+
is extended with the socket's address only behind `DMS_TRUSTED_PROXY_HOPS`
|
|
642
|
+
trusted proxies, and replaced by it otherwise.
|
|
479
643
|
|
|
480
644
|
### Workspaces
|
|
481
645
|
|
|
@@ -525,15 +689,202 @@ merged catalogs the application imports: the next server render uses them, and
|
|
|
525
689
|
the open page applies them in place, without a reload. Adding or removing a
|
|
526
690
|
locale reloads the page instead.
|
|
527
691
|
|
|
692
|
+
An edit to a layer's `app/app.config.ts` reaches the page the same way (see
|
|
693
|
+
[App config](#app-config-appappconfigts)).
|
|
694
|
+
|
|
528
695
|
The other files the workspace derives from every module follow their sources
|
|
529
696
|
the same way. A file added to, changed in or removed from a module's `public/`
|
|
530
|
-
is served, or no longer served, at once.
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
697
|
+
is served, or no longer served, at once. Public files and catalogs are written
|
|
698
|
+
before the other files saved with them, so an app config or a component that
|
|
699
|
+
names a new logo or a new translation, saved at the same time, never finds it
|
|
700
|
+
missing. Adding or removing an `app/config/shortcuts-registry.ts` or a
|
|
701
|
+
`dms.frontend.ts` regenerates the aggregated shortcuts or the module loader,
|
|
702
|
+
and the page reloads. Adding, changing or removing a `dms.frontend.build.ts`
|
|
703
|
+
restarts Vite. A layer directory added to or removed from `layers/` is the
|
|
704
|
+
exception: Vite reads the `#<layer>` aliases and the auto-imported directories
|
|
705
|
+
at startup only, so the dev server says it needs a restart.
|
|
706
|
+
|
|
707
|
+
A page whose render declares no icon, through `useHead`, gets an empty
|
|
708
|
+
`<link rel="icon" href="data:,">`, which keeps the browser from asking for
|
|
709
|
+
`/favicon.ico`. A page that declares its icons, one per color scheme for
|
|
710
|
+
instance, gets only those.
|
|
711
|
+
|
|
712
|
+
### Navigation and the site layout
|
|
713
|
+
|
|
714
|
+
Every visit's page object carries the page (`page`: its route and layout), the
|
|
715
|
+
session (`user`, `session`) and the site layout (`shared`: the menu with each
|
|
716
|
+
entry's access, quick actions, modules). The site layout is most of that
|
|
717
|
+
object, and the same from page to page, so it is an Inertia once prop:
|
|
718
|
+
|
|
719
|
+
- The server sends it when the browser does not hold it for the session's
|
|
720
|
+
account and workspace, then asks the backend for the page alone
|
|
721
|
+
(`/dms/page?path=…&shared=false`) while the browser's copy is less than five
|
|
722
|
+
minutes old. Signing in, signing out, or switching account or workspace
|
|
723
|
+
changes the key the copy is held under, so the next visit brings the new
|
|
724
|
+
session's layout.
|
|
725
|
+
- `router.reload({ only: ["shared"] })` reloads it at once, and `only` /
|
|
726
|
+
`except` name any of `page`, `shared`, `user` and `session`: the server
|
|
727
|
+
fetches only what is named. The DMS reloads the menu this way when the
|
|
728
|
+
backend announces a change.
|
|
729
|
+
- The access a menu entry shows is for display: the backend checks access on
|
|
730
|
+
every request, whatever the menu says.
|
|
731
|
+
|
|
732
|
+
A navigation that changes only the query or the hash of the page on screen
|
|
733
|
+
(`useDmsRouter().push({ query })`, `navigateDms`, `DmsLink`) is an Inertia
|
|
734
|
+
client-side visit: the URL and the history entry change, the page stays
|
|
735
|
+
mounted with its props, and no request is made, since the page props depend
|
|
736
|
+
on the path alone. Pass a server-only option (`only`, `data`, `headers`,
|
|
737
|
+
`onStart`…) to make the request anyway; an error page is always left through
|
|
738
|
+
the server.
|
|
739
|
+
|
|
740
|
+
`DmsLink` prefetches the page it points to on hover; once Inertia has the
|
|
741
|
+
props, the page, its layout and its block components start loading, so
|
|
742
|
+
following the link waits for neither. A change of account or workspace, and
|
|
743
|
+
signing out, drop the prefetched pages.
|
|
744
|
+
|
|
745
|
+
### Data loading
|
|
746
|
+
|
|
747
|
+
`useDmsAsyncData(key, handler, options)` loads data once for every component
|
|
748
|
+
that shows `key`, with the semantics of Nuxt 4's `useAsyncData` and the
|
|
749
|
+
deviations listed below (calls of one key joined, the data of the last success
|
|
750
|
+
kept after a failure, the first `lazy` run started after the caller's turn):
|
|
751
|
+
|
|
752
|
+
- **A server render waits for it.** A component's data is awaited before the
|
|
753
|
+
component renders, whether or not its setup awaits the call, and the browser
|
|
754
|
+
takes the result over while it hydrates instead of fetching it again. Keys a
|
|
755
|
+
setup does not await load in parallel. Data a plugin starts is awaited
|
|
756
|
+
before the page renders.
|
|
757
|
+
- **The result works with or without `await`.** Its refs are there at once;
|
|
758
|
+
awaiting it waits for the data.
|
|
759
|
+
- **`lazy: true`** spares client navigations the wait; a server render still
|
|
760
|
+
waits.
|
|
761
|
+
- **`server: false`** leaves the data to the browser: the server renders the
|
|
762
|
+
default, and the browser fetches the data once the page is mounted.
|
|
763
|
+
- **The handler receives `{ signal }`.** It is aborted when a newer run
|
|
764
|
+
replaces this one (`dedupe: "cancel"`, the default; `"defer"` waits for the
|
|
765
|
+
run in flight instead), when `clear()` resets the data, or when the last
|
|
766
|
+
component showing the key unmounts.
|
|
767
|
+
- **A page renders once its middleware is done.** On a server, the page that
|
|
768
|
+
calls `defineDmsPageMeta` waits for the middleware it starts, so what a
|
|
769
|
+
middleware loads or sets (with `transfer: true`) is what the HTML shows and
|
|
770
|
+
the browser hydrates with; its redirect still answers the request. A
|
|
771
|
+
middleware that throws fails the page: the error page shows in its place,
|
|
772
|
+
and a server render answers with the error's status code. Middlewares run
|
|
773
|
+
in order; those registered with `{ parallel: true }` next to each other
|
|
774
|
+
start together (for checks that read nothing another middleware sets),
|
|
775
|
+
their results applying in registration order.
|
|
776
|
+
- **The browser hydrates with what the HTML shows.** Data that settles once
|
|
777
|
+
the HTML is complete is left to the browser. A component receives the server's data when the server's
|
|
778
|
+
render did: once its setup awaits the call, or right before it renders. A
|
|
779
|
+
value its setup derives from data it did not await, in an immediate
|
|
780
|
+
watcher for instance, is then the one the server rendered.
|
|
781
|
+
- **A failure is transferred too,** so hydration does not run the handler
|
|
782
|
+
again: an HTTP failure keeps its `statusCode`, `statusMessage`, `message`
|
|
783
|
+
and `data`, any other failure becomes a `500` whose cause only the server
|
|
784
|
+
logs. A fetch error's message names the backend's address on a server, so
|
|
785
|
+
its status text replaces it.
|
|
786
|
+
- **`useDmsState(key, init, { transfer: true })`** hands the server's value
|
|
787
|
+
to the browser, which then starts from it instead of calling `init`. State
|
|
788
|
+
is never transferred unless a key opts in, and must never hold a token or
|
|
789
|
+
a secret: the value is written into the page.
|
|
790
|
+
- One `<script id="dms-payload" type="application/json">` carries the data,
|
|
791
|
+
the failures and the transferred state, in
|
|
792
|
+
[devalue](https://github.com/sveltejs/devalue)'s format, which keeps a
|
|
793
|
+
`Set`, a `Map` or a `Date`, escaped like Inertia's page object. A value it
|
|
794
|
+
cannot write, such as a class instance, is left out and logged.
|
|
795
|
+
- **Stale while revalidate.** In the browser, data loaded before shows at
|
|
796
|
+
once, and is read again in the background on each mount of a component
|
|
797
|
+
showing it and on each return of a reactive key to it; awaiting the call
|
|
798
|
+
does not wait for that read. The first mount after a server render does not
|
|
799
|
+
read again: its data is the server's, just read. Of the data no component
|
|
800
|
+
shows any more, the browser keeps the 50 most recently shown across
|
|
801
|
+
navigations, so a return to a page shows its data at once.
|
|
802
|
+
- **`error` stays until the next answer.** A run in flight keeps the last
|
|
803
|
+
failure; a success clears it, a failure replaces it, and a failed refresh
|
|
804
|
+
keeps the data of the last success on screen.
|
|
805
|
+
- **`key`** is the key the data is loaded under (a reactive key's current
|
|
806
|
+
one). `transform(value, { key })` receives it, and `useDmsFetch`'s
|
|
807
|
+
`transform(value, { key, url })` the URL the response answers too, even when
|
|
808
|
+
a reactive URL changed since. While a reactive key shows the previous key's
|
|
809
|
+
data, `placeholder` is `true`.
|
|
810
|
+
- **`data` is a shallow ref** (`deep: false`, as in Nuxt 4): replace the value
|
|
811
|
+
to update it, or pass `deep: true` to track changes inside it.
|
|
812
|
+
- **`immediate: false`** waits for `execute()` (a dialog, a drawer), and
|
|
813
|
+
**`refreshInterval: ms`** reads again on an interval in the browser, while a
|
|
814
|
+
component shows the data and the page is visible: every read of a module can
|
|
815
|
+
go through `useDmsAsyncData` or `useDmsFetch`. The interval may be a ref or
|
|
816
|
+
a getter: the polling follows it, and `0`, `null` or `undefined` stops it.
|
|
817
|
+
- **A call outside a component** (a plugin, also past its first `await`, a
|
|
818
|
+
callback) belongs to the application: its watchers and its polling run for
|
|
819
|
+
as long as the application does, so a plugin can poll a status with
|
|
820
|
+
`refreshInterval` and stop it through a reactive interval.
|
|
821
|
+
- **`useDmsPagedData(key, handler, { nextCursor, initialCursor })`** reads a
|
|
822
|
+
list page by page under one key ("load more", an infinite list). The
|
|
823
|
+
handler receives `{ cursor, index, signal }` and returns a page;
|
|
824
|
+
`nextCursor(page, pages)` gives the next page's cursor, or `null` at the
|
|
825
|
+
end. The first page loads like any data (a server render waits for it and
|
|
826
|
+
transfers it); `loadMore()` appends the next one, `pages`, `hasMore` and
|
|
827
|
+
`loadingMore` follow it, and a failure lands in `error` with the pages
|
|
828
|
+
kept. A revalidation (a mount, `refresh()`, `refreshDmsPageData()`, the
|
|
829
|
+
polling) reads every page loaded so far again and replaces them once all
|
|
830
|
+
are in. Put what filters the list in the key: a new key starts again from
|
|
831
|
+
its first page.
|
|
832
|
+
- **`refreshDmsPageData()`** reads again the data the components on screen
|
|
833
|
+
show, after a save or on a realtime event; a call opts out with
|
|
834
|
+
`refreshOnPageRefresh: false`, and data waiting for `execute()` is left
|
|
835
|
+
alone. `refreshDmsData(keys?)` reads given keys again.
|
|
836
|
+
- **`setDmsAsyncDataScope(scope)`** puts the keys in a scope, such as a digest
|
|
837
|
+
of the account and the workspace: the same key in another scope is another
|
|
838
|
+
entry, and moving to another scope drops the data of every key. Set it on
|
|
839
|
+
both sides before the page renders, with the same value: the scope is
|
|
840
|
+
written in the page with each key.
|
|
841
|
+
- **A call without a key** (`useDmsAsyncData(handler)`, or `useDmsFetch` with
|
|
842
|
+
a body it cannot key, such as a `FormData`) cannot be transferred: the
|
|
843
|
+
server leaves it to the browser, as `server: false` does, so the page
|
|
844
|
+
hydrates with what it shows. A development render warns about it, and about
|
|
845
|
+
a backend read made outside any handler on the server (an immediate
|
|
846
|
+
watcher, a setup's own `await $fetch`), which the browser makes again.
|
|
847
|
+
- **`useDmsFetch(url, options)`** is `useDmsAsyncData` around a request,
|
|
848
|
+
keyed on its method, base URL, URL, query and body (not its headers): the
|
|
849
|
+
callers of one request share it, and a server render transfers it. The URL,
|
|
850
|
+
`method`, `query`, `params` and `body` may each be a ref or a getter, read
|
|
851
|
+
again for each request: the caller moves to the key of each request, as in
|
|
852
|
+
Nuxt's `useFetch`. A URL that is `null` or `undefined` requests nothing
|
|
853
|
+
until it has a value, on the server as in the browser. `key` names a key
|
|
854
|
+
instead (a ref or a getter too: `null` requests nothing), and `$fetch`
|
|
855
|
+
replaces the fetch function, which receives the signal.
|
|
856
|
+
- **`dmsRequestKey(url, { method, baseURL, query, params, body })`** is the
|
|
857
|
+
key `useDmsFetch` gives a request, without the scope: for
|
|
858
|
+
`refreshDmsData(keys)`, and for a test that looks for a response's entry
|
|
859
|
+
instead of computing the digest itself.
|
|
860
|
+
- **A key that is `null` or `undefined`** (a ref or a getter for it) has
|
|
861
|
+
nothing to load: the caller holds its default, and runs nothing until the
|
|
862
|
+
key has a value.
|
|
863
|
+
- **A new key is read where the first one would have been.** On a server,
|
|
864
|
+
`server: false` and `immediate: false` read nothing, whatever key the
|
|
865
|
+
render gives them. Another read follows a key its setup sets: the
|
|
866
|
+
component's render waits for it and transfers it, `lazy` or not. A key that
|
|
867
|
+
changes once that wait has started (the answer of another read of the
|
|
868
|
+
same component, a later component) is not read on the server: the HTML
|
|
869
|
+
shows the default, and the browser reads it once the component is mounted.
|
|
870
|
+
To render a read that depends on another one's answer, await the first one
|
|
871
|
+
in the setup.
|
|
872
|
+
- **`useDmsData(key)`** gives the `data` another caller loads under `key`,
|
|
873
|
+
as Nuxt's `useNuxtData`: assigning it, or changing it in place (unless the
|
|
874
|
+
key is `deep: false`), updates every component showing the key without
|
|
875
|
+
reading it again. It is how a write's answer (an optimistic update, put
|
|
876
|
+
back if the write fails) or a realtime event patches data, including the
|
|
877
|
+
data of a key the component does not show. `data` is `undefined` until a
|
|
878
|
+
caller loads the key, while a reactive key still shows the previous key's
|
|
879
|
+
data, and once the browser lets the key go (past the 50 most recent keys
|
|
880
|
+
it keeps) or the account or workspace changes; a write to a key
|
|
881
|
+
nobody loaded is ignored. The key may be a ref or a getter, and a key
|
|
882
|
+
loaded by `useDmsFetch` is the `key` option it was given. It loads
|
|
883
|
+
nothing and keeps nothing alive: a component showing the data calls
|
|
884
|
+
`useDmsAsyncData` with the key. A run in flight still writes its answer
|
|
885
|
+
over an update, so a writer refreshes the key once the write is done.
|
|
886
|
+
In a server render, a component called after the caller's call waits for
|
|
887
|
+
its data, so it renders what the browser hydrates it with.
|
|
537
888
|
|
|
538
889
|
### Icons
|
|
539
890
|
|
|
@@ -562,6 +913,12 @@ the page, so the browser draws the same icons as it hydrates; an icon no
|
|
|
562
913
|
installed collection holds stays empty on the server and is fetched in the
|
|
563
914
|
browser as before.
|
|
564
915
|
|
|
916
|
+
Each icon is drawn once per page: a hidden `<svg id="dms-icon-sprite">` holds
|
|
917
|
+
one `<symbol>` per icon (and per flip or rotation), and every occurrence is an
|
|
918
|
+
`<svg>` with a `<use>` of it, so an icon on each row of a table is sent once.
|
|
919
|
+
The browser draws the same way, adding the symbols of the icons it draws later
|
|
920
|
+
to the same sprite.
|
|
921
|
+
|
|
565
922
|
## Options
|
|
566
923
|
|
|
567
924
|
| Option | Environment | Purpose |
|
|
@@ -572,14 +929,15 @@ browser as before.
|
|
|
572
929
|
| `--offline` | `DMS_OFFLINE` | Reuse cached manifest and archives; the only way `build` uses them |
|
|
573
930
|
| `--strict` | `DMS_PREPARE_STRICT` | Make `prepare` fail instead of warning; see [Commands](#commands) |
|
|
574
931
|
| `--bootstrap-secret` | `DMS_BOOTSTRAP_SECRET` | Backend bootstrap credential |
|
|
575
|
-
| | `HOST` | Address the frontend server binds to
|
|
932
|
+
| | `HOST` | Address the frontend server binds to: `127.0.0.1` by default for `ajs dms dev`, so only this machine reaches the development server (set `HOST=0.0.0.0` to open it to the network); `0.0.0.0` for `ajs dms start` |
|
|
576
933
|
| | `DMS_COOKIE_SECURE` | Secure cookies (`true` by default; `ajs dms dev` defaults to `false`) |
|
|
577
934
|
| | `DMS_TRUSTED_PROXY_HOPS` | Number of trusted, rightmost reverse-proxy hops (default `0`) |
|
|
578
935
|
| | `DMS_COUNTRY_HEADER` | Client country header the server relays to the backend, set by a trusted reverse proxy (default `CF-IPCountry`; empty relays none). Name the same header in the backend's `auth.signInCountry.header`. Relayed only when `DMS_TRUSTED_PROXY_HOPS` is above `0`, like the client address |
|
|
579
936
|
| | `DMS_SESSION_SECRET` | Session cookie encryption key, 32 characters or more (required for login) |
|
|
580
|
-
| | `DMS_HTML_RENDER_SECRET` | Secret that verifies the backend's signed HTML/email render requests; without it, e-mail renders are refused.
|
|
581
|
-
| | `DMS_OAUTH_RELAY_SECRET` | Secret the server presents to the backend's OAuth endpoints; without it, the backend refuses OAuth sign-in.
|
|
937
|
+
| | `DMS_HTML_RENDER_SECRET` | Secret that verifies the backend's signed HTML/email render requests; without it, e-mail renders are refused. The `htmlRender.serviceSecret` the backend publishes in the frontend manifest wins; the variable is the fallback for a manifest without one |
|
|
938
|
+
| | `DMS_OAUTH_RELAY_SECRET` | Secret the server presents to the backend's OAuth endpoints; without it, the backend refuses OAuth sign-in. The `oauth.relaySecret` the backend publishes in the frontend manifest wins; the variable is the fallback for a manifest without one |
|
|
582
939
|
| | `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) |
|
|
940
|
+
| | `DMS_BACKEND_PROXIES` | Extra backend prefixes the server proxies, on top of those the modules declare: a JSON array of `{ path, auth? }` (see [Routing and the backend proxy](#routing-and-the-backend-proxy)) |
|
|
583
941
|
| | `DMS_PUBLIC_ENDPOINTS` | Extra backend routes the proxy serves to anyone, on top of those the backend's modules declare: a JSON array of `{ path, methods?, headers? }` (see [Public endpoints](#public-endpoints)) |
|
|
584
942
|
| | `DMS_CLIENT_BASE_URL` | Public frontend URL used in generated links and emails |
|
|
585
943
|
|
|
@@ -615,6 +973,6 @@ ajs dms verify-source \
|
|
|
615
973
|
|
|
616
974
|
The checks look for what the core layer ships: its lazily loaded chart and rich-text libraries, the pages every DMS serves and its e-mail templates. A `--layer` that holds no core layer, such as a project's own `frontend-vue`, is refused before anything is installed, with exit code 2 and the command that verifies the same folder with `--module` instead.
|
|
617
975
|
|
|
618
|
-
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 generates a temporary workspace, installs it, builds client, SSR
|
|
976
|
+
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 generates a temporary workspace, installs it, builds the client, SSR and email bundles while `vue-tsc` runs alongside them (it does not compress the client assets, which only a served build needs), checks what the client bundle loads, renders eight DMS email templates, and checks that the email bundle excludes browser-only modules. Each check is one line that ends in ✔ or ✖, and the run ends with `Verified <n> modules` and its duration, or with the first failure: a type or Vite error points at the file in the package you passed (`./frontend-vue/app/components/Callout.vue:17:7`), not at its copy in the workspace, and the command exits 1. `--verbose` streams the pnpm, Vite and `vue-tsc` output. The workspace is removed when the run ends. It does not start a backend or publish packages. Repository development can invoke the same runner through `pnpm test:real-source`, with `DMS_LAYER_SOURCE` set to a core layer root and `DMS_MODULE_SOURCES` to a JSON array of module roots.
|
|
619
977
|
|
|
620
978
|
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.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface AppConfigModules {
|
|
2
|
+
modules: ReadonlyArray<{
|
|
3
|
+
id: string;
|
|
4
|
+
root: string;
|
|
5
|
+
}>;
|
|
6
|
+
}
|
|
7
|
+
export declare function writeAppConfigs(workspaceDir: string, registry: AppConfigModules): boolean;
|
|
8
|
+
export declare function affectsAppConfigs(relativePath: string): boolean;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.writeAppConfigs = writeAppConfigs;
|
|
4
|
+
exports.affectsAppConfigs = affectsAppConfigs;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const asset_roots_1 = require("./asset-roots");
|
|
8
|
+
const fs_sync_1 = require("./fs-sync");
|
|
9
|
+
const APP_CONFIG_FILE = "app/app.config.ts";
|
|
10
|
+
const APP_CONFIGS_MODULE = "app-configs.generated.ts";
|
|
11
|
+
function discoverAppConfigs(registry) {
|
|
12
|
+
return registry.modules.flatMap((module) => (0, asset_roots_1.moduleAssetRoots)(module.root)
|
|
13
|
+
.reverse()
|
|
14
|
+
.filter((root) => (0, node_fs_1.existsSync)((0, node_path_1.join)(root.absolutePath, APP_CONFIG_FILE)))
|
|
15
|
+
.map((root) => ({
|
|
16
|
+
moduleId: module.id,
|
|
17
|
+
relativePath: `${root.relativePrefix}${APP_CONFIG_FILE}`,
|
|
18
|
+
})));
|
|
19
|
+
}
|
|
20
|
+
function appConfigModuleSource(configs) {
|
|
21
|
+
const imports = configs.map((config, index) => `import config${index} from "@frontend/${config.moduleId}/${config.relativePath}";`);
|
|
22
|
+
const entries = configs.map((config, index) => `{ module: ${JSON.stringify(config.moduleId)}, config: config${index} }`);
|
|
23
|
+
return `${imports.join("\n")}
|
|
24
|
+
|
|
25
|
+
import type { DmsLayerAppConfig } from "./frontend-module";
|
|
26
|
+
|
|
27
|
+
type AppConfigsListener = (appConfigs: DmsLayerAppConfig[]) => void;
|
|
28
|
+
|
|
29
|
+
/** Every layer's \`app/app.config.ts\`, highest precedence first. */
|
|
30
|
+
export const appConfigs: DmsLayerAppConfig[] = [${entries.join(", ")}];
|
|
31
|
+
const listeners: Set<AppConfigsListener> =
|
|
32
|
+
import.meta.hot?.data.listeners ?? new Set();
|
|
33
|
+
|
|
34
|
+
/** Development only: hands \`listener\` the configs of every hot update. */
|
|
35
|
+
export function syncAppConfigs(listener: AppConfigsListener): void {
|
|
36
|
+
if (import.meta.hot) listeners.add(listener);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (import.meta.hot) {
|
|
40
|
+
import.meta.hot.data.listeners = listeners;
|
|
41
|
+
import.meta.hot.accept((next) => {
|
|
42
|
+
if (next) for (const listener of listeners) listener(next.appConfigs);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
function writeAppConfigs(workspaceDir, registry) {
|
|
48
|
+
return (0, fs_sync_1.applyContent)(appConfigModuleSource(discoverAppConfigs(registry)), (0, node_path_1.join)(workspaceDir, APP_CONFIGS_MODULE));
|
|
49
|
+
}
|
|
50
|
+
function affectsAppConfigs(relativePath) {
|
|
51
|
+
return (0, asset_roots_1.affectsAssetDirectory)(APP_CONFIG_FILE, relativePath);
|
|
52
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface AssetRoot {
|
|
2
|
+
absolutePath: string;
|
|
3
|
+
relativePrefix: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function moduleAssetRoots(moduleRoot: string): AssetRoot[];
|
|
6
|
+
export declare function isLayerDirectory(relativePath: string): boolean;
|
|
7
|
+
export declare function affectsAssetDirectory(directory: string, relativePath: string): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.moduleAssetRoots = moduleAssetRoots;
|
|
4
|
+
exports.isLayerDirectory = isLayerDirectory;
|
|
5
|
+
exports.affectsAssetDirectory = affectsAssetDirectory;
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const fs_sync_1 = require("./fs-sync");
|
|
9
|
+
const layer_aliases_1 = require("./layer-aliases");
|
|
10
|
+
function moduleAssetRoots(moduleRoot) {
|
|
11
|
+
const layersRoot = (0, node_path_1.join)(moduleRoot, "layers");
|
|
12
|
+
if (!(0, node_fs_1.existsSync)(layersRoot)) {
|
|
13
|
+
return [{ absolutePath: moduleRoot, relativePrefix: "" }];
|
|
14
|
+
}
|
|
15
|
+
return (0, node_fs_1.readdirSync)(layersRoot)
|
|
16
|
+
.sort()
|
|
17
|
+
.map((name) => ({
|
|
18
|
+
absolutePath: (0, node_path_1.join)(layersRoot, name),
|
|
19
|
+
relativePrefix: `layers/${name}/`,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
function isLayerDirectory(relativePath) {
|
|
23
|
+
const segments = (0, fs_sync_1.toPosixPath)(relativePath).split("/");
|
|
24
|
+
return segments[0] === layer_aliases_1.MODULE_LAYERS_DIRNAME && segments.length <= 2;
|
|
25
|
+
}
|
|
26
|
+
function affectsAssetDirectory(directory, relativePath) {
|
|
27
|
+
if (isLayerDirectory(relativePath))
|
|
28
|
+
return true;
|
|
29
|
+
const segments = (0, fs_sync_1.toPosixPath)(relativePath).split("/");
|
|
30
|
+
const withinRoot = segments[0] === layer_aliases_1.MODULE_LAYERS_DIRNAME ? segments.slice(2) : segments;
|
|
31
|
+
return directory
|
|
32
|
+
.split("/")
|
|
33
|
+
.every((segment, index) => index >= withinRoot.length || withinRoot[index] === segment);
|
|
34
|
+
}
|
|
@@ -47,6 +47,15 @@ exports.BUILD_STEPS = [
|
|
|
47
47
|
failed: "E-mail bundle failed",
|
|
48
48
|
stopped: "E-mail bundle stopped",
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
script: "build:server",
|
|
52
|
+
name: "server",
|
|
53
|
+
subject: "server modules bundle",
|
|
54
|
+
running: "Building the server modules",
|
|
55
|
+
done: "Built the server modules",
|
|
56
|
+
failed: "Server modules bundle failed",
|
|
57
|
+
stopped: "Server modules bundle stopped",
|
|
58
|
+
},
|
|
50
59
|
];
|
|
51
60
|
function buildStepFailure(step, result, isVerbose, rerun, nameFile) {
|
|
52
61
|
const problem = (0, vite_output_1.describeViteFailure)(result.lines, step.subject, isVerbose, rerun, nameFile) ??
|