@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
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Diagnostic } from "./diagnostics.js";
|
|
2
|
+
import type { PackageManager } from "./init.js";
|
|
3
|
+
export interface LibraryAnswers {
|
|
4
|
+
/** The library's package name (`acme-ui`, `@acme/ui`). */
|
|
5
|
+
readonly name: string;
|
|
6
|
+
/** Where the workspace is written: absolute. */
|
|
7
|
+
readonly dir: string;
|
|
8
|
+
readonly packageManager: PackageManager;
|
|
9
|
+
readonly cli?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Every file \`vlx init library\` writes, without writing any: relative path (\`/\` separated) → text. */
|
|
12
|
+
export declare const planLibrary: (answers: LibraryAnswers) => ReadonlyMap<string, string>;
|
|
13
|
+
/** Writes the workspace. Throws if the directory has something in it. Returns the files written. */
|
|
14
|
+
export declare const scaffoldLibrary: (answers: LibraryAnswers) => ReadonlyArray<string>;
|
|
15
|
+
/**
|
|
16
|
+
* The rules a consumer's build relies on, held at the library where its author can act (K7, K8, K11, K13): the marker, the entry that exists, a peer on `@valancex/common`, no entry Node would load
|
|
17
|
+
* unmarked, stylesheets that exist and a `sideEffects` that keeps them, and the playground: an application that depends on this package, builds every target the package declares, and uses every
|
|
18
|
+
* template the package ships (so no template is shipped that no target has checked).
|
|
19
|
+
*/
|
|
20
|
+
export declare const checkLibrary: (dir: string, options?: {
|
|
21
|
+
readonly app?: string;
|
|
22
|
+
}) => Promise<ReadonlyArray<Diagnostic>>;
|
|
23
|
+
export interface BuiltLibrary {
|
|
24
|
+
readonly diagnostics: ReadonlyArray<Diagnostic>;
|
|
25
|
+
/** The files copied beside the compiled JavaScript: templates and stylesheets, relative to `dist/`. */
|
|
26
|
+
readonly copied: ReadonlyArray<string>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Writes `dist/`: the TypeScript compiled to JavaScript with declarations (the contracts of the components are the types `component()` gives them), and every `.mprx` and `.css` under `src/` copied beside
|
|
30
|
+
* it, because templates ship as source and the consumer's build compiles them (K3). It does not check the package; `vlx library check` does.
|
|
31
|
+
*/
|
|
32
|
+
export declare const buildLibrary: (dir: string) => BuiltLibrary;
|
package/dist/library.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// The author's side of a library (Bet 12; K7, K8, K13): `vlx init library` makes a workspace (the library, and a playground application that uses it), `vlx library build` writes what is published
|
|
2
|
+
// (JavaScript, declarations, and the templates and stylesheets as source), `vlx library check` holds the package to the rules a consumer's build relies on. The consumer's side (finding a marked
|
|
3
|
+
// package, the build settings, the diagnostics at the use site) is `libraries.ts`.
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { check } from "./check.js";
|
|
8
|
+
import { loadConfig } from "./config.js";
|
|
9
|
+
import { diagnostic } from "./diagnostics.js";
|
|
10
|
+
import { cliSpec, commonSpec, packageName, runCommand } from "./init.js";
|
|
11
|
+
import { CONDITION, hasCondition, installedAt } from "./libraries.js";
|
|
12
|
+
import { themeFileProblems } from "./theme.js";
|
|
13
|
+
const own = () => JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
14
|
+
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
15
|
+
const MPRX_D_TS = `// What importing a template file gives TypeScript: the file's address. (The bundler makes the import; see @valancex/common/types/mprx.d.ts.)
|
|
16
|
+
declare module "*.mprx" {
|
|
17
|
+
const template: { readonly mprx: string };
|
|
18
|
+
export default template;
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
/** Every file \`vlx init library\` writes, without writing any: relative path (\`/\` separated) → text. */
|
|
22
|
+
export const planLibrary = (answers) => {
|
|
23
|
+
const out = new Map();
|
|
24
|
+
const manager = answers.packageManager;
|
|
25
|
+
const slug = packageName(answers.name.replace(/^@[^/]+\//, ""));
|
|
26
|
+
const lib = `packages/${slug}`;
|
|
27
|
+
const common = own().dependencies["@valancex/common"];
|
|
28
|
+
const range = common === undefined || common.startsWith("workspace:") ? `^${own().version}` : common;
|
|
29
|
+
const link = manager === "pnpm" ? "workspace:*" : "*";
|
|
30
|
+
const effect = own().dependencies["effect"] ?? "^3.10.0";
|
|
31
|
+
const q = '"';
|
|
32
|
+
out.set("package.json", json({
|
|
33
|
+
name: `${slug}-workspace`,
|
|
34
|
+
version: "0.0.0",
|
|
35
|
+
private: true,
|
|
36
|
+
type: "module",
|
|
37
|
+
...(manager === "pnpm" ? {} : { workspaces: ["packages/*", "playground"] }),
|
|
38
|
+
scripts: {
|
|
39
|
+
// each through the package's own script: `vlx` is installed in the package that uses it, not at the workspace root, where a script cannot find it
|
|
40
|
+
"build:library": `cd ${lib} && ${runCommand(manager)} build`,
|
|
41
|
+
"check:library": `cd ${lib} && ${runCommand(manager)} check`,
|
|
42
|
+
playground: `cd playground && ${runCommand(manager)} dev`,
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
if (manager === "pnpm") {
|
|
46
|
+
out.set("pnpm-workspace.yaml", `packages:\n - "packages/*"\n - playground\nallowBuilds:\n esbuild: false\n`);
|
|
47
|
+
}
|
|
48
|
+
out.set(".gitignore", "node_modules\ndist\n.valance\n");
|
|
49
|
+
out.set(`${lib}/package.json`, json({
|
|
50
|
+
name: answers.name,
|
|
51
|
+
version: "0.0.0",
|
|
52
|
+
description: "A VALANCE component library.",
|
|
53
|
+
license: "MIT",
|
|
54
|
+
type: "module",
|
|
55
|
+
files: ["dist"],
|
|
56
|
+
// the condition is what marks a library: the consumer's build processes the package instead of loading it in Node, which does not know .mprx or .css (decision K11)
|
|
57
|
+
exports: { ".": { types: "./dist/index.d.ts", [CONDITION]: "./dist/index.js" } },
|
|
58
|
+
sideEffects: ["**/*.css"],
|
|
59
|
+
valance: { kind: "components", targets: ["web", "desktop"] },
|
|
60
|
+
scripts: { build: "vlx library build", check: "vlx library check" },
|
|
61
|
+
peerDependencies: { "@valancex/common": range, effect: ">=3.10.0 <4" },
|
|
62
|
+
devDependencies: { "@types/node": "^22.0.0", "@valancex/cli": cliSpec(manager, answers.cli), "@valancex/common": commonSpec(manager), effect, typescript: "^5.6.0" },
|
|
63
|
+
}));
|
|
64
|
+
out.set(`${lib}/tsconfig.json`, json({
|
|
65
|
+
compilerOptions: {
|
|
66
|
+
target: "ES2022",
|
|
67
|
+
module: "ESNext",
|
|
68
|
+
moduleResolution: "Bundler",
|
|
69
|
+
lib: ["ES2022", "DOM"],
|
|
70
|
+
types: ["node"],
|
|
71
|
+
strict: true,
|
|
72
|
+
noUncheckedIndexedAccess: true,
|
|
73
|
+
exactOptionalPropertyTypes: true,
|
|
74
|
+
skipLibCheck: true,
|
|
75
|
+
declaration: true,
|
|
76
|
+
rootDir: "src",
|
|
77
|
+
outDir: "dist",
|
|
78
|
+
},
|
|
79
|
+
include: ["src"],
|
|
80
|
+
}));
|
|
81
|
+
out.set(`${lib}/src/mprx.d.ts`, MPRX_D_TS);
|
|
82
|
+
out.set(`${lib}/src/index.ts`, `// What the library exports: its components, and the list of them an application can pass to \`components\`. Removing an export, a component, a prop or an event is a breaking change (decision K8).
|
|
83
|
+
export { greeting } from "./greeting.js";
|
|
84
|
+
import { greeting } from "./greeting.js";
|
|
85
|
+
|
|
86
|
+
export const components = [greeting];
|
|
87
|
+
`);
|
|
88
|
+
out.set(`${lib}/src/greeting.mprx`, `<column>
|
|
89
|
+
<heading>{title}</heading>
|
|
90
|
+
<text>{message}</text>
|
|
91
|
+
<button on.click={dismiss()}>Dismiss</button>
|
|
92
|
+
</column>
|
|
93
|
+
`);
|
|
94
|
+
out.set(`${lib}/src/greeting.ts`, `import { component } from "@valancex/common";
|
|
95
|
+
import { Schema } from "effect";
|
|
96
|
+
import template from "./greeting.mprx";
|
|
97
|
+
|
|
98
|
+
// The interface of \`<greeting>\`, stated once, where it is defined. It is built from the portable core (\`column\`, \`heading\`, \`text\`, \`button\`), so it runs on every target the package declares.
|
|
99
|
+
export const greeting = component(template, {
|
|
100
|
+
props: { title: Schema.String, message: Schema.String },
|
|
101
|
+
events: { dismiss: Schema.Void },
|
|
102
|
+
});
|
|
103
|
+
`);
|
|
104
|
+
out.set(`${lib}/README.md`, `# ${answers.name}
|
|
105
|
+
|
|
106
|
+
A VALANCE component library. Its templates ship as source and the application's build compiles them; the package says so with the \`valance\` export condition, and says what it supports in its \`valance\` field.
|
|
107
|
+
|
|
108
|
+
\`\`\`
|
|
109
|
+
${runCommand(manager)} build:library # vlx library build: JavaScript, declarations, templates and stylesheets into dist/
|
|
110
|
+
${runCommand(manager)} check:library # vlx library check: the package, and the playground that uses every component
|
|
111
|
+
${runCommand(manager)} playground # the playground application, served
|
|
112
|
+
\`\`\`
|
|
113
|
+
|
|
114
|
+
The playground (\`../../playground\`) is an application that depends on this package the way a consumer does; it is how each target the package declares is checked.
|
|
115
|
+
`);
|
|
116
|
+
out.set("playground/package.json", json({
|
|
117
|
+
name: `${slug}-playground`,
|
|
118
|
+
version: "0.0.0",
|
|
119
|
+
private: true,
|
|
120
|
+
type: "module",
|
|
121
|
+
scripts: { dev: "vlx dev", build: "vlx build", check: "vlx check", prepare: "vlx sync" },
|
|
122
|
+
dependencies: { "@valancex/common": commonSpec(manager), effect, [answers.name]: link },
|
|
123
|
+
devDependencies: { "@types/node": "^22.0.0", "@valancex/cli": cliSpec(manager, answers.cli), typescript: "^5.6.0" },
|
|
124
|
+
}));
|
|
125
|
+
out.set("playground/valance.json", json({ $schema: "./node_modules/@valancex/cli/schemas/valance.schema.json", version: 1, name: `${slug} playground` }));
|
|
126
|
+
out.set("playground/valance.web.json", json({ $schema: "./node_modules/@valancex/cli/schemas/valance.web.schema.json", version: 1 }));
|
|
127
|
+
out.set("playground/valance.desktop.json", json({ $schema: "./node_modules/@valancex/cli/schemas/valance.desktop.schema.json", version: 1 }));
|
|
128
|
+
out.set("playground/tsconfig.json", json({
|
|
129
|
+
compilerOptions: {
|
|
130
|
+
types: ["node"],
|
|
131
|
+
target: "ES2022",
|
|
132
|
+
module: "NodeNext",
|
|
133
|
+
moduleResolution: "NodeNext",
|
|
134
|
+
lib: ["ES2022", "DOM"],
|
|
135
|
+
strict: true,
|
|
136
|
+
noUncheckedIndexedAccess: true,
|
|
137
|
+
exactOptionalPropertyTypes: true,
|
|
138
|
+
skipLibCheck: true,
|
|
139
|
+
isolatedModules: true,
|
|
140
|
+
noEmit: true,
|
|
141
|
+
allowArbitraryExtensions: true,
|
|
142
|
+
rootDirs: ["src", ".valance/types"],
|
|
143
|
+
},
|
|
144
|
+
include: ["src"],
|
|
145
|
+
}));
|
|
146
|
+
out.set("playground/.gitignore", "node_modules\ndist\n.valance\n");
|
|
147
|
+
out.set("playground/content/index.md", `---\ntitle: ${answers.name} playground\n---\n\n# ${answers.name}\n\nEvery component of the library, on every target it declares.\n`);
|
|
148
|
+
out.set("playground/src/main.ts", `import { component, defineApp, field } from "@valancex/common";
|
|
149
|
+
import { Schema } from "effect";
|
|
150
|
+
import { components } from ${q}${answers.name}${q};
|
|
151
|
+
import layout from "./layout.mprx";
|
|
152
|
+
import desktopLayout from "./desktop/layout.mprx";
|
|
153
|
+
|
|
154
|
+
// The playground uses the library the way an application does: it lists its components, and a template writes their tags. Each target the library declares has a layout here, so \`vlx library check\`
|
|
155
|
+
// holds every component to every target it says it supports.
|
|
156
|
+
export default defineApp({
|
|
157
|
+
targets: { desktop: { layout: component(desktopLayout) } },
|
|
158
|
+
layout: component(layout),
|
|
159
|
+
components,
|
|
160
|
+
state: { dismissed: field(Schema.Boolean, false) },
|
|
161
|
+
commands: (command) => ({ dismiss: command((state) => ({ ...state, dismissed: true })) }),
|
|
162
|
+
});
|
|
163
|
+
`);
|
|
164
|
+
const layout = `<column>
|
|
165
|
+
{#if dismissed}
|
|
166
|
+
<text>Dismissed</text>
|
|
167
|
+
{:else}
|
|
168
|
+
<greeting title="Hello" message="This is the playground." on.dismiss={dismiss()} />
|
|
169
|
+
{/if}
|
|
170
|
+
</column>
|
|
171
|
+
`;
|
|
172
|
+
out.set("playground/src/layout.mprx", layout);
|
|
173
|
+
out.set("playground/src/desktop/layout.mprx", layout);
|
|
174
|
+
return out;
|
|
175
|
+
};
|
|
176
|
+
/** Writes the workspace. Throws if the directory has something in it. Returns the files written. */
|
|
177
|
+
export const scaffoldLibrary = (answers) => {
|
|
178
|
+
if (existsSync(answers.dir) && readdirSync(answers.dir).filter((each) => each !== ".git").length > 0) {
|
|
179
|
+
throw new Error(`${answers.dir} is not empty`);
|
|
180
|
+
}
|
|
181
|
+
const made = planLibrary(answers);
|
|
182
|
+
for (const [name, text] of made) {
|
|
183
|
+
const path = join(answers.dir, ...name.split("/"));
|
|
184
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
185
|
+
writeFileSync(path, text);
|
|
186
|
+
}
|
|
187
|
+
return [...made.keys()].sort();
|
|
188
|
+
};
|
|
189
|
+
const FILE = "package.json";
|
|
190
|
+
/** The files under `dir` with `extension`, relative and sorted. */
|
|
191
|
+
const under = (dir, extension) => existsSync(dir)
|
|
192
|
+
? readdirSync(dir, { recursive: true, encoding: "utf8" })
|
|
193
|
+
.map((each) => each.split(sep).join("/"))
|
|
194
|
+
.filter((each) => each.endsWith(extension) && !each.split("/").includes("node_modules"))
|
|
195
|
+
.sort()
|
|
196
|
+
: [];
|
|
197
|
+
/**
|
|
198
|
+
* The rules a consumer's build relies on, held at the library where its author can act (K7, K8, K11, K13): the marker, the entry that exists, a peer on `@valancex/common`, no entry Node would load
|
|
199
|
+
* unmarked, stylesheets that exist and a `sideEffects` that keeps them, and the playground: an application that depends on this package, builds every target the package declares, and uses every
|
|
200
|
+
* template the package ships (so no template is shipped that no target has checked).
|
|
201
|
+
*/
|
|
202
|
+
export const checkLibrary = async (dir, options = {}) => {
|
|
203
|
+
const problems = [];
|
|
204
|
+
const problem = (code, message) => void problems.push(diagnostic(FILE, 0, code, message));
|
|
205
|
+
let manifest;
|
|
206
|
+
try {
|
|
207
|
+
manifest = JSON.parse(readFileSync(join(dir, FILE), "utf8"));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return [diagnostic(FILE, 0, "library-package", `there is no ${FILE} in ${dir}: run \`vlx library check\` in the library's package directory`)];
|
|
211
|
+
}
|
|
212
|
+
if (!hasCondition(manifest.exports)) {
|
|
213
|
+
problem("library-unmarked", `${FILE} has no "valance" export condition: add ${JSON.stringify({ exports: { ".": { types: "./dist/index.d.ts", valance: "./dist/index.js" } } })}; the condition is what marks a library`);
|
|
214
|
+
return problems;
|
|
215
|
+
}
|
|
216
|
+
const entry = manifest.exports?.["."];
|
|
217
|
+
const marked = typeof entry?.[CONDITION] === "string" ? entry[CONDITION] : undefined;
|
|
218
|
+
if (marked === undefined) {
|
|
219
|
+
problem("library-entry", `exports["."] needs a "valance" condition that is the file: "valance": "./dist/index.js"`);
|
|
220
|
+
}
|
|
221
|
+
else if (!existsSync(resolve(dir, marked))) {
|
|
222
|
+
problem("library-entry", `exports["."].valance is ${marked}, which does not exist: run \`vlx library build\``);
|
|
223
|
+
}
|
|
224
|
+
// Node would load whatever else the entry offers; that is the .mprx failure the condition exists to prevent.
|
|
225
|
+
for (const key of ["default", "import", "require", "node"]) {
|
|
226
|
+
const other = entry?.[key];
|
|
227
|
+
if (other !== undefined && other !== marked) {
|
|
228
|
+
problem("library-default", `exports["."].${key} is ${JSON.stringify(other)}: a tool that does not know the "valance" condition would load it and meet a template. Remove it, or make it the same file as "valance"`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const isTheme = manifest.valance?.kind === "theme";
|
|
232
|
+
if (!isTheme && manifest.peerDependencies?.["@valancex/common"] === undefined) {
|
|
233
|
+
problem("library-peer", `${FILE} should declare @valancex/common as a peer dependency (one copy of the framework in an application; removing it from the peers is breaking, K8)`);
|
|
234
|
+
}
|
|
235
|
+
const styles = manifest.valance?.styles ?? [];
|
|
236
|
+
if (styles.length > 0) {
|
|
237
|
+
const sideEffects = manifest.sideEffects;
|
|
238
|
+
const keepsCss = Array.isArray(sideEffects) && sideEffects.some((each) => typeof each === "string" && each.includes(".css"));
|
|
239
|
+
if (sideEffects === true || sideEffects === undefined || keepsCss) {
|
|
240
|
+
// (true and absent keep every file; a list must name the stylesheets)
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
problem("library-side-effects", `"sideEffects" in ${FILE} drops stylesheets from a bundle: use ["**/*.css"] (K6)`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// A theme is data: it is checked as a project's theme is (tokens, recipes, the contrast of the pairs it declares), and needs no playground.
|
|
247
|
+
if (isTheme) {
|
|
248
|
+
const file = manifest.valance?.theme === undefined ? undefined : resolve(dir, manifest.valance.theme);
|
|
249
|
+
if (file === undefined || !existsSync(file)) {
|
|
250
|
+
problem("library-field", `"valance.theme" in ${FILE} must name the package's theme.json`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
problems.push(...themeFileProblems(file, dir));
|
|
254
|
+
}
|
|
255
|
+
return problems;
|
|
256
|
+
}
|
|
257
|
+
// the playground
|
|
258
|
+
const app = options.app ?? ["../../playground", "playground", "../playground"].map((each) => resolve(dir, each)).find((each) => existsSync(join(each, FILE)));
|
|
259
|
+
if (app === undefined) {
|
|
260
|
+
problem("library-playground", "no playground application was found (../../playground, ./playground or ../playground; or --app <dir>): an application that depends on this package and uses each of its components is how each target it declares is checked");
|
|
261
|
+
return problems;
|
|
262
|
+
}
|
|
263
|
+
const label = relative(dir, app).split(sep).join("/");
|
|
264
|
+
const config = await (async () => {
|
|
265
|
+
try {
|
|
266
|
+
return loadConfig(app);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
problem("library-playground", `${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
})();
|
|
273
|
+
if (config === undefined) {
|
|
274
|
+
return problems;
|
|
275
|
+
}
|
|
276
|
+
const declared = manifest.valance?.targets ?? ["web", "desktop"];
|
|
277
|
+
for (const target of declared) {
|
|
278
|
+
if (!config.targets.includes(target)) {
|
|
279
|
+
problem("library-playground-target", `the package declares ${target}, but the playground (${label}) does not build for it: add valance.${target}.json there, so that target is checked`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (installedAt(app, manifest.name ?? "") === undefined) {
|
|
283
|
+
problem("library-playground", `the playground (${label}) has no ${manifest.name} installed: install the workspace, and run \`vlx library build\` first`);
|
|
284
|
+
return problems;
|
|
285
|
+
}
|
|
286
|
+
const result = await check(app);
|
|
287
|
+
for (const each of result.diagnostics) {
|
|
288
|
+
problems.push({ ...each, file: `${label}/${each.file}` });
|
|
289
|
+
}
|
|
290
|
+
// every template the package ships is used by the playground: a target it declares has then checked it
|
|
291
|
+
const shipped = under(resolve(dir, dirname(marked ?? "dist/index.js")), ".mprx").map((each) => basename(each, ".mprx"));
|
|
292
|
+
const checked = new Set(result.components);
|
|
293
|
+
for (const name of shipped) {
|
|
294
|
+
if (!checked.has(name)) {
|
|
295
|
+
problems.push(diagnostic(`${label}/src/layout.mprx`, 0, "library-unused", `the package ships <${name}>, but the playground's templates never write it, so no target has checked it: use it there`));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return problems;
|
|
299
|
+
};
|
|
300
|
+
/** The TypeScript compiler of the library's own installation. */
|
|
301
|
+
const tscOf = (dir) => {
|
|
302
|
+
const installed = installedAt(dir, "typescript");
|
|
303
|
+
return installed === undefined ? undefined : join(installed, "bin", "tsc");
|
|
304
|
+
};
|
|
305
|
+
/**
|
|
306
|
+
* Writes `dist/`: the TypeScript compiled to JavaScript with declarations (the contracts of the components are the types `component()` gives them), and every `.mprx` and `.css` under `src/` copied beside
|
|
307
|
+
* it, because templates ship as source and the consumer's build compiles them (K3). It does not check the package; `vlx library check` does.
|
|
308
|
+
*/
|
|
309
|
+
export const buildLibrary = (dir) => {
|
|
310
|
+
const src = join(dir, "src");
|
|
311
|
+
if (!existsSync(src)) {
|
|
312
|
+
return { diagnostics: [diagnostic("src", 0, "library-source", `there is no src/ in ${dir}: run \`vlx library build\` in the library's package directory`)], copied: [] };
|
|
313
|
+
}
|
|
314
|
+
const tsc = tscOf(dir);
|
|
315
|
+
if (tsc === undefined) {
|
|
316
|
+
return { diagnostics: [diagnostic(FILE, 0, "library-build", "typescript is not installed here: add it to the library's devDependencies and install")], copied: [] };
|
|
317
|
+
}
|
|
318
|
+
// dist/ is made by this build alone: a file left from a component that was renamed or removed would be shipped (and its template checked as if it were a component)
|
|
319
|
+
rmSync(join(dir, "dist"), { recursive: true, force: true });
|
|
320
|
+
const ran = spawnSync(process.execPath, [tsc, "-p", join(dir, "tsconfig.json")], { cwd: dir, encoding: "utf8" });
|
|
321
|
+
if (ran.status !== 0) {
|
|
322
|
+
const lines = `${ran.stdout}${ran.stderr}`.split("\n").filter((each) => each.trim() !== "");
|
|
323
|
+
return { diagnostics: lines.map((line) => diagnostic("tsconfig.json", 0, "library-build", line)), copied: [] };
|
|
324
|
+
}
|
|
325
|
+
const dist = join(dir, "dist");
|
|
326
|
+
const copied = [];
|
|
327
|
+
for (const file of [...under(src, ".mprx"), ...under(src, ".css")]) {
|
|
328
|
+
mkdirSync(dirname(join(dist, file)), { recursive: true });
|
|
329
|
+
copyFileSync(join(src, file), join(dist, file));
|
|
330
|
+
copied.push(file);
|
|
331
|
+
}
|
|
332
|
+
return { diagnostics: [], copied };
|
|
333
|
+
};
|
package/dist/package.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// `vlx package`: the desktop target as something to hand to a person. The engine becomes one executable (Node's single executable application: the engine bundled to one file, with the MESH
|
|
2
2
|
// runtime's module inside as an asset, injected into a `node` binary), set beside the Desktop PORT's window program. Unsigned: signing and notarizing are not done here (the artifacts say so).
|
|
3
|
-
import { build as bundle } from "vite";
|
|
3
|
+
import { build as bundle, defaultServerConditions } from "vite";
|
|
4
|
+
import { CONDITION } from "./libraries.js";
|
|
4
5
|
import { execFileSync } from "node:child_process";
|
|
5
6
|
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { createRequire } from "node:module";
|
|
@@ -95,14 +96,17 @@ export const packageDesktop = async (root, options = {}) => {
|
|
|
95
96
|
logLevel: "warn",
|
|
96
97
|
plugins: [templates(root)],
|
|
97
98
|
cacheDir: join(root, GENERATED_DIR, "cache"),
|
|
98
|
-
|
|
99
|
+
// the desktop engine is one file: everything is bundled, and a library resolves by its `valance` condition (K4)
|
|
100
|
+
resolve: { conditions: [CONDITION, ...defaultServerConditions] },
|
|
101
|
+
ssr: { noExternal: true, target: "node", resolve: { conditions: [CONDITION, ...defaultServerConditions] } },
|
|
102
|
+
esbuild: { keepNames: true },
|
|
99
103
|
build: {
|
|
100
104
|
ssr: join(root, GENERATED_DIR, "desktop.sea.ts"),
|
|
101
105
|
outDir: work,
|
|
102
106
|
emptyOutDir: false,
|
|
103
107
|
target: "node22",
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
// W10: the engine is minified (half the bytes; start-up measured no worse, 2026-10-24). Names are kept: errors and their `_tag` are read by name.
|
|
109
|
+
minify: "esbuild",
|
|
106
110
|
rollupOptions: { output: { format: "cjs", entryFileNames: "engine.cjs" } },
|
|
107
111
|
},
|
|
108
112
|
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Config } from "./config.js";
|
|
2
|
+
/** The lines `vlx dev --record` prints before it records: where, what the log holds, what is redacted, and whether git would commit it. */
|
|
3
|
+
export declare const recordingNotice: (root: string, file: string, config: Config) => ReadonlyArray<string>;
|
|
4
|
+
/** What `vlx replay` says when it is given a log that holds user data without being told it may read it. */
|
|
5
|
+
export declare const userDataRefusal: (file: string) => string;
|
|
@@ -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 {};
|