@valancex/cli 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -6
- package/dist/budgets.json +175 -0
- package/dist/build.d.ts +8 -0
- package/dist/build.js +38 -9
- package/dist/check.js +6 -1
- package/dist/cli.js +164 -6
- package/dist/compile.d.ts +8 -0
- package/dist/compile.js +135 -16
- package/dist/config.d.ts +12 -0
- package/dist/config.js +34 -1
- package/dist/contract.d.ts +18 -1
- package/dist/contract.js +92 -3
- package/dist/dev.d.ts +5 -0
- package/dist/dev.js +60 -26
- package/dist/emit.d.ts +3 -1
- package/dist/emit.js +56 -17
- package/dist/libraries.d.ts +41 -0
- package/dist/libraries.js +169 -0
- package/dist/library.d.ts +32 -0
- package/dist/library.js +333 -0
- package/dist/package.js +8 -4
- package/dist/recording-guard.d.ts +5 -0
- package/dist/recording-guard.js +28 -0
- package/dist/replay.d.ts +15 -0
- package/dist/replay.js +59 -0
- package/dist/rules.d.ts +23 -0
- package/dist/rules.js +123 -0
- package/dist/runtime-guard.d.ts +11 -0
- package/dist/runtime-guard.js +62 -0
- package/dist/security.d.ts +2 -0
- package/dist/security.js +8 -1
- package/dist/size.d.ts +54 -0
- package/dist/size.js +153 -0
- package/dist/theme.d.ts +41 -0
- package/dist/theme.js +84 -0
- package/dist/tree.d.ts +14 -2
- package/dist/tree.js +16 -1
- package/dist/ui/report.js +1 -1
- package/dist/ui/run.d.ts +1 -0
- package/dist/ui/run.js +13 -0
- package/dist/vite.js +17 -3
- package/package.json +13 -8
- package/schemas/valance.schema.json +76 -0
- package/templates/desktop/gitignore +3 -0
- package/templates/multi/src/web/layout.mprx +2 -2
- package/templates/web/gitignore +3 -0
- package/templates/web/src/layout.mprx +2 -2
- package/views/block.mprx +1 -1
- package/views/layout.mprx +2 -2
- package/views/list-items.mprx +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
// vlx dev [--port N] [--host H] serve the project and rebuild it as it changes
|
|
5
5
|
// vlx build write the site to dist/web/
|
|
6
6
|
// vlx check [--json] find mistakes in authored files without building
|
|
7
|
+
// vlx theme check [--json] the theme: its tokens, recipes, contrast and what each target applies
|
|
7
8
|
// vlx generate page|component <name> add a page, or a component registered in src/main.ts (alias: g)
|
|
8
9
|
// vlx sync bring .valance/ (generated types, manifest, entries) up to date; runs by itself after install and generate
|
|
9
10
|
// vlx help [command] what a command does
|
|
10
11
|
//
|
|
11
12
|
// In a terminal, `init`, `dev`, `build` and `check` draw an interface (Ink, `ui/`). Anywhere else (a pipe, CI, `--json`, `--plain`) they print the plain text they always have.
|
|
13
|
+
import { resolve } from "node:path";
|
|
12
14
|
import { readFileSync } from "node:fs";
|
|
13
15
|
import { pathToFileURL } from "node:url";
|
|
14
16
|
import { INIT_HELP, parseInit } from "./args.js";
|
|
@@ -16,19 +18,26 @@ import { build } from "./build.js";
|
|
|
16
18
|
import { packageDesktop } from "./package.js";
|
|
17
19
|
import { generateComponent, generatePage, generateTarget, GenerateError, KINDS } from "./add.js";
|
|
18
20
|
import { check } from "./check.js";
|
|
21
|
+
import { themeReport } from "./theme.js";
|
|
19
22
|
import { findRoot } from "./config.js";
|
|
20
23
|
import { localCli } from "./delegate.js";
|
|
21
24
|
import { dev } from "./dev.js";
|
|
25
|
+
import { defaultRecording, replayFile } from "./replay.js";
|
|
22
26
|
import { AppError, format } from "./diagnostics.js";
|
|
23
|
-
import { detectPackageManager, directoryFor, directoryProblem, execute, nameProblem, runCommand, titleOf } from "./init.js";
|
|
27
|
+
import { detectPackageManager, directoryFor, directoryProblem, execute, install as installDependencies, nameProblem, PACKAGE_MANAGERS, runCommand, titleOf } from "./init.js";
|
|
28
|
+
import { buildLibrary, checkLibrary, scaffoldLibrary } from "./library.js";
|
|
24
29
|
import { DEFAULT_TARGETS, TARGETS } from "./targets.js";
|
|
25
30
|
import { canAsk, canDraw } from "./tty.js";
|
|
26
31
|
const HELP = `vlx <command>
|
|
27
32
|
|
|
28
33
|
init [name] [--web] make a new application
|
|
29
|
-
dev [--port N] [--host H] serve it and rebuild as it changes
|
|
34
|
+
dev [--port N] [--host H] serve it and rebuild as it changes (--record [file]: write a session log)
|
|
35
|
+
replay <log> [--route R] [--allow-user-data] play a session log against this project and find where it differs
|
|
30
36
|
build write the site to dist/web/
|
|
31
37
|
check [--json] find mistakes in your files without building
|
|
38
|
+
theme check [--json] check the theme: tokens, recipes, contrast, and what each target applies
|
|
39
|
+
init library <name> make a component library: a workspace with the package and a playground
|
|
40
|
+
library build|check build a library's dist/ (JavaScript, declarations, templates, styles); check the package
|
|
32
41
|
generate page|component <name> add a page, or a component (alias: g)
|
|
33
42
|
generate target web|desktop add a target to the project
|
|
34
43
|
package --desktop the desktop target as one executable beside its window (unsigned)
|
|
@@ -51,14 +60,40 @@ const GENERATE_HELP = `vlx generate <page|component|target> <name> (alias: vlx
|
|
|
51
60
|
|
|
52
61
|
Nothing is overwritten: a name that is taken is refused.
|
|
53
62
|
`;
|
|
63
|
+
const LIBRARY_HELP = `vlx library <build|check> [--json] [--app <dir>]
|
|
64
|
+
|
|
65
|
+
Run in a library's package directory (the one with the "valance" export condition; \`vlx init library\` makes one).
|
|
66
|
+
|
|
67
|
+
vlx library build dist/: the TypeScript compiled to JavaScript with declarations (the components' contracts are the
|
|
68
|
+
types component() gives them), and every .mprx and .css under src/ copied beside it. Templates ship
|
|
69
|
+
as source: the application's build compiles them. Needs typescript installed in the library.
|
|
70
|
+
vlx library check the package as a consumer's build needs it: the "valance" condition on exports["."] and the file it
|
|
71
|
+
names, no other entry a tool without the condition would load, a peer dependency on
|
|
72
|
+
@valancex/common, stylesheets that exist and "sideEffects" that keeps them; then the playground
|
|
73
|
+
(--app <dir>, default ../../playground, ./playground or ../playground), an application that depends
|
|
74
|
+
on this package: it must build every target the package declares and write every template the
|
|
75
|
+
package ships, and its own \`vlx check\` problems are reported with the library's.
|
|
76
|
+
|
|
77
|
+
Exits 0 when there is nothing to say, 1 when there is. --json prints the diagnostics.
|
|
78
|
+
`;
|
|
79
|
+
const INIT_LIBRARY_HELP = `vlx init library <name> [--pm <npm|pnpm|yarn|bun>] [--no-install] [--cli <spec>]
|
|
80
|
+
|
|
81
|
+
Make a component library: a workspace with packages/<name> (a sample component, written with the portable core, its contract,
|
|
82
|
+
a tsconfig and a package.json that is marked as a library) and playground/ (an application that depends on it and builds
|
|
83
|
+
the web and the desktop). <name> is the package name (acme-ui, @acme/ui); the directory is the name's last part.
|
|
84
|
+
Then: cd <dir>, install, \`vlx library build\` in packages/<name>, \`vlx library check\`, and the playground's \`vlx dev\`.
|
|
85
|
+
`;
|
|
54
86
|
const COMMAND_HELP = {
|
|
55
|
-
init: INIT_HELP
|
|
87
|
+
init: `${INIT_HELP}\n${INIT_LIBRARY_HELP}`,
|
|
88
|
+
library: LIBRARY_HELP,
|
|
56
89
|
generate: GENERATE_HELP,
|
|
57
90
|
g: GENERATE_HELP,
|
|
58
|
-
|
|
91
|
+
replay: "vlx replay <log> [--route changes,update,render] [--json] [--allow-user-data]\n\nPlay a session log (written by vlx dev --record, or one of MESH's conformance logs) against this project's templates by the three routes of the equivalence law, and find the first line where one differs from the log: a tree, an event's outcome, a program or manifest that is not the one recorded. It runs no command and reads no clock. Exits 0 when every route gave what the log recorded, 1 when one did not, 2 when the log cannot be read. A log that holds user data (every recording does: its header says containsUserData) is refused unless you pass --allow-user-data: a divergence prints what was typed and shown.\n",
|
|
92
|
+
dev: "vlx dev [--web|--desktop] [--port N] [--host H] [--record [file]]\n\nServe the project (the web is served, a desktop project opens its window; a project of more than one target does both, or the one named) and rebuild it as files change. A mistake leaves the last good build serving and shows in the terminal and in the page. --record writes the web page's session as a log (default .valance/sessions/<time>.jsonl) for vlx replay: development only, never uploaded, and it holds what the person typed.\n",
|
|
59
93
|
build: "vlx build [--web] [--desktop]\n\nBuild every target the project has (or the ones named). The web: the site, in dist/web/; any host that serves files can serve it. The desktop: the engine, in dist/desktop/.\n",
|
|
60
94
|
sync: "vlx sync\n\nBring .valance/ up to date: the generated types for your templates (so your editor and tsc agree with them), the manifest and the entries. You rarely run it: a new project does it after install (the `prepare` script), `vlx generate` does it after it writes, and dev, build and check do it as they run. It never fails: mistakes in your files are reported by vlx check.\n",
|
|
61
95
|
package: "vlx package --desktop [--platform linux-x64|win32-x64] [--window <path>] [--node <path>] [--archive]\n\nBuild the desktop target and package it: the engine as one executable (Node's single executable application, with the MESH runtime inside) beside the Desktop PORT's window program, in dist/package/<platform>/. --window is the window program for that platform (default: VLX_WINDOW, else vlx-window on the PATH; build it from the PORT repository). For another platform, --node is a node binary of this version for it. --archive also writes the package as one file, a .tar.gz (Linux) or a .zip (Windows), with its SHA-256 beside it. The result is unsigned.\n",
|
|
96
|
+
theme: "vlx theme check [--json]\n\nCheck the theme: the project's `theme/theme.json` (its tokens by mode, in the Design Tokens format, and its recipes), or the default theme when it has none. Reports a token that does not resolve, a recipe that names a part, state, variant, property or token that does not exist, a removed focus ring, the contrast of every pair the theme declares (WCAG 2.2), and what the web and the desktop apply of it. --json prints the diagnostics, the contrast and the coverage.\n",
|
|
62
97
|
check: "vlx check [--json]\n\nFind mistakes in your files without building. --json prints them as { file, line, column, code, message } for editors and CI.\n",
|
|
63
98
|
};
|
|
64
99
|
/** Refreshes `.valance/` and says so in a line. Best effort: a mistake in the project is `check`'s to report, and a project with no `valance.json` (the install ran before init finished) is left alone. */
|
|
@@ -79,6 +114,16 @@ const option = (args, name) => {
|
|
|
79
114
|
const at = args.indexOf(name);
|
|
80
115
|
return at < 0 ? undefined : args[at + 1];
|
|
81
116
|
};
|
|
117
|
+
/** `--record` alone, `--record file` or `--record=file`: the file, `""` for the default, nothing when it is not there. A word after it that is another flag is not its file. */
|
|
118
|
+
const recordOption = (args) => {
|
|
119
|
+
const joined = args.find((each) => each.startsWith("--record="));
|
|
120
|
+
if (joined !== undefined) {
|
|
121
|
+
return joined.slice("--record=".length);
|
|
122
|
+
}
|
|
123
|
+
const at = args.indexOf("--record");
|
|
124
|
+
const next = at < 0 ? undefined : args[at + 1];
|
|
125
|
+
return at < 0 ? undefined : next === undefined || next.startsWith("-") ? "" : next;
|
|
126
|
+
};
|
|
82
127
|
const version = () => JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
83
128
|
const runInit = async (args, cwd) => {
|
|
84
129
|
if (args.name !== undefined) {
|
|
@@ -124,6 +169,34 @@ const runInit = async (args, cwd) => {
|
|
|
124
169
|
console.log(`vlx init: next: cd ${answers.dir}${outcome.installed ? "" : ` && ${answers.packageManager} install`} && ${runCommand(answers.packageManager)} dev`);
|
|
125
170
|
return 0;
|
|
126
171
|
};
|
|
172
|
+
const runInitLibrary = async (args, cwd) => {
|
|
173
|
+
const positional = args.filter((each, index) => !each.startsWith("-") && !["--pm", "--cli"].includes(args[index - 1] ?? ""));
|
|
174
|
+
const name = positional[0];
|
|
175
|
+
const manager = option(args, "--pm");
|
|
176
|
+
if (name === undefined || !/^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name)) {
|
|
177
|
+
console.error(`vlx init library: name the package: vlx init library acme-ui (lowercase letters, digits, - . _; or @scope/name)\n\n${INIT_LIBRARY_HELP}`);
|
|
178
|
+
return 2;
|
|
179
|
+
}
|
|
180
|
+
if (manager !== undefined && !PACKAGE_MANAGERS.includes(manager)) {
|
|
181
|
+
console.error(`vlx init library: --pm is one of ${PACKAGE_MANAGERS.join(", ")}`);
|
|
182
|
+
return 2;
|
|
183
|
+
}
|
|
184
|
+
const dir = directoryFor(name.replace(/^@[^/]+\//, ""), cwd);
|
|
185
|
+
const problem = directoryProblem(dir);
|
|
186
|
+
if (problem !== undefined) {
|
|
187
|
+
console.error(`vlx init library: ${problem}`);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
const answers = { name, dir, packageManager: manager ?? detectPackageManager(), ...(option(args, "--cli") === undefined ? {} : { cli: option(args, "--cli") }) };
|
|
191
|
+
const files = scaffoldLibrary(answers);
|
|
192
|
+
console.log(`vlx init library: created ${name} in ${dir} (${files.length} files)`);
|
|
193
|
+
if (!args.includes("--no-install")) {
|
|
194
|
+
const ran = await installDependencies(answers);
|
|
195
|
+
console.log(ran.ok ? "vlx init library: installed" : `vlx init library: the dependencies were not installed (${answers.packageManager} install failed); run it yourself in ${dir}`);
|
|
196
|
+
}
|
|
197
|
+
console.log(`vlx init library: next: cd ${dir}/packages/${name.replace(/^@[^/]+\//, "")} && vlx library build && vlx library check`);
|
|
198
|
+
return 0;
|
|
199
|
+
};
|
|
127
200
|
export const main = async (args, cwd = process.cwd()) => {
|
|
128
201
|
const [command, ...rest] = args;
|
|
129
202
|
// `vlx dev --help` says what dev does and runs nothing (`init` and `generate` answer for themselves below).
|
|
@@ -137,7 +210,16 @@ export const main = async (args, cwd = process.cwd()) => {
|
|
|
137
210
|
return 0;
|
|
138
211
|
}
|
|
139
212
|
// A global `vlx` runs the project's own (see `delegate.ts`).
|
|
140
|
-
if (command === "dev" ||
|
|
213
|
+
if (command === "dev" ||
|
|
214
|
+
command === "replay" ||
|
|
215
|
+
command === "build" ||
|
|
216
|
+
command === "package" ||
|
|
217
|
+
command === "check" ||
|
|
218
|
+
command === "theme" ||
|
|
219
|
+
command === "library" ||
|
|
220
|
+
command === "sync" ||
|
|
221
|
+
command === "generate" ||
|
|
222
|
+
command === "g") {
|
|
141
223
|
const local = localCli(cwd);
|
|
142
224
|
if (local !== undefined) {
|
|
143
225
|
const theirs = (await import(pathToFileURL(local).href));
|
|
@@ -152,6 +234,9 @@ export const main = async (args, cwd = process.cwd()) => {
|
|
|
152
234
|
console.log(INIT_HELP);
|
|
153
235
|
return 0;
|
|
154
236
|
}
|
|
237
|
+
if (rest[0] === "library") {
|
|
238
|
+
return await runInitLibrary(rest.slice(1), cwd);
|
|
239
|
+
}
|
|
155
240
|
const parsed = parseInit(rest);
|
|
156
241
|
if (typeof parsed === "string") {
|
|
157
242
|
console.error(`vlx init: ${parsed}\n\n${INIT_HELP}`);
|
|
@@ -168,9 +253,12 @@ export const main = async (args, cwd = process.cwd()) => {
|
|
|
168
253
|
return 2;
|
|
169
254
|
}
|
|
170
255
|
const target = picked[0];
|
|
256
|
+
const asked = recordOption(rest);
|
|
257
|
+
const record = asked === undefined ? undefined : asked === "" ? defaultRecording(findRoot(cwd)) : resolve(cwd, asked);
|
|
171
258
|
if (draws) {
|
|
172
259
|
const { drawDev } = await import("./ui/run.js");
|
|
173
260
|
await drawDev(findRoot(cwd), {
|
|
261
|
+
...(record === undefined ? {} : { record }),
|
|
174
262
|
...(target === undefined ? {} : { target }),
|
|
175
263
|
...(port === undefined ? {} : { port: Number(port) }),
|
|
176
264
|
...(host === undefined ? {} : { host }),
|
|
@@ -178,6 +266,7 @@ export const main = async (args, cwd = process.cwd()) => {
|
|
|
178
266
|
return 0;
|
|
179
267
|
}
|
|
180
268
|
const running = await dev(findRoot(cwd), {
|
|
269
|
+
...(record === undefined ? {} : { record }),
|
|
181
270
|
...(target === undefined ? {} : { target }),
|
|
182
271
|
...(port === undefined ? {} : { port: Number(port) }),
|
|
183
272
|
...(host === undefined ? {} : { host }),
|
|
@@ -192,6 +281,21 @@ export const main = async (args, cwd = process.cwd()) => {
|
|
|
192
281
|
await running.close();
|
|
193
282
|
return 0;
|
|
194
283
|
}
|
|
284
|
+
case "replay": {
|
|
285
|
+
const file = rest.find((each, index) => !each.startsWith("-") && rest[index - 1] !== "--route");
|
|
286
|
+
if (file === undefined) {
|
|
287
|
+
console.error("vlx replay: name a log: vlx replay <log> [--route changes,update,render] [--json]");
|
|
288
|
+
return 2;
|
|
289
|
+
}
|
|
290
|
+
const routes = option(rest, "--route");
|
|
291
|
+
const replayed = await replayFile(findRoot(cwd), resolve(cwd, file), {
|
|
292
|
+
...(routes === undefined ? {} : { routes: routes.split(",") }),
|
|
293
|
+
json: rest.includes("--json"),
|
|
294
|
+
allowUserData: rest.includes("--allow-user-data"),
|
|
295
|
+
});
|
|
296
|
+
(replayed.code === 0 ? console.log : console.error)(replayed.output);
|
|
297
|
+
return replayed.code;
|
|
298
|
+
}
|
|
195
299
|
case "build": {
|
|
196
300
|
if (draws) {
|
|
197
301
|
const { drawBuild } = await import("./ui/run.js");
|
|
@@ -200,7 +304,7 @@ export const main = async (args, cwd = process.cwd()) => {
|
|
|
200
304
|
}
|
|
201
305
|
const started = Date.now();
|
|
202
306
|
const flagged = targetFlags(rest);
|
|
203
|
-
const built = await build(findRoot(cwd), flagged.length === 0 ? {} : { targets: flagged });
|
|
307
|
+
const built = await build(findRoot(cwd), { ...(flagged.length === 0 ? {} : { targets: flagged }), onReport: (report) => report.forEach((line) => console.log(line)) });
|
|
204
308
|
console.log(`vlx build: ${built.pages.length} pages, ${built.files.length} files → ${built.dir} (${((Date.now() - started) / 1000).toFixed(1)}s)`);
|
|
205
309
|
return 0;
|
|
206
310
|
}
|
|
@@ -242,6 +346,60 @@ export const main = async (args, cwd = process.cwd()) => {
|
|
|
242
346
|
}
|
|
243
347
|
return result.diagnostics.length === 0 ? 0 : 1;
|
|
244
348
|
}
|
|
349
|
+
case "library": {
|
|
350
|
+
const sub = rest[0];
|
|
351
|
+
if (sub !== "build" && sub !== "check") {
|
|
352
|
+
console.error("vlx library: say what: vlx library build | vlx library check [--json] [--app <dir>]");
|
|
353
|
+
return 2;
|
|
354
|
+
}
|
|
355
|
+
const dir = cwd;
|
|
356
|
+
const json = rest.includes("--json");
|
|
357
|
+
const given = option(rest, "--app");
|
|
358
|
+
const report = (diagnostics) => {
|
|
359
|
+
if (json) {
|
|
360
|
+
console.log(JSON.stringify({ diagnostics }, null, 2));
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
diagnostics.forEach((each) => console.error(`${each.file}${each.line > 0 ? `:${each.line}` : ""} ${each.code}: ${each.message}`));
|
|
364
|
+
console.log(diagnostics.length === 0 ? `vlx library ${sub}: no problems` : `vlx library ${sub}: ${diagnostics.length} problem${diagnostics.length === 1 ? "" : "s"}`);
|
|
365
|
+
}
|
|
366
|
+
return diagnostics.length === 0 ? 0 : 1;
|
|
367
|
+
};
|
|
368
|
+
if (sub === "build") {
|
|
369
|
+
const built = buildLibrary(dir);
|
|
370
|
+
if (!json && built.diagnostics.length === 0) {
|
|
371
|
+
console.log(`vlx library build: dist/ written; ${built.copied.length} template and stylesheet file${built.copied.length === 1 ? "" : "s"} copied as source`);
|
|
372
|
+
}
|
|
373
|
+
return report(built.diagnostics);
|
|
374
|
+
}
|
|
375
|
+
return report(await checkLibrary(dir, given === undefined ? {} : { app: resolve(cwd, given) }));
|
|
376
|
+
}
|
|
377
|
+
case "theme": {
|
|
378
|
+
if (rest[0] !== "check") {
|
|
379
|
+
console.error("vlx theme: say what: vlx theme check [--json]");
|
|
380
|
+
return 2;
|
|
381
|
+
}
|
|
382
|
+
const report = themeReport(findRoot(cwd));
|
|
383
|
+
if (rest.includes("--json")) {
|
|
384
|
+
console.log(JSON.stringify({ theme: report.name, custom: report.custom, diagnostics: report.diagnostics, contrast: report.contrast, coverage: report.coverage }, null, 2));
|
|
385
|
+
return report.diagnostics.length === 0 ? 0 : 1;
|
|
386
|
+
}
|
|
387
|
+
console.log(`vlx theme check: ${report.custom ? "the project's theme" : "the default theme"} "${report.name}" (modes: ${report.modes.join(", ")})`);
|
|
388
|
+
for (const each of report.contrast) {
|
|
389
|
+
console.log(` contrast ${each.mode.padEnd(9)} ${each.text} on ${each.on} ${each.ratio.toFixed(2)}:1 (min ${each.min}) ${each.passes ? "ok" : "BELOW"}`);
|
|
390
|
+
}
|
|
391
|
+
for (const each of report.coverage) {
|
|
392
|
+
console.log(` ${each.target}: ${each.styled.length} of ${each.styled.length + each.unstyled.length} primitives of the portable core styled${each.unstyled.length === 0 ? "" : ` (not styled: ${each.unstyled.join(", ")})`}`);
|
|
393
|
+
for (const note of each.notApplied) {
|
|
394
|
+
console.log(` not applied: ${note.primitive} ${note.kind} ${note.name} (${note.support})`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
report.diagnostics.forEach((each) => {
|
|
398
|
+
console.error(format(each));
|
|
399
|
+
});
|
|
400
|
+
console.log(report.diagnostics.length === 0 ? "vlx theme check: no problems" : `vlx theme check: ${report.diagnostics.length} problem${report.diagnostics.length === 1 ? "" : "s"}`);
|
|
401
|
+
return report.diagnostics.length === 0 ? 0 : 1;
|
|
402
|
+
}
|
|
245
403
|
case "sync": {
|
|
246
404
|
const synced = await sync(cwd);
|
|
247
405
|
console.log(synced);
|
package/dist/compile.d.ts
CHANGED
|
@@ -17,8 +17,16 @@ interface Template {
|
|
|
17
17
|
readonly source: string;
|
|
18
18
|
readonly builtIn: boolean;
|
|
19
19
|
}
|
|
20
|
+
/** A library component left out of a target's program because its library does not support the target (K5): the tag is reported at each use, once, where the application can act. */
|
|
21
|
+
export interface Excluded {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly library: string;
|
|
24
|
+
readonly supports: ReadonlyArray<string>;
|
|
25
|
+
}
|
|
20
26
|
export interface Templates {
|
|
21
27
|
readonly all: ReadonlyArray<Template>;
|
|
28
|
+
/** The library components this target leaves out, by tag. */
|
|
29
|
+
readonly excluded: ReadonlyMap<string, Excluded>;
|
|
22
30
|
/** The declared tree, in `src/main.ts`'s own words. */
|
|
23
31
|
readonly tree: ReadonlyArray<Node>;
|
|
24
32
|
}
|
package/dist/compile.js
CHANGED
|
@@ -8,12 +8,14 @@ import { basename, join, relative, sep } from "node:path";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { commandTable, scopeSchema } from "@valancex/common/internal";
|
|
10
10
|
import { AppError, diagnostic } from "./diagnostics.js";
|
|
11
|
-
import { declarationsOf, linked, nameOf, treeOf } from "./tree.js";
|
|
11
|
+
import { declarationsOf, declaredOf, linked, nameOf, treeOf } from "./tree.js";
|
|
12
12
|
import { tagsOf } from "./outline.js";
|
|
13
13
|
import { entryOf } from "./contract.js";
|
|
14
|
+
import { idOf, libraryOf, librariesOf } from "./libraries.js";
|
|
14
15
|
import { locateKey } from "./wiring.js";
|
|
15
16
|
import { namesOf, webVocabulary, WHOLE } from "@valancex/common/internal";
|
|
16
|
-
import {
|
|
17
|
+
import { checkReferences, checkTemplate } from "./rules.js";
|
|
18
|
+
import { ENTRY_FILE, GENERATED_DIR } from "./project.js";
|
|
17
19
|
export const SOURCE_DIR = "src";
|
|
18
20
|
/** The component the page is drawn by. */
|
|
19
21
|
export const ROOT = "layout";
|
|
@@ -47,7 +49,26 @@ export const templatesOf = (root, author, vocabulary = webVocabulary, views = BU
|
|
|
47
49
|
}
|
|
48
50
|
const layout = author?.layout;
|
|
49
51
|
const overrides = author?.override ?? [];
|
|
50
|
-
const
|
|
52
|
+
const { libraries } = librariesOf(root);
|
|
53
|
+
const resolved = author?.resolve ?? {};
|
|
54
|
+
const overridden = new Set(overrides.map((each) => nameOf(each.template.mprx)));
|
|
55
|
+
const excluded = new Map();
|
|
56
|
+
const idFor = (component) => idOf(libraries, root, component.template.mprx, nameOf(component.template.mprx));
|
|
57
|
+
// The components of this target's program. A library's component is left out when its library does not support this target (K5: one diagnostic at the use, not several inside the library), when the
|
|
58
|
+
// application's own template of that name replaces it (`override`), and when `resolve` chose another of that name (K14).
|
|
59
|
+
const components = (author?.components ?? []).filter((each) => {
|
|
60
|
+
const name = nameOf(each.template.mprx);
|
|
61
|
+
const library = libraryOf(libraries, root, each.template.mprx);
|
|
62
|
+
if (library === undefined) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (!library.targets.includes(vocabulary.target)) {
|
|
66
|
+
excluded.set(name, { id: idFor(each), library: library.name, supports: library.targets });
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return !overridden.has(name) && (resolved[name] === undefined || resolved[name] === idFor(each));
|
|
70
|
+
});
|
|
71
|
+
const declared = treeOf(layout, overrides, components);
|
|
51
72
|
const declarations = declarationsOf(root);
|
|
52
73
|
const problems = [];
|
|
53
74
|
/** Where a template's component is defined: its `component(…)` call, else where it is imported, else the top of the entry file. */
|
|
@@ -68,17 +89,46 @@ export const templatesOf = (root, author, vocabulary = webVocabulary, views = BU
|
|
|
68
89
|
const builtInNames = new Set(found.keys());
|
|
69
90
|
for (const each of overrides) {
|
|
70
91
|
const name = nameOf(each.template.mprx);
|
|
71
|
-
if (!builtInNames.has(name) &&
|
|
72
|
-
|
|
92
|
+
if (!builtInNames.has(name) &&
|
|
93
|
+
name !== ROOT &&
|
|
94
|
+
!(author?.components ?? []).some((other) => nameOf(other.template.mprx) === name && libraryOf(libraries, root, other.template.mprx) !== undefined)) {
|
|
95
|
+
problem(at(each.template.mprx), "override-unknown", `<${name}> is not a built-in component and no library component in \`components\` has that name, so there is nothing to override; register it in \`components\` instead`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const [tag, id] of Object.entries(resolved)) {
|
|
99
|
+
if (!(author?.components ?? []).some((each) => idFor(each) === id)) {
|
|
100
|
+
const named = (author?.components ?? []).filter((each) => nameOf(each.template.mprx) === tag).map(idFor);
|
|
101
|
+
problem({ file: ENTRY_FILE, line: 1, column: 1 }, "resolve-unknown", `\`resolve\` says <${tag}> is ${id}, but no component in \`components\` has that id${named.length === 0 ? ` (nothing named ${tag} is listed)` : ` (listed with that name: ${named.join(", ")})`}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// A name is one component. Two files of one name, from anywhere in the definition (two libraries', or a library's and the application's), are an error: the first listed must not win silently
|
|
105
|
+
// (decision K14). A component that is not listed in `override` and is named like a built-in is the same collision, with the built-in.
|
|
106
|
+
// A library's component is named by its qualified id (`acme-ui/disclosure`) in a diagnostic and in `resolve`; the application's own, by the bare name.
|
|
107
|
+
const listed = declaredOf(layout, overrides, components);
|
|
108
|
+
const firstOf = new Map();
|
|
109
|
+
const idOfFile = (file, name) => idOf(libraries, root, file, name);
|
|
110
|
+
for (const each of listed) {
|
|
111
|
+
const first = firstOf.get(each.name);
|
|
112
|
+
const id = idOfFile(each.file, each.name);
|
|
113
|
+
if (first === undefined) {
|
|
114
|
+
firstOf.set(each.name, { file: each.file, id });
|
|
115
|
+
if (each.role === "component" &&
|
|
116
|
+
builtInNames.has(each.name) &&
|
|
117
|
+
resolved[each.name] !== id &&
|
|
118
|
+
!listed.some((other) => other.role !== "component" && other.name === each.name)) {
|
|
119
|
+
problem(at(each.file), "template-builtin", `${id} is <${each.name}>, which is also a built-in component: list your own template of that name in \`override\` to replace the built-in, or choose this one with \`resolve: { ${each.name}: "${id}" }\``);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (first.file !== each.file) {
|
|
123
|
+
const libraryMade = id !== each.name || first.id !== each.name;
|
|
124
|
+
problem(at(each.file), "template-duplicate", libraryMade
|
|
125
|
+
? `${id === each.name ? each.file : id} and ${first.id === each.name ? first.file : first.id} are both <${each.name}>: a component is named by its file, so the name must be unique; choose one with \`resolve: { ${each.name}: "${id === each.name ? first.id : id}" }\` in defineApp, or replace both with your own template of that name in \`override\``
|
|
126
|
+
: `${each.file} is also <${each.name}>, like ${first.file}: a component is named by its file, so the name must be unique; rename one of the files`);
|
|
73
127
|
}
|
|
74
128
|
}
|
|
75
129
|
const files = new Map();
|
|
76
130
|
for (const node of declared) {
|
|
77
131
|
const place = at(node.file);
|
|
78
|
-
if (files.has(node.name) && files.get(node.name) !== node.file) {
|
|
79
|
-
problem(place, "template-duplicate", `${files.get(node.name)} is also <${node.name}>: a component is named by its file, so the name must be unique`);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
132
|
if (namesOf(vocabulary).includes(node.name)) {
|
|
83
133
|
problem(place, "template-primitive", `<${node.name}> is already a tag the framework draws as an element; name the file something else`);
|
|
84
134
|
continue;
|
|
@@ -98,7 +148,7 @@ export const templatesOf = (root, author, vocabulary = webVocabulary, views = BU
|
|
|
98
148
|
const claimed = new Set(files.values());
|
|
99
149
|
const whole = (author?.[WHOLE] ?? author);
|
|
100
150
|
for (const parts of [whole, ...Object.values(whole?.targets ?? {})]) {
|
|
101
|
-
for (const node of
|
|
151
|
+
for (const node of declaredOf(parts?.layout, parts?.override ?? [], parts?.components ?? [])) {
|
|
102
152
|
claimed.add(node.file);
|
|
103
153
|
}
|
|
104
154
|
}
|
|
@@ -113,7 +163,7 @@ export const templatesOf = (root, author, vocabulary = webVocabulary, views = BU
|
|
|
113
163
|
if (problems.length > 0) {
|
|
114
164
|
throw new AppError(problems);
|
|
115
165
|
}
|
|
116
|
-
return { all: [...found.values()], tree };
|
|
166
|
+
return { all: [...found.values()], tree, excluded };
|
|
117
167
|
};
|
|
118
168
|
/** The templates the root reaches, following the tags each uses. A template nothing uses is not part of the program (the compiler refuses one). */
|
|
119
169
|
const reachable = (all) => {
|
|
@@ -143,8 +193,19 @@ export const manifestOf = (author, vocabulary = webVocabulary) => {
|
|
|
143
193
|
if (!("tags" in vocabulary)) {
|
|
144
194
|
return { ...derived, components: { ...vocabulary.components, ...derived.components } };
|
|
145
195
|
}
|
|
146
|
-
//
|
|
196
|
+
// What the vocabulary declares of a tag's props and events (the specification's) settles what the table alone derived.
|
|
147
197
|
const components = { ...derived.components };
|
|
198
|
+
for (const [tag, declared] of Object.entries(vocabulary.declared ?? {})) {
|
|
199
|
+
const entry = components[tag];
|
|
200
|
+
if (entry !== undefined) {
|
|
201
|
+
components[tag] = {
|
|
202
|
+
...entry,
|
|
203
|
+
props: Object.fromEntries(Object.entries(entry.props).map(([name, prop]) => [name, declared.props[name] === undefined ? prop : { ...prop, ...declared.props[name] }])),
|
|
204
|
+
events: Object.fromEntries(Object.entries(entry.events).map(([name, event]) => [name, declared.events[name] === undefined ? event : { ...event, ...declared.events[name] }])),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// The props the vocabulary says a template may leave out.
|
|
148
209
|
for (const [tag, names] of Object.entries(vocabulary.optional ?? {})) {
|
|
149
210
|
const entry = components[tag];
|
|
150
211
|
if (entry !== undefined) {
|
|
@@ -188,8 +249,10 @@ const modelOf = (root, author, tree, vocabulary) => {
|
|
|
188
249
|
const { entry, problems: bad } = entryOf(node.interface);
|
|
189
250
|
const site = declarations.get(node.file);
|
|
190
251
|
for (const each of bad) {
|
|
191
|
-
const place = site?.member(each.kind, each.name) ?? site?.call ?? site?.imported ?? { file: ENTRY_FILE, line: 1, column: 1 };
|
|
192
|
-
problems.push(diagnostic(place.file, place.line, "component-schema",
|
|
252
|
+
const place = site?.member(each.kind === "unit" ? "props" : each.kind, each.name) ?? site?.call ?? site?.imported ?? { file: ENTRY_FILE, line: 1, column: 1 };
|
|
253
|
+
problems.push(diagnostic(place.file, place.line, "component-schema", each.message === undefined
|
|
254
|
+
? `${node.name}'s ${each.kind === "props" ? "prop" : each.kind === "unit" ? "unit field or command" : "event"} "${each.name}" has no type the model can hold: ${each.reason}`
|
|
255
|
+
: `${node.name}: ${each.message}`, place.column));
|
|
193
256
|
}
|
|
194
257
|
entries[node.name] = entry;
|
|
195
258
|
}
|
|
@@ -198,9 +261,35 @@ const modelOf = (root, author, tree, vocabulary) => {
|
|
|
198
261
|
}
|
|
199
262
|
return { ...derived, components: { ...derived.components, ...entries } };
|
|
200
263
|
};
|
|
264
|
+
/** The anchors of the content's headings: the ids the built-in views give them (the generated `content.json`, which the content stage wrote before this one). */
|
|
265
|
+
const anchorsOf = (root) => {
|
|
266
|
+
try {
|
|
267
|
+
const content = JSON.parse(readFileSync(join(root, GENERATED_DIR, "content.json"), "utf8"));
|
|
268
|
+
return new Set((content.pages ?? []).flatMap((page) => (page.blocks ?? []).flatMap((block) => (block.anchor === undefined ? [] : [block.anchor]))));
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return new Set();
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
/** A tag the web renamed (decision P7: `para` is `paragraph`): said where a template still writes the old one. */
|
|
275
|
+
const renamedHint = (vocabulary, used, at) => {
|
|
276
|
+
// a prop the web renamed (decision I29: a popover's `kind` is `mode`): the diagnostic names the tag and the prop
|
|
277
|
+
const prop = /component "([^"]+)" has no prop "([^"]+)"/.exec(at.message ?? "");
|
|
278
|
+
const renamedProp = prop === null || !("tags" in vocabulary) ? undefined : vocabulary.renamedProps?.[prop[1]]?.[prop[2]];
|
|
279
|
+
if (renamedProp !== undefined) {
|
|
280
|
+
return ` (\`${prop[2]}\` on \`${prop[1]}\` is now \`${renamedProp}\`)`;
|
|
281
|
+
}
|
|
282
|
+
const renamed = "tags" in vocabulary ? vocabulary.renamed : undefined;
|
|
283
|
+
const line = used.find((template) => template.path === at.path)?.source.split("\n")[at.span.start.line - 1];
|
|
284
|
+
if (renamed === undefined || line === undefined) {
|
|
285
|
+
return "";
|
|
286
|
+
}
|
|
287
|
+
const tag = /<\/?([A-Za-z][\w-]*)/.exec(line.slice(Math.max(0, at.span.start.column - 2)))?.[1];
|
|
288
|
+
return tag !== undefined && Object.hasOwn(renamed, tag) ? ` (\`${tag}\` is now \`${renamed[tag]}\`)` : "";
|
|
289
|
+
};
|
|
201
290
|
/** Compiles the application's templates against a target's vocabulary (the web's by default). Fails with every diagnostic, each pointing into the file it is about. */
|
|
202
291
|
export const compileViews = async (root, author, vocabulary = webVocabulary, views = BUILT_IN) => {
|
|
203
|
-
const { all, tree } = templatesOf(root, author, vocabulary, views);
|
|
292
|
+
const { all, tree, excluded } = templatesOf(root, author, vocabulary, views);
|
|
204
293
|
const used = reachable(all);
|
|
205
294
|
const model = modelOf(root, author, tree, vocabulary);
|
|
206
295
|
if (!used.some((template) => template.component === ROOT)) {
|
|
@@ -215,12 +304,42 @@ export const compileViews = async (root, author, vocabulary = webVocabulary, vie
|
|
|
215
304
|
const found = [
|
|
216
305
|
...(result.components ?? []).flatMap((entry) => entry.diagnostics.diagnostics
|
|
217
306
|
.filter((each) => each.severity === "error")
|
|
218
|
-
.map((each) =>
|
|
307
|
+
.map((each) => {
|
|
308
|
+
// a library component this target leaves out (K5): one sentence at the use, in place of the compiler's "unknown component"
|
|
309
|
+
const left = each.code === "unknown-component" ? excluded.get(/component "([^"]+)"/.exec(each.message)?.[1] ?? "") : undefined;
|
|
310
|
+
return left === undefined
|
|
311
|
+
? each
|
|
312
|
+
: {
|
|
313
|
+
...each,
|
|
314
|
+
code: "unsupported-target",
|
|
315
|
+
message: `<${each.message.match(/component "([^"]+)"/)[1]}> is ${left.id}, and ${left.library} supports ${left.supports.join(" and ")} only; this project builds for ${vocabulary.target}. Use a component that ${vocabulary.target} has, or build ${left.library} for ${vocabulary.target}`,
|
|
316
|
+
suggestions: [],
|
|
317
|
+
};
|
|
318
|
+
})
|
|
319
|
+
.map((each) => diagnostic(each.path, each.span.start.line, each.code, each.message +
|
|
320
|
+
(each.suggestions.length > 0 ? ` (did you mean ${each.suggestions.map((suggestion) => `"${suggestion.replacement}"`).join(" or ")}?)` : "") +
|
|
321
|
+
renamedHint(vocabulary, used, each), each.span.start.column))),
|
|
219
322
|
...(result.assembly?.diagnostics ?? []).map((each) => diagnostic(each.path ?? "program", each.span?.start.line ?? 0, each.code, each.message, each.span?.start.column ?? 0)),
|
|
220
323
|
];
|
|
221
324
|
throw new AppError(found.map((each) => relocate(each, root)));
|
|
222
325
|
}
|
|
223
326
|
const program = result.program;
|
|
327
|
+
const rules = "tags" in vocabulary ? vocabulary.rules : undefined;
|
|
328
|
+
if (rules !== undefined) {
|
|
329
|
+
const texts = program.templates;
|
|
330
|
+
const known = new Set(namesOf(vocabulary));
|
|
331
|
+
const broken = used.flatMap((template, index) => checkTemplate(template.path, template.source, JSON.parse(texts[index] ?? "{}"), rules, known, tree.find((node) => node.name === template.component)?.interface?.name));
|
|
332
|
+
const compiled = used.map((template, index) => ({
|
|
333
|
+
file: template.path,
|
|
334
|
+
source: template.source,
|
|
335
|
+
template: JSON.parse(texts[index] ?? "{}"),
|
|
336
|
+
builtIn: template.builtIn,
|
|
337
|
+
}));
|
|
338
|
+
broken.push(...checkReferences(compiled, anchorsOf(root)));
|
|
339
|
+
if (broken.length > 0) {
|
|
340
|
+
throw new AppError(broken.map((each) => relocate(each, root)));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
224
343
|
const enriched = program.model;
|
|
225
344
|
const manifestOfProgram = typeof enriched === "string" ? JSON.parse(enriched) : enriched;
|
|
226
345
|
return {
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Budget } from "./size.js";
|
|
1
2
|
import type { SiteInfo } from "@valancex/common/internal";
|
|
2
3
|
export declare const PROJECT_FILE = "valance.json";
|
|
3
4
|
export declare const WEB_FILE = "valance.web.json";
|
|
@@ -23,6 +24,17 @@ export interface Config {
|
|
|
23
24
|
readonly base: string;
|
|
24
25
|
readonly proxy: string;
|
|
25
26
|
};
|
|
27
|
+
/** The size budgets a production build is held to (Tier 2): the project's own, or none, in which case the framework's start-up budgets apply (W14). Never checked by `vlx dev`. */
|
|
28
|
+
readonly budgets: ReadonlyArray<Budget> | undefined;
|
|
29
|
+
/** What `vlx dev --record` leaves out of a session log (Tier 2; R9, R14): nothing, unless the project says. */
|
|
30
|
+
/** The theme package the project uses instead of the default theme (Tier 2; K12): an installed package whose `valance` field is `{ "kind": "theme" }`. Absent: the `theme/` directory, else the default. */
|
|
31
|
+
readonly theme: string | undefined;
|
|
32
|
+
readonly record: {
|
|
33
|
+
readonly redact: {
|
|
34
|
+
readonly scope: ReadonlyArray<ReadonlyArray<string>>;
|
|
35
|
+
readonly commands: ReadonlyArray<string>;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
26
38
|
}
|
|
27
39
|
/** Reads and validates the project's configuration. Every problem in both files is reported together. */
|
|
28
40
|
export declare const loadConfig: (root: string) => Config;
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// `schemas/` describe the same files for editors; this is the check the tools make.
|
|
4
4
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
+
import { budgetProblems } from "./size.js";
|
|
6
7
|
import { AppError, diagnostic, lineOf } from "./diagnostics.js";
|
|
7
8
|
import { DEFAULT_TITLE } from "@valancex/common/internal";
|
|
8
9
|
export const PROJECT_FILE = "valance.json";
|
|
@@ -65,6 +66,9 @@ const url = (value) => {
|
|
|
65
66
|
const title = (value) => typeof value === "string" && value.includes("{page}") && value.length <= 120 && !/[{}]/.test(value.replaceAll("{page}", "").replaceAll("{site}", ""))
|
|
66
67
|
? undefined
|
|
67
68
|
: 'must contain "{page}" and may use "{site}", for example "{page} · {site}" (at most 120 characters)';
|
|
69
|
+
const themePackage = (value) => typeof value === "string" && /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(value)
|
|
70
|
+
? undefined
|
|
71
|
+
: 'must be the name of an installed theme package, such as "acme-theme" or "@acme/theme"';
|
|
68
72
|
const flag = (value) => (typeof value === "boolean" ? undefined : "must be true or false");
|
|
69
73
|
const api = (value) => {
|
|
70
74
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
@@ -91,6 +95,32 @@ const api = (value) => {
|
|
|
91
95
|
}
|
|
92
96
|
return undefined;
|
|
93
97
|
};
|
|
98
|
+
const record = (value) => {
|
|
99
|
+
const redact = value?.redact;
|
|
100
|
+
if (typeof value !== "object" ||
|
|
101
|
+
value === null ||
|
|
102
|
+
Array.isArray(value) ||
|
|
103
|
+
Object.keys(value).some((key) => key !== "redact") ||
|
|
104
|
+
typeof redact !== "object" ||
|
|
105
|
+
redact === null ||
|
|
106
|
+
Array.isArray(redact)) {
|
|
107
|
+
return 'must be an object such as { "redact": { "scope": [["password"]], "commands": ["login/type"] } }';
|
|
108
|
+
}
|
|
109
|
+
const { scope, commands, ...others } = redact;
|
|
110
|
+
if (Object.keys(others).length > 0) {
|
|
111
|
+
return `"redact" has unknown settings ${Object.keys(others)
|
|
112
|
+
.map((key) => `"${key}"`)
|
|
113
|
+
.join(", ")} (known: scope, commands)`;
|
|
114
|
+
}
|
|
115
|
+
if (scope !== undefined &&
|
|
116
|
+
(!Array.isArray(scope) || scope.some((path) => !Array.isArray(path) || path.length === 0 || path.some((step) => typeof step !== "string" || step === "")))) {
|
|
117
|
+
return '"redact.scope" must be a list of paths, each a non-empty list of field names ("*" is any list index or field): [["password"], ["people", "*", "pin"]]';
|
|
118
|
+
}
|
|
119
|
+
if (commands !== undefined && (!Array.isArray(commands) || commands.some((name) => typeof name !== "string" || !/^[\w-]+\/[\w-]+$/.test(name)))) {
|
|
120
|
+
return '"redact.commands" must be a list of "component/name" commands whose parameters are not recorded: ["login/type"]';
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
};
|
|
94
124
|
/** Reads and validates the project's configuration. Every problem in both files is reported together. */
|
|
95
125
|
export const loadConfig = (root) => {
|
|
96
126
|
const project = read(root, PROJECT_FILE);
|
|
@@ -101,7 +131,7 @@ export const loadConfig = (root) => {
|
|
|
101
131
|
const desktop = read(root, DESKTOP_FILE);
|
|
102
132
|
const problems = [
|
|
103
133
|
...(desktop === undefined ? [] : check(DESKTOP_FILE, desktop.text, desktop.value, { $schema: schema, version }, [])),
|
|
104
|
-
...check(PROJECT_FILE, project.text, project.value, { $schema: schema, version, name: text, title }, ["name"]),
|
|
134
|
+
...check(PROJECT_FILE, project.text, project.value, { $schema: schema, version, name: text, title, record, budgets: budgetProblems, theme: themePackage }, ["name"]),
|
|
105
135
|
...(web === undefined ? [] : check(WEB_FILE, web.text, web.value, { $schema: schema, version, base, url, headers: flag, announce: flag, api }, [])),
|
|
106
136
|
];
|
|
107
137
|
if (problems.length > 0) {
|
|
@@ -115,6 +145,9 @@ export const loadConfig = (root) => {
|
|
|
115
145
|
title: named.title ?? DEFAULT_TITLE,
|
|
116
146
|
web: { base: given.base ?? "/", url: given.url ?? "", headers: given.headers ?? true, announce: given.announce ?? true },
|
|
117
147
|
api: { base: given.api?.base ?? "", proxy: given.api?.proxy ?? "" },
|
|
148
|
+
budgets: named.budgets,
|
|
149
|
+
theme: named.theme,
|
|
150
|
+
record: { redact: { scope: named.record?.redact.scope ?? [], commands: named.record?.redact.commands ?? [] } },
|
|
118
151
|
};
|
|
119
152
|
};
|
|
120
153
|
export const siteInfo = (config) => ({
|