@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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -11
  3. package/dist/add.d.ts +9 -1
  4. package/dist/add.js +68 -5
  5. package/dist/archive.d.ts +17 -0
  6. package/dist/archive.js +111 -0
  7. package/dist/args.js +2 -2
  8. package/dist/budgets.json +175 -0
  9. package/dist/build.d.ts +11 -0
  10. package/dist/build.js +120 -30
  11. package/dist/check.js +2 -2
  12. package/dist/cli.js +131 -17
  13. package/dist/compile.d.ts +11 -3
  14. package/dist/compile.js +123 -25
  15. package/dist/config.d.ts +13 -0
  16. package/dist/config.js +34 -1
  17. package/dist/contract.d.ts +18 -1
  18. package/dist/contract.js +85 -3
  19. package/dist/dev-desktop.d.ts +8 -0
  20. package/dist/dev-desktop.js +114 -0
  21. package/dist/dev.d.ts +8 -0
  22. package/dist/dev.js +91 -29
  23. package/dist/emit.d.ts +9 -4
  24. package/dist/emit.js +129 -28
  25. package/dist/init.js +14 -4
  26. package/dist/package.d.ts +18 -0
  27. package/dist/package.js +142 -0
  28. package/dist/project.js +1 -1
  29. package/dist/recording-guard.d.ts +5 -0
  30. package/dist/recording-guard.js +28 -0
  31. package/dist/replay.d.ts +15 -0
  32. package/dist/replay.js +59 -0
  33. package/dist/rules.d.ts +23 -0
  34. package/dist/rules.js +123 -0
  35. package/dist/runtime-guard.d.ts +11 -0
  36. package/dist/runtime-guard.js +62 -0
  37. package/dist/security.d.ts +2 -0
  38. package/dist/security.js +8 -1
  39. package/dist/size.d.ts +54 -0
  40. package/dist/size.js +153 -0
  41. package/dist/targets.d.ts +6 -1
  42. package/dist/targets.js +26 -4
  43. package/dist/theme.d.ts +35 -0
  44. package/dist/theme.js +44 -0
  45. package/dist/tree.d.ts +14 -2
  46. package/dist/tree.js +16 -1
  47. package/dist/ui/init.js +4 -4
  48. package/dist/ui/report.js +1 -1
  49. package/dist/ui/run.d.ts +4 -1
  50. package/dist/ui/run.js +18 -3
  51. package/dist/vite.d.ts +3 -2
  52. package/dist/vite.js +3 -3
  53. package/package.json +15 -7
  54. package/schemas/valance.desktop.schema.json +17 -0
  55. package/schemas/valance.schema.json +71 -0
  56. package/templates/desktop/README.md +14 -0
  57. package/templates/desktop/content/404.md +7 -0
  58. package/templates/desktop/content/guide/getting-started.md +15 -0
  59. package/templates/desktop/content/index.md +14 -0
  60. package/templates/desktop/gitignore +7 -0
  61. package/templates/desktop/src/layout.mprx +23 -0
  62. package/templates/desktop/src/main.ts +15 -0
  63. package/templates/desktop/tsconfig.json +18 -0
  64. package/templates/multi/README.md +13 -0
  65. package/templates/multi/content/404.md +7 -0
  66. package/templates/multi/content/guide/getting-started.md +15 -0
  67. package/templates/multi/content/index.md +14 -0
  68. package/templates/multi/src/desktop/layout.mprx +21 -0
  69. package/templates/multi/src/main.ts +23 -0
  70. package/templates/multi/src/web/layout.mprx +27 -0
  71. package/templates/multi/src/web/styles.css +28 -0
  72. package/templates/multi/src/web/theme-toggle.mprx +1 -0
  73. package/templates/multi/src/web/theme-toggle.ts +10 -0
  74. package/templates/web/content/guide/getting-started.md +1 -1
  75. package/templates/web/gitignore +3 -0
  76. package/templates/web/src/layout.mprx +2 -2
  77. package/views/block.mprx +1 -1
  78. package/views/layout.mprx +2 -2
  79. package/views/list-items.mprx +1 -1
  80. package/views-desktop/block.mprx +3 -0
  81. package/views-desktop/blocks.mprx +3 -0
  82. package/views-desktop/inline.mprx +3 -0
  83. package/views-desktop/layout.mprx +20 -0
  84. package/views-desktop/list-block.mprx +3 -0
  85. package/views-desktop/list-items.mprx +3 -0
  86. package/views-desktop/runs.mprx +3 -0
  87. /package/views/{heading.mprx → heading-block.mprx} +0 -0
package/dist/build.d.ts CHANGED
@@ -1,7 +1,14 @@
1
+ import type { TargetId } from "./targets.js";
2
+ import type { BudgetResult, SizeReport } from "./size.js";
1
3
  export interface Built {
2
4
  readonly dir: string;
3
5
  readonly files: ReadonlyArray<string>;
4
6
  readonly pages: ReadonlyArray<string>;
7
+ /** The web build's start-up and after-start sets, measured, and each budget it was held to (a production build only). */
8
+ readonly size?: SizeReport;
9
+ readonly budgets?: ReadonlyArray<BudgetResult>;
10
+ /** The size report as the lines a terminal prints. */
11
+ readonly report?: ReadonlyArray<string>;
5
12
  }
6
13
  /** The file a pathname is written to in the site: `/` → `index.html`, `/guide/x` → `guide/x/index.html`, relative to the base. */
7
14
  export declare const fileOf: (pathname: string, base: string) => string;
@@ -10,5 +17,9 @@ export type BuildStage = "configuration" | "content" | "templates" | "bundle" |
10
17
  export interface BuildOptions {
11
18
  /** Called as each stage begins. */
12
19
  readonly onStage?: (stage: BuildStage) => void;
20
+ /** Which of the project's targets to build; all of them by default. */
21
+ readonly targets?: ReadonlyArray<TargetId>;
22
+ /** Called with the size report as soon as the bundle is measured, before a budget error stops the build (a terminal prints it). */
23
+ readonly onReport?: (lines: ReadonlyArray<string>) => void;
13
24
  }
14
25
  export declare const build: (root: string, options?: BuildOptions) => Promise<Built>;
package/dist/build.js CHANGED
@@ -1,7 +1,8 @@
1
- // `vlx build`: the project → a static site in `dist/`. The stages, in order: configuration, content, templates, the browser bundle, then one document per page (and a 404 page, a sitemap
2
- // when the site has a published address). Everything is generated under `.valance/` first; `dist/` holds only the site.
1
+ // `vlx build`: the project → a static site in `dist/web/`. The stages, in order: configuration, content, templates, the browser bundle, then one document per page (and a 404 page, a sitemap
2
+ // when the site has a published address). Everything is generated under `.valance/` first; `dist/` holds only what is built.
3
3
  import { build as bundle } from "vite";
4
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { createRequire } from "node:module";
5
6
  import { dirname, join } from "node:path";
6
7
  import { loadAuthor } from "./author.js";
7
8
  import { loadConfig } from "./config.js";
@@ -9,44 +10,89 @@ import { headersFile } from "./security.js";
9
10
  import { AppError } from "./diagnostics.js";
10
11
  import { generateContent, generateEntries, generateProgram } from "./emit.js";
11
12
  import { OUTPUT_DIR } from "./project.js";
12
- import { baseConfig, createLoader, SERVER_ENTRY } from "./vite.js";
13
+ import { check, DEFAULT_BUDGETS, failure, lines, measure } from "./size.js";
14
+ import { assertOneRuntime } from "./runtime-guard.js";
15
+ import { baseConfig, createLoader, SERVER_ENTRY, templates } from "./vite.js";
16
+ import { GENERATED_DIR } from "./project.js";
13
17
  /** The file a pathname is written to in the site: `/` → `index.html`, `/guide/x` → `guide/x/index.html`, relative to the base. */
14
18
  export const fileOf = (pathname, base) => {
15
19
  const inside = pathname.slice(base.length).replace(/\/+$/, "");
16
20
  return inside === "" ? "index.html" : `${inside}/index.html`;
17
21
  };
18
22
  const sitemap = (content) => `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${content.pages.map((page) => ` <url><loc>${content.site.url}${content.site.base}${page.path.replace(/^\//, "")}</loc></url>`).join("\n")}\n</urlset>\n`;
19
- export const build = async (root, options = {}) => {
20
- const stage = options.onStage ?? (() => undefined);
21
- stage("configuration");
22
- const config = loadConfig(root);
23
- generateEntries(root);
24
- stage("content");
25
- const content = generateContent(root, config);
26
- const loader = await createLoader(root, config);
27
- let rendered;
28
- try {
29
- stage("templates");
30
- const author = await loadAuthor(loader, root);
31
- await generateProgram(root, author);
32
- // The browser bundle first: it empties dist/, and the documents are written into it after.
33
- stage("bundle");
34
- await bundle({ ...baseConfig(root, config), mode: "production" });
35
- rendered = (await loader.ssrLoadModule(SERVER_ENTRY));
36
- }
37
- catch (error) {
38
- await loader.close();
39
- throw error;
23
+ /** The MESH runtime's module, found the way the project's own dependency would be: through `@valancex/common`, which depends on it. */
24
+ const wasmOf = (root) => {
25
+ const fromRoot = createRequire(join(root, "package.json"));
26
+ const fromCommon = createRequire(fromRoot.resolve("@valancex/common/package.json"));
27
+ return fromCommon.resolve("@valancex/mesh-runtime/mesh-runtime.wasm");
28
+ };
29
+ /** The desktop build: the engine, one ES module with everything it needs bundled in, and the MESH runtime's module beside it, in `dist/desktop/`. The window is the Desktop PORT's. */
30
+ const buildDesktop = async (root) => {
31
+ const out = join(root, OUTPUT_DIR, "desktop");
32
+ rmSync(out, { recursive: true, force: true });
33
+ await bundle({
34
+ root,
35
+ configFile: false,
36
+ publicDir: false, // the site's public files are the web's
37
+ logLevel: "warn",
38
+ plugins: [templates(root)],
39
+ cacheDir: join(root, GENERATED_DIR, "cache"),
40
+ ssr: { noExternal: true, target: "node" },
41
+ esbuild: { keepNames: true },
42
+ build: {
43
+ ssr: join(root, GENERATED_DIR, "desktop.ts"),
44
+ outDir: out,
45
+ emptyOutDir: true,
46
+ target: "node22",
47
+ // 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.
48
+ minify: "esbuild",
49
+ rollupOptions: {
50
+ output: {
51
+ format: "es",
52
+ entryFileNames: "engine.mjs",
53
+ banner: 'import { createRequire as __vlxRequire } from "node:module"; const require = __vlxRequire(import.meta.url);',
54
+ },
55
+ },
56
+ },
57
+ });
58
+ copyFileSync(wasmOf(root), join(out, "mesh-runtime.wasm"));
59
+ // The files images are drawn from, beside the engine (the site's `public/`, which the desktop's content refers to the same way).
60
+ const publicDir = join(root, "public");
61
+ if (existsSync(publicDir)) {
62
+ cpSync(publicDir, join(out, "public"), { recursive: true });
40
63
  }
41
- try {
42
- const dist = join(root, OUTPUT_DIR);
64
+ return { dir: out, files: ["desktop/engine.mjs", "desktop/mesh-runtime.wasm"], pages: [] };
65
+ };
66
+ /** The web build: the browser bundle into `out`, then one document per page. The loader is the one the templates were compiled with. */
67
+ const buildWeb = async (root, config, loader, content, out, prefix, stage, onReport) => {
68
+ // The browser bundle first: it empties its directory, and the documents are written into it after.
69
+ stage("bundle");
70
+ await bundle({ ...baseConfig(root, config, out), mode: "production" });
71
+ const rendered = (await loader.ssrLoadModule(SERVER_ENTRY));
72
+ {
73
+ const dist = join(root, out);
43
74
  const manifest = JSON.parse(readFileSync(join(dist, ".vite", "manifest.json"), "utf8"));
44
75
  const entry = Object.values(manifest).find((chunk) => chunk.isEntry === true);
45
76
  if (entry === undefined) {
46
- throw new AppError([{ file: OUTPUT_DIR, line: 0, column: 0, code: "build-entry", message: "the bundle has no entry chunk" }]);
77
+ throw new AppError([{ file: out, line: 0, column: 0, code: "build-entry", message: "the bundle has no entry chunk" }]);
47
78
  }
48
79
  rmSync(join(dist, ".vite"), { recursive: true, force: true }); // the bundler's bookkeeping is not part of the site
49
- const built = { script: `${config.web.base}${entry.file}`, styles: (entry.css ?? []).map((file) => `${config.web.base}${file}`) };
80
+ // The WebAssembly runtime is in the start-up set (H7): the head starts compiling it while the page's script downloads (W11).
81
+ const wasm = Object.values(manifest).find((chunk) => chunk.file.endsWith(".wasm"))?.file;
82
+ const built = {
83
+ script: `${config.web.base}${entry.file}`,
84
+ styles: (entry.css ?? []).map((file) => `${config.web.base}${file}`),
85
+ ...(wasm === undefined ? {} : { wasm: `${config.web.base}${wasm}` }),
86
+ };
87
+ // The size report: the start-up set and the after-start set, held to the project's budgets or the framework's (W9, W14). Production only.
88
+ const size = measure(dist, manifest, wasm);
89
+ const budgets = check(size, config.budgets ?? DEFAULT_BUDGETS);
90
+ const report = lines(size, budgets);
91
+ onReport(report);
92
+ const over = failure(budgets);
93
+ if (over !== undefined) {
94
+ throw over;
95
+ }
50
96
  const files = [];
51
97
  const write = (relative, text) => {
52
98
  mkdirSync(dirname(join(dist, relative)), { recursive: true });
@@ -67,9 +113,53 @@ export const build = async (root, options = {}) => {
67
113
  if (config.web.headers) {
68
114
  write("_headers", headersFile(config));
69
115
  }
70
- return { dir: dist, files, pages };
116
+ return { dir: dist, files: files.map((file) => `${prefix}${file}`), pages, size, budgets, report };
117
+ }
118
+ };
119
+ export const build = async (root, options = {}) => {
120
+ const stage = options.onStage ?? (() => undefined);
121
+ stage("configuration");
122
+ const config = loadConfig(root);
123
+ const selected = options.targets ?? config.targets;
124
+ const unknown = selected.filter((id) => !config.targets.includes(id));
125
+ if (unknown.length > 0) {
126
+ throw new AppError([
127
+ {
128
+ file: "valance.json",
129
+ line: 0,
130
+ column: 0,
131
+ code: "build-target",
132
+ message: `this project is not built for ${unknown.join(", ")} (its targets: ${config.targets.join(", ")}): add valance.${unknown[0]}.json`,
133
+ },
134
+ ]);
135
+ }
136
+ // Production only (`vlx dev` does not build through here): one copy of the MESH runtime, or the site ships its WebAssembly module more than once (W20).
137
+ assertOneRuntime(root);
138
+ generateEntries(root, config);
139
+ stage("content");
140
+ const content = generateContent(root, config);
141
+ const loader = await createLoader(root, config);
142
+ const built = [];
143
+ try {
144
+ stage("templates");
145
+ await generateProgram(root, await loadAuthor(loader, root), config, selected);
146
+ // Each target writes to a directory of its own, whatever else the project has: dist/web/ and dist/desktop/.
147
+ if (selected.includes("web")) {
148
+ built.push(await buildWeb(root, config, loader, content, `${OUTPUT_DIR}/web`, "web/", stage, options.onReport ?? (() => undefined)));
149
+ }
150
+ if (selected.includes("desktop")) {
151
+ stage("bundle");
152
+ built.push(await buildDesktop(root));
153
+ }
71
154
  }
72
155
  finally {
73
156
  await loader.close();
74
157
  }
158
+ const web = built.find((each) => each.size !== undefined);
159
+ return {
160
+ dir: join(root, OUTPUT_DIR),
161
+ files: built.flatMap((each) => each.files),
162
+ pages: built.flatMap((each) => each.pages),
163
+ ...(web?.size === undefined ? {} : { size: web.size, budgets: web.budgets, report: web.report }),
164
+ };
75
165
  };
package/dist/check.js CHANGED
@@ -22,13 +22,13 @@ export const check = async (root) => {
22
22
  if (config === undefined) {
23
23
  return { diagnostics: problems, pages: 0, components: [] };
24
24
  }
25
- generateEntries(root);
25
+ generateEntries(root, config);
26
26
  const content = await collect(problems, () => generateContent(root, config));
27
27
  const server = await createLoader(root, config);
28
28
  try {
29
29
  const author = await collect(problems, () => loadAuthor(server, root));
30
30
  const failedAuthor = problems.some((each) => each.file === "src/main.ts");
31
- const components = failedAuthor ? undefined : await collect(problems, () => generateProgram(root, author));
31
+ const components = failedAuthor ? undefined : await collect(problems, () => generateProgram(root, author, config));
32
32
  return { diagnostics: problems, pages: content?.pages.length ?? 0, components: components ?? [] };
33
33
  }
34
34
  finally {
package/dist/cli.js CHANGED
@@ -2,46 +2,58 @@
2
2
  //
3
3
  // vlx init [name] [--web ...] make a new application (asks what the flags do not say)
4
4
  // vlx dev [--port N] [--host H] serve the project and rebuild it as it changes
5
- // vlx build write the site to dist/
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";
15
17
  import { build } from "./build.js";
16
- import { generateComponent, generatePage, GenerateError, KINDS } from "./add.js";
18
+ import { packageDesktop } from "./package.js";
19
+ import { generateComponent, generatePage, generateTarget, GenerateError, KINDS } from "./add.js";
17
20
  import { check } from "./check.js";
21
+ import { themeReport } from "./theme.js";
18
22
  import { findRoot } from "./config.js";
19
23
  import { localCli } from "./delegate.js";
20
24
  import { dev } from "./dev.js";
25
+ import { defaultRecording, replayFile } from "./replay.js";
21
26
  import { AppError, format } from "./diagnostics.js";
22
27
  import { detectPackageManager, directoryFor, directoryProblem, execute, nameProblem, runCommand, titleOf } from "./init.js";
23
- import { TARGETS } from "./targets.js";
28
+ import { DEFAULT_TARGETS, TARGETS } from "./targets.js";
24
29
  import { canAsk, canDraw } from "./tty.js";
25
30
  const HELP = `vlx <command>
26
31
 
27
32
  init [name] [--web] make a new application
28
- dev [--port N] [--host H] serve it and rebuild as it changes
29
- build write the site to dist/
33
+ dev [--port N] [--host H] serve it and rebuild as it changes (--record [file]: write a session log)
34
+ replay <log> [--route R] [--allow-user-data] play a session log against this project and find where it differs
35
+ build write the site to dist/web/
30
36
  check [--json] find mistakes in your files without building
37
+ theme check [--json] check the theme: tokens, recipes, contrast, and what each target applies
31
38
  generate page|component <name> add a page, or a component (alias: g)
39
+ generate target web|desktop add a target to the project
40
+ package --desktop the desktop target as one executable beside its window (unsigned)
32
41
  sync refresh the generated files (this runs by itself; see vlx help sync)
33
42
  help [command] say more about one command
34
43
 
35
44
  Run it in a project (a directory with a valance.json), or any directory inside one.
36
45
  In a terminal the commands draw an interface; --plain (or a pipe, or CI) prints plain text.
37
46
  `;
38
- const GENERATE_HELP = `vlx generate <page|component> <name> (alias: vlx g)
47
+ const GENERATE_HELP = `vlx generate <page|component|target> <name> (alias: vlx g)
39
48
 
40
49
  vlx generate page guide/getting-started content/guide/getting-started.md, with its front matter
41
50
  (--title "Getting started" to say the title)
42
51
  vlx generate component post-card src/post-card.mprx and src/post-card.ts, imported in
43
52
  src/main.ts and listed in its \`components\`. Write
44
53
  <post-card></post-card> in a template to use it.
54
+ vlx generate target desktop valance.desktop.json and a layout of its own in src/desktop/,
55
+ registered in \`targets\` of src/main.ts; the content, the state and
56
+ the commands are shared. Nothing the project has is moved.
45
57
 
46
58
  Nothing is overwritten: a name that is taken is refused.
47
59
  `;
@@ -49,9 +61,12 @@ const COMMAND_HELP = {
49
61
  init: INIT_HELP,
50
62
  generate: GENERATE_HELP,
51
63
  g: GENERATE_HELP,
52
- dev: "vlx dev [--port N] [--host H]\n\nServe the project and rebuild it as files change. A mistake leaves the last good build serving and shows in the terminal and in the page.\n",
53
- build: "vlx build\n\nWrite the site to dist/. Any host that serves files can serve it.\n",
64
+ 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",
65
+ 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",
66
+ 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",
54
67
  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",
68
+ 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",
69
+ 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",
55
70
  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",
56
71
  };
57
72
  /** 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. */
@@ -66,10 +81,22 @@ const sync = async (from) => {
66
81
  return `skipped (${error instanceof Error ? error.message.split("\n")[0] : String(error)})`;
67
82
  }
68
83
  };
84
+ /** The targets named by flags (`--web`, `--desktop`), in the order given. */
85
+ const targetFlags = (args) => TARGETS.map((each) => each.id).filter((id) => args.includes(`--${id}`));
69
86
  const option = (args, name) => {
70
87
  const at = args.indexOf(name);
71
88
  return at < 0 ? undefined : args[at + 1];
72
89
  };
90
+ /** `--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. */
91
+ const recordOption = (args) => {
92
+ const joined = args.find((each) => each.startsWith("--record="));
93
+ if (joined !== undefined) {
94
+ return joined.slice("--record=".length);
95
+ }
96
+ const at = args.indexOf("--record");
97
+ const next = at < 0 ? undefined : args[at + 1];
98
+ return at < 0 ? undefined : next === undefined || next.startsWith("-") ? "" : next;
99
+ };
73
100
  const version = () => JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
74
101
  const runInit = async (args, cwd) => {
75
102
  if (args.name !== undefined) {
@@ -90,13 +117,13 @@ const runInit = async (args, cwd) => {
90
117
  return 2;
91
118
  }
92
119
  if (args.targets.length === 0 && !args.yes) {
93
- console.error(`vlx init: choose a target (${TARGETS.map((each) => `--${each.id}`).join(", ")}), or --yes for all of them`);
120
+ console.error(`vlx init: choose a target (${TARGETS.map((each) => `--${each.id}`).join(", ")}), or --yes for the web`);
94
121
  return 2;
95
122
  }
96
123
  const answers = {
97
124
  name: args.name,
98
125
  dir: directoryFor(args.name, cwd),
99
- targets: args.targets.length === 0 ? TARGETS.map((each) => each.id) : args.targets,
126
+ targets: args.targets.length === 0 ? DEFAULT_TARGETS : args.targets,
100
127
  web: { base: args.base ?? "", url: args.url ?? "" },
101
128
  packageManager: args.packageManager ?? detectPackageManager(),
102
129
  install: args.install ?? true,
@@ -128,7 +155,15 @@ export const main = async (args, cwd = process.cwd()) => {
128
155
  return 0;
129
156
  }
130
157
  // A global `vlx` runs the project's own (see `delegate.ts`).
131
- if (command === "dev" || command === "build" || command === "check" || command === "sync" || command === "generate" || command === "g") {
158
+ if (command === "dev" ||
159
+ command === "replay" ||
160
+ command === "build" ||
161
+ command === "package" ||
162
+ command === "check" ||
163
+ command === "theme" ||
164
+ command === "sync" ||
165
+ command === "generate" ||
166
+ command === "g") {
132
167
  const local = localCli(cwd);
133
168
  if (local !== undefined) {
134
169
  const theirs = (await import(pathToFileURL(local).href));
@@ -153,32 +188,85 @@ export const main = async (args, cwd = process.cwd()) => {
153
188
  case "dev": {
154
189
  const port = option(rest, "--port");
155
190
  const host = option(rest, "--host");
191
+ const picked = targetFlags(rest);
192
+ if (picked.length > 1) {
193
+ console.error("vlx dev: name one target, or none for all of them: vlx dev --web or vlx dev --desktop");
194
+ return 2;
195
+ }
196
+ const target = picked[0];
197
+ const asked = recordOption(rest);
198
+ const record = asked === undefined ? undefined : asked === "" ? defaultRecording(findRoot(cwd)) : resolve(cwd, asked);
156
199
  if (draws) {
157
200
  const { drawDev } = await import("./ui/run.js");
158
- await drawDev(findRoot(cwd), { ...(port === undefined ? {} : { port: Number(port) }), ...(host === undefined ? {} : { host }) });
201
+ await drawDev(findRoot(cwd), {
202
+ ...(record === undefined ? {} : { record }),
203
+ ...(target === undefined ? {} : { target }),
204
+ ...(port === undefined ? {} : { port: Number(port) }),
205
+ ...(host === undefined ? {} : { host }),
206
+ });
159
207
  return 0;
160
208
  }
161
- const running = await dev(findRoot(cwd), { ...(port === undefined ? {} : { port: Number(port) }), ...(host === undefined ? {} : { host }) });
209
+ const running = await dev(findRoot(cwd), {
210
+ ...(record === undefined ? {} : { record }),
211
+ ...(target === undefined ? {} : { target }),
212
+ ...(port === undefined ? {} : { port: Number(port) }),
213
+ ...(host === undefined ? {} : { host }),
214
+ });
162
215
  console.log(`vlx dev: ${running.url}`);
163
- // Until interrupted.
216
+ // Until interrupted (or, for a desktop project, until the window is closed).
164
217
  await new Promise((resolve) => {
165
218
  process.once("SIGINT", resolve);
166
219
  process.once("SIGTERM", resolve);
220
+ void running.closed?.then(resolve);
167
221
  });
168
222
  await running.close();
169
223
  return 0;
170
224
  }
225
+ case "replay": {
226
+ const file = rest.find((each, index) => !each.startsWith("-") && rest[index - 1] !== "--route");
227
+ if (file === undefined) {
228
+ console.error("vlx replay: name a log: vlx replay <log> [--route changes,update,render] [--json]");
229
+ return 2;
230
+ }
231
+ const routes = option(rest, "--route");
232
+ const replayed = await replayFile(findRoot(cwd), resolve(cwd, file), {
233
+ ...(routes === undefined ? {} : { routes: routes.split(",") }),
234
+ json: rest.includes("--json"),
235
+ allowUserData: rest.includes("--allow-user-data"),
236
+ });
237
+ (replayed.code === 0 ? console.log : console.error)(replayed.output);
238
+ return replayed.code;
239
+ }
171
240
  case "build": {
172
241
  if (draws) {
173
242
  const { drawBuild } = await import("./ui/run.js");
174
- await drawBuild(findRoot(cwd));
243
+ await drawBuild(findRoot(cwd), targetFlags(rest));
175
244
  return 0;
176
245
  }
177
246
  const started = Date.now();
178
- const built = await build(findRoot(cwd));
247
+ const flagged = targetFlags(rest);
248
+ const built = await build(findRoot(cwd), { ...(flagged.length === 0 ? {} : { targets: flagged }), onReport: (report) => report.forEach((line) => console.log(line)) });
179
249
  console.log(`vlx build: ${built.pages.length} pages, ${built.files.length} files → ${built.dir} (${((Date.now() - started) / 1000).toFixed(1)}s)`);
180
250
  return 0;
181
251
  }
252
+ case "package": {
253
+ if (!rest.includes("--desktop")) {
254
+ console.error("vlx package: say what: vlx package --desktop [--platform linux-x64|win32-x64] [--window <path>] [--node <path>] [--archive]");
255
+ return 2;
256
+ }
257
+ const platform = option(rest, "--platform");
258
+ const window = option(rest, "--window");
259
+ const node = option(rest, "--node");
260
+ const packaged = await packageDesktop(findRoot(cwd), {
261
+ ...(platform === undefined ? {} : { platform }),
262
+ ...(window === undefined ? {} : { window }),
263
+ ...(node === undefined ? {} : { node }),
264
+ ...(rest.includes("--archive") ? { archive: true } : {}),
265
+ });
266
+ console.log(`vlx package: ${packaged.files.join(", ")} → ${packaged.dir}`);
267
+ packaged.notes.forEach((note) => console.log(`vlx package: ${note}`));
268
+ return 0;
269
+ }
182
270
  case "check": {
183
271
  if (draws && !rest.includes("--json")) {
184
272
  const { drawCheck } = await import("./ui/run.js");
@@ -199,6 +287,32 @@ export const main = async (args, cwd = process.cwd()) => {
199
287
  }
200
288
  return result.diagnostics.length === 0 ? 0 : 1;
201
289
  }
290
+ case "theme": {
291
+ if (rest[0] !== "check") {
292
+ console.error("vlx theme: say what: vlx theme check [--json]");
293
+ return 2;
294
+ }
295
+ const report = themeReport(findRoot(cwd));
296
+ if (rest.includes("--json")) {
297
+ console.log(JSON.stringify({ theme: report.name, custom: report.custom, diagnostics: report.diagnostics, contrast: report.contrast, coverage: report.coverage }, null, 2));
298
+ return report.diagnostics.length === 0 ? 0 : 1;
299
+ }
300
+ console.log(`vlx theme check: ${report.custom ? "the project's theme" : "the default theme"} "${report.name}" (modes: ${report.modes.join(", ")})`);
301
+ for (const each of report.contrast) {
302
+ console.log(` contrast ${each.mode.padEnd(9)} ${each.text} on ${each.on} ${each.ratio.toFixed(2)}:1 (min ${each.min}) ${each.passes ? "ok" : "BELOW"}`);
303
+ }
304
+ for (const each of report.coverage) {
305
+ 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(", ")})`}`);
306
+ for (const note of each.notApplied) {
307
+ console.log(` not applied: ${note.primitive} ${note.kind} ${note.name} (${note.support})`);
308
+ }
309
+ }
310
+ report.diagnostics.forEach((each) => {
311
+ console.error(format(each));
312
+ });
313
+ console.log(report.diagnostics.length === 0 ? "vlx theme check: no problems" : `vlx theme check: ${report.diagnostics.length} problem${report.diagnostics.length === 1 ? "" : "s"}`);
314
+ return report.diagnostics.length === 0 ? 0 : 1;
315
+ }
202
316
  case "sync": {
203
317
  const synced = await sync(cwd);
204
318
  console.log(synced);
@@ -219,7 +333,7 @@ export const main = async (args, cwd = process.cwd()) => {
219
333
  }
220
334
  const root = findRoot(cwd);
221
335
  try {
222
- const made = kind === "page" ? generatePage(root, positional[1], title) : generateComponent(root, positional[1]);
336
+ const made = kind === "page" ? generatePage(root, positional[1], title) : kind === "target" ? generateTarget(root, positional[1]) : generateComponent(root, positional[1]);
223
337
  made.created.forEach((file) => console.log(`vlx generate: created ${file}`));
224
338
  made.changed.forEach((file) => console.log(`vlx generate: changed ${file}`));
225
339
  console.log(`vlx generate: ${await sync(root)}`);
package/dist/compile.d.ts CHANGED
@@ -6,6 +6,8 @@ export declare const SOURCE_DIR = "src";
6
6
  /** The component the page is drawn by. */
7
7
  export declare const ROOT = "layout";
8
8
  export declare const BUILT_IN: string;
9
+ /** The desktop target's built-in views: the same components as the web's, written in the desktop vocabulary. */
10
+ export declare const BUILT_IN_DESKTOP: string;
9
11
  /** The package directory: where its README, which documents the framework's own names, is. */
10
12
  export declare const PACKAGE_DIR: string;
11
13
  interface Template {
@@ -24,8 +26,14 @@ export interface Templates {
24
26
  * Built-in templates, then the author's. The author's are exactly the ones `src/main.ts` declares: `layout`, `components` and `override`. A file under `src/` that is not declared is an error, not
25
27
  * a guess; a registered component that no template writes is allowed (it is not part of the program until one does). Which components a template uses is read from the template. A name that is a built-in's replaces it only through `override`.
26
28
  */
27
- export declare const templatesOf: (root: string, author: DefinedApp | undefined, vocabulary?: Vocabulary) => Templates;
28
- export declare const manifestOf: (author: DefinedApp | undefined, vocabulary?: Vocabulary) => import("@valancex/valance/web/build").Manifest;
29
+ export declare const templatesOf: (root: string, author: DefinedApp | undefined, vocabulary?: Vocabulary, views?: string) => Templates;
30
+ export declare const manifestOf: (author: DefinedApp | undefined, vocabulary?: Vocabulary) => {
31
+ components: {
32
+ [x: string]: unknown;
33
+ };
34
+ version: 1;
35
+ types: Readonly<Record<string, never>>;
36
+ };
29
37
  export interface Compiled {
30
38
  readonly program: Valance.Program;
31
39
  /**
@@ -48,5 +56,5 @@ export interface Compiled {
48
56
  }>;
49
57
  }
50
58
  /** 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. */
51
- export declare const compileViews: (root: string, author: DefinedApp | undefined, vocabulary?: Vocabulary) => Promise<Compiled>;
59
+ export declare const compileViews: (root: string, author: DefinedApp | undefined, vocabulary?: Vocabulary, views?: string) => Promise<Compiled>;
52
60
  export {};