@antelopejs/dms-frontend 0.6.0 → 0.6.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 +55 -939
- package/dist/config.js +1 -0
- package/dist/module-set.js +5 -1
- package/package.json +17 -5
- package/skills/dms-frontend/SKILL.md +50 -0
- package/templates/vue/frontend-module.ts +20 -2
- package/templates/vue/main.ts +36 -23
- package/templates/vue/server/built-files.mjs +48 -0
- package/templates/vue/server/content.mjs +46 -6
- package/templates/vue/server/email-bundle-watch.mjs +24 -3
- package/templates/vue/server/email.mjs +58 -15
- package/templates/vue/server/proxy-compression.mjs +101 -0
- package/templates/vue/server/proxy-headers.mjs +29 -2
- package/templates/vue/server/static-files.mjs +109 -0
- package/templates/vue/server.mjs +26 -50
- package/templates/vue/ui-request-state.mjs +53 -0
- package/templates/vue/vite.config.ts +11 -2
package/README.md
CHANGED
|
@@ -1,978 +1,94 @@
|
|
|
1
|
-
# @antelopejs/dms-frontend
|
|
2
|
-
|
|
3
1
|
<div align="center">
|
|
4
|
-
<a href="./LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-blue?style=for-the-badge&labelColor=000000"></a>
|
|
5
|
-
<a href="https://discord.gg/sjK28QHrA7"><img src="https://img.shields.io/badge/Discord-18181B?logo=discord&style=for-the-badge&color=000000" alt="Discord"></a>
|
|
6
|
-
<a href="https://antelopejs.com"><img src="https://img.shields.io/badge/Docs-18181B?style=for-the-badge&color=000000" alt="Documentation"></a>
|
|
7
|
-
</div>
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
SSR — is the one shipped today. The package itself installs a single executable,
|
|
14
|
-
`ajs-dms`; `ajs dms` is the core CLI delegating to it, and is the name every
|
|
15
|
-
project, script and document uses.
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/AntelopeJS/dms/main/packages/dms/frontend-vue/layers/dms-layout/public/images/antelope-logo/dark.svg">
|
|
5
|
+
<img alt="Antelope DMS" src="https://raw.githubusercontent.com/AntelopeJS/dms/main/packages/dms/frontend-vue/layers/dms-layout/public/images/antelope-logo/light.svg" height="72">
|
|
6
|
+
</picture>
|
|
16
7
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
A renderer is the target framework a generated workspace is built for. The
|
|
20
|
-
loader's contract with a renderer has three parts:
|
|
21
|
-
|
|
22
|
-
- **Manifest negotiation.** `prepare`, `dev`, and `build` request the versioned
|
|
23
|
-
`/dms/frontend` manifest with `renderer=vue&rendererVersion=3`
|
|
24
|
-
(`src/manifest.ts`) and reject every module whose `renderer` does not match.
|
|
25
|
-
The backend side is renderer-keyed too: `AddFrontendModule` takes a
|
|
26
|
-
`renderer: { name, version }`, and each manifest module carries it back
|
|
27
|
-
(`ManifestModule.renderer` in `src/workspace.ts`).
|
|
28
|
-
- **Workspace templates.** Every file of the generated workspace comes from
|
|
29
|
-
`templates/<renderer>/` — `templates/vue/` today. `TEMPLATE_FILES` in
|
|
30
|
-
`src/config.ts` lists what is copied verbatim, and `src/materialize.ts`
|
|
31
|
-
resolves the template root, copies it, materializes the frontend modules
|
|
32
|
-
under `frontend-modules/`, and writes the files derived from them
|
|
33
|
-
(`src/derived-outputs.ts`), `generated-frontend-modules.json` first, in
|
|
34
|
-
manifest-priority order.
|
|
35
|
-
- **Generated server.** `templates/vue/server.mjs` and `templates/vue/server/`
|
|
36
|
-
become the Node server that `ajs dms start` runs from the built workspace:
|
|
37
|
-
Inertia visits, backend proxying, sessions, and email rendering.
|
|
8
|
+
# @antelopejs/dms-frontend
|
|
38
9
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
making the template root (`src/materialize.ts`) and the manifest query
|
|
44
|
-
(`src/manifest.ts`) renderer-aware instead of hardcoding `vue`. There is no
|
|
45
|
-
`--renderer` flag and no renderer registry; the single-renderer assumption is
|
|
46
|
-
deliberate until a second renderer exists. Whatever a renderer names its module
|
|
47
|
-
entry is its own convention: `dms.frontend.ts` and the
|
|
48
|
-
`#dms/frontend-module` alias belong to the Vue renderer, not to the
|
|
49
|
-
loader.
|
|
10
|
+
<a href="https://www.npmjs.com/package/@antelopejs/dms-frontend"><img alt="npm" src="https://img.shields.io/npm/v/@antelopejs/dms-frontend?style=for-the-badge&labelColor=000000&color=000000"></a>
|
|
11
|
+
<a href="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/@antelopejs/dms-frontend?style=for-the-badge&labelColor=000000&color=000000"></a>
|
|
12
|
+
<a href="https://discord.gg/sjK28QHrA7"><img alt="Discord" src="https://img.shields.io/badge/Discord-18181B?logo=discord&style=for-the-badge&color=000000"></a>
|
|
13
|
+
<a href="https://dms.antelopejs.com/docs/frontend"><img alt="Documentation" src="https://img.shields.io/badge/Docs-18181B?style=for-the-badge&color=000000"></a>
|
|
50
14
|
|
|
51
|
-
|
|
15
|
+
**The frontend of the AntelopeJS DMS: the `ajs dms` CLI, the Vue renderer and the frontend-module SDK.**
|
|
52
16
|
|
|
53
|
-
|
|
54
|
-
root Vue tree. Every frontend module explicitly owns its native
|
|
55
|
-
`dms.frontend.ts` entry. That entry registers reusable pages, layouts,
|
|
56
|
-
components, plugins, and middleware through the frontend SDK; the loader does
|
|
57
|
-
not infer application ownership from other framework configuration.
|
|
17
|
+
[Documentation](https://dms.antelopejs.com/docs/frontend) · [DMS](https://github.com/AntelopeJS/dms) · [Modules](https://dms.antelopejs.com/docs/modules)
|
|
58
18
|
|
|
59
|
-
|
|
19
|
+
</div>
|
|
60
20
|
|
|
61
|
-
The
|
|
62
|
-
`@antelopejs/core` and run it as `ajs dms <command>`. That is the only supported
|
|
63
|
-
way to invoke it — in a shell, in a package script, in CI and in a container
|
|
64
|
-
alike.
|
|
21
|
+

|
|
65
22
|
|
|
66
|
-
|
|
67
|
-
# in a project (the usual case: both are already project dependencies)
|
|
68
|
-
pnpm add @antelopejs/core @antelopejs/dms-frontend
|
|
23
|
+
## What it does
|
|
69
24
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
25
|
+
- Asks a DMS backend which frontend modules it loads, materializes them into a generated workspace, builds it, and runs the frontend server.
|
|
26
|
+
- Renders the dashboard with Vue 3, Vite, Inertia and server-side rendering.
|
|
27
|
+
- Keeps the backend token on the server: the browser only holds a sealed, `HttpOnly` session cookie, and declared backend prefixes are proxied with the session's token.
|
|
28
|
+
- Lets a module extend the dashboard with pages, components and plugins (`dms.frontend.ts`), server routes (`dms.server.ts`) and build-time declarations (`dms.frontend.build.ts`).
|
|
29
|
+
- Serves development with hot reload, and production with pre-compressed assets.
|
|
73
30
|
|
|
74
|
-
|
|
75
|
-
(`@antelopejs/core/cli`), so it needs `@antelopejs/core` 1.13.2 or later.
|
|
76
|
-
`npm install -g` works too; this repository and every generated workspace use
|
|
77
|
-
pnpm. Inside a package script, `ajs` resolves from `node_modules/.bin`, and the
|
|
78
|
-
`dms` command it delegates to resolves the project-local plugin, so a script
|
|
79
|
-
never depends on a global install.
|
|
31
|
+
## Install
|
|
80
32
|
|
|
81
|
-
|
|
33
|
+
Add the package next to `@antelopejs/core`; the core `ajs` command runs it as `ajs dms`:
|
|
82
34
|
|
|
83
35
|
```bash
|
|
84
|
-
|
|
85
|
-
ajs dms dev -b http://localhost:5010 -p 3001
|
|
86
|
-
ajs dms build -b https://dms.example.com
|
|
87
|
-
ajs dms start -b https://dms.example.com -p 3001
|
|
88
|
-
ajs dms workspaces
|
|
89
|
-
ajs dms workspaces --json
|
|
90
|
-
ajs dms clean -b https://dms.example.com
|
|
91
|
-
ajs dms clean --all
|
|
92
|
-
ajs dms help environment
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
`ajs dms <command> --help` shows what a command does, its options and a few
|
|
96
|
-
examples. `ajs dms help environment` lists every variable the CLI and the
|
|
97
|
-
generated server read, from the environment or the project's `.env`.
|
|
98
|
-
|
|
99
|
-
`--help`, `--version`, `workspaces` and `clean` run from any directory. `build` and `start`
|
|
100
|
-
need a backend URL through `-b` or `DMS_API_BASE_URL`, and exit 2 without one.
|
|
101
|
-
`dev` also discovers it from the enclosing antelope project's
|
|
102
|
-
`.antelope/dev.json`, and exits 1 when there is none to discover.
|
|
103
|
-
`verify-source` needs no backend: it runs in the project that installs `@antelopejs/dms`.
|
|
104
|
-
|
|
105
|
-
`prepare` runs anywhere: when it cannot prepare the workspace it warns on
|
|
106
|
-
stderr and exits 0, so a frontend module's `postinstall` hook never fails an
|
|
107
|
-
install. The warning says whether prepare was skipped or failed. Skipped means
|
|
108
|
-
no backend is in reach (no URL, an unreachable backend, `--offline` without a
|
|
109
|
-
cached manifest), as in CI: the generated types are refreshed later from a
|
|
110
|
-
development environment. Failed means something a later run will not fix: a
|
|
111
|
-
refused credential, an incompatible manifest or renderer range, an invalid URL.
|
|
112
|
-
A step that needs the types, such as a CI type-check, passes `--strict` (or
|
|
113
|
-
sets `DMS_PREPARE_STRICT=1`): prepare then exits 1 in every one of these cases,
|
|
114
|
-
and 2 when the backend URL is missing or invalid. On success, it says how many
|
|
115
|
-
modules it found and how long it took.
|
|
116
|
-
|
|
117
|
-
`build` ends with where it wrote the production frontend (the `dist`
|
|
118
|
-
directory of the workspace), how long it took, and the `ajs dms start` command
|
|
119
|
-
to run next, with the same backend URL and `DMS_SESSION_SECRET`. Its output is
|
|
120
|
-
deployed, so it never builds silently from the cache: when the backend cannot
|
|
121
|
-
be reached or fails, `build` exits 1, says whether a cached manifest exists and
|
|
122
|
-
how old it is, and builds from it only when asked with `--offline`. `dev` and
|
|
123
|
-
`prepare` still fall back to the cache, with a warning.
|
|
124
|
-
|
|
125
|
-
The commands follow the output contract of `ajs`: what the CLI itself prints
|
|
126
|
-
on stdout is only the help and the version, and everything else (progress,
|
|
127
|
-
warnings, errors) goes to stderr. The dependency install and each step of the
|
|
128
|
-
production build run as one progress line each: their output is kept and its
|
|
129
|
-
last lines are shown when they fail, with paths into the generated workspace
|
|
130
|
-
pointing at the frontend-module sources instead. The server `dev` and `start`
|
|
131
|
-
run keeps its own output. A command exits 0 on success, 1 on a failure, 2 on a
|
|
132
|
-
usage error (an unknown command or option, a missing or invalid argument, a
|
|
133
|
-
missing configuration value) and 130 when stopped with Ctrl+C. `--no-color` or
|
|
134
|
-
`NO_COLOR=1` turns colors off, and terminals that cannot draw Unicode, such as
|
|
135
|
-
`TERM=dumb`, get ASCII symbols. `--verbose` or `ANTELOPEJS_VERBOSE` streams
|
|
136
|
-
the full `pnpm` and Vite output, each line behind the name of the step, and adds
|
|
137
|
-
the stack trace of the underlying error to a failure; `--verbose=<channels>`
|
|
138
|
-
turns all of it on too, whatever the channels. `-q`, `--quiet` or
|
|
139
|
-
`ANTELOPEJS_QUIET=1` prints only results, warnings and errors: no command
|
|
140
|
-
header, progress, summary or update notice, and of the block a ready `dev` or
|
|
141
|
-
`start` server prints, only its Local URL. `ajs --no-color dms …`,
|
|
142
|
-
`ajs --verbose dms …`, `ajs --verbose=<channels> dms …` and `ajs -q dms …` set
|
|
143
|
-
those variables for the plugin; the same flags are also accepted after the
|
|
144
|
-
command, as in `ajs dms build -q`.
|
|
145
|
-
|
|
146
|
-
The CLI checks npm for a newer release at most once a day and prints a one-line
|
|
147
|
-
notice on stderr, only to a terminal: as the last line of a command that
|
|
148
|
-
succeeded, or under the ready block of `dev` and `start`. Help, `--version`,
|
|
149
|
-
`--json`, failed commands, pipes and `TERM=dumb` never show it. The lookup starts
|
|
150
|
-
with the command and never delays its exit; a lookup that comes back empty —
|
|
151
|
-
offline or throttled — is retried after an hour instead of counting as the day's
|
|
152
|
-
attempt. The throttle stamp lives at
|
|
153
|
-
`~/.antelopejs/dms-frontend/update-check.json`. Set `NO_UPDATE_NOTIFIER=1`, pass
|
|
154
|
-
`--no-update-check`, or run under `CI` to turn the check off.
|
|
155
|
-
|
|
156
|
-
Manifest negotiation and module materialization are the renderer contract described in [Renderers](#renderers).
|
|
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=…`; 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
`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.
|
|
167
|
-
|
|
168
|
-
## Frontend modules
|
|
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
|
-
|
|
173
|
-
A materialized module opts into the Vue adapter with a root `dms.frontend.ts`:
|
|
174
|
-
|
|
175
|
-
```ts
|
|
176
|
-
import MyBlock from "./components/MyBlock.vue";
|
|
177
|
-
import MyPage from "./pages/MyPage.vue";
|
|
178
|
-
import type { DmsFrontendModule } from "#dms/frontend-module";
|
|
179
|
-
|
|
180
|
-
const frontendModule: DmsFrontendModule = {
|
|
181
|
-
setup(sdk) {
|
|
182
|
-
sdk.registerComponent("MyBlock", MyBlock);
|
|
183
|
-
sdk.registerPage("default", MyPage);
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
export default frontendModule;
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
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.
|
|
191
|
-
|
|
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.
|
|
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
|
-
|
|
211
|
-
### Component prefix
|
|
212
|
-
|
|
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:
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
const frontendModule: DmsFrontendModule = {
|
|
217
|
-
componentPrefix: "Acme",
|
|
218
|
-
setup(sdk) {
|
|
219
|
-
sdk.registerComponent("Banner", Banner); // registered as AcmeBanner
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
The full name, prefix included, is the one a template uses (`<AcmeBanner>`) and a backend sends (`CustomComponent("AcmeBanner")`, or `acme-banner`). Names are compared ignoring case, a leading `lazy` and non-alphanumeric characters; a leading `Dms` is no longer dropped, since it is the DMS module's own prefix.
|
|
225
|
-
|
|
226
|
-
When two modules register the same name, the module of higher priority keeps it and a warning names both. A module that means to replace another's component registers its full name without its own prefix, which is not reported:
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
sdk.registerComponent("AcmeBanner", MyBanner, { prefix: false });
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
A module's `app/build/` directory is never auto-imported (see below): its components are registered so backend pages can name them, but they are the module's internals, not a surface other modules should build on.
|
|
233
|
-
|
|
234
|
-
### Auto-imports (`dms.frontend.build.ts`)
|
|
235
|
-
|
|
236
|
-
Nothing a module ships is auto-imported unless the module declares it, in an optional `dms.frontend.build.ts` next to its `dms.frontend.ts`:
|
|
237
|
-
|
|
238
|
-
```ts
|
|
239
|
-
import { defineDmsFrontendBuild } from "#dms/frontend-build";
|
|
240
|
-
|
|
241
|
-
export default defineDmsFrontendBuild((build) => {
|
|
242
|
-
build.registerAutoImports([
|
|
243
|
-
"layers/*/app/composables",
|
|
244
|
-
"layers/*/app/utils",
|
|
245
|
-
"layers/*/app/types",
|
|
246
|
-
]);
|
|
247
|
-
});
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
Each entry is a directory relative to the module root, glob patterns allowed; every export of the files under it becomes available to the whole application without an import, in scripts and templates. A module without the file has nothing auto-imported. `app/build/` holds a module's private code and is never auto-imported: it is left out of what a broader entry covers, and an entry naming it, or a directory outside the module, is ignored with a warning when Vite starts.
|
|
251
|
-
|
|
252
|
-
The file runs in Node.js when Vite starts, under `ajs dms dev` and `ajs dms build`, not in the application: it may import `#dms/frontend-build` and Node.js built-ins, never the application's code. Vite restarts when one changes.
|
|
253
|
-
|
|
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.
|
|
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
|
-
});
|
|
36
|
+
pnpm add -D @antelopejs/core @antelopejs/dms-frontend
|
|
319
37
|
```
|
|
320
38
|
|
|
321
|
-
|
|
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
|
-
|
|
345
|
-
### Page loading state
|
|
346
|
-
|
|
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:
|
|
348
|
-
|
|
349
|
-
```vue
|
|
350
|
-
<script setup lang="ts">
|
|
351
|
-
const pageLoading = useDmsPageLoading();
|
|
352
|
-
</script>
|
|
353
|
-
|
|
354
|
-
<template>
|
|
355
|
-
<slot />
|
|
356
|
-
<PageSkeleton v-if="pageLoading" />
|
|
357
|
-
</template>
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
It stays `false` on the server and during hydration, which render the page resolved, for a page that does not suspend, and once a pending page is left.
|
|
361
|
-
|
|
362
|
-
### Interface language
|
|
363
|
-
|
|
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.
|
|
365
|
-
|
|
366
|
-
### Declaring the loader releases a module supports
|
|
39
|
+
## Quick start
|
|
367
40
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
```json
|
|
371
|
-
{
|
|
372
|
-
"name": "@acme/dms-billing-frontend-vue",
|
|
373
|
-
"engines": {
|
|
374
|
-
"@antelopejs/dms-frontend": ">=0.6.0 <0.7.0"
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
`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, unless `--strict` is given. 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.
|
|
380
|
-
|
|
381
|
-
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.
|
|
382
|
-
|
|
383
|
-
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.
|
|
384
|
-
|
|
385
|
-
### Redirecting typed access refusals
|
|
386
|
-
|
|
387
|
-
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:
|
|
388
|
-
|
|
389
|
-
```ts
|
|
390
|
-
sdk.registerAccessRedirect(
|
|
391
|
-
"saas.errors.workspace.access_blocked",
|
|
392
|
-
"/workspace-suspended",
|
|
393
|
-
);
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
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.
|
|
397
|
-
|
|
398
|
-
### Components rendered inside `<svg>`
|
|
399
|
-
|
|
400
|
-
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:
|
|
401
|
-
|
|
402
|
-
```ts
|
|
403
|
-
import { markRaw } from "vue";
|
|
404
|
-
import RelationEdge from "./RelationEdge.vue";
|
|
405
|
-
|
|
406
|
-
const edgeTypes = markRaw({ relation: RelationEdge });
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
### Layer aliases outside the workspace
|
|
410
|
-
|
|
411
|
-
Every layer a module ships under `layers/<name>/` answers to `#<name>` inside
|
|
412
|
-
the generated workspace. That convention is exported, so a module's own tooling
|
|
413
|
-
can reuse it instead of restating it — a unit test runs the module's Vue
|
|
414
|
-
sources outside the workspace, where nothing otherwise supplies `#dms-core` or
|
|
415
|
-
`#dms-ui`:
|
|
416
|
-
|
|
417
|
-
```ts
|
|
418
|
-
// vitest.config.ts, in a DMS frontend module
|
|
419
|
-
import { frontendLayerAliases } from "@antelopejs/dms-frontend/common";
|
|
420
|
-
|
|
421
|
-
export default defineConfig({
|
|
422
|
-
resolve: {
|
|
423
|
-
alias: frontendLayerAliases(
|
|
424
|
-
resolve(__dirname, ".antelope/cache/@antelopejs/dms/frontend-vue"),
|
|
425
|
-
),
|
|
426
|
-
},
|
|
427
|
-
});
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
`frontendLayerAliases` reads a module root on disk and needs no backend,
|
|
431
|
-
manifest or materialized workspace. `frontendLayerTypePaths` returns the same
|
|
432
|
-
layers in tsconfig `paths` shape, and `resolveFrontendLayers` is the primitive
|
|
433
|
-
behind both. Pass several module roots in increasing precedence to overlay
|
|
434
|
-
them: when two modules expose a layer of the same name, the last one wins,
|
|
435
|
-
which is the order the loader itself materializes them in.
|
|
436
|
-
|
|
437
|
-
## Discovery, caching, and security
|
|
438
|
-
|
|
439
|
-
In development, `ajs dms` discovers the backend from the nearest live `.antelope/dev.json`. It reads the local bootstrap credential from `.antelope/dms-dev.json` only when that discovered backend matches the destination URL. For production and CI, set `DMS_API_BASE_URL` and `DMS_BOOTSTRAP_SECRET` in the environment, or in the project's `.env`, rather than passing credentials on the command line.
|
|
440
|
-
|
|
441
|
-
Each canonical backend URL gets an owner-only workspace under `~/.antelopejs/dms-frontend` (see [Workspaces](#workspaces) for how the key is derived). Manifest caches, private module configuration, and extracted archives retain restrictive permissions. `--offline` reuses the last successful manifest and archive; `dev` and `prepare` also fall back to them when the backend cannot be reached or fails, and `build` only with `--offline`. An authorization failure never falls back to privileged cached data.
|
|
442
|
-
|
|
443
|
-
## Configuration
|
|
444
|
-
|
|
445
|
-
Every command loads `.env.local` then `.env` from the **current working
|
|
446
|
-
directory** before it parses its options, so a project can keep its
|
|
447
|
-
configuration in a file instead of exporting variables by hand:
|
|
41
|
+
In a DMS project started with `ajs project run -w`, serve the dashboard with hot reload:
|
|
448
42
|
|
|
449
43
|
```bash
|
|
450
|
-
|
|
451
|
-
DMS_API_BASE_URL=http://localhost:5010
|
|
452
|
-
DMS_CLIENT_BASE_URL=http://localhost:3001
|
|
453
|
-
DMS_BOOTSTRAP_SECRET=replace_with_a_strong_random_value
|
|
454
|
-
DMS_SESSION_SECRET=replace_with_at_least_32_characters
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
Precedence is environment, then `.env.local`, then `.env`: a variable already
|
|
458
|
-
present in the environment is never overwritten, so `DMS_API_BASE_URL=… ajs dms
|
|
459
|
-
build` and a CI job's injected secrets always win over a file left in the
|
|
460
|
-
checkout. A variable exported as an empty string counts as set. Only the
|
|
461
|
-
current directory is read — never a parent directory, and never the generated
|
|
462
|
-
workspace under `~/.antelopejs/dms-frontend`, which is this tool's own output
|
|
463
|
-
and is handed its environment explicitly by the command that spawns it. The
|
|
464
|
-
values loaded here reach the workspace build started by `build`, the dev server
|
|
465
|
-
started by `dev`, and the production server started by `start`, because those
|
|
466
|
-
child processes inherit the environment. A missing file is not an error; an
|
|
467
|
-
unreadable one is reported and skipped.
|
|
468
|
-
|
|
469
|
-
`ajs dms dev` generates a fresh ephemeral 32-byte secret when
|
|
470
|
-
`DMS_SESSION_SECRET` is absent; all sessions are invalidated when that dev
|
|
471
|
-
server restarts. An explicit value is preserved, but empty or shorter than 32
|
|
472
|
-
characters is rejected. `build` and `start` require a configured secret and
|
|
473
|
-
never generate one, which is required for production so sessions survive
|
|
474
|
-
restarts. The generated server also validates the value as a safety net.
|
|
475
|
-
Generate one with `openssl rand -hex 32`.
|
|
476
|
-
|
|
477
|
-
### Opening a session from a module flow
|
|
478
|
-
|
|
479
|
-
`/auth/login`, `/auth/signup` and `/auth/verify-2fa` are not the only ways a
|
|
480
|
-
visitor becomes authenticated: a module can own a flow that ends in an
|
|
481
|
-
authenticated user — a self-service registration completing after payment, an
|
|
482
|
-
invitation being redeemed — and needs the session cookie opened at the end of
|
|
483
|
-
it. `POST /auth/establish` is the generic form of those three routes.
|
|
484
|
-
|
|
485
|
-
The browser names a backend endpoint and the payload to send it:
|
|
486
|
-
|
|
487
|
-
```ts
|
|
488
|
-
await $fetch("/auth/establish", {
|
|
489
|
-
method: "POST",
|
|
490
|
-
body: {
|
|
491
|
-
endpoint: "/api/saas/register/finalize",
|
|
492
|
-
payload: { /* whatever that backend route expects */ },
|
|
493
|
-
},
|
|
494
|
-
});
|
|
44
|
+
pnpm exec ajs dms dev
|
|
495
45
|
```
|
|
496
46
|
|
|
497
|
-
|
|
498
|
-
channel to `DMS_API_BASE_URL`, exactly as it calls `/api/auth/login`, and
|
|
499
|
-
writes the session from the token pair the backend answers with. The browser
|
|
500
|
-
never sends a token and never receives one: it gets back the same
|
|
501
|
-
`{ user, account }` body the login route returns, and the two-factor and
|
|
502
|
-
tenant-assignment outcomes are handled identically.
|
|
503
|
-
|
|
504
|
-
Because the route turns a backend endpoint into a login, it only calls the
|
|
505
|
-
endpoints that were declared for it.
|
|
506
|
-
|
|
507
|
-
Backend modules declare their own. A module registering its frontend names the
|
|
508
|
-
routes that finish its flow, the DMS aggregates them into the frontend manifest
|
|
509
|
-
it serves with the bootstrap secret, and `dev`/`build` write them into the
|
|
510
|
-
workspace as it is materialized:
|
|
511
|
-
|
|
512
|
-
```ts
|
|
513
|
-
// in the backend module
|
|
514
|
-
await AddFrontendModule({
|
|
515
|
-
name: "@antelopejs/dms-saas-frontend-vue",
|
|
516
|
-
sourcePath: path.join(__dirname, "../frontend-vue"),
|
|
517
|
-
renderer: { name: "vue", version: "3" },
|
|
518
|
-
authEstablishEndpoints: ["/api/saas/register/finalize"],
|
|
519
|
-
});
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
A standard deployment therefore needs no configuration at all. The environment
|
|
523
|
-
variable stays as an override, for a backend route no module declares — or one
|
|
524
|
-
declared by a DMS too old to carry the field:
|
|
47
|
+
For production, build against the backend, then start the frontend server, with `DMS_SESSION_SECRET` and `DMS_BOOTSTRAP_SECRET` set:
|
|
525
48
|
|
|
526
49
|
```bash
|
|
527
|
-
|
|
528
|
-
|
|
50
|
+
pnpm exec ajs dms build -b https://dms.example.com
|
|
51
|
+
pnpm exec ajs dms start -b https://dms.example.com -p 3001
|
|
529
52
|
```
|
|
530
53
|
|
|
531
|
-
|
|
532
|
-
source, is matched verbatim against absolute `/api/…` paths — no prefixes, no
|
|
533
|
-
query strings, no traversal — so no backend route that happens to mint a token
|
|
534
|
-
pair can be turned into a login by a request from the browser. An undeclared
|
|
535
|
-
endpoint is answered `403` and never called. The route is `POST`-only and
|
|
536
|
-
same-origin, like every other auth action.
|
|
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.
|
|
54
|
+
`ajs dms --help` lists every command, and `ajs dms help environment` every environment variable.
|
|
553
55
|
|
|
554
|
-
|
|
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
|
-
```
|
|
56
|
+
## Compatibility
|
|
569
57
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
- `auth: "none"` never sends the session, so it needs no `Origin`: for media,
|
|
576
|
-
or a webhook that authenticates its caller itself.
|
|
58
|
+
| `@antelopejs/dms` | `@antelopejs/dms-frontend` |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| 0.9.1 and later | `>=0.6.1 <0.7.0` |
|
|
61
|
+
| 0.9.0 | `>=0.6.0 <0.7.0` |
|
|
62
|
+
| 0.6.0 – 0.8.3 | 0.5 (exact floor per release in the full table) |
|
|
577
63
|
|
|
578
|
-
|
|
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`).
|
|
64
|
+
The 0.6 line needs Node.js 20.12 or later and `@antelopejs/core` `>=1.13.2 <2`. The [compatibility table](https://dms.antelopejs.com/docs/frontend/introduction#compatibility) lists every release.
|
|
587
65
|
|
|
588
|
-
|
|
66
|
+
## Documentation
|
|
589
67
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
68
|
+
- [Introduction](https://dms.antelopejs.com/docs/frontend/introduction) and [Installation](https://dms.antelopejs.com/docs/frontend/installation)
|
|
69
|
+
- [CLI reference](https://dms.antelopejs.com/docs/frontend/cli/overview)
|
|
70
|
+
- [Configuration and environment variables](https://dms.antelopejs.com/docs/frontend/configuration)
|
|
71
|
+
- [Writing a frontend module](https://dms.antelopejs.com/docs/frontend/frontend-modules/anatomy)
|
|
72
|
+
- [Rendering model](https://dms.antelopejs.com/docs/frontend/rendering/model)
|
|
73
|
+
- [Deploying to production](https://dms.antelopejs.com/docs/frontend/deployment/production)
|
|
74
|
+
- [Troubleshooting](https://dms.antelopejs.com/docs/frontend/troubleshooting)
|
|
596
75
|
|
|
597
|
-
|
|
598
|
-
frontend:
|
|
76
|
+
The guide's sources live in [`docs/`](./docs). The package also ships the `dms-frontend` skill for coding agents in [`skills/`](./skills), declared under `antelopeJs.skills`.
|
|
599
77
|
|
|
600
|
-
|
|
601
|
-
await AddFrontendModule({
|
|
602
|
-
name: "@antelopejs/dms-mailing-frontend-vue",
|
|
603
|
-
sourcePath: path.join(__dirname, "../frontend-vue"),
|
|
604
|
-
renderer: { name: "vue", version: "3" },
|
|
605
|
-
publicEndpoints: [
|
|
606
|
-
{
|
|
607
|
-
path: "/api/mailing/events/*/*",
|
|
608
|
-
methods: ["POST"],
|
|
609
|
-
headers: ["x-mailing-webhook-secret"],
|
|
610
|
-
},
|
|
611
|
-
],
|
|
612
|
-
});
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
A request to a public endpoint:
|
|
616
|
-
|
|
617
|
-
- needs no `Origin`;
|
|
618
|
-
- is never sent the session, even from a signed-in browser, so a cross-site
|
|
619
|
-
form posting to it acts as nobody: the route authenticates its caller
|
|
620
|
-
itself (a shared secret, a signature);
|
|
621
|
-
- carries the module's declared headers on top of the usual ones.
|
|
622
|
-
|
|
623
|
-
`path` is an absolute `/api/` path whose first segment is literal (the
|
|
624
|
-
module's own prefix, never `*`) and whose later `*` segments match exactly one
|
|
625
|
-
segment; nothing else matches (no prefix, no query string, no `.` or `..`
|
|
626
|
-
segment). The proxy matches the request's normalized path, the one it sends
|
|
627
|
-
the backend, so `..`, `%2e%2e` or `\` cannot reach another route through a
|
|
628
|
-
public one. `methods` defaults
|
|
629
|
-
to `["POST"]`. `headers` names the module's own `x-` headers; the ones a proxy
|
|
630
|
-
or the DMS gives a meaning to (`x-forwarded-*`, `x-real-ip`, `x-dms-*`,
|
|
631
|
-
`x-inertia*`, method overrides) are refused, as are `authorization` and
|
|
632
|
-
`cookie`. A malformed declaration is dropped whole, with a warning, never
|
|
633
|
-
widened. `DMS_PUBLIC_ENDPOINTS` adds entries of the same shape, as a JSON
|
|
634
|
-
array, for a route no module declares.
|
|
635
|
-
|
|
636
|
-
Every proxied request also relays `If-Match`, so a backend route can refuse a
|
|
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.
|
|
643
|
-
|
|
644
|
-
### Workspaces
|
|
645
|
-
|
|
646
|
-
Each canonical backend URL gets its own owner-only workspace under
|
|
647
|
-
`~/.antelopejs/dms-frontend/<sha256>/`; `build`, `start`, `clean -b` and
|
|
648
|
-
`dev -b` all key on that URL, so one backend means one workspace shared by
|
|
649
|
-
every command.
|
|
650
|
-
|
|
651
|
-
`dev` without `-b` is the deliberate exception. It discovers the backend from
|
|
652
|
-
the enclosing antelope project's `.antelope/dev.json` and keys its workspace on
|
|
653
|
-
the **project directory** instead, because a development backend can land on a
|
|
654
|
-
different port between runs and re-keying on the URL would discard
|
|
655
|
-
`node_modules`, the manifest cache and the client-side appId scope every time it
|
|
656
|
-
does. The consequence is that `ajs dms dev` followed by `ajs dms build -b <url>`
|
|
657
|
-
against the same backend creates two workspaces of their own — several hundred
|
|
658
|
-
megabytes each. Pass `-b` to `dev` to share a single one. `workspaces` lists
|
|
659
|
-
both, with the project a workspace is keyed on, its size and its last use;
|
|
660
|
-
`clean -b <url>` only reaches the URL-keyed one. `clean` takes its target from
|
|
661
|
-
`-b` or `--all` only: it never reads `DMS_API_BASE_URL` from the environment or
|
|
662
|
-
`.env`, so running it without either is an error rather than a deletion.
|
|
663
|
-
|
|
664
|
-
`workspaces` prints an aligned table on a terminal. Piped, it prints one
|
|
665
|
-
tab-separated line per workspace, without a header: id, backend URL, key type
|
|
666
|
-
(`url` or `project`), project directory, size in bytes, last use (ISO 8601) and
|
|
667
|
-
workspace directory. `--json` prints one JSON document with the same fields;
|
|
668
|
-
everything else the command says goes to stderr.
|
|
669
|
-
|
|
670
|
-
`clean --all` lists what it removes and asks before removing it. Without a
|
|
671
|
-
terminal to ask on (a pipe, CI), it exits 2 unless `--yes` is given. Both forms
|
|
672
|
-
of `clean` say how much space they freed.
|
|
673
|
-
|
|
674
|
-
A `DMS_API_BASE_URL` line in `.env` counts as an explicit backend, so a project
|
|
675
|
-
that configures one gets the single shared workspace and gives up autodiscovery
|
|
676
|
-
— including its tolerance for the backend moving to another port. Leave the
|
|
677
|
-
variable out of `.env` to keep autodiscovery for `dev`.
|
|
678
|
-
|
|
679
|
-
## Rendering model
|
|
680
|
-
|
|
681
|
-
The Node server renders full-page requests through the Vite SSR bundle and sends
|
|
682
|
-
the matching Inertia page object to the browser for hydration. Subsequent
|
|
683
|
-
`X-Inertia` visits receive page objects and render client-side. Client-only
|
|
684
|
-
plugins and `DmsClientOnly` defer browser-only work while the same generated
|
|
685
|
-
frontend-module registry drives server and client entries.
|
|
686
|
-
|
|
687
|
-
Under `ajs dms dev`, an edit to a module's `i18n/locales/*.json` regenerates the
|
|
688
|
-
merged catalogs the application imports: the next server render uses them, and
|
|
689
|
-
the open page applies them in place, without a reload. Adding or removing a
|
|
690
|
-
locale reloads the page instead.
|
|
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
|
-
|
|
695
|
-
The other files the workspace derives from every module follow their sources
|
|
696
|
-
the same way. A file added to, changed in or removed from a module's `public/`
|
|
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.
|
|
888
|
-
|
|
889
|
-
### Icons
|
|
890
|
-
|
|
891
|
-
The browser never asks a third party for an icon, so a Content-Security-Policy
|
|
892
|
-
needs no exception for the Iconify API. Icons reach the page two ways:
|
|
893
|
-
|
|
894
|
-
- **Bundled.** Nuxt UI's client bundle holds every icon its scanner finds in
|
|
895
|
-
the workspace's Vue, Markdown and YAML files, the TypeScript of each layer's
|
|
896
|
-
`app/` directory — `app.config.ts` and its `ui.icons` mappings included — and
|
|
897
|
-
Nuxt UI's own defaults.
|
|
898
|
-
- **Served.** Any other icon, such as a page icon a backend module declares,
|
|
899
|
-
is fetched from the frontend server itself: `main.ts` points `@iconify/vue`
|
|
900
|
-
at `GET /api/_dms/icons/<prefix>.json?icons=<name>,…`, which answers in the
|
|
901
|
-
Iconify API format from the collections installed in the workspace
|
|
902
|
-
(`@iconify-json/<prefix>`, or `@iconify/json`). The workspace ships
|
|
903
|
-
`@iconify-json/lucide` and `@iconify-json/ph`; a module using another
|
|
904
|
-
collection depends on its `@iconify-json/*` package. An unknown collection
|
|
905
|
-
answers `404`, and a name outside Iconify's naming rule `400`. `ajs dms dev`
|
|
906
|
-
and `ajs dms start` run the same server, so both serve the route.
|
|
907
|
-
|
|
908
|
-
The server render draws every icon, bundled or not: the frontend server loads
|
|
909
|
-
each installed `@iconify-json/*` collection once and hands it to the renderer,
|
|
910
|
-
and an icon is drawn in its first render as soon as its data is loaded, instead
|
|
911
|
-
of after the component mounts. The data of each icon a render drew travels in
|
|
912
|
-
the page, so the browser draws the same icons as it hydrates; an icon no
|
|
913
|
-
installed collection holds stays empty on the server and is fetched in the
|
|
914
|
-
browser as before.
|
|
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
|
-
|
|
922
|
-
## Options
|
|
923
|
-
|
|
924
|
-
| Option | Environment | Purpose |
|
|
925
|
-
| --- | --- | --- |
|
|
926
|
-
| `-b, --backend-url` | `DMS_API_BASE_URL` (except `clean`) | DMS backend URL, `http://` or `https://` |
|
|
927
|
-
| `-p, --port` | `PORT` | Frontend port from 1 to 65535, default `3001`; `dev` moves to the next free port, `start` stops when it is in use |
|
|
928
|
-
| `-f, --force` | | Reinstall workspace dependencies; `build` also extracts the layers archive from scratch |
|
|
929
|
-
| `--offline` | `DMS_OFFLINE` | Reuse cached manifest and archives; the only way `build` uses them |
|
|
930
|
-
| `--strict` | `DMS_PREPARE_STRICT` | Make `prepare` fail instead of warning; see [Commands](#commands) |
|
|
931
|
-
| `--bootstrap-secret` | `DMS_BOOTSTRAP_SECRET` | Backend bootstrap credential |
|
|
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` |
|
|
933
|
-
| | `DMS_COOKIE_SECURE` | Secure cookies (`true` by default; `ajs dms dev` defaults to `false`) |
|
|
934
|
-
| | `DMS_TRUSTED_PROXY_HOPS` | Number of trusted, rightmost reverse-proxy hops (default `0`) |
|
|
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 |
|
|
936
|
-
| | `DMS_SESSION_SECRET` | Session cookie encryption key, 32 characters or more (required for login) |
|
|
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 |
|
|
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)) |
|
|
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)) |
|
|
942
|
-
| | `DMS_CLIENT_BASE_URL` | Public frontend URL used in generated links and emails |
|
|
943
|
-
|
|
944
|
-
All of these can be set in the project's `.env` instead of the environment; see [Configuration](#configuration).
|
|
945
|
-
|
|
946
|
-
`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 warn about each secret that is not set and what stops working without it; once the server is ready, they list where the others come from (the environment, the manifest or the build-time manifest), never their values. 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.
|
|
947
|
-
|
|
948
|
-
Use pnpm for all repository and workspace operations.
|
|
949
|
-
|
|
950
|
-
## Verify local frontend modules
|
|
951
|
-
|
|
952
|
-
Use the packaged source verifier to validate unpublished DMS frontend modules against this exact adapter version. Run it from the project:
|
|
953
|
-
|
|
954
|
-
```bash
|
|
955
|
-
ajs dms verify-source
|
|
956
|
-
```
|
|
957
|
-
|
|
958
|
-
The modules are verified on top of the DMS core layer, the `frontend-vue` directory of `@antelopejs/dms`. The verifier finds the `@antelopejs/dms` the project installs the way Node does, from the current directory, and names it before it starts: `Verifying against @antelopejs/dms 0.5.5 (installed in this project)`. When the project does not install it, the verifier stops with the command that adds it as a development dependency (`pnpm add -D @antelopejs/dms`, or the equivalent for the project's package manager).
|
|
959
|
-
|
|
960
|
-
Without `--module`, the verifier takes the current directory when it is a frontend module (it contains `dms.frontend.ts`), and its `frontend-vue` directory otherwise. `--module` names other ones and is repeatable, as is `--local-package`, which binds a local build of a package into the generated workspace. All supplied frontend packages are copied into the generated workspace and bound with `workspace:*`.
|
|
961
|
-
|
|
962
|
-
```bash
|
|
963
|
-
ajs dms verify-source --module ./admin-vue --module ./shop-vue
|
|
964
|
-
```
|
|
78
|
+
## Contributing
|
|
965
79
|
|
|
966
|
-
|
|
80
|
+
Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a pull request.
|
|
967
81
|
|
|
968
82
|
```bash
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
83
|
+
pnpm install
|
|
84
|
+
pnpm build
|
|
85
|
+
pnpm lint # oxlint, oxfmt, interface ranges and the docs reference check
|
|
86
|
+
pnpm typecheck
|
|
87
|
+
pnpm test
|
|
972
88
|
```
|
|
973
89
|
|
|
974
|
-
|
|
90
|
+
`pnpm docs:reference` regenerates the environment-variable table of `docs/4.configuration.md` from the list `ajs dms help environment` prints; `pnpm lint` fails when the table is stale.
|
|
975
91
|
|
|
976
|
-
|
|
92
|
+
## License
|
|
977
93
|
|
|
978
|
-
|
|
94
|
+
[Apache-2.0](./LICENSE)
|