@valancex/cli 0.12.0 → 0.14.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/LICENSE +21 -0
- package/README.md +69 -11
- package/dist/add.d.ts +9 -1
- package/dist/add.js +68 -5
- package/dist/archive.d.ts +17 -0
- package/dist/archive.js +111 -0
- package/dist/args.js +2 -2
- package/dist/budgets.json +175 -0
- package/dist/build.d.ts +11 -0
- package/dist/build.js +120 -30
- package/dist/check.js +2 -2
- package/dist/cli.js +131 -17
- package/dist/compile.d.ts +11 -3
- package/dist/compile.js +123 -25
- package/dist/config.d.ts +13 -0
- package/dist/config.js +34 -1
- package/dist/contract.d.ts +18 -1
- package/dist/contract.js +85 -3
- package/dist/dev-desktop.d.ts +8 -0
- package/dist/dev-desktop.js +114 -0
- package/dist/dev.d.ts +8 -0
- package/dist/dev.js +91 -29
- package/dist/emit.d.ts +9 -4
- package/dist/emit.js +129 -28
- package/dist/init.js +14 -4
- package/dist/package.d.ts +18 -0
- package/dist/package.js +142 -0
- package/dist/project.js +1 -1
- 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/targets.d.ts +6 -1
- package/dist/targets.js +26 -4
- package/dist/theme.d.ts +35 -0
- package/dist/theme.js +44 -0
- package/dist/tree.d.ts +14 -2
- package/dist/tree.js +16 -1
- package/dist/ui/init.js +4 -4
- package/dist/ui/report.js +1 -1
- package/dist/ui/run.d.ts +4 -1
- package/dist/ui/run.js +18 -3
- package/dist/vite.d.ts +3 -2
- package/dist/vite.js +3 -3
- package/package.json +15 -7
- package/schemas/valance.desktop.schema.json +17 -0
- package/schemas/valance.schema.json +71 -0
- package/templates/desktop/README.md +14 -0
- package/templates/desktop/content/404.md +7 -0
- package/templates/desktop/content/guide/getting-started.md +15 -0
- package/templates/desktop/content/index.md +14 -0
- package/templates/desktop/gitignore +7 -0
- package/templates/desktop/src/layout.mprx +23 -0
- package/templates/desktop/src/main.ts +15 -0
- package/templates/desktop/tsconfig.json +18 -0
- package/templates/multi/README.md +13 -0
- package/templates/multi/content/404.md +7 -0
- package/templates/multi/content/guide/getting-started.md +15 -0
- package/templates/multi/content/index.md +14 -0
- package/templates/multi/src/desktop/layout.mprx +21 -0
- package/templates/multi/src/main.ts +23 -0
- package/templates/multi/src/web/layout.mprx +27 -0
- package/templates/multi/src/web/styles.css +28 -0
- package/templates/multi/src/web/theme-toggle.mprx +1 -0
- package/templates/multi/src/web/theme-toggle.ts +10 -0
- package/templates/web/content/guide/getting-started.md +1 -1
- 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/views-desktop/block.mprx +3 -0
- package/views-desktop/blocks.mprx +3 -0
- package/views-desktop/inline.mprx +3 -0
- package/views-desktop/layout.mprx +20 -0
- package/views-desktop/list-block.mprx +3 -0
- package/views-desktop/list-items.mprx +3 -0
- package/views-desktop/runs.mprx +3 -0
- /package/views/{heading.mprx → heading-block.mprx} +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The three guards of a recording that holds user data (decision R16): the warning `vlx dev --record` gives when it starts, the flag a log must be given to be replayed or attached
|
|
2
|
+
// (`--allow-user-data`), and the places a project keeps recordings out of its repository. A recording holds what a person typed and saw; a recorder that cannot keep a secret out says so every time.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { relative } from "node:path";
|
|
5
|
+
/** True when `file` is in a git work tree of `root` and git does not ignore it. Nothing is said when `git` is not there or `root` is not a work tree. */
|
|
6
|
+
const trackedByMistake = (root, file) => {
|
|
7
|
+
const inside = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root, encoding: "utf8" });
|
|
8
|
+
if (inside.status !== 0) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
return spawnSync("git", ["check-ignore", "--quiet", file], { cwd: root }).status === 1;
|
|
12
|
+
};
|
|
13
|
+
/** The lines `vlx dev --record` prints before it records: where, what the log holds, what is redacted, and whether git would commit it. */
|
|
14
|
+
export const recordingNotice = (root, file, config) => {
|
|
15
|
+
const redact = config.record.redact;
|
|
16
|
+
const shown = relative(root, file) || file;
|
|
17
|
+
const redacting = redact.scope.length > 0 || redact.commands.length > 0;
|
|
18
|
+
return [
|
|
19
|
+
`vlx dev --record: WARNING. This session is being written to ${shown}.`,
|
|
20
|
+
" It holds what is typed and shown: names, passwords, anything on the page. It is for you, on this machine. Do not commit it, attach it to an issue or share it.",
|
|
21
|
+
redacting
|
|
22
|
+
? ` Redacted (valance.json "record"): ${[...redact.scope.map((path) => path.join(".")), ...redact.commands].join(", ")}. Everything else is written as it is.`
|
|
23
|
+
: ' Nothing is redacted. Mark what must not be written in valance.json: { "record": { "redact": { "scope": [["password"]], "commands": ["login/type"] } } } (docs/use/replay.md).',
|
|
24
|
+
...(trackedByMistake(root, file) ? [` git does NOT ignore ${shown}: add it to .gitignore before you commit anything.`] : []),
|
|
25
|
+
];
|
|
26
|
+
};
|
|
27
|
+
/** What `vlx replay` says when it is given a log that holds user data without being told it may read it. */
|
|
28
|
+
export const userDataRefusal = (file) => `vlx replay: ${file} holds user data (its header says containsUserData). Replaying it prints what was typed and shown when a route differs; run it again with --allow-user-data when you mean to read it, and do not attach it anywhere.`;
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReplaySetup } from "@valancex/valance/replay";
|
|
2
|
+
/** Where `vlx dev --record` writes when it is not told: `.valance/sessions/<time>.jsonl`. */
|
|
3
|
+
export declare const defaultRecording: (root: string, now?: Date) => string;
|
|
4
|
+
/** The project's web program, compiled as `vlx build` would, with the hashes a log's header is checked against. */
|
|
5
|
+
export declare const setupOf: (root: string) => Promise<ReplaySetup>;
|
|
6
|
+
export interface ReplayRun {
|
|
7
|
+
readonly code: number;
|
|
8
|
+
readonly output: string;
|
|
9
|
+
}
|
|
10
|
+
/** Replays the log at `file` in the project at `root`. 0 when every route gave what the log recorded, 1 when one did not, 2 for a log or a route that cannot be read. */
|
|
11
|
+
export declare const replayFile: (root: string, file: string, options?: {
|
|
12
|
+
readonly routes?: ReadonlyArray<string>;
|
|
13
|
+
readonly json?: boolean;
|
|
14
|
+
readonly allowUserData?: boolean;
|
|
15
|
+
}) => Promise<ReplayRun>;
|
package/dist/replay.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// `vlx replay <log>`: plays a session log (recorded by `vlx dev --record`, or one of MESH's conformance logs) against the project's own templates, by the three routes of the equivalence law,
|
|
2
|
+
// and says where the first differs from the log. It runs no command and reads no clock: a log is a bug report a machine can run. Development only.
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { holdsUserData, parseLog, programHash, replay, ROUTES, sha256 } from "@valancex/valance/replay";
|
|
6
|
+
import { userDataRefusal } from "./recording-guard.js";
|
|
7
|
+
import { loadAuthor } from "./author.js";
|
|
8
|
+
import { loadConfig } from "./config.js";
|
|
9
|
+
import { generateContent, generateProgram, programFile } from "./emit.js";
|
|
10
|
+
import { generated } from "./project.js";
|
|
11
|
+
import { createLoader } from "./vite.js";
|
|
12
|
+
/** Where `vlx dev --record` writes when it is not told: `.valance/sessions/<time>.jsonl`. */
|
|
13
|
+
export const defaultRecording = (root, now = new Date()) => join(root, ".valance", "sessions", `${now.toISOString().replace(/[:.]/g, "-")}.jsonl`);
|
|
14
|
+
/** The project's web program, compiled as `vlx build` would, with the hashes a log's header is checked against. */
|
|
15
|
+
export const setupOf = async (root) => {
|
|
16
|
+
const config = await loadConfig(root);
|
|
17
|
+
await generateContent(root, config);
|
|
18
|
+
const server = await createLoader(root, config);
|
|
19
|
+
try {
|
|
20
|
+
await generateProgram(root, await loadAuthor(server, root), config, ["web"]);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await server.close();
|
|
24
|
+
}
|
|
25
|
+
const program = JSON.parse(readFileSync(generated(root, programFile(config, "web")), "utf8"));
|
|
26
|
+
const model = typeof program.model === "string" ? program.model : JSON.stringify(program.model);
|
|
27
|
+
return { program: { root: program.root, templates: program.templates }, model, hashes: { program: await programHash(program.templates), manifest: await sha256(model) } };
|
|
28
|
+
};
|
|
29
|
+
const describe = (result) => result.routes.map((route) => route.ok
|
|
30
|
+
? ` ${route.route}: ${route.hashes.length} trees as recorded`
|
|
31
|
+
: ` ${route.route}: line ${route.divergence.line}, ${route.divergence.kind}: the log has ${JSON.stringify(route.divergence.expected)}, this gave ${JSON.stringify(route.divergence.actual)}`);
|
|
32
|
+
/** Replays the log at `file` in the project at `root`. 0 when every route gave what the log recorded, 1 when one did not, 2 for a log or a route that cannot be read. */
|
|
33
|
+
export const replayFile = async (root, file, options) => {
|
|
34
|
+
if (!existsSync(file)) {
|
|
35
|
+
return { code: 2, output: `vlx replay: ${file} is not a file` };
|
|
36
|
+
}
|
|
37
|
+
const routes = (options?.routes ?? ROUTES).filter((each) => ROUTES.includes(each));
|
|
38
|
+
if (routes.length === 0 || routes.length !== (options?.routes ?? ROUTES).length) {
|
|
39
|
+
return { code: 2, output: `vlx replay: --route is one or more of ${ROUTES.join(", ")}` };
|
|
40
|
+
}
|
|
41
|
+
let lines;
|
|
42
|
+
try {
|
|
43
|
+
lines = parseLog(readFileSync(file, "utf8"));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return { code: 2, output: `vlx replay: ${error instanceof Error ? error.message : String(error)}` };
|
|
47
|
+
}
|
|
48
|
+
if (holdsUserData(lines) && options?.allowUserData !== true) {
|
|
49
|
+
return { code: 2, output: userDataRefusal(file) };
|
|
50
|
+
}
|
|
51
|
+
const result = await replay(lines, await setupOf(root), { routes });
|
|
52
|
+
if (options?.json === true) {
|
|
53
|
+
return { code: result.ok && result.agree ? 0 : 1, output: JSON.stringify({ file, ...result }) };
|
|
54
|
+
}
|
|
55
|
+
const header = `vlx replay: ${file}: ${lines.length} lines, ${result.trees} trees`;
|
|
56
|
+
return result.ok && result.agree
|
|
57
|
+
? { code: 0, output: `${header}\n${describe(result).join("\n")}\nevery route gave the recorded trees${routes.length > 1 ? " and the routes agree" : ""}` }
|
|
58
|
+
: { code: 1, output: `${header}\n${describe(result).join("\n")}\n${result.ok ? "the routes gave different trees from one another" : "a route differs from the log"}` };
|
|
59
|
+
};
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Template } from "@valancex/mesh-compiler";
|
|
2
|
+
import type { WebVocabulary } from "@valancex/common/internal";
|
|
3
|
+
import type { Diagnostic } from "./diagnostics.js";
|
|
4
|
+
type Rules = NonNullable<WebVocabulary["rules"]>;
|
|
5
|
+
/**
|
|
6
|
+
* The diagnostics for one template, written in `file`. `known` are the primitives' tags. `forwarded` is the prop its component declares as its name (`component(template, { name })`): the name of a
|
|
7
|
+
* control in it then arrives from whoever uses the component, so a control with none of its own is not reported, and the template must read the prop.
|
|
8
|
+
*/
|
|
9
|
+
export declare const checkTemplate: (file: string, source: string, template: Template, rules: Rules, known: ReadonlySet<string>, forwarded?: string) => ReadonlyArray<Diagnostic>;
|
|
10
|
+
/** One template, as the reference check reads it: where it is written, its compiled form, and whether it is the package's own (a built-in view). */
|
|
11
|
+
export interface Written {
|
|
12
|
+
readonly file: string;
|
|
13
|
+
readonly source: string;
|
|
14
|
+
readonly template: Template;
|
|
15
|
+
readonly builtIn: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* `unknown-ref`: a `describedby` that is a literal names an `id` that nothing in the program has. The program is every template of the target, and the headings of the content (the built-in views
|
|
19
|
+
* give each its anchor as its `id`). An `id` that the author's own template computes could be any text, so the check says nothing then. It reads the program, not one template, because an id is
|
|
20
|
+
* global to the page: the layout may hold the control and a page the text it points at.
|
|
21
|
+
*/
|
|
22
|
+
export declare const checkReferences: (written: ReadonlyArray<Written>, anchors: ReadonlySet<string>) => ReadonlyArray<Diagnostic>;
|
|
23
|
+
export {};
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { diagnostic } from "./diagnostics.js";
|
|
2
|
+
/** The program's own constructs (`mesh-if`, `mesh-each`): they draw their children where they stand, so they are not part of the structure the rules judge. */
|
|
3
|
+
const transparent = (name) => name.startsWith("mesh-");
|
|
4
|
+
const placeOf = (source, offset) => {
|
|
5
|
+
const before = source.slice(0, offset).split("\n");
|
|
6
|
+
return { line: before.length, column: (before.at(-1)?.length ?? 0) + 1 };
|
|
7
|
+
};
|
|
8
|
+
/** Whether an expression reads the name `name` from the scope. */
|
|
9
|
+
const reads = (expression, name) => {
|
|
10
|
+
switch (expression.kind) {
|
|
11
|
+
case "scope":
|
|
12
|
+
return expression.name === name;
|
|
13
|
+
case "member":
|
|
14
|
+
return reads(expression.object, name);
|
|
15
|
+
case "unary":
|
|
16
|
+
case "has":
|
|
17
|
+
return reads(expression.operand, name);
|
|
18
|
+
case "binary":
|
|
19
|
+
return reads(expression.left, name) || reads(expression.right, name);
|
|
20
|
+
case "conditional":
|
|
21
|
+
return reads(expression.condition, name) || reads(expression.consequent, name) || reads(expression.alternate, name);
|
|
22
|
+
case "list":
|
|
23
|
+
return expression.elements.some((each) => reads(each, name));
|
|
24
|
+
case "record":
|
|
25
|
+
return expression.fields.some((each) => reads(each.value, name));
|
|
26
|
+
default:
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const elementReads = (element, name) => element.props.some((each) => reads(each.value, name)) ||
|
|
31
|
+
element.events.some((each) => each.arguments.some((argument) => reads(argument, name))) ||
|
|
32
|
+
element.children.some((child) => (child.kind === "expression" ? reads(child.expression, name) : child.kind === "element" && elementReads(child.element, name)));
|
|
33
|
+
/**
|
|
34
|
+
* The diagnostics for one template, written in `file`. `known` are the primitives' tags. `forwarded` is the prop its component declares as its name (`component(template, { name })`): the name of a
|
|
35
|
+
* control in it then arrives from whoever uses the component, so a control with none of its own is not reported, and the template must read the prop.
|
|
36
|
+
*/
|
|
37
|
+
export const checkTemplate = (file, source, template, rules, known, forwarded) => {
|
|
38
|
+
const found = [];
|
|
39
|
+
const report = (element, code, message) => {
|
|
40
|
+
const at = placeOf(source, element.span.start.utf16);
|
|
41
|
+
found.push(diagnostic(file, at.line, code, message, at.column));
|
|
42
|
+
};
|
|
43
|
+
const visit = (element, ancestors) => {
|
|
44
|
+
const tag = element.component;
|
|
45
|
+
const structural = ancestors.filter((each) => !transparent(each));
|
|
46
|
+
if (forwarded === undefined && rules.named.includes(tag) && !element.props.some((each) => each.prop === "label") && !structural.includes("label")) {
|
|
47
|
+
report(element, "unnamed-control", `<${tag}> has no accessible name: give it a \`label\`, or put it inside a <label> that says what it is for. In a component that takes its name from whoever uses it, declare the prop as the name (\`component(template, { props: { label: Schema.String }, name: "label" })\`) and write it here`);
|
|
48
|
+
}
|
|
49
|
+
// one modal layer at a time (decision I29)
|
|
50
|
+
const outer = rules.modal?.includes(tag) === true ? [...structural].reverse().find((each) => rules.modal?.includes(each) === true) : undefined;
|
|
51
|
+
if (outer !== undefined) {
|
|
52
|
+
report(element, "nested-modal", `<${tag}> is a modal layer inside <${outer}>, another modal layer: a dialog inside a dialog is not supported, so open one at a time or put them side by side`);
|
|
53
|
+
}
|
|
54
|
+
const parent = structural.at(-1);
|
|
55
|
+
const containers = rules.within[tag];
|
|
56
|
+
if (containers !== undefined && parent !== undefined && known.has(parent) && !containers.includes(parent) && !(parent === "label")) {
|
|
57
|
+
report(element, "outside-container", `<${tag}> belongs inside ${containers.map((each) => `<${each}>`).join(" or ")}, not <${parent}>`);
|
|
58
|
+
}
|
|
59
|
+
const only = rules.only[tag];
|
|
60
|
+
for (const child of element.children) {
|
|
61
|
+
if (only !== undefined) {
|
|
62
|
+
reportChild(element, child, only);
|
|
63
|
+
}
|
|
64
|
+
if (child.kind === "element") {
|
|
65
|
+
visit(child.element, [...ancestors, tag]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const reportChild = (parent, child, only) => {
|
|
70
|
+
if (child.kind === "text" && child.value.trim() !== "") {
|
|
71
|
+
report(parent, "list-child", `<${parent.component}> holds only ${only.map((each) => `<${each}>`).join(" or ")}; it has the text "${child.value.trim().slice(0, 20)}"`);
|
|
72
|
+
}
|
|
73
|
+
else if (child.kind === "element" && known.has(child.element.component) && !transparent(child.element.component) && !only.includes(child.element.component)) {
|
|
74
|
+
report(child.element, "list-child", `<${parent.component}> holds only ${only.map((each) => `<${each}>`).join(" or ")}, not <${child.element.component}>`);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
visit(template.root, []);
|
|
78
|
+
if (forwarded !== undefined && !elementReads(template.root, forwarded)) {
|
|
79
|
+
report(template.root, "name-unused", `the component declares \`${forwarded}\` as its name, but its template never reads \`${forwarded}\`: the name of whoever uses it would go nowhere`);
|
|
80
|
+
}
|
|
81
|
+
return found;
|
|
82
|
+
};
|
|
83
|
+
const literal = (expression) => (expression.kind === "literal" && typeof expression.value === "string" ? expression.value : undefined);
|
|
84
|
+
const elementsOf = (element) => [
|
|
85
|
+
element,
|
|
86
|
+
...element.children.flatMap((child) => (child.kind === "element" ? elementsOf(child.element) : [])),
|
|
87
|
+
];
|
|
88
|
+
/**
|
|
89
|
+
* `unknown-ref`: a `describedby` that is a literal names an `id` that nothing in the program has. The program is every template of the target, and the headings of the content (the built-in views
|
|
90
|
+
* give each its anchor as its `id`). An `id` that the author's own template computes could be any text, so the check says nothing then. It reads the program, not one template, because an id is
|
|
91
|
+
* global to the page: the layout may hold the control and a page the text it points at.
|
|
92
|
+
*/
|
|
93
|
+
export const checkReferences = (written, anchors) => {
|
|
94
|
+
const ids = new Set(anchors);
|
|
95
|
+
let computed = false;
|
|
96
|
+
const references = [];
|
|
97
|
+
for (const { file, source, template, builtIn } of written) {
|
|
98
|
+
for (const element of elementsOf(template.root)) {
|
|
99
|
+
for (const prop of element.props) {
|
|
100
|
+
const value = literal(prop.value);
|
|
101
|
+
if (prop.prop === "id") {
|
|
102
|
+
if (value !== undefined) {
|
|
103
|
+
ids.add(value);
|
|
104
|
+
}
|
|
105
|
+
else if (!builtIn) {
|
|
106
|
+
computed = true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (prop.prop === "describedby" && value !== undefined) {
|
|
110
|
+
references.push({ file, source, element, id: value });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return computed
|
|
116
|
+
? []
|
|
117
|
+
: references
|
|
118
|
+
.filter((each) => !ids.has(each.id))
|
|
119
|
+
.map((each) => {
|
|
120
|
+
const at = placeOf(each.source, each.element.span.start.utf16);
|
|
121
|
+
return diagnostic(each.file, at.line, "unknown-ref", `<${each.element.component}> is described by "${each.id}", and no element in the program has that id`, at.column);
|
|
122
|
+
});
|
|
123
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface Copy {
|
|
2
|
+
/** The copy's directory, resolved through links. */
|
|
3
|
+
readonly dir: string;
|
|
4
|
+
readonly version: string;
|
|
5
|
+
/** The packages that resolve to it. */
|
|
6
|
+
readonly usedBy: ReadonlyArray<string>;
|
|
7
|
+
}
|
|
8
|
+
/** The copies of the runtime that the project's packages resolve, each with the packages that use it. */
|
|
9
|
+
export declare const runtimeCopies: (root: string) => ReadonlyArray<Copy>;
|
|
10
|
+
/** Throws `runtime-duplicate`, naming every copy and who uses it, when the build would bundle more than one. */
|
|
11
|
+
export declare const assertOneRuntime: (root: string) => void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// A production build bundles the MESH runtime's WebAssembly module once for each copy of `@valancex/mesh-runtime` the packages resolve, and the browser fetches every one (decision W20: a plain
|
|
2
|
+
// install of the published set resolved two, 0.12.0 through `@valancex/valance` and 0.11.0 through `@valancex/common`, and shipped 171 KB of brotli twice). `vlx build` therefore refuses to build
|
|
3
|
+
// when more than one copy is in play, naming each. `vlx dev` does not look: development says nothing about size (decision W9).
|
|
4
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
5
|
+
import { dirname, join, relative } from "node:path";
|
|
6
|
+
import { AppError } from "./diagnostics.js";
|
|
7
|
+
const RUNTIME = "@valancex/mesh-runtime";
|
|
8
|
+
/** The packages whose code imports the runtime, or is imported by one that does. */
|
|
9
|
+
const IMPORTERS = ["@valancex/common", "@valancex/valance", "@valancex/nexus", "@valancex/port-web", "@valancex/router"];
|
|
10
|
+
/** The directory of `name` as Node would find it from `from`: the nearest `node_modules/<name>` going up, resolved through links. */
|
|
11
|
+
const find = (from, name) => {
|
|
12
|
+
for (let dir = from;; dir = dirname(dir)) {
|
|
13
|
+
const candidate = join(dir, "node_modules", name);
|
|
14
|
+
if (existsSync(join(candidate, "package.json"))) {
|
|
15
|
+
return realpathSync(candidate);
|
|
16
|
+
}
|
|
17
|
+
if (dirname(dir) === dir) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/** The copies of the runtime that the project's packages resolve, each with the packages that use it. */
|
|
23
|
+
export const runtimeCopies = (root) => {
|
|
24
|
+
const copies = new Map();
|
|
25
|
+
for (const name of IMPORTERS) {
|
|
26
|
+
// From the project first; a package the project does not list directly is found through one it does (pnpm links only the direct ones).
|
|
27
|
+
const importer = find(root, name) ??
|
|
28
|
+
IMPORTERS.map((other) => find(root, other))
|
|
29
|
+
.flatMap((dir) => (dir === undefined ? [] : [find(dir, name)]))
|
|
30
|
+
.find((dir) => dir !== undefined);
|
|
31
|
+
const dir = importer === undefined ? undefined : find(importer, RUNTIME);
|
|
32
|
+
if (dir === undefined) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const entry = copies.get(dir) ?? { version: JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version, usedBy: [] };
|
|
36
|
+
entry.usedBy.push(name);
|
|
37
|
+
copies.set(dir, entry);
|
|
38
|
+
}
|
|
39
|
+
return [...copies].map(([dir, { version, usedBy }]) => ({ dir, version, usedBy }));
|
|
40
|
+
};
|
|
41
|
+
/** Throws `runtime-duplicate`, naming every copy and who uses it, when the build would bundle more than one. */
|
|
42
|
+
export const assertOneRuntime = (root) => {
|
|
43
|
+
const copies = runtimeCopies(root);
|
|
44
|
+
if (copies.length < 2) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const newest = copies
|
|
48
|
+
.map((copy) => copy.version)
|
|
49
|
+
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
50
|
+
.at(-1);
|
|
51
|
+
const listed = copies.map((copy) => `${copy.version} at ${relative(root, copy.dir) || "."} (used by ${copy.usedBy.join(", ")})`).join("; ");
|
|
52
|
+
throw new AppError([
|
|
53
|
+
{
|
|
54
|
+
file: "package.json",
|
|
55
|
+
line: 0,
|
|
56
|
+
column: 0,
|
|
57
|
+
code: "runtime-duplicate",
|
|
58
|
+
message: `${copies.length} copies of ${RUNTIME} would be built into the site, and the browser would fetch the WebAssembly runtime ${copies.length} times: ${listed}. ` +
|
|
59
|
+
`Make them one version: add "overrides": { "${RUNTIME}": "${newest}" } to package.json (npm; pnpm.overrides with pnpm, resolutions with yarn) and install again`,
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
};
|
package/dist/security.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Config } from "./config.js";
|
|
2
|
+
/** The CSP source that allows the early-input shell (the same bytes in every page) and nothing else inline: a hash, not `'unsafe-inline'`. */
|
|
3
|
+
export declare const earlyShellSource: () => string;
|
|
2
4
|
export declare const contentSecurityPolicy: (config: Config) => string;
|
|
3
5
|
/** The header lines, for every path under the site's base. */
|
|
4
6
|
export declare const headersFile: (config: Config) => string;
|
package/dist/security.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// The security headers a built site should be served with, written beside it as `_headers` (the format Netlify and Cloudflare Pages read; other hosts take the same lines). The VALANCE
|
|
2
|
+
// browser runtime is WebAssembly, so a strict Content-Security-Policy needs `'wasm-unsafe-eval'` in `script-src` and nothing more permissive: no `'unsafe-inline'`, no `'unsafe-eval'`. Without it
|
|
3
|
+
// the page's server HTML stays and the application never starts (measured in Chromium). Everything the build writes is a same-origin file, so every other source is `'self'`.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { earlyShell } from "@valancex/common/internal";
|
|
6
|
+
/** The CSP source that allows the early-input shell (the same bytes in every page) and nothing else inline: a hash, not `'unsafe-inline'`. */
|
|
7
|
+
export const earlyShellSource = () => `'sha256-${createHash("sha256").update(earlyShell).digest("base64")}'`;
|
|
1
8
|
/** The origin of the backend when `api.base` is an absolute address (the page may connect to it), else nothing: a path is the page's own origin. */
|
|
2
9
|
const apiOrigin = (config) => {
|
|
3
10
|
try {
|
|
@@ -11,7 +18,7 @@ export const contentSecurityPolicy = (config) => {
|
|
|
11
18
|
const origin = apiOrigin(config);
|
|
12
19
|
return [
|
|
13
20
|
"default-src 'self'",
|
|
14
|
-
|
|
21
|
+
`script-src 'self' 'wasm-unsafe-eval' ${earlyShellSource()}`,
|
|
15
22
|
"style-src 'self'",
|
|
16
23
|
"img-src 'self' data:",
|
|
17
24
|
`connect-src 'self'${origin === undefined ? "" : ` ${origin}`}`,
|
package/dist/size.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AppError } from "./diagnostics.js";
|
|
2
|
+
export type FileKind = "javascript" | "css" | "wasm" | "other";
|
|
3
|
+
export interface SizedFile {
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly kind: FileKind;
|
|
6
|
+
readonly raw: number;
|
|
7
|
+
readonly brotli: number;
|
|
8
|
+
}
|
|
9
|
+
export interface SetReport {
|
|
10
|
+
readonly files: ReadonlyArray<SizedFile>;
|
|
11
|
+
readonly brotli: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SizeReport {
|
|
14
|
+
readonly startup: SetReport;
|
|
15
|
+
readonly afterStart: SetReport;
|
|
16
|
+
}
|
|
17
|
+
/** What the Vite manifest says about one output file. */
|
|
18
|
+
export interface ManifestEntry {
|
|
19
|
+
readonly file: string;
|
|
20
|
+
readonly isEntry?: boolean;
|
|
21
|
+
readonly imports?: ReadonlyArray<string>;
|
|
22
|
+
readonly dynamicImports?: ReadonlyArray<string>;
|
|
23
|
+
readonly css?: ReadonlyArray<string>;
|
|
24
|
+
readonly assets?: ReadonlyArray<string>;
|
|
25
|
+
}
|
|
26
|
+
/** Splits the build in `dist` into the start-up and after-start sets, from the bundler's manifest. The WebAssembly runtime is in the start-up set whenever the entry names it. */
|
|
27
|
+
export declare const measure: (dist: string, manifest: Readonly<Record<string, ManifestEntry>>, wasm?: string) => SizeReport;
|
|
28
|
+
/** What a budget measures. */
|
|
29
|
+
export type BudgetType = "initial" | "runtime" | "startup" | "afterStart" | "anyChunk";
|
|
30
|
+
/** A budget in the shape Angular's are: a type and a warning and an error threshold, each in bytes (`500000`, `"500kB"`, `"1MB"`; 1 kB is 1,000 bytes). */
|
|
31
|
+
export interface Budget {
|
|
32
|
+
readonly type: BudgetType;
|
|
33
|
+
readonly warning?: string | number;
|
|
34
|
+
readonly error?: string | number;
|
|
35
|
+
}
|
|
36
|
+
export interface BudgetResult {
|
|
37
|
+
readonly type: BudgetType;
|
|
38
|
+
readonly measured: number;
|
|
39
|
+
readonly warning?: number;
|
|
40
|
+
readonly error?: number;
|
|
41
|
+
readonly status: "ok" | "warning" | "error";
|
|
42
|
+
}
|
|
43
|
+
export declare const BUDGET_TYPES: ReadonlyArray<BudgetType>;
|
|
44
|
+
export declare const DEFAULT_BUDGETS: ReadonlyArray<Budget>;
|
|
45
|
+
/** `"500kB"` or `500000` as bytes, or `undefined` if it is neither. */
|
|
46
|
+
export declare const bytesOf: (value: string | number) => number | undefined;
|
|
47
|
+
/** Holds `report` to `budgets` (a project's, else the framework's). A threshold that is not a size is a mistake in the settings, said with the file. */
|
|
48
|
+
export declare const check: (report: SizeReport, budgets: ReadonlyArray<Budget>) => ReadonlyArray<BudgetResult>;
|
|
49
|
+
/** The settings problems of a project's `budgets`: a type that is not one, a threshold that is not a size, a warning above its error. */
|
|
50
|
+
export declare const budgetProblems: (value: unknown) => string | undefined;
|
|
51
|
+
/** The size report a production build prints: both sets with each file, then each budget. */
|
|
52
|
+
export declare const lines: (report: SizeReport, results: ReadonlyArray<BudgetResult>) => ReadonlyArray<string>;
|
|
53
|
+
/** The failure of a build whose measured size is over an error threshold. */
|
|
54
|
+
export declare const failure: (results: ReadonlyArray<BudgetResult>) => AppError | undefined;
|
package/dist/size.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// What `vlx build` ships, measured, and the budgets it is held to (Bet 3; decisions W9, W10, W14). A production build prints this; `vlx dev` never does (W9: development is quiet and fast).
|
|
2
|
+
//
|
|
3
|
+
// A build's files fall in two sets (W11, W12): the **start-up set** is what the page cannot start without (the entry chunk, the chunks it statically imports, their styles, and the WebAssembly runtime),
|
|
4
|
+
// and the **after-start set** is every chunk only a dynamic `import()` reaches (loaded once the application has started, at low priority). The server's HTML and the styles the document names paint
|
|
5
|
+
// first and are in the start-up set's styles. Sizes are brotli bytes, which is what travels (W2); the raw size is shown beside them.
|
|
6
|
+
import { brotliCompressSync } from "node:zlib";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { AppError, diagnostic } from "./diagnostics.js";
|
|
10
|
+
const kindOf = (file) => file.endsWith(".js") || file.endsWith(".mjs") ? "javascript" : file.endsWith(".css") ? "css" : file.endsWith(".wasm") ? "wasm" : "other";
|
|
11
|
+
const sized = (dist, file) => {
|
|
12
|
+
const bytes = readFileSync(join(dist, file));
|
|
13
|
+
return { file, kind: kindOf(file), raw: bytes.length, brotli: brotliCompressSync(bytes).length };
|
|
14
|
+
};
|
|
15
|
+
/** The files reachable from `starts` by the manifest's static `imports` (and their styles), or by `dynamicImports` only. */
|
|
16
|
+
const reach = (manifest, starts) => {
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
const files = new Set();
|
|
19
|
+
const visit = (key) => {
|
|
20
|
+
const entry = manifest[key];
|
|
21
|
+
if (entry === undefined || seen.has(key)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
seen.add(key);
|
|
25
|
+
files.add(entry.file);
|
|
26
|
+
entry.css?.forEach((file) => files.add(file));
|
|
27
|
+
entry.assets?.forEach((file) => files.add(file));
|
|
28
|
+
entry.imports?.forEach(visit);
|
|
29
|
+
};
|
|
30
|
+
starts.forEach(visit);
|
|
31
|
+
return [...files];
|
|
32
|
+
};
|
|
33
|
+
/** Splits the build in `dist` into the start-up and after-start sets, from the bundler's manifest. The WebAssembly runtime is in the start-up set whenever the entry names it. */
|
|
34
|
+
export const measure = (dist, manifest, wasm) => {
|
|
35
|
+
const entries = Object.entries(manifest).filter(([, entry]) => entry.isEntry === true);
|
|
36
|
+
const startup = new Set(reach(manifest, entries.map(([key]) => key)));
|
|
37
|
+
if (wasm !== undefined) {
|
|
38
|
+
startup.add(wasm);
|
|
39
|
+
}
|
|
40
|
+
const dynamic = new Set();
|
|
41
|
+
for (const [, entry] of Object.entries(manifest)) {
|
|
42
|
+
if (startup.has(entry.file) || entry.isEntry === true) {
|
|
43
|
+
entry.dynamicImports?.forEach((key) => reach(manifest, [key]).forEach((file) => dynamic.add(file)));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const file of startup) {
|
|
47
|
+
dynamic.delete(file);
|
|
48
|
+
}
|
|
49
|
+
const set = (files) => {
|
|
50
|
+
const list = [...files].sort().map((file) => sized(dist, file));
|
|
51
|
+
return { files: list, brotli: list.reduce((sum, each) => sum + each.brotli, 0) };
|
|
52
|
+
};
|
|
53
|
+
return { startup: set(startup), afterStart: set(dynamic) };
|
|
54
|
+
};
|
|
55
|
+
export const BUDGET_TYPES = ["initial", "runtime", "startup", "afterStart", "anyChunk"];
|
|
56
|
+
/** The framework's own start-up budgets (decision W14): what a project gets when it sets none. They are data (`budgets.json`, beside this file, copied next to the compiled code by `scripts/copy-data.mjs`), so a gate can read the same numbers. */
|
|
57
|
+
const defaults = JSON.parse(readFileSync(new URL("./budgets.json", import.meta.url), "utf8"));
|
|
58
|
+
export const DEFAULT_BUDGETS = defaults.project.budgets;
|
|
59
|
+
/** `"500kB"` or `500000` as bytes, or `undefined` if it is neither. */
|
|
60
|
+
export const bytesOf = (value) => {
|
|
61
|
+
if (typeof value === "number") {
|
|
62
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
63
|
+
}
|
|
64
|
+
const match = /^(\d+(?:\.\d+)?)\s*(B|kB|KB|MB)?$/.exec(value.trim());
|
|
65
|
+
return match === null ? undefined : Math.round(Number(match[1]) * ({ B: 1, kB: 1000, KB: 1000, MB: 1_000_000 }[match[2] ?? "B"] ?? 1));
|
|
66
|
+
};
|
|
67
|
+
const measuredFor = (type, report) => {
|
|
68
|
+
const startup = report.startup.files;
|
|
69
|
+
const sum = (files, kinds) => files.filter((each) => kinds.includes(each.kind)).reduce((total, each) => total + each.brotli, 0);
|
|
70
|
+
switch (type) {
|
|
71
|
+
case "initial":
|
|
72
|
+
return sum(startup, ["javascript", "css"]);
|
|
73
|
+
case "runtime":
|
|
74
|
+
return sum(startup, ["wasm"]);
|
|
75
|
+
case "startup":
|
|
76
|
+
return report.startup.brotli;
|
|
77
|
+
case "afterStart":
|
|
78
|
+
return report.afterStart.brotli;
|
|
79
|
+
case "anyChunk":
|
|
80
|
+
return Math.max(0, ...[...startup, ...report.afterStart.files].filter((each) => each.kind === "javascript").map((each) => each.brotli));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/** Holds `report` to `budgets` (a project's, else the framework's). A threshold that is not a size is a mistake in the settings, said with the file. */
|
|
84
|
+
export const check = (report, budgets) => budgets.map((budget) => {
|
|
85
|
+
const warning = budget.warning === undefined ? undefined : bytesOf(budget.warning);
|
|
86
|
+
const error = budget.error === undefined ? undefined : bytesOf(budget.error);
|
|
87
|
+
const measured = measuredFor(budget.type, report);
|
|
88
|
+
return {
|
|
89
|
+
type: budget.type,
|
|
90
|
+
measured,
|
|
91
|
+
...(warning === undefined ? {} : { warning }),
|
|
92
|
+
...(error === undefined ? {} : { error }),
|
|
93
|
+
status: error !== undefined && measured > error ? "error" : warning !== undefined && measured > warning ? "warning" : "ok",
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
/** The settings problems of a project's `budgets`: a type that is not one, a threshold that is not a size, a warning above its error. */
|
|
97
|
+
export const budgetProblems = (value) => {
|
|
98
|
+
if (!Array.isArray(value)) {
|
|
99
|
+
return 'must be a list such as [{ "type": "initial", "warning": "200kB", "error": "400kB" }]';
|
|
100
|
+
}
|
|
101
|
+
for (const [index, each] of value.entries()) {
|
|
102
|
+
const budget = each;
|
|
103
|
+
if (typeof budget !== "object" || budget === null || !BUDGET_TYPES.includes(budget.type)) {
|
|
104
|
+
return `[${index}] needs a "type" of ${BUDGET_TYPES.join(", ")}`;
|
|
105
|
+
}
|
|
106
|
+
const extra = Object.keys(budget).filter((key) => !["type", "warning", "error"].includes(key));
|
|
107
|
+
if (extra.length > 0) {
|
|
108
|
+
return `[${index}] has unknown settings ${extra.map((key) => `"${key}"`).join(", ")} (known: type, warning, error)`;
|
|
109
|
+
}
|
|
110
|
+
for (const key of ["warning", "error"]) {
|
|
111
|
+
const given = budget[key];
|
|
112
|
+
if (given !== undefined && (typeof given !== "string" && typeof given !== "number" ? true : bytesOf(given) === undefined)) {
|
|
113
|
+
return `[${index}] "${key}" must be a size such as 500000, "500kB" or "1MB"`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const warning = budget.warning === undefined ? undefined : bytesOf(budget.warning);
|
|
117
|
+
const error = budget.error === undefined ? undefined : bytesOf(budget.error);
|
|
118
|
+
if (warning !== undefined && error !== undefined && warning > error) {
|
|
119
|
+
return `[${index}] the warning (${warning}) is above the error (${error})`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
};
|
|
124
|
+
const label = {
|
|
125
|
+
initial: "start-up JavaScript and CSS",
|
|
126
|
+
runtime: "WebAssembly runtime",
|
|
127
|
+
startup: "start-up set",
|
|
128
|
+
afterStart: "after-start set",
|
|
129
|
+
anyChunk: "largest JavaScript chunk",
|
|
130
|
+
};
|
|
131
|
+
const kB = (bytes) => `${(bytes / 1000).toFixed(1)} kB`;
|
|
132
|
+
/** The size report a production build prints: both sets with each file, then each budget. */
|
|
133
|
+
export const lines = (report, results) => {
|
|
134
|
+
const set = (name, each) => [
|
|
135
|
+
` ${name}: ${kB(each.brotli)} brotli`,
|
|
136
|
+
...each.files.map((file) => ` ${file.file} ${kB(file.raw)} raw, ${kB(file.brotli)} brotli`),
|
|
137
|
+
];
|
|
138
|
+
return [
|
|
139
|
+
"Size report (brotli, what travels):",
|
|
140
|
+
...set("start-up (what the page cannot start without)", report.startup),
|
|
141
|
+
...(report.afterStart.files.length === 0
|
|
142
|
+
? [" after-start: nothing (no chunk is loaded after the application has started)"]
|
|
143
|
+
: set("after-start (loaded once the application is running)", report.afterStart)),
|
|
144
|
+
...results.map((each) => ` ${each.status === "ok" ? "ok " : each.status === "warning" ? "WARNING" : "ERROR "} ${label[each.type]}: ${kB(each.measured)}${each.error === undefined ? "" : ` (error above ${kB(each.error)})`}${each.warning === undefined ? "" : ` (warning above ${kB(each.warning)})`}`),
|
|
145
|
+
];
|
|
146
|
+
};
|
|
147
|
+
/** The failure of a build whose measured size is over an error threshold. */
|
|
148
|
+
export const failure = (results) => {
|
|
149
|
+
const over = results.filter((each) => each.status === "error");
|
|
150
|
+
return over.length === 0
|
|
151
|
+
? undefined
|
|
152
|
+
: new AppError(over.map((each) => diagnostic("valance.json", 0, "budget-exceeded", `the ${label[each.type]} is ${kB(each.measured)}, over the error budget of ${kB(each.error)}. Make it smaller, or raise the "budgets" in valance.json for this project.`)));
|
|
153
|
+
};
|
package/dist/targets.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { Vocabulary } from "@valancex/common/internal";
|
|
2
2
|
export interface Target {
|
|
3
3
|
/** The flag (`--web`) and the name in `valance.<id>.json`. */
|
|
4
|
-
readonly id: "web";
|
|
4
|
+
readonly id: "web" | "desktop";
|
|
5
5
|
readonly label: string;
|
|
6
6
|
readonly hint: string;
|
|
7
7
|
/** The tags its templates may write. A project builds against its target's vocabulary. */
|
|
8
8
|
readonly vocabulary: Vocabulary;
|
|
9
|
+
/** Where the framework's own templates for this target are (the layout and the views that draw a page's blocks): a project's own `layout` and `override`s replace them by name. */
|
|
10
|
+
readonly views: string;
|
|
9
11
|
}
|
|
10
12
|
export declare const TARGETS: ReadonlyArray<Target>;
|
|
13
|
+
export declare const targetOf: (id: TargetId) => Target;
|
|
11
14
|
export type TargetId = Target["id"];
|
|
12
15
|
export declare const isTarget: (name: string) => name is TargetId;
|
|
16
|
+
/** What `--yes` makes: the web alone is the one that needs nothing else installed. */
|
|
17
|
+
export declare const DEFAULT_TARGETS: ReadonlyArray<TargetId>;
|
package/dist/targets.js
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
|
-
// The targets an application can be developed for
|
|
2
|
-
// here (and its template under `templates
|
|
3
|
-
import { webVocabulary } from "@valancex/common/internal";
|
|
1
|
+
// The targets an application can be developed for: the web, and a desktop window. The wizard, `vlx init` and the flags are written against this list, so a target that arrives later is one more
|
|
2
|
+
// entry here (and its template under `templates/`, its built-in views, its vocabulary), not a change to them.
|
|
3
|
+
import { fromManifests, webVocabulary } from "@valancex/common/internal";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { BUILT_IN, BUILT_IN_DESKTOP } from "./compile.js";
|
|
7
|
+
/**
|
|
8
|
+
* The Desktop PORT's vocabulary: its components as data (`vocabulary.json`: columns, rows, text, a button, a checkbox, a text field; `content.json`: what Markdown produces), read from the
|
|
9
|
+
* PORT's own package (`@valancex/port-desktop-vocabulary`), not from a copy kept here.
|
|
10
|
+
*/
|
|
11
|
+
const desktopVocabulary = () => {
|
|
12
|
+
const need = createRequire(import.meta.url);
|
|
13
|
+
const read = (name) => JSON.parse(readFileSync(need.resolve(`@valancex/port-desktop-vocabulary/${name}`), "utf8"));
|
|
14
|
+
return fromManifests("desktop", read("vocabulary.json"), read("content.json"));
|
|
15
|
+
};
|
|
4
16
|
export const TARGETS = [
|
|
5
|
-
{ id: "web", label: "Web", hint: "a website: Markdown pages and templates, built to a static site (settings in valance.web.json)", vocabulary: webVocabulary },
|
|
17
|
+
{ id: "web", label: "Web", hint: "a website: Markdown pages and templates, built to a static site (settings in valance.web.json)", vocabulary: webVocabulary, views: BUILT_IN },
|
|
18
|
+
{
|
|
19
|
+
id: "desktop",
|
|
20
|
+
label: "Desktop",
|
|
21
|
+
hint: "a native window on Linux and Windows: Markdown pages and templates, drawn by the Desktop PORT (settings in valance.desktop.json)",
|
|
22
|
+
vocabulary: desktopVocabulary(),
|
|
23
|
+
views: BUILT_IN_DESKTOP,
|
|
24
|
+
},
|
|
6
25
|
];
|
|
26
|
+
export const targetOf = (id) => TARGETS.find((each) => each.id === id);
|
|
7
27
|
export const isTarget = (name) => TARGETS.some((each) => each.id === name);
|
|
28
|
+
/** What `--yes` makes: the web alone is the one that needs nothing else installed. */
|
|
29
|
+
export const DEFAULT_TARGETS = ["web"];
|