@valancex/cli 0.11.0 → 0.13.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -9
  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/build.d.ts +3 -0
  9. package/dist/build.js +98 -29
  10. package/dist/check.js +2 -2
  11. package/dist/cli.js +59 -16
  12. package/dist/compile.d.ts +13 -4
  13. package/dist/compile.js +46 -19
  14. package/dist/config.d.ts +14 -0
  15. package/dist/config.js +51 -4
  16. package/dist/content.js +2 -1
  17. package/dist/dev-desktop.d.ts +8 -0
  18. package/dist/dev-desktop.js +114 -0
  19. package/dist/dev.d.ts +3 -0
  20. package/dist/dev.js +41 -6
  21. package/dist/emit.d.ts +7 -4
  22. package/dist/emit.js +105 -27
  23. package/dist/init.js +14 -4
  24. package/dist/markdown.js +2 -1
  25. package/dist/package.d.ts +18 -0
  26. package/dist/package.js +141 -0
  27. package/dist/project.js +1 -1
  28. package/dist/security.d.ts +4 -0
  29. package/dist/security.js +35 -0
  30. package/dist/targets.d.ts +9 -1
  31. package/dist/targets.js +28 -1
  32. package/dist/ui/init.js +4 -4
  33. package/dist/ui/run.d.ts +3 -1
  34. package/dist/ui/run.js +5 -3
  35. package/dist/vite.d.ts +3 -2
  36. package/dist/vite.js +3 -3
  37. package/package.json +7 -3
  38. package/schemas/valance.desktop.schema.json +17 -0
  39. package/schemas/valance.schema.json +22 -4
  40. package/schemas/valance.web.schema.json +46 -4
  41. package/templates/desktop/README.md +14 -0
  42. package/templates/desktop/content/404.md +7 -0
  43. package/templates/desktop/content/guide/getting-started.md +15 -0
  44. package/templates/desktop/content/index.md +14 -0
  45. package/templates/desktop/gitignore +4 -0
  46. package/templates/desktop/src/layout.mprx +23 -0
  47. package/templates/desktop/src/main.ts +15 -0
  48. package/templates/desktop/tsconfig.json +18 -0
  49. package/templates/multi/README.md +13 -0
  50. package/templates/multi/content/404.md +7 -0
  51. package/templates/multi/content/guide/getting-started.md +15 -0
  52. package/templates/multi/content/index.md +14 -0
  53. package/templates/multi/src/desktop/layout.mprx +21 -0
  54. package/templates/multi/src/main.ts +23 -0
  55. package/templates/multi/src/web/layout.mprx +27 -0
  56. package/templates/multi/src/web/styles.css +28 -0
  57. package/templates/multi/src/web/theme-toggle.mprx +1 -0
  58. package/templates/multi/src/web/theme-toggle.ts +10 -0
  59. package/templates/web/content/guide/getting-started.md +1 -1
  60. package/views/block.mprx +1 -1
  61. package/views-desktop/block.mprx +3 -0
  62. package/views-desktop/blocks.mprx +3 -0
  63. package/views-desktop/inline.mprx +3 -0
  64. package/views-desktop/layout.mprx +20 -0
  65. package/views-desktop/list-block.mprx +3 -0
  66. package/views-desktop/list-items.mprx +3 -0
  67. package/views-desktop/runs.mprx +3 -0
  68. /package/views/{heading.mprx → heading-block.mprx} +0 -0
@@ -0,0 +1,141 @@
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
+ // 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";
4
+ import { execFileSync } from "node:child_process";
5
+ import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
6
+ import { createRequire } from "node:module";
7
+ import { delimiter, join } from "node:path";
8
+ import { checksums, tarGz, zip } from "./archive.js";
9
+ import { loadAuthor } from "./author.js";
10
+ import { build } from "./build.js";
11
+ import { loadConfig } from "./config.js";
12
+ import { AppError } from "./diagnostics.js";
13
+ import { hasEntry } from "./emit.js";
14
+ import { ENTRY_FILE, GENERATED_DIR, OUTPUT_DIR } from "./project.js";
15
+ import { programFile } from "./emit.js";
16
+ import { createLoader, templates } from "./vite.js";
17
+ export const PLATFORMS = ["linux-x64", "win32-x64"];
18
+ const hostPlatform = () => `${process.platform}-${process.arch}`;
19
+ const fail = (message) => {
20
+ throw new AppError([{ file: "valance.desktop.json", line: 0, column: 0, code: "package", message }]);
21
+ };
22
+ /** A program on the PATH, as the shell would find it. */
23
+ const onPath = (name) => (process.env["PATH"] ?? "")
24
+ .split(delimiter)
25
+ .map((dir) => join(dir, name))
26
+ .find((candidate) => existsSync(candidate));
27
+ /** The entry of the packaged engine: the generated desktop entry, with the MESH runtime's module read from the executable's own assets (a single file has nothing beside it). */
28
+ const seaSource = (root, config) => `// Generated by \`vlx package\`. Do not edit.
29
+ // @ts-nocheck
30
+ import { getRawAsset } from "node:sea";
31
+ import { dirname, join } from "node:path";
32
+ import { start } from "@valancex/common/desktop";
33
+ import content from "./content.json";
34
+ import program from "./${programFile(config, "desktop")}";
35
+ ${hasEntry(root) ? `import author from "../${ENTRY_FILE}";\n` : "const author = undefined;\n"}
36
+ const wasm = new Uint8Array(getRawAsset("mesh-runtime.wasm"));
37
+
38
+ // the project's public/ files sit beside the executable
39
+ const assets = join(dirname(process.execPath), "public");
40
+
41
+ start({ content, program, author, wasm, assets }).then(
42
+ () => process.exit(0),
43
+ (error) => {
44
+ console.error("the engine stopped:", error);
45
+ process.exit(1);
46
+ },
47
+ );
48
+ `;
49
+ /** Every file under `dir` as an archive entry inside `top/`, in a stable order (a program keeps its executable bit). */
50
+ const entriesOf = (dir, top, at = "") => readdirSync(join(dir, at))
51
+ .sort()
52
+ .flatMap((name) => {
53
+ const relative = at === "" ? name : `${at}/${name}`;
54
+ const file = join(dir, relative);
55
+ return statSync(file).isDirectory()
56
+ ? entriesOf(dir, top, relative)
57
+ : [{ path: `${top}/${relative}`, data: readFileSync(file), mode: statSync(file).mode & 0o111 ? 0o755 : 0o644 }];
58
+ });
59
+ export const packageDesktop = async (root, options = {}) => {
60
+ const config = loadConfig(root);
61
+ if (!config.targets.includes("desktop")) {
62
+ return fail("this project has no desktop target (vlx generate target desktop)");
63
+ }
64
+ const platform = options.platform ?? hostPlatform();
65
+ if (!PLATFORMS.includes(platform)) {
66
+ return fail(`cannot package for ${platform}: the platforms are ${PLATFORMS.join(", ")}`);
67
+ }
68
+ const windows = platform.startsWith("win32");
69
+ const node = options.node ?? (platform === hostPlatform() ? process.execPath : undefined);
70
+ const window = options.window ?? process.env["VLX_WINDOW"] ?? onPath(windows ? "vlx-window.exe" : "vlx-window");
71
+ if (node === undefined) {
72
+ return fail(`packaging for ${platform} from ${hostPlatform()} needs a node binary of this version for ${platform}: --node <path>`);
73
+ }
74
+ if (window === undefined || !existsSync(window)) {
75
+ return fail(`no window program: build it from the PORT repository (cargo build --release --bin vlx-window) and give its path with --window <path> (or set VLX_WINDOW)`);
76
+ }
77
+ // The engine, built first: the content, the program and the entries are the project's as they are now.
78
+ await build(root, { targets: ["desktop"] });
79
+ const work = join(root, GENERATED_DIR, "package");
80
+ rmSync(work, { recursive: true, force: true });
81
+ mkdirSync(work, { recursive: true });
82
+ writeFileSync(join(root, GENERATED_DIR, "desktop.sea.ts"), seaSource(root, config));
83
+ // Loading the author's definition once checks it still loads; the bundle below is the one that ships.
84
+ const loader = await createLoader(root, config);
85
+ try {
86
+ await loadAuthor(loader, root);
87
+ }
88
+ finally {
89
+ await loader.close();
90
+ }
91
+ await bundle({
92
+ root,
93
+ configFile: false,
94
+ publicDir: false,
95
+ logLevel: "warn",
96
+ plugins: [templates(root)],
97
+ cacheDir: join(root, GENERATED_DIR, "cache"),
98
+ ssr: { noExternal: true, target: "node" },
99
+ build: {
100
+ ssr: join(root, GENERATED_DIR, "desktop.sea.ts"),
101
+ outDir: work,
102
+ emptyOutDir: false,
103
+ target: "node22",
104
+ minify: false,
105
+ chunkSizeWarningLimit: 4000,
106
+ rollupOptions: { output: { format: "cjs", entryFileNames: "engine.cjs" } },
107
+ },
108
+ });
109
+ const wasm = createRequire(createRequire(join(root, "package.json")).resolve("@valancex/common/package.json")).resolve("@valancex/mesh-runtime/mesh-runtime.wasm");
110
+ writeFileSync(join(work, "sea-config.json"), JSON.stringify({ main: join(work, "engine.cjs"), output: join(work, "engine.blob"), disableExperimentalSEAWarning: true, assets: { "mesh-runtime.wasm": wasm } }));
111
+ execFileSync(process.execPath, ["--experimental-sea-config", join(work, "sea-config.json")], { stdio: "pipe" });
112
+ const out = join(root, OUTPUT_DIR, "package", platform);
113
+ rmSync(out, { recursive: true, force: true });
114
+ mkdirSync(out, { recursive: true });
115
+ const engine = join(out, windows ? "vlx-engine.exe" : "vlx-engine");
116
+ const app = join(out, `${config.name.replace(/[^\w.-]+/g, "-")}${windows ? ".exe" : ""}`);
117
+ copyFileSync(node, engine);
118
+ chmodSync(engine, 0o755);
119
+ const injector = "postject"; // (no type declarations of its own)
120
+ const { inject } = (await import(injector));
121
+ await inject(engine, "NODE_SEA_BLOB", readFileSync(join(work, "engine.blob")), { sentinelFuse: "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2" });
122
+ copyFileSync(window, app);
123
+ chmodSync(app, 0o755);
124
+ // The files images are drawn from, beside the engine.
125
+ const publicDir = join(root, "public");
126
+ if (existsSync(publicDir)) {
127
+ cpSync(publicDir, join(out, "public"), { recursive: true });
128
+ }
129
+ const files = [engine, app].map((file) => file.slice(out.length + 1));
130
+ const notes = ["unsigned: macOS notarization and Windows signing are not done here; an unsigned program draws a warning on first run"];
131
+ if (options.archive === true) {
132
+ const stem = `${config.name.replace(/[^\w.-]+/g, "-")}-${platform}`;
133
+ const archive = `${stem}.${windows ? "zip" : "tar.gz"}`;
134
+ const data = (windows ? zip : tarGz)(entriesOf(out, stem));
135
+ writeFileSync(join(root, OUTPUT_DIR, "package", archive), data);
136
+ writeFileSync(join(root, OUTPUT_DIR, "package", `${archive}.sha256`), checksums([{ name: archive, data }]));
137
+ files.push(`../${archive}`, `../${archive}.sha256`);
138
+ notes.push(`${archive} is the package as one file, with its SHA-256 beside it; the checksum says the file is the one that was made, not who made it (that is what signing is for)`);
139
+ }
140
+ return { dir: out, files, notes };
141
+ };
package/dist/project.js CHANGED
@@ -1,4 +1,4 @@
1
- // Where things are in a project. Authored: `valance.json`, `valance.web.json`, `content/`, `public/`, `src/`. Generated and never edited or committed: `.valance/`. The final site: `dist/`.
1
+ // Where things are in a project. Authored: `valance.json`, `valance.web.json`, `content/`, `public/`, `src/`. Generated and never edited or committed: `.valance/`. The final site: `dist/web/`.
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  export const GENERATED_DIR = ".valance";
@@ -0,0 +1,4 @@
1
+ import type { Config } from "./config.js";
2
+ export declare const contentSecurityPolicy: (config: Config) => string;
3
+ /** The header lines, for every path under the site's base. */
4
+ export declare const headersFile: (config: Config) => string;
@@ -0,0 +1,35 @@
1
+ /** 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
+ const apiOrigin = (config) => {
3
+ try {
4
+ return new URL(config.api.base).origin;
5
+ }
6
+ catch {
7
+ return undefined;
8
+ }
9
+ };
10
+ export const contentSecurityPolicy = (config) => {
11
+ const origin = apiOrigin(config);
12
+ return [
13
+ "default-src 'self'",
14
+ "script-src 'self' 'wasm-unsafe-eval'",
15
+ "style-src 'self'",
16
+ "img-src 'self' data:",
17
+ `connect-src 'self'${origin === undefined ? "" : ` ${origin}`}`,
18
+ "font-src 'self'",
19
+ "base-uri 'none'",
20
+ "form-action 'self'",
21
+ "frame-ancestors 'none'",
22
+ "object-src 'none'",
23
+ ].join("; ");
24
+ };
25
+ /** The header lines, for every path under the site's base. */
26
+ export const headersFile = (config) => {
27
+ const lines = [
28
+ `Content-Security-Policy: ${contentSecurityPolicy(config)}`,
29
+ "X-Content-Type-Options: nosniff",
30
+ "Referrer-Policy: strict-origin-when-cross-origin",
31
+ "Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()",
32
+ "Cross-Origin-Opener-Policy: same-origin",
33
+ ];
34
+ return `# Written by vlx build. Serve these headers with the site (Netlify and Cloudflare Pages read this file; copy the lines to any other host's configuration).\n# Turn it off with "headers": false in valance.web.json.\n${config.web.base}*\n${lines.map((line) => ` ${line}`).join("\n")}\n`;
35
+ };
package/dist/targets.d.ts CHANGED
@@ -1,9 +1,17 @@
1
+ import type { Vocabulary } from "@valancex/common/internal";
1
2
  export interface Target {
2
3
  /** The flag (`--web`) and the name in `valance.<id>.json`. */
3
- readonly id: "web";
4
+ readonly id: "web" | "desktop";
4
5
  readonly label: string;
5
6
  readonly hint: string;
7
+ /** The tags its templates may write. A project builds against its target's vocabulary. */
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;
6
11
  }
7
12
  export declare const TARGETS: ReadonlyArray<Target>;
13
+ export declare const targetOf: (id: TargetId) => Target;
8
14
  export type TargetId = Target["id"];
9
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,2 +1,29 @@
1
- export const TARGETS = [{ id: "web", label: "Web", hint: "a website: Markdown pages and templates, built to a static site (settings in valance.web.json)" }];
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
+ };
16
+ export const TARGETS = [
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
+ },
25
+ ];
26
+ export const targetOf = (id) => TARGETS.find((each) => each.id === id);
2
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"];
package/dist/ui/init.js CHANGED
@@ -4,7 +4,7 @@ import { Box, Text, useApp } from "ink";
4
4
  import { useEffect, useRef, useState } from "react";
5
5
  import { relative } from "node:path";
6
6
  import { directoryFor, directoryProblem, execute as defaultExecute, nameProblem, PACKAGE_MANAGERS, runCommand, titleOf } from "../init.js";
7
- import { TARGETS } from "../targets.js";
7
+ import { DEFAULT_TARGETS, TARGETS } from "../targets.js";
8
8
  import { Spinner } from "./Spinner.js";
9
9
  import { Answered, Confirm, MultiSelect, Select, TextPrompt } from "./prompts.js";
10
10
  const ORDER = ["name", "targets", "base", "url", "pm", "install", "git"];
@@ -75,7 +75,7 @@ export const Wizard = ({ args, cwd, detected, version, execute = defaultExecute,
75
75
  return;
76
76
  }
77
77
  finished.current = true;
78
- const targets = draft.targets.length === 0 ? TARGETS.map((each) => each.id) : draft.targets;
78
+ const targets = draft.targets.length === 0 ? DEFAULT_TARGETS : draft.targets;
79
79
  const name = draft.name ?? "my-app";
80
80
  const answers = {
81
81
  name,
@@ -107,10 +107,10 @@ export const Wizard = ({ args, cwd, detected, version, execute = defaultExecute,
107
107
  }
108
108
  }, [outcome, failure]);
109
109
  const question = running ? undefined : ORDER[at];
110
- const picked = draft.targets.length === 0 ? TARGETS.map((each) => each.id) : draft.targets;
110
+ const picked = draft.targets.length === 0 ? DEFAULT_TARGETS : draft.targets;
111
111
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: "cyan", children: "vlx init" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 VALANCE ", version] })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [answered.map(([label, value]) => (_jsx(Answered, { label: label, value: value }, label))), question === "name" ? (_jsx(TextPrompt, { label: "What is the application called?", hint: "a name, or a path: it is made in a directory of that name", initial: "my-app", validate: (value) => nameProblem(value) ?? directoryProblem(directoryFor(value, cwd)), onSubmit: (value) => {
112
112
  answer({ name: value.trim() }, "Application", `${titleOf(value)} → ${relative(cwd, directoryFor(value, cwd)) || "."}`);
113
- } })) : null, question === "targets" ? (_jsx(MultiSelect, { label: "Which targets is it for?", choices: TARGETS.map((each) => ({ value: each.id, label: each.label, hint: each.hint })), initial: TARGETS.map((each) => each.id), onSubmit: (values) => {
113
+ } })) : null, question === "targets" ? (_jsx(MultiSelect, { label: "Which targets is it for?", hint: "one application, built for each", choices: TARGETS.map((each) => ({ value: each.id, label: each.label, hint: each.hint })), initial: DEFAULT_TARGETS, onSubmit: (values) => {
114
114
  answer({ targets: values }, "Targets", values.map((id) => TARGETS.find((each) => each.id === id)?.label ?? id).join(", "));
115
115
  } })) : null, question === "base" ? (_jsx(TextPrompt, { label: "Web: where is the site served?", hint: "a base path: / or /docs/", initial: "/", validate: (value) => (BASE.test(value) ? undefined : "a path that starts and ends with /, like / or /docs/"), onSubmit: (value) => {
116
116
  answer({ base: value }, "Base path", value);
package/dist/ui/run.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { TargetId } from "../targets.js";
1
2
  import type { InitArgs } from "../args.js";
2
3
  import type { Checked } from "../check.js";
3
4
  import type { Diagnostic } from "../diagnostics.js";
@@ -6,10 +7,11 @@ import type { Result } from "./init.js";
6
7
  import type { Built } from "../build.js";
7
8
  export declare const drawInit: (args: InitArgs, cwd: string, detected: PackageManager, version: string) => Promise<Result>;
8
9
  export declare const drawCheck: (root: string) => Promise<Checked>;
9
- export declare const drawBuild: (root: string) => Promise<Built>;
10
+ export declare const drawBuild: (root: string, targets?: ReadonlyArray<TargetId>) => Promise<Built>;
10
11
  /** Mistakes that stopped a command, drawn the way `check` draws them. */
11
12
  export declare const drawDiagnostics: (diagnostics: ReadonlyArray<Diagnostic>) => Promise<void>;
12
13
  export declare const drawDev: (root: string, options: {
14
+ readonly target?: TargetId;
13
15
  readonly port?: number;
14
16
  readonly host?: string;
15
17
  }) => Promise<void>;
package/dist/ui/run.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- // Putting the views on the terminal. Ink and React are loaded only here, and only when a command is going to draw: a command that prints plain text never pays for them.
3
2
  import { render } from "ink";
4
3
  import { build as runBuild } from "../build.js";
5
4
  import { check as runCheck } from "../check.js";
@@ -33,11 +32,14 @@ export const drawCheck = async (root) => {
33
32
  await shown.waitUntilExit();
34
33
  }
35
34
  };
36
- export const drawBuild = async (root) => {
35
+ export const drawBuild = async (root, targets = []) => {
37
36
  const started = Date.now();
38
37
  const shown = show(_jsx(BuildProgress, { current: "configuration" }));
39
38
  try {
40
- const built = await runBuild(root, { onStage: ((stage) => shown.rerender(_jsx(BuildProgress, { current: stage }))) });
39
+ const built = await runBuild(root, {
40
+ ...(targets.length === 0 ? {} : { targets }),
41
+ onStage: ((stage) => shown.rerender(_jsx(BuildProgress, { current: stage }))),
42
+ });
41
43
  shown.rerender(_jsx(BuildResult, { built: built, seconds: (Date.now() - started) / 1000 }));
42
44
  return built;
43
45
  }
package/dist/vite.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { InlineConfig, ViteDevServer } from "vite";
1
+ import type { InlineConfig, Plugin, ViteDevServer } from "vite";
2
2
  import type { Config } from "./config.js";
3
3
  export declare const CLIENT_ENTRY = ".valance/client.ts";
4
4
  export declare const SERVER_ENTRY = "/.valance/server.ts";
5
- export declare const baseConfig: (root: string, config: Config) => InlineConfig;
5
+ export declare const templates: (root: string) => Plugin;
6
+ export declare const baseConfig: (root: string, config: Config, out?: string) => InlineConfig;
6
7
  /** A Vite server that serves nothing: it loads the author's TypeScript and the generated server entry in Node. Used by build and check. */
7
8
  export declare const createLoader: (root: string, config: Config) => Promise<ViteDevServer>;
package/dist/vite.js CHANGED
@@ -20,7 +20,7 @@ logger.warnOnce = (message, options) => {
20
20
  };
21
21
  // `import layout from "./layout.mprx"` in `src/main.ts` is the author naming a template. The import gives the file's address and nothing else: the compiler reads the file itself, and the
22
22
  // browser bundle must not carry template text it has already been compiled from.
23
- const templates = (root) => ({
23
+ export const templates = (root) => ({
24
24
  name: "vlx-templates",
25
25
  enforce: "pre",
26
26
  load(id) {
@@ -30,7 +30,7 @@ const templates = (root) => ({
30
30
  // The module is only the file's address, so a change to the template changes nothing the browser holds; the rebuild (dev.ts) recompiles it and reloads once, with the overlay if it fails.
31
31
  handleHotUpdate: ({ file }) => (file.endsWith(".mprx") ? [] : undefined),
32
32
  });
33
- export const baseConfig = (root, config) => ({
33
+ export const baseConfig = (root, config, out = OUTPUT_DIR) => ({
34
34
  root,
35
35
  configFile: false,
36
36
  base: config.web.base,
@@ -41,7 +41,7 @@ export const baseConfig = (root, config) => ({
41
41
  cacheDir: join(root, GENERATED_DIR, "cache"),
42
42
  customLogger: logger,
43
43
  clearScreen: false,
44
- build: { outDir: join(root, OUTPUT_DIR), emptyOutDir: true, target: "esnext", manifest: true, chunkSizeWarningLimit: 1500, rollupOptions: { input: join(root, CLIENT_ENTRY) } },
44
+ build: { outDir: join(root, out), emptyOutDir: true, target: "esnext", manifest: true, chunkSizeWarningLimit: 1500, rollupOptions: { input: join(root, CLIENT_ENTRY) } },
45
45
  // The MESH runtime refers to node:fs/promises on a path the browser never takes.
46
46
  optimizeDeps: { exclude: ["@valancex/mesh-runtime", "@valancex/cli", "@valancex/common"] },
47
47
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valancex/cli",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "vlx, the VALANCE command: make an application (vlx init), serve it as you edit (vlx dev), build it to a static site (vlx build), check it (vlx check). Content, layout, components and styles in, a website out.",
5
5
  "keywords": [
6
6
  "valance",
@@ -40,6 +40,7 @@
40
40
  "dist",
41
41
  "bin",
42
42
  "views",
43
+ "views-desktop",
43
44
  "schemas",
44
45
  "templates"
45
46
  ],
@@ -49,16 +50,19 @@
49
50
  },
50
51
  "dependencies": {
51
52
  "@valancex/mesh-compiler": "^0.11.0",
53
+ "@valancex/port-desktop-vocabulary": "^0.6.0",
52
54
  "effect": "^3.10.0",
53
55
  "ink": "7.1.1",
56
+ "postject": "1.0.0-alpha.6",
54
57
  "react": "19.2.8",
55
58
  "vite": "7.3.6",
56
- "@valancex/common": "^0.11.0",
57
- "@valancex/valance": "^0.11.0"
59
+ "@valancex/valance": "^0.13.0",
60
+ "@valancex/common": "^0.13.0"
58
61
  },
59
62
  "devDependencies": {
60
63
  "@types/node": "^22.0.0",
61
64
  "@types/react": "19.2.18",
65
+ "@valancex/mesh-runtime": "^0.11.0",
62
66
  "ink-testing-library": "4.0.0",
63
67
  "playwright": "1.56.1",
64
68
  "typescript": "^5.6.0",
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://valancex.dev/schemas/valance.desktop.schema.json",
4
+ "title": "valance.desktop.json",
5
+ "description": "Marks the project as a desktop one: it is built to an engine that a native window runs. It has no settings yet.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string"
11
+ },
12
+ "version": {
13
+ "const": 1,
14
+ "description": "The version of this file's format."
15
+ }
16
+ }
17
+ }
@@ -5,10 +5,28 @@
5
5
  "description": "The project: what the application is called. Settings for a target are in valance.<target>.json (valance.web.json for the web).",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["name"],
8
+ "required": [
9
+ "name"
10
+ ],
9
11
  "properties": {
10
- "$schema": { "type": "string" },
11
- "version": { "const": 1, "description": "The version of this file's format." },
12
- "name": { "type": "string", "minLength": 1, "description": "The application's name: the site's title in navigation and in every page's <title>." }
12
+ "$schema": {
13
+ "type": "string"
14
+ },
15
+ "version": {
16
+ "const": 1,
17
+ "description": "The version of this file's format."
18
+ },
19
+ "name": {
20
+ "type": "string",
21
+ "minLength": 1,
22
+ "description": "The application's name: the site's title in navigation and in every page's <title>."
23
+ },
24
+ "title": {
25
+ "type": "string",
26
+ "default": "{page} \u00b7 {site}",
27
+ "pattern": "^[^{}]*(\\{page\\}|\\{site\\})[^{}]*(\\{page\\}|\\{site\\}|[^{}])*$",
28
+ "maxLength": 120,
29
+ "description": "How a document's title is written: \"{page}\" is the page's title and \"{site}\" the name above. Must contain \"{page}\". A page whose title is the site's name is that name alone."
30
+ }
13
31
  }
14
32
  }
@@ -6,9 +6,51 @@
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "properties": {
9
- "$schema": { "type": "string" },
10
- "version": { "const": 1, "description": "The version of this file's format." },
11
- "base": { "type": "string", "pattern": "^/([\\w.~-]+/)*$", "default": "/", "description": "The path the site is served under: \"/\" or \"/docs/\". Every address the build writes is under it." },
12
- "url": { "type": "string", "pattern": "^https?://.*[^/]$", "description": "Where the site is published, without a trailing slash. With it, pages get canonical links and the build writes sitemap.xml." }
9
+ "$schema": {
10
+ "type": "string"
11
+ },
12
+ "version": {
13
+ "const": 1,
14
+ "description": "The version of this file's format."
15
+ },
16
+ "base": {
17
+ "type": "string",
18
+ "pattern": "^/([\\w.~-]+/)*$",
19
+ "default": "/",
20
+ "description": "The path the site is served under: \"/\" or \"/docs/\". Every address the build writes is under it."
21
+ },
22
+ "url": {
23
+ "type": "string",
24
+ "pattern": "^https?://.*[^/]$",
25
+ "description": "Where the site is published, without a trailing slash. With it, pages get canonical links and the build writes sitemap.xml."
26
+ },
27
+ "headers": {
28
+ "type": "boolean",
29
+ "default": true,
30
+ "description": "Write the recommended security headers (a strict Content-Security-Policy and others) as _headers beside the built site."
31
+ },
32
+ "announce": {
33
+ "type": "boolean",
34
+ "default": true,
35
+ "description": "After the application changes page in place, tell assistive technology: say the new title in a live region and focus the main content."
36
+ },
37
+ "api": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": [
41
+ "base"
42
+ ],
43
+ "description": "The application's backend.",
44
+ "properties": {
45
+ "base": {
46
+ "type": "string",
47
+ "description": "Where its requests go: a path on the site's own origin (\"/api\") or an absolute https address (\"https://api.example.com\", which the Content-Security-Policy then allows)."
48
+ },
49
+ "proxy": {
50
+ "type": "string",
51
+ "description": "For vlx dev: forward requests under a path \"base\" to this address (\"http://localhost:3000\"), so the browser sees one origin and no CORS settings are needed."
52
+ }
53
+ }
54
+ }
13
55
  }
14
56
  }
@@ -0,0 +1,14 @@
1
+ # {{name}}
2
+
3
+ A VALANCE application for the desktop, made with `vlx init --desktop`.
4
+
5
+ ```console
6
+ $ {{run}} dev # open the window, and update it as you edit
7
+ $ {{run}} build # write the engine to dist/desktop/
8
+ $ {{run}} check # find mistakes in your files without building
9
+ ```
10
+
11
+ - `content/` is Markdown: a page's path is its address.
12
+ - `src/layout.mprx` is the window around a page; `src/main.ts` declares it, the state and the commands.
13
+ - `valance.desktop.json` marks the project as a desktop one; `valance.json` names the application.
14
+ - The window is the Desktop PORT's (`valancex-port-desktop`); `vlx build` writes the engine it runs, `dist/desktop/engine.mjs`.
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: Not found
3
+ ---
4
+
5
+ # Not found
6
+
7
+ There is no page at that address. Go back to the [first page](/).
@@ -0,0 +1,15 @@
1
+ ---
2
+ title: Getting started
3
+ description: Create the project, run it, change a page.
4
+ order: 1
5
+ ---
6
+
7
+ # Getting started
8
+
9
+ Edit `content/index.md` and save: the window shows it.
10
+
11
+ > A link is an address. In a window, following one is the application's own navigation, the same command the web uses.
12
+
13
+ 1. Add a page: a Markdown file under `content/`.
14
+ 2. Change the window: `src/layout.mprx`, with the components `column`, `row`, `heading`, `text`, `button`, `checkbox` and `textfield`.
15
+ 3. Add state or a command: `src/main.ts`.
@@ -0,0 +1,14 @@
1
+ ---
2
+ title: {{name}}
3
+ description: A desktop application, written the way a VALANCE application is written.
4
+ ---
5
+
6
+ # {{name}}
7
+
8
+ This is a **VALANCE application** in a window. Everything you are reading is a Markdown file in `content/`; the window around it is `src/layout.mprx`.
9
+
10
+ - Pages are Markdown files. Their path is their address.
11
+ - The layout is a template, drawn with the Desktop PORT's components.
12
+ - `vlx dev` opens the window and updates it as you edit.
13
+
14
+ Start with [Getting started](/guide/getting-started).
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ .valance
4
+ .work*
@@ -0,0 +1,23 @@
1
+ <column>
2
+ <row>
3
+ <button disabled={!canGoBack} on.click={back()}>Back</button>
4
+ <button disabled={!canGoForward} on.click={forward()}>Forward</button>
5
+ <link href={home}>{site}</link>
6
+ <button on.click={toggleNavigation()}>{#if navigation}Hide pages{:else}Show pages{/if}</button>
7
+ </row>
8
+ {#if navigation}
9
+ {#each sections as s (s.id)}
10
+ <column>
11
+ {#if s.title != ""}<text>{s.title}</text>{/if}
12
+ <list ordered={false}>
13
+ {#each s.items as n (n.id)}<listitem><link href={n.href}>{n.label}</link></listitem>{/each}
14
+ </list>
15
+ </column>
16
+ {/each}
17
+ {/if}
18
+ <blocks items={blocks} />
19
+ <row>
20
+ {#if prev.href != ""}<link href={prev.href}>{prev.label}</link>{/if}
21
+ {#if next.href != ""}<link href={next.href}>{next.label}</link>{/if}
22
+ </row>
23
+ </column>
@@ -0,0 +1,15 @@
1
+ import { component, defineApp, field } from "@valancex/common";
2
+ import { Schema } from "effect";
3
+ import layout from "./layout.mprx";
4
+
5
+ // The application, from the top. `layout` is the window's content around a page; the Markdown in content/ is what goes in `<blocks>`. The state is the application's own, beyond "which page is
6
+ // shown": each field is its type and its default, together. A field is a name the layout can read (`navigation`), and the commands below are names it can run (`toggleNavigation()`).
7
+ export default defineApp({
8
+ layout: component(layout),
9
+ state: {
10
+ navigation: field(Schema.Boolean, true),
11
+ },
12
+ commands: (command) => ({
13
+ toggleNavigation: command((state) => ({ ...state, navigation: !state.navigation })),
14
+ }),
15
+ });
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "types": ["node"],
4
+ "target": "ES2022",
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "lib": ["ES2022"],
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "skipLibCheck": true,
12
+ "isolatedModules": true,
13
+ "noEmit": true,
14
+ "allowArbitraryExtensions": true,
15
+ "rootDirs": ["src", ".valance/types"]
16
+ },
17
+ "include": ["src"]
18
+ }