@antelopejs/dms-frontend 0.9.0 → 0.9.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms-frontend",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelope",
|
|
@@ -60,17 +60,26 @@
|
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "rimraf dist && tsc",
|
|
62
62
|
"prepack": "pnpm run build",
|
|
63
|
-
"release": "pnpm run lint && pnpm run
|
|
64
|
-
"test": "node --import tsx --test tests
|
|
63
|
+
"release": "pnpm run lint && pnpm run format:check && pnpm run test && pnpm run test:cli && release-it",
|
|
64
|
+
"test": "node --import tsx --test 'tests/!(cli|cli-*).test.{ts,mts}'",
|
|
65
|
+
"test:cli": "pnpm run build && DMS_PREBUILT=1 node --import tsx --test tests/cli.test.ts 'tests/cli-*.test.{ts,mts}'",
|
|
65
66
|
"test:real-source": "pnpm run build && node dist/verify-source-runner.js",
|
|
66
67
|
"format": "oxfmt .",
|
|
67
|
-
"lint": "oxlint &&
|
|
68
|
+
"lint": "oxlint && antelopejs-check-interface-ranges && pnpm run docs:reference:check",
|
|
68
69
|
"typecheck": "tsc --noEmit",
|
|
69
70
|
"lint:fix": "oxlint --fix --max-warnings=1000000 && oxfmt .",
|
|
70
71
|
"knip": "knip --dependencies",
|
|
71
72
|
"knip:all": "knip",
|
|
72
73
|
"docs:reference": "tsx scripts/docs-reference.ts",
|
|
73
|
-
"docs:reference:check": "tsx scripts/docs-reference.ts --check"
|
|
74
|
+
"docs:reference:check": "tsx scripts/docs-reference.ts --check",
|
|
75
|
+
"prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
|
|
76
|
+
"checks": "antelopejs-checks",
|
|
77
|
+
"size": "antelopejs-check-size",
|
|
78
|
+
"ratchet": "antelopejs-ratchet",
|
|
79
|
+
"sync": "antelopejs-sync",
|
|
80
|
+
"ratchet:update": "antelopejs-ratchet --update",
|
|
81
|
+
"sync:check": "antelopejs-sync --check",
|
|
82
|
+
"format:check": "oxfmt --check ."
|
|
74
83
|
},
|
|
75
84
|
"dependencies": {
|
|
76
85
|
"@intlify/message-compiler": "^11.1.12",
|
|
@@ -81,7 +90,7 @@
|
|
|
81
90
|
},
|
|
82
91
|
"devDependencies": {
|
|
83
92
|
"@antelopejs/core": "^1.16.0",
|
|
84
|
-
"@antelopejs/tooling-configs": "
|
|
93
|
+
"@antelopejs/tooling-configs": "^0.2.0",
|
|
85
94
|
"@iconify/utils": "^3.1.0",
|
|
86
95
|
"@iconify/vue": "^5.0.1",
|
|
87
96
|
"@inertiajs/vue3": "^3.7.1",
|
|
@@ -94,6 +103,7 @@
|
|
|
94
103
|
"defu": "^6.1.4",
|
|
95
104
|
"devalue": "^5.9.4",
|
|
96
105
|
"knip": "^6.34.0",
|
|
106
|
+
"lefthook": "^2.2.2",
|
|
97
107
|
"ofetch": "^1.5.1",
|
|
98
108
|
"oxfmt": "^0.66.0",
|
|
99
109
|
"oxlint": "1.81.0",
|
|
@@ -29,6 +29,7 @@ tags: [frontend, cli, renderer, vue, inertia, ssr, deployment, dms-module]
|
|
|
29
29
|
- The entry is `dms.frontend.ts`, default-exporting `{ componentPrefix?, setup(sdk) }`. `setup` only registers: under `dev` it runs again on every layer change.
|
|
30
30
|
- Set a `componentPrefix`; the backend names a component by its full name (`CustomComponent("AcmeBanner")`). Replace another module's component with `registerComponent(fullName, c, { prefix: false })`.
|
|
31
31
|
- Nothing is auto-imported unless `dms.frontend.build.ts` declares it; `app/build/` never is.
|
|
32
|
+
- `dms.frontend.build.ts` can also add Vite plugins with `build.registerVitePlugins(() => [plugin])`, for dev, build and preview, after the renderer's own plugins.
|
|
32
33
|
- Server routes, auth hooks, route rules and backend proxies go in `dms.server.ts` (`defineDmsServer` from `#dms/server`). It is trusted Node.js code: import what it uses, never `.vue` files or `dms.frontend.ts`.
|
|
33
34
|
- The browser reaches the backend only through declared prefixes (`registerBackendProxy`, `backendProxies`, `DMS_BACKEND_PROXIES`). An undeclared backend route answers `404`; in development, the `x-dms-route-owner` header names who answered.
|
|
34
35
|
- Declare `engines["@antelopejs/dms-frontend"]`, capped below the next minor (`">=0.6.0 <0.7.0"`); a range that excludes the installed loader stops `dev`, `build` and `verify-source`.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isAbsolute, posix, resolve } from "node:path";
|
|
2
|
-
import { normalizePath } from "vite";
|
|
3
|
-
import type {
|
|
2
|
+
import { normalizePath, type Plugin } from "vite";
|
|
3
|
+
import type {
|
|
4
|
+
DmsFrontendBuild,
|
|
5
|
+
DmsFrontendBuildSetup,
|
|
6
|
+
} from "./frontend-build.ts";
|
|
4
7
|
|
|
5
8
|
/** A module's `dms.frontend.build.ts`, as the generated loader imports it. */
|
|
6
9
|
export interface FrontendBuildDeclaration {
|
|
@@ -22,12 +25,31 @@ export interface AutoImportDirectory {
|
|
|
22
25
|
/** A module's private code: never auto-imported, whatever it declares. */
|
|
23
26
|
const PRIVATE_DIRECTORY = /(?:^|\/)app\/build(?:\/|$)/;
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
interface FrontendModuleName {
|
|
29
|
+
id: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type VitePluginFactory = Parameters<DmsFrontendBuild["registerVitePlugins"]>[0];
|
|
34
|
+
|
|
35
|
+
interface DeclaredBuild {
|
|
36
|
+
directories: string[];
|
|
37
|
+
pluginFactories: VitePluginFactory[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Each setup runs once, whichever declaration is read first. */
|
|
41
|
+
const declarations = new WeakMap<DmsFrontendBuildSetup, DeclaredBuild>();
|
|
42
|
+
|
|
43
|
+
function declared(setup: DmsFrontendBuildSetup): DeclaredBuild {
|
|
44
|
+
const known = declarations.get(setup);
|
|
45
|
+
if (known) return known;
|
|
46
|
+
const build: DeclaredBuild = { directories: [], pluginFactories: [] };
|
|
27
47
|
setup({
|
|
28
|
-
registerAutoImports: (entries) => directories.push(...entries),
|
|
48
|
+
registerAutoImports: (entries) => build.directories.push(...entries),
|
|
49
|
+
registerVitePlugins: (create) => build.pluginFactories.push(create),
|
|
29
50
|
});
|
|
30
|
-
|
|
51
|
+
declarations.set(setup, build);
|
|
52
|
+
return build;
|
|
31
53
|
}
|
|
32
54
|
|
|
33
55
|
function rejection(directory: string): string | undefined {
|
|
@@ -50,15 +72,15 @@ function rejection(directory: string): string | undefined {
|
|
|
50
72
|
*/
|
|
51
73
|
export function autoImportDirectories(
|
|
52
74
|
modules: readonly FrontendModuleRoot[],
|
|
53
|
-
|
|
75
|
+
buildDeclarations: readonly FrontendBuildDeclaration[],
|
|
54
76
|
warn: (message: string) => void,
|
|
55
77
|
): AutoImportDirectory[] {
|
|
56
78
|
const roots = new Map(modules.map((module) => [module.id, module.root]));
|
|
57
|
-
return
|
|
79
|
+
return buildDeclarations.flatMap(({ id, setup }) => {
|
|
58
80
|
const root = roots.get(id);
|
|
59
81
|
if (root === undefined) return [];
|
|
60
82
|
const scan = (pattern: string) => normalizePath(resolve(root, pattern));
|
|
61
|
-
const accepted =
|
|
83
|
+
const accepted = declared(setup).directories.filter((directory) => {
|
|
62
84
|
const reason = rejection(directory);
|
|
63
85
|
if (reason)
|
|
64
86
|
warn(
|
|
@@ -76,3 +98,199 @@ export function autoImportDirectories(
|
|
|
76
98
|
];
|
|
77
99
|
});
|
|
78
100
|
}
|
|
101
|
+
|
|
102
|
+
/** The keys of a Vite plugin that hold a value, not a function Vite calls. */
|
|
103
|
+
const PLUGIN_PROPERTIES = new Set([
|
|
104
|
+
"name",
|
|
105
|
+
"enforce",
|
|
106
|
+
"apply",
|
|
107
|
+
"sharedDuringBuild",
|
|
108
|
+
"perEnvironmentState",
|
|
109
|
+
"api",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
/** The hooks whose function result is itself a hook, run after the server starts. */
|
|
113
|
+
const SERVER_HOOKS = new Set(["configureServer", "configurePreviewServer"]);
|
|
114
|
+
|
|
115
|
+
const attributedErrors = new WeakSet<object>();
|
|
116
|
+
|
|
117
|
+
/** The fields of a Rollup or Vite error that diagnostics and the overlay read. */
|
|
118
|
+
const ERROR_FIELDS = [
|
|
119
|
+
"id",
|
|
120
|
+
"loc",
|
|
121
|
+
"frame",
|
|
122
|
+
"plugin",
|
|
123
|
+
"pluginCode",
|
|
124
|
+
"hook",
|
|
125
|
+
"code",
|
|
126
|
+
] as const;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A new error naming the module, with the original as its cause: the original
|
|
130
|
+
* may be frozen. It carries the original's diagnostic fields and its stack
|
|
131
|
+
* frames, so the error still points at the file and the line.
|
|
132
|
+
*/
|
|
133
|
+
function attribute(label: string, error: unknown): Error {
|
|
134
|
+
if (error instanceof Error && attributedErrors.has(error)) return error;
|
|
135
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
136
|
+
const attributed = new Error(`Frontend module ${label}: ${message}`, {
|
|
137
|
+
cause: error,
|
|
138
|
+
});
|
|
139
|
+
attributedErrors.add(attributed);
|
|
140
|
+
if (!(error instanceof Error)) return attributed;
|
|
141
|
+
for (const field of ERROR_FIELDS) {
|
|
142
|
+
if (field in error)
|
|
143
|
+
Reflect.set(attributed, field, Reflect.get(error, field));
|
|
144
|
+
}
|
|
145
|
+
const frames = error.stack?.split(error.message).slice(1).join(error.message);
|
|
146
|
+
if (frames !== undefined && error.stack?.includes(error.message)) {
|
|
147
|
+
attributed.stack = `${error.name}: ${attributed.message}${frames}`;
|
|
148
|
+
}
|
|
149
|
+
return attributed;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
153
|
+
return (
|
|
154
|
+
typeof value === "object" &&
|
|
155
|
+
value !== null &&
|
|
156
|
+
"then" in value &&
|
|
157
|
+
typeof value.then === "function"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function attributedHook(
|
|
162
|
+
label: string,
|
|
163
|
+
hook: unknown,
|
|
164
|
+
returnsHook: boolean,
|
|
165
|
+
): unknown {
|
|
166
|
+
if (typeof hook === "function") {
|
|
167
|
+
const attributedResult = (result: unknown): unknown =>
|
|
168
|
+
returnsHook ? attributedHook(label, result, false) : result;
|
|
169
|
+
return function (this: unknown, ...args: unknown[]) {
|
|
170
|
+
try {
|
|
171
|
+
const result: unknown = hook.apply(this, args);
|
|
172
|
+
if (isThenable(result)) {
|
|
173
|
+
return Promise.resolve(result).then(
|
|
174
|
+
attributedResult,
|
|
175
|
+
(error: unknown) => {
|
|
176
|
+
throw attribute(label, error);
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return attributedResult(result);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw attribute(label, error);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (typeof hook === "object" && hook !== null && "handler" in hook) {
|
|
187
|
+
return {
|
|
188
|
+
...hook,
|
|
189
|
+
handler: attributedHook(label, hook.handler, returnsHook),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return hook;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isPlugin(value: unknown): value is Plugin {
|
|
196
|
+
return (
|
|
197
|
+
typeof value === "object" &&
|
|
198
|
+
value !== null &&
|
|
199
|
+
!Array.isArray(value) &&
|
|
200
|
+
"name" in value &&
|
|
201
|
+
typeof value.name === "string" &&
|
|
202
|
+
value.name !== ""
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Every property name of an object, inherited ones included. */
|
|
207
|
+
function propertyNames(plugin: object): Set<string> {
|
|
208
|
+
const names = new Set<string>();
|
|
209
|
+
for (
|
|
210
|
+
let object: object | null = plugin;
|
|
211
|
+
object !== null && object !== Object.prototype;
|
|
212
|
+
object = Object.getPrototypeOf(object)
|
|
213
|
+
) {
|
|
214
|
+
for (const name of Object.getOwnPropertyNames(object)) {
|
|
215
|
+
if (name !== "constructor") names.add(name);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return names;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A copy of the plugin whose hooks, own or inherited from its class, name the
|
|
223
|
+
* module in the errors they throw. The module's own object is left as it is.
|
|
224
|
+
*/
|
|
225
|
+
function attributedPlugin(label: string, plugin: Plugin): Plugin {
|
|
226
|
+
const copy: Record<string, unknown> = {};
|
|
227
|
+
for (const key of propertyNames(plugin)) {
|
|
228
|
+
const value: unknown = Reflect.get(plugin, key);
|
|
229
|
+
copy[key] = PLUGIN_PROPERTIES.has(key)
|
|
230
|
+
? value
|
|
231
|
+
: attributedHook(label, value, SERVER_HOOKS.has(key));
|
|
232
|
+
}
|
|
233
|
+
if (!isPlugin(copy)) throw invalidPlugins(label);
|
|
234
|
+
return copy;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function invalidPlugins(label: string): Error {
|
|
238
|
+
return new Error(
|
|
239
|
+
`Frontend module ${label} registered Vite plugins that are not valid: the function given to registerVitePlugins must return an array of Vite plugin objects, each with a name (false, null and undefined entries are skipped, nested arrays are flattened), or nothing.`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function flattened(label: string, entries: readonly unknown[]): Plugin[] {
|
|
244
|
+
return entries.flatMap((entry): Plugin[] => {
|
|
245
|
+
if (entry === false || entry === null || entry === undefined) return [];
|
|
246
|
+
if (Array.isArray(entry)) return flattened(label, entry);
|
|
247
|
+
if (isThenable(entry)) throw promisedPlugins(label);
|
|
248
|
+
if (!isPlugin(entry)) throw invalidPlugins(label);
|
|
249
|
+
return [entry];
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function promisedPlugins(label: string): Error {
|
|
254
|
+
return new Error(
|
|
255
|
+
`Frontend module ${label} registered Vite plugins as a Promise: Vite's configuration is built synchronously, so the function given to registerVitePlugins must return the plugins themselves, not a Promise.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function createdPlugins(label: string, create: VitePluginFactory): Plugin[] {
|
|
260
|
+
let created: unknown;
|
|
261
|
+
try {
|
|
262
|
+
created = create();
|
|
263
|
+
} catch (error) {
|
|
264
|
+
throw attribute(label, error);
|
|
265
|
+
}
|
|
266
|
+
if (created === undefined) return [];
|
|
267
|
+
if (isThenable(created)) throw promisedPlugins(label);
|
|
268
|
+
if (!Array.isArray(created)) throw invalidPlugins(label);
|
|
269
|
+
return flattened(label, created);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The Vite plugins the modules registered with `registerVitePlugins`, in module
|
|
274
|
+
* order and then call order. Each module's factories run here, in the build
|
|
275
|
+
* process. A factory that throws, returns a Promise, or returns anything but an
|
|
276
|
+
* array of plugin objects (or nothing) fails with a message naming the module,
|
|
277
|
+
* and so does an error thrown by a hook of one of its plugins. As in a Vite
|
|
278
|
+
* configuration, false, null and undefined entries are skipped and nested
|
|
279
|
+
* arrays are flattened.
|
|
280
|
+
*/
|
|
281
|
+
export function vitePlugins(
|
|
282
|
+
modules: readonly FrontendModuleName[],
|
|
283
|
+
buildDeclarations: readonly FrontendBuildDeclaration[],
|
|
284
|
+
): Plugin[] {
|
|
285
|
+
const names = new Map(
|
|
286
|
+
modules.map((module) => [module.id, module.name ?? module.id]),
|
|
287
|
+
);
|
|
288
|
+
return buildDeclarations.flatMap(({ id, setup }) => {
|
|
289
|
+
const label = names.get(id) ?? id;
|
|
290
|
+
return declared(setup).pluginFactories.flatMap((create) =>
|
|
291
|
+
createdPlugins(label, create).map((plugin) =>
|
|
292
|
+
attributedPlugin(label, plugin),
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a `registerVitePlugins` function may return, as in a Vite
|
|
5
|
+
* configuration's `plugins`, except that a Promise is refused: Vite's
|
|
6
|
+
* configuration is built synchronously.
|
|
7
|
+
*/
|
|
8
|
+
export type DmsVitePluginEntry =
|
|
9
|
+
| Plugin
|
|
10
|
+
| false
|
|
11
|
+
| null
|
|
12
|
+
| undefined
|
|
13
|
+
| readonly DmsVitePluginEntry[];
|
|
14
|
+
|
|
1
15
|
/**
|
|
2
16
|
* What a frontend module's `dms.frontend.build.ts` declares about how the
|
|
3
17
|
* application is built from it. Read when Vite starts, by `ajs dms dev` and
|
|
@@ -14,6 +28,24 @@ export interface DmsFrontendBuild {
|
|
|
14
28
|
* holds the module's private code, which its own files import by path.
|
|
15
29
|
*/
|
|
16
30
|
registerAutoImports(directories: readonly string[]): void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Adds Vite plugins to the application's build, for `ajs dms dev`,
|
|
34
|
+
* `ajs dms build` and the preview alike.
|
|
35
|
+
*
|
|
36
|
+
* `create` runs in the build process each time Vite loads its configuration,
|
|
37
|
+
* and returns real Vite plugin objects (an array, each with a `name`; false,
|
|
38
|
+
* null and undefined entries are skipped and nested arrays flattened, as in a
|
|
39
|
+
* Vite configuration) or nothing. It is synchronous: a Promise is refused,
|
|
40
|
+
* because the configuration is built synchronously. The plugins come after
|
|
41
|
+
* the renderer's own plugins, in module order, then in the order of the
|
|
42
|
+
* calls. A value that is not an array of plugin objects, or an error thrown
|
|
43
|
+
* by `create` or by any hook of a plugin (the function `configureServer`
|
|
44
|
+
* returns included), fails Vite with a message that names the module.
|
|
45
|
+
*/
|
|
46
|
+
registerVitePlugins(
|
|
47
|
+
create: () => readonly DmsVitePluginEntry[] | undefined,
|
|
48
|
+
): void;
|
|
17
49
|
}
|
|
18
50
|
|
|
19
51
|
export type DmsFrontendBuildSetup = (build: DmsFrontendBuild) => void;
|
|
@@ -27,10 +59,11 @@ export type DmsFrontendBuildSetup = (build: DmsFrontendBuild) => void;
|
|
|
27
59
|
*
|
|
28
60
|
* export default defineDmsFrontendBuild((build) => {
|
|
29
61
|
* build.registerAutoImports(["layers/*\/app/composables"]);
|
|
62
|
+
* build.registerVitePlugins(() => [boundaryPlugin()]);
|
|
30
63
|
* });
|
|
31
64
|
* ```
|
|
32
65
|
*
|
|
33
|
-
* A module without this file has nothing auto-imported. The file runs in
|
|
66
|
+
* A module without this file has nothing auto-imported and adds no plugin. The file runs in
|
|
34
67
|
* Node.js when Vite starts, not in the application: it may import Node.js
|
|
35
68
|
* built-ins, never the application's own code.
|
|
36
69
|
*/
|
|
@@ -15,7 +15,7 @@ import { rejectServerSourcesInClient } from "./client-server-guard.mjs";
|
|
|
15
15
|
import { uiInertiaPage } from "./ui-inertia-page.mjs";
|
|
16
16
|
import { uiKbd } from "./ui-kbd.mjs";
|
|
17
17
|
import { uiRequestState } from "./ui-request-state.mjs";
|
|
18
|
-
import { autoImportDirectories } from "./frontend-build-loader.ts";
|
|
18
|
+
import { autoImportDirectories, vitePlugins } from "./frontend-build-loader.ts";
|
|
19
19
|
import { frontendBuilds } from "./frontend-builds.generated.ts";
|
|
20
20
|
import { orderHeadForFirstPaint } from "./head-order.mjs";
|
|
21
21
|
import { iconScanGlobs } from "./icon-scan.mjs";
|
|
@@ -263,6 +263,7 @@ export default defineConfig({
|
|
|
263
263
|
},
|
|
264
264
|
}),
|
|
265
265
|
paintBeforeHydrate(),
|
|
266
|
+
...vitePlugins(registry.modules, frontendBuilds),
|
|
266
267
|
],
|
|
267
268
|
resolve: {
|
|
268
269
|
// `@iconify/vue` keeps its API providers in module state: `main.ts` and
|