@antelopejs/dms-frontend 0.1.0 → 0.1.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 CHANGED
@@ -6,7 +6,44 @@
6
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
7
  </div>
8
8
 
9
- Official Vue 3, Vite, Inertia, and SSR frontend loader for AntelopeJS DMS. The `ajs dms` CLI generates a Vue workspace and runs its Node frontend server.
9
+ Frontend-agnostic loader for AntelopeJS DMS. The backend serves a frontend
10
+ manifest and the matching frontend-module archives; the `ajs dms` CLI
11
+ materializes them into a generated workspace for one renderer, builds it, and
12
+ runs its Node frontend server. The `vue` renderer — Vue 3, Vite, Inertia, and
13
+ SSR — is the one shipped today.
14
+
15
+ ## Renderers
16
+
17
+ A renderer is the target framework a generated workspace is built for. The
18
+ loader's contract with a renderer has three parts:
19
+
20
+ - **Manifest negotiation.** `prepare`, `dev`, and `build` request the versioned
21
+ `/dms/frontend` manifest with `renderer=vue&rendererVersion=3`
22
+ (`src/manifest.ts`) and reject every module whose `renderer` does not match.
23
+ The backend side is renderer-keyed too: `AddFrontendModule` takes a
24
+ `renderer: { name, version }`, and each manifest module carries it back
25
+ (`ManifestModule.renderer` in `src/workspace.ts`).
26
+ - **Workspace templates.** Every file of the generated workspace comes from
27
+ `templates/<renderer>/` — `templates/vue/` today. `TEMPLATE_FILES` in
28
+ `src/config.ts` lists what is copied verbatim, and `src/materialize.ts`
29
+ resolves the template root, copies it, materializes the frontend modules
30
+ under `frontend-modules/`, and writes `generated-frontend-modules.json` in
31
+ manifest-priority order.
32
+ - **Generated server.** `templates/vue/server.mjs` and `templates/vue/server/`
33
+ become the Node server that `ajs dms start` runs from the built workspace:
34
+ Inertia visits, backend proxying, sessions, and email rendering.
35
+
36
+ `vue` (version `3`) is the only renderer this package ships, and the loader
37
+ rejects a manifest that declares any other. Renderers for other frameworks —
38
+ React, Svelte, Solid — are a direction, not a promise: nothing in the package
39
+ implements them yet. Adding one means a new `templates/<renderer>/` tree, plus
40
+ making the template root (`src/materialize.ts`) and the manifest query
41
+ (`src/manifest.ts`) renderer-aware instead of hardcoding `vue`. There is no
42
+ `--renderer` flag and no renderer registry; the single-renderer assumption is
43
+ deliberate until a second renderer exists. Whatever a renderer names its module
44
+ entry is its own convention: `dms.frontend.ts` and the
45
+ `#dms-inertia/frontend-module` alias belong to the Vue renderer, not to the
46
+ loader.
10
47
 
11
48
  ## Application ownership
12
49
 
@@ -56,9 +93,9 @@ throttle stamp lives at `~/.antelopejs/dms-frontend/update-check.json`. Set
56
93
  `NO_UPDATE_NOTIFIER=1`, pass `--no-update-check`, or run under `CI` to turn the
57
94
  check off.
58
95
 
59
- The loader renders Vue 3 only: a manifest that declares any other renderer is rejected. `prepare`, `dev`, and `build` request the versioned `/dms/frontend` manifest with `renderer=vue&rendererVersion=3`. Modules are materialized under `frontend-modules/` and registered in deterministic manifest-priority order in `generated-frontend-modules.json`.
96
+ Manifest negotiation and module materialization are the renderer contract described in [Renderers](#renderers).
60
97
 
61
- The generated 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=…`, including fresh shared data so account, tenant, and permission changes update navigation state. 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.
98
+ 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=…`, including fresh shared data so account, tenant, and permission changes update navigation state. 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.
62
99
 
63
100
  Vue modules use `dms.frontend.ts` and the `#dms-inertia/frontend-module` SDK alias, which replaces the former `#cms-inertia` alias and is the import path every DMS frontend module now uses. Email templates register separately through `dms.email.ts`.
64
101
 
@@ -89,9 +126,64 @@ The SDK also exposes `use` for Vue plugins. Entries execute by descending manife
89
126
 
90
127
  ## Discovery, caching, and security
91
128
 
92
- 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 rather than passing credentials on the command line.
129
+ 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.
93
130
 
94
- Each canonical backend URL gets an owner-only workspace under `~/.antelopejs/dms-frontend`. Manifest caches, private module configuration, and extracted archives retain restrictive permissions. `--offline` reuses the last successful manifest and archive; an authorization failure never falls back to privileged cached data.
131
+ 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; an authorization failure never falls back to privileged cached data.
132
+
133
+ ## Configuration
134
+
135
+ Every command loads `.env.local` then `.env` from the **current working
136
+ directory** before it parses its options, so a project can keep its
137
+ configuration in a file instead of exporting variables by hand:
138
+
139
+ ```bash
140
+ # .env
141
+ DMS_API_BASE_URL=http://localhost:5010
142
+ DMS_CLIENT_BASE_URL=http://localhost:3001
143
+ DMS_BOOTSTRAP_SECRET=replace_with_a_strong_random_value
144
+ DMS_SESSION_SECRET=replace_with_at_least_32_characters
145
+ ```
146
+
147
+ Precedence is environment, then `.env.local`, then `.env`: a variable already
148
+ present in the environment is never overwritten, so `DMS_API_BASE_URL=… ajs dms
149
+ build` and a CI job's injected secrets always win over a file left in the
150
+ checkout. A variable exported as an empty string counts as set. Only the
151
+ current directory is read — never a parent directory, and never the generated
152
+ workspace under `~/.antelopejs/dms-frontend`, which is this tool's own output
153
+ and is handed its environment explicitly by the command that spawns it. The
154
+ values loaded here reach the workspace build started by `build`, the dev server
155
+ started by `dev`, and the production server started by `start`, because those
156
+ child processes inherit the environment. A missing file is not an error; an
157
+ unreadable one is reported and skipped.
158
+
159
+ `DMS_SESSION_SECRET` is mandatory for anything that touches a session. The
160
+ generated server encrypts its session cookie with it, and with no value — or
161
+ one shorter than 32 characters — the login page at `/auth` fails the first
162
+ sign-in attempt rather than starting degraded. Generate one with
163
+ `openssl rand -hex 32`.
164
+
165
+ ### Workspaces
166
+
167
+ Each canonical backend URL gets its own owner-only workspace under
168
+ `~/.antelopejs/dms-frontend/<sha256>/`; `build`, `start`, `clean -b` and
169
+ `dev -b` all key on that URL, so one backend means one workspace shared by
170
+ every command.
171
+
172
+ `dev` without `-b` is the deliberate exception. It discovers the backend from
173
+ the enclosing antelope project's `.antelope/dev.json` and keys its workspace on
174
+ the **project directory** instead, because a development backend can land on a
175
+ different port between runs and re-keying on the URL would discard
176
+ `node_modules`, the manifest cache and the client-side appId scope every time it
177
+ does. The consequence is that `ajs dms dev` followed by `ajs dms build -b <url>`
178
+ against the same backend creates two workspaces of their own — several hundred
179
+ megabytes each. Pass `-b` to `dev` to share a single one. `clean --all` lists
180
+ both and names the project a workspace is keyed on; `clean -b <url>` only
181
+ reaches the URL-keyed one.
182
+
183
+ A `DMS_API_BASE_URL` line in `.env` counts as an explicit backend, so a project
184
+ that configures one gets the single shared workspace and gives up autodiscovery
185
+ — including its tolerance for the backend moving to another port. Leave the
186
+ variable out of `.env` to keep autodiscovery for `dev`.
95
187
 
96
188
  ## Rendering model
97
189
 
@@ -112,6 +204,10 @@ frontend-module registry drives server and client entries.
112
204
  | `--bootstrap-secret` | `DMS_BOOTSTRAP_SECRET` | Backend bootstrap credential |
113
205
  | | `DMS_COOKIE_SECURE` | Secure cookies (`true` by default; `ajs dms dev` defaults to `false`) |
114
206
  | | `DMS_TRUSTED_PROXY_HOPS` | Number of trusted, rightmost reverse-proxy hops (default `0`) |
207
+ | | `DMS_SESSION_SECRET` | Session cookie encryption key, 32 characters or more (required for login) |
208
+ | | `DMS_CLIENT_BASE_URL` | Public frontend URL used in generated links and emails |
209
+
210
+ All of these can be set in the project's `.env` instead of the environment; see [Configuration](#configuration).
115
211
 
116
212
  Use pnpm for all repository and workspace operations.
117
213
 
@@ -21,14 +21,17 @@ function cmdClean() {
21
21
  }
22
22
  for (const ws of workspaces) {
23
23
  (0, node_fs_1.rmSync)(ws.dir, { recursive: true, force: true });
24
- (0, cli_ui_1.success)(`Removed ${ws.dir} (${ws.backendUrl})`);
24
+ (0, cli_ui_1.success)(`Removed ${ws.dir} (${(0, common_1.describeWorkspace)(ws)})`);
25
25
  }
26
26
  console.log("");
27
27
  (0, cli_ui_1.success)(`Cleaned ${workspaces.length} workspace(s).`);
28
28
  return;
29
29
  }
30
30
  if (!options.backendUrl) {
31
- (0, cli_ui_1.warning)("Specify -b <url> to clean a specific workspace, or --all to clean everything.");
31
+ (0, cli_ui_1.warning)("Specify -b <url> to clean a specific workspace, or --all to clean everything.\n" +
32
+ " -b only reaches the workspace 'build', 'start' and 'dev -b' share for that URL;\n" +
33
+ " a workspace 'dev' created without -b is keyed on the project directory and is\n" +
34
+ " only removable with --all.");
32
35
  process.exit(1);
33
36
  }
34
37
  const workspaceDir = (0, common_1.getWorkspaceDir)(options.backendUrl);
package/dist/config.js CHANGED
@@ -88,7 +88,9 @@ exports.Options = {
88
88
  .default("3001")
89
89
  .env("PORT"),
90
90
  force: new commander_1.Option("-f, --force", "Force reinstall dependencies"),
91
- offline: new commander_1.Option("--offline", "Skip the backend manifest fetch and reuse the last cached manifest (env: DMS_OFFLINE)").default(booleanFromEnv("DMS_OFFLINE")),
91
+ get offline() {
92
+ return new commander_1.Option("--offline", "Skip the backend manifest fetch and reuse the last cached manifest (env: DMS_OFFLINE)").default(booleanFromEnv("DMS_OFFLINE"));
93
+ },
92
94
  bootstrapSecret: new commander_1.Option("--bootstrap-secret <secret>", "Credential presented to the backend's layer endpoints (env: DMS_BOOTSTRAP_SECRET, preferred — " +
93
95
  "a secret passed on the command line is visible to every process on the machine). In dev it is " +
94
96
  "discovered from the antelope project's .antelope/dms-dev.json.").env("DMS_BOOTSTRAP_SECRET"),
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ENV_FILE_NAMES = void 0;
4
+ exports.loadProjectEnv = loadProjectEnv;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const node_util_1 = require("node:util");
8
+ exports.ENV_FILE_NAMES = [".env.local", ".env"];
9
+ function loadProjectEnv(options = {}) {
10
+ const cwd = options.cwd ?? process.cwd();
11
+ const env = options.env ?? process.env;
12
+ const onWarning = options.onWarning ?? ((message) => console.warn(message));
13
+ const loaded = [];
14
+ for (const name of exports.ENV_FILE_NAMES) {
15
+ const file = (0, node_path_1.join)(cwd, name);
16
+ if (!(0, node_fs_1.existsSync)(file))
17
+ continue;
18
+ let parsed;
19
+ try {
20
+ parsed = (0, node_util_1.parseEnv)((0, node_fs_1.readFileSync)(file, "utf-8"));
21
+ }
22
+ catch (err) {
23
+ onWarning(`⚠ Ignoring ${file}: ${err?.message ?? err}`);
24
+ continue;
25
+ }
26
+ for (const [key, value] of Object.entries(parsed)) {
27
+ if (value !== undefined && env[key] === undefined)
28
+ env[key] = value;
29
+ }
30
+ loaded.push(file);
31
+ }
32
+ return loaded;
33
+ }
package/dist/index.js CHANGED
@@ -12,10 +12,12 @@ const dev_1 = require("./commands/dev");
12
12
  const prepare_1 = require("./commands/prepare");
13
13
  const start_1 = require("./commands/start");
14
14
  const verify_source_1 = require("./commands/verify-source");
15
+ const env_file_1 = require("./env-file");
15
16
  const update_check_1 = require("./update-check");
16
17
  const cli_ui_1 = require("./utils/cli-ui");
17
18
  const { version } = require("../package.json");
18
19
  const runCLI = async () => {
20
+ (0, env_file_1.loadProjectEnv)();
19
21
  const argv = process.argv.slice(2);
20
22
  void (0, update_check_1.checkForUpdate)({ currentVersion: version, argv });
21
23
  if (process.argv.length <= 2) {
@@ -28,7 +30,21 @@ const runCLI = async () => {
28
30
  `Materializes frontend modules from an AntelopeJS backend and starts a Vue or React Vite and Inertia application.`)
29
31
  .version(version, "-v, --version", "Display version number")
30
32
  .option("--no-update-check", "Skip the daily check for a newer DMS frontend release")
31
- .helpCommand("help [command]", "Display help for a specific command");
33
+ .helpCommand("help [command]", "Display help for a specific command")
34
+ .addHelpText("after", `
35
+ Environment:
36
+ Every command reads ${env_file_1.ENV_FILE_NAMES.join(" then ")} from the current directory before parsing
37
+ its options, so DMS_API_BASE_URL, DMS_BOOTSTRAP_SECRET, DMS_SESSION_SECRET and
38
+ the other variables below can live in the project's .env. A variable already
39
+ set in the environment always wins over a file, and .env.local wins over .env.
40
+ The generated workspace never loads a .env of its own.
41
+
42
+ Workspaces:
43
+ Each canonical backend URL gets its own workspace under
44
+ ~/.antelopejs/dms-frontend. 'dev' without -b is the exception: it keys the
45
+ workspace on the antelope project directory instead, so a backend that lands
46
+ on a different port between runs keeps its node_modules and manifest cache.
47
+ Pass -b to 'dev' to share one workspace with 'build' and 'start'.`);
32
48
  program.addCommand((0, dev_1.cmdDev)());
33
49
  program.addCommand((0, build_1.cmdBuild)());
34
50
  program.addCommand((0, start_1.cmdStart)());
package/dist/workspace.js CHANGED
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FRONTEND_MANIFEST_VERSION = void 0;
4
4
  exports.getWorkspaceDirForKey = getWorkspaceDirForKey;
5
5
  exports.projectWorkspaceKey = projectWorkspaceKey;
6
+ exports.projectDirFromWorkspaceKey = projectDirFromWorkspaceKey;
6
7
  exports.getWorkspaceDir = getWorkspaceDir;
7
8
  exports.ensureWorkspace = ensureWorkspace;
9
+ exports.describeWorkspace = describeWorkspace;
8
10
  exports.listWorkspaces = listWorkspaces;
9
11
  exports.writeWorkspaceMeta = writeWorkspaceMeta;
10
12
  exports.bootstrapHeaders = bootstrapHeaders;
@@ -16,8 +18,14 @@ const WORKSPACE_META_FILE = ".ajs-dms-meta.json";
16
18
  function getWorkspaceDirForKey(key) {
17
19
  return (0, node_path_1.join)(config_1.DMS_FRONTEND_HOME, (0, config_1.sha256Hex)(key));
18
20
  }
21
+ const PROJECT_KEY_PREFIX = "project:";
19
22
  function projectWorkspaceKey(projectDir) {
20
- return `project:${(0, node_path_1.resolve)(projectDir)}`;
23
+ return `${PROJECT_KEY_PREFIX}${(0, node_path_1.resolve)(projectDir)}`;
24
+ }
25
+ function projectDirFromWorkspaceKey(workspaceKey) {
26
+ return workspaceKey?.startsWith(PROJECT_KEY_PREFIX)
27
+ ? workspaceKey.slice(PROJECT_KEY_PREFIX.length)
28
+ : undefined;
21
29
  }
22
30
  function getWorkspaceDir(backendUrl) {
23
31
  return getWorkspaceDirForKey((0, config_1.canonicalizeBackendUrl)(backendUrl));
@@ -30,6 +38,12 @@ function ensureWorkspace(workspaceKey) {
30
38
  (0, node_fs_1.chmodSync)(dir, config_1.WORKSPACE_DIR_MODE);
31
39
  return dir;
32
40
  }
41
+ function describeWorkspace(entry) {
42
+ const projectDir = projectDirFromWorkspaceKey(entry.workspaceKey);
43
+ return projectDir
44
+ ? `${entry.backendUrl}, keyed on project ${projectDir}`
45
+ : entry.backendUrl;
46
+ }
33
47
  function listWorkspaces() {
34
48
  if (!(0, node_fs_1.existsSync)(config_1.DMS_FRONTEND_HOME))
35
49
  return [];
@@ -38,16 +52,20 @@ function listWorkspaces() {
38
52
  if (!entry.isDirectory())
39
53
  continue;
40
54
  const dir = (0, node_path_1.join)(config_1.DMS_FRONTEND_HOME, entry.name);
41
- const backendUrl = readWorkspaceBackendUrl(dir);
42
- if (backendUrl === undefined) {
55
+ const meta = readWorkspaceMeta(dir);
56
+ if (meta?.backendUrl === undefined) {
43
57
  console.warn(`⚠ Skipping ${dir}: no readable ${WORKSPACE_META_FILE}.`);
44
58
  continue;
45
59
  }
46
- workspaces.push({ dir, backendUrl });
60
+ workspaces.push({
61
+ dir,
62
+ backendUrl: meta.backendUrl,
63
+ workspaceKey: meta.workspaceKey,
64
+ });
47
65
  }
48
66
  return workspaces;
49
67
  }
50
- function readWorkspaceBackendUrl(dir) {
68
+ function readWorkspaceMeta(dir) {
51
69
  let meta;
52
70
  try {
53
71
  meta = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, WORKSPACE_META_FILE), "utf-8"));
@@ -55,8 +73,13 @@ function readWorkspaceBackendUrl(dir) {
55
73
  catch {
56
74
  return undefined;
57
75
  }
58
- const backendUrl = meta?.backendUrl;
59
- return typeof backendUrl === "string" ? backendUrl : undefined;
76
+ const record = meta;
77
+ return {
78
+ backendUrl: typeof record?.backendUrl === "string" ? record.backendUrl : undefined,
79
+ workspaceKey: typeof record?.workspaceKey === "string"
80
+ ? record.workspaceKey
81
+ : undefined,
82
+ };
60
83
  }
61
84
  function writeWorkspaceMeta(workspaceDir, backendUrl, workspaceKey) {
62
85
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(workspaceDir, WORKSPACE_META_FILE), JSON.stringify({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.1.0",
4
- "description": "Vue 3, Vite, and Inertia frontend loader for AntelopeJS DMS",
3
+ "version": "0.1.2",
4
+ "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
7
7
  "antelopejs",
@@ -10,6 +10,7 @@
10
10
  "frontend-modules",
11
11
  "inertia",
12
12
  "loader",
13
+ "renderer",
13
14
  "ui",
14
15
  "vite",
15
16
  "vue"
@@ -81,7 +82,7 @@
81
82
  "vue-i18n": "^11.1.12"
82
83
  },
83
84
  "peerDependencies": {
84
- "@antelopejs/core": ">=1.5.0 <2"
85
+ "@antelopejs/core": ">=1.6.0 <2"
85
86
  },
86
87
  "peerDependenciesMeta": {
87
88
  "@antelopejs/core": {
@@ -95,5 +96,8 @@
95
96
  "@oxc-parser/binding-linux-x64-gnu": "^0.95.0",
96
97
  "@oxc-parser/binding-win32-x64-msvc": "^0.95.0"
97
98
  },
99
+ "engines": {
100
+ "node": ">=20.12.0"
101
+ },
98
102
  "packageManager": "pnpm@10.6.5"
99
103
  }
@@ -17,7 +17,10 @@ const UUID =
17
17
  function key() {
18
18
  const secret = process.env.DMS_SESSION_SECRET;
19
19
  if (!secret || secret.length < 32)
20
- throw new Error("DMS_SESSION_SECRET must contain at least 32 characters");
20
+ throw new Error(
21
+ "DMS_SESSION_SECRET must contain at least 32 characters; " +
22
+ "generate one with: openssl rand -hex 32",
23
+ );
21
24
  return createHash("sha256").update(secret).digest();
22
25
  }
23
26