@valancex/cli 0.0.0-stage → 0.11.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 (84) hide show
  1. package/README.md +263 -2
  2. package/bin/vlx.js +4 -0
  3. package/dist/add.d.ts +20 -0
  4. package/dist/add.js +143 -0
  5. package/dist/args.d.ts +20 -0
  6. package/dist/args.js +78 -0
  7. package/dist/author.d.ts +4 -0
  8. package/dist/author.js +30 -0
  9. package/dist/build.d.ts +14 -0
  10. package/dist/build.js +70 -0
  11. package/dist/check.d.ts +8 -0
  12. package/dist/check.js +37 -0
  13. package/dist/cli.d.ts +1 -0
  14. package/dist/cli.js +280 -0
  15. package/dist/compile.d.ts +51 -0
  16. package/dist/compile.js +217 -0
  17. package/dist/config.d.ts +20 -0
  18. package/dist/config.js +99 -0
  19. package/dist/content.d.ts +12 -0
  20. package/dist/content.js +193 -0
  21. package/dist/contract.d.ts +31 -0
  22. package/dist/contract.js +45 -0
  23. package/dist/delegate.d.ts +2 -0
  24. package/dist/delegate.js +23 -0
  25. package/dist/dev.d.ts +21 -0
  26. package/dist/dev.js +250 -0
  27. package/dist/diagnostics.d.ts +16 -0
  28. package/dist/diagnostics.js +16 -0
  29. package/dist/emit.d.ts +13 -0
  30. package/dist/emit.js +107 -0
  31. package/dist/init.d.ts +69 -0
  32. package/dist/init.js +185 -0
  33. package/dist/markdown.d.ts +20 -0
  34. package/dist/markdown.js +185 -0
  35. package/dist/node.d.ts +10 -0
  36. package/dist/node.js +6 -0
  37. package/dist/outline.d.ts +17 -0
  38. package/dist/outline.js +88 -0
  39. package/dist/project.d.ts +8 -0
  40. package/dist/project.js +20 -0
  41. package/dist/targets.d.ts +9 -0
  42. package/dist/targets.js +2 -0
  43. package/dist/tree.d.ts +47 -0
  44. package/dist/tree.js +149 -0
  45. package/dist/tty.d.ts +13 -0
  46. package/dist/tty.js +4 -0
  47. package/dist/ui/Spinner.d.ts +3 -0
  48. package/dist/ui/Spinner.js +16 -0
  49. package/dist/ui/init.d.ts +22 -0
  50. package/dist/ui/init.js +137 -0
  51. package/dist/ui/prompts.d.ts +39 -0
  52. package/dist/ui/prompts.js +98 -0
  53. package/dist/ui/report.d.ts +31 -0
  54. package/dist/ui/report.js +30 -0
  55. package/dist/ui/run.d.ts +15 -0
  56. package/dist/ui/run.js +75 -0
  57. package/dist/vite.d.ts +7 -0
  58. package/dist/vite.js +49 -0
  59. package/dist/wiring.d.ts +108 -0
  60. package/dist/wiring.js +330 -0
  61. package/package.json +69 -3
  62. package/schemas/valance.schema.json +14 -0
  63. package/schemas/valance.web.schema.json +14 -0
  64. package/templates/web/README.md +14 -0
  65. package/templates/web/content/404.md +7 -0
  66. package/templates/web/content/guide/getting-started.md +33 -0
  67. package/templates/web/content/guide/writing-pages.md +21 -0
  68. package/templates/web/content/index.md +16 -0
  69. package/templates/web/gitignore +4 -0
  70. package/templates/web/public/logo.svg +1 -0
  71. package/templates/web/src/layout.mprx +27 -0
  72. package/templates/web/src/main.ts +20 -0
  73. package/templates/web/src/styles.css +28 -0
  74. package/templates/web/src/theme-toggle.mprx +1 -0
  75. package/templates/web/src/theme-toggle.ts +10 -0
  76. package/templates/web/tsconfig.json +18 -0
  77. package/views/block.mprx +3 -0
  78. package/views/blocks.mprx +3 -0
  79. package/views/heading.mprx +3 -0
  80. package/views/inline.mprx +3 -0
  81. package/views/layout.mprx +26 -0
  82. package/views/list-block.mprx +5 -0
  83. package/views/list-items.mprx +3 -0
  84. package/views/runs.mprx +3 -0
package/dist/vite.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { InlineConfig, ViteDevServer } from "vite";
2
+ import type { Config } from "./config.js";
3
+ export declare const CLIENT_ENTRY = ".valance/client.ts";
4
+ export declare const SERVER_ENTRY = "/.valance/server.ts";
5
+ export declare const baseConfig: (root: string, config: Config) => InlineConfig;
6
+ /** 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
+ export declare const createLoader: (root: string, config: Config) => Promise<ViteDevServer>;
package/dist/vite.js ADDED
@@ -0,0 +1,49 @@
1
+ import { createLogger, createServer } from "vite";
2
+ import { join, relative, sep } from "node:path";
3
+ import { GENERATED_DIR, OUTPUT_DIR } from "./project.js";
4
+ export const CLIENT_ENTRY = `${GENERATED_DIR}/client.ts`;
5
+ export const SERVER_ENTRY = `/${GENERATED_DIR}/server.ts`;
6
+ // Published packages ship source maps whose sources they do not ship; Vite warns about each, in every project, and the author can do nothing about it. Every other warning is kept.
7
+ const logger = createLogger("warn");
8
+ // oxlint-disable-next-line no-control-regex -- ESC introduces an ANSI colour sequence
9
+ const noise = (message) => /^Sourcemap for .* points to missing source files/.test(message.replace(/\u001b\[[0-9;]*m/g, ""));
10
+ const { warn, warnOnce } = logger;
11
+ logger.warn = (message, options) => {
12
+ if (!noise(message)) {
13
+ warn(message, options);
14
+ }
15
+ };
16
+ logger.warnOnce = (message, options) => {
17
+ if (!noise(message)) {
18
+ warnOnce(message, options);
19
+ }
20
+ };
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
+ // browser bundle must not carry template text it has already been compiled from.
23
+ const templates = (root) => ({
24
+ name: "vlx-templates",
25
+ enforce: "pre",
26
+ load(id) {
27
+ const file = id.split("?")[0];
28
+ return file.endsWith(".mprx") ? `export default ${JSON.stringify({ mprx: relative(root, file).split(sep).join("/") })};` : null;
29
+ },
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
+ handleHotUpdate: ({ file }) => (file.endsWith(".mprx") ? [] : undefined),
32
+ });
33
+ export const baseConfig = (root, config) => ({
34
+ root,
35
+ configFile: false,
36
+ base: config.web.base,
37
+ appType: "custom",
38
+ plugins: [templates(root)],
39
+ publicDir: join(root, "public"),
40
+ // The bundler's cache is an intermediate like any other.
41
+ cacheDir: join(root, GENERATED_DIR, "cache"),
42
+ customLogger: logger,
43
+ clearScreen: false,
44
+ build: { outDir: join(root, OUTPUT_DIR), emptyOutDir: true, target: "esnext", manifest: true, chunkSizeWarningLimit: 1500, rollupOptions: { input: join(root, CLIENT_ENTRY) } },
45
+ // The MESH runtime refers to node:fs/promises on a path the browser never takes.
46
+ optimizeDeps: { exclude: ["@valancex/mesh-runtime", "@valancex/cli", "@valancex/common"] },
47
+ });
48
+ /** A Vite server that serves nothing: it loads the author's TypeScript and the generated server entry in Node. Used by build and check. */
49
+ export const createLoader = async (root, config) => createServer({ ...baseConfig(root, config), optimizeDeps: { noDiscovery: true, include: [] }, server: { middlewareMode: true, hmr: false, watch: null, ws: false } });
@@ -0,0 +1,108 @@
1
+ import type { Compiled } from "./compile.js";
2
+ import type { DefinedApp } from "@valancex/common";
3
+ import type { Place } from "./tree.js";
4
+ export type { Place };
5
+ export interface Use {
6
+ /** The tag in a template. */
7
+ readonly at: Place;
8
+ readonly tag: string;
9
+ /** The template that defines it. */
10
+ readonly target: Place;
11
+ /** What the caller passes: the attribute (where it is written), and the expression or string it is given. */
12
+ readonly props: ReadonlyArray<{
13
+ readonly name: string;
14
+ readonly from: string;
15
+ readonly at: Place;
16
+ }>;
17
+ /** What the caller does when the component raises an event: `on.toggle={toggleTheme()}`. */
18
+ readonly events: ReadonlyArray<{
19
+ readonly name: string;
20
+ readonly to: string;
21
+ readonly at: Place;
22
+ }>;
23
+ }
24
+ /** A component as seen from inside: what it takes and raises, where each is first used in its template, and who passes or handles it. */
25
+ export interface Component {
26
+ readonly name: string;
27
+ readonly file: string;
28
+ /** `at` is where the template first reads the prop; `declared` where `src/main.ts` lists it. */
29
+ readonly props: ReadonlyArray<{
30
+ readonly name: string;
31
+ readonly at: Place;
32
+ readonly declared?: Place;
33
+ readonly passedBy: ReadonlyArray<{
34
+ readonly at: Place;
35
+ readonly from: string;
36
+ }>;
37
+ }>;
38
+ /** `at` is where the template first raises the event; `declared` where `src/main.ts` lists it. */
39
+ readonly events: ReadonlyArray<{
40
+ readonly name: string;
41
+ readonly at: Place;
42
+ readonly declared?: Place;
43
+ readonly handledBy: ReadonlyArray<{
44
+ readonly at: Place;
45
+ readonly to: string;
46
+ }>;
47
+ }>;
48
+ }
49
+ /** A component as `src/main.ts` declares it: the application's component tree, one entry per component, from `layout` down. */
50
+ export interface Declaration {
51
+ readonly name: string;
52
+ readonly file: string;
53
+ /** Where it is declared in `src/main.ts` (its `component(…)` call, or its import). */
54
+ readonly declared?: Place;
55
+ /** The components that list it in `uses`; `(application)` for the layout and an override. */
56
+ readonly parents: ReadonlyArray<string>;
57
+ readonly uses: ReadonlyArray<string>;
58
+ }
59
+ export interface Wiring {
60
+ readonly version: 1;
61
+ /** A path into manifest.json (`components/theme-toggle/props/theme`) → the authored place it is declared. The framework's own names point into its README. */
62
+ readonly definitions: Readonly<Record<string, Place>>;
63
+ /** The same path → where the caller writes it: the attribute that passes a prop, the `on.x` that handles an event. What a click goes to from inside the component. */
64
+ readonly bindings: Readonly<Record<string, ReadonlyArray<Place>>>;
65
+ readonly uses: ReadonlyArray<Use>;
66
+ readonly components: ReadonlyArray<Component>;
67
+ /** The component tree, as declared. */
68
+ readonly tree: ReadonlyArray<Declaration>;
69
+ readonly state: ReadonlyArray<{
70
+ readonly name: string;
71
+ readonly source: Place;
72
+ readonly readBy: ReadonlyArray<Place>;
73
+ }>;
74
+ readonly commands: ReadonlyArray<{
75
+ readonly name: string;
76
+ readonly source: Place;
77
+ readonly runBy: ReadonlyArray<Place>;
78
+ }>;
79
+ }
80
+ interface Read {
81
+ readonly name: string;
82
+ readonly call: boolean;
83
+ readonly at: number;
84
+ }
85
+ interface Element {
86
+ readonly tag: string;
87
+ readonly at: number;
88
+ readonly attributes: ReadonlyArray<{
89
+ readonly name: string;
90
+ readonly string: boolean;
91
+ readonly text: string;
92
+ readonly at: number;
93
+ }>;
94
+ }
95
+ interface Scanned {
96
+ readonly reads: ReadonlyArray<Read>;
97
+ readonly elements: ReadonlyArray<Element>;
98
+ }
99
+ /** A forgiving scan of MPRX: the elements with their attributes, and every identifier read inside `{ … }`. It reads a template the compiler has accepted, so it does not validate. */
100
+ export declare const scan: (text: string) => Scanned;
101
+ /** The first place a key `name:` is written in `src/main.ts`: where a state field or a command is declared. A text search, not a parse: it is for pointing, and falls back to the top of the file. */
102
+ export declare const locateKey: (text: string, name: string) => Place;
103
+ /** Where the framework documents a name an author did not write: a line of its README, which ships with the package. `undefined` when there is none to point at. */
104
+ export interface Docs {
105
+ readonly file: string;
106
+ readonly lines: ReadonlyArray<string>;
107
+ }
108
+ export declare const buildWiring: (root: string, author: DefinedApp | undefined, compiled: Compiled, docs?: Docs) => Wiring;
package/dist/wiring.js ADDED
@@ -0,0 +1,330 @@
1
+ // Where every name came from, and what uses it: the connections between main.ts, the layout and the components, recorded at both ends. MESH knows the manifest, not the files it was derived from;
2
+ // VALANCE knows both. Written as `.valance/wiring.json` for the editor, which uses it so that ctrl+click lands on YOUR file instead of the generated manifest, and so that both ends of a
3
+ // connection (an event and its handler, a prop and what is passed) point at each other.
4
+ import { readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { ownNames } from "@valancex/common/internal";
7
+ import { ENTRY_FILE } from "./project.js";
8
+ import { declarationsOf, placeIn } from "./tree.js";
9
+ const KEYWORDS = new Set(["true", "false", "null"]);
10
+ /** What a block's braces start with: its keyword. */
11
+ const BLOCK = /^\s*(#if|#each|:else if|:else|\/if|\/each)(?![\w-])/;
12
+ /** The `as item (key)` of an `{#each}`: the name it binds and, if written, its key. */
13
+ const EACH = /\s+as\s+([A-Za-z_]\w*)\s*(?:\(([\s\S]*)\))?\s*$/;
14
+ /** The identifiers an expression reads (and the commands it calls), with their offsets; strings and member names are not reads. */
15
+ const identifiers = (expression, base) => {
16
+ const plain = expression.replace(/"([^"\\]|\\.)*"/g, (found) => " ".repeat(found.length));
17
+ const found = [];
18
+ for (const match of plain.matchAll(/(?<![.\w$])(\$?[A-Za-z_]\w*)(\s*\()?/g)) {
19
+ const name = match[1];
20
+ const after = plain.slice(match.index + name.length).trimStart();
21
+ const before = plain.slice(0, match.index).trimEnd().slice(-1);
22
+ if (KEYWORDS.has(name) || name.startsWith("$")) {
23
+ continue;
24
+ }
25
+ if (after.startsWith(":") && (before === "{" || before === ",")) {
26
+ continue;
27
+ } // an object key
28
+ found.push({ name, call: match[2] !== undefined, at: base + match.index });
29
+ }
30
+ return found;
31
+ };
32
+ /** The text of a balanced `{ … }` starting at `from`, strings respected. */
33
+ const balanced = (text, from) => {
34
+ let depth = 0;
35
+ for (let at = from; at < text.length; at += 1) {
36
+ const char = text[at];
37
+ if (char === '"') {
38
+ at += 1;
39
+ while (at < text.length && text[at] !== '"') {
40
+ at += text[at] === "\\" ? 2 : 1;
41
+ }
42
+ }
43
+ else if (char === "{") {
44
+ depth += 1;
45
+ }
46
+ else if (char === "}") {
47
+ depth -= 1;
48
+ if (depth === 0) {
49
+ return at;
50
+ }
51
+ }
52
+ }
53
+ return text.length - 1;
54
+ };
55
+ /** A forgiving scan of MPRX: the elements with their attributes, and every identifier read inside `{ … }`. It reads a template the compiler has accepted, so it does not validate. */
56
+ export const scan = (text) => {
57
+ const reads = [];
58
+ const elements = [];
59
+ /** The names the enclosing `{#each}` blocks bind, innermost last, and the kind of each open block. */
60
+ const bound = [];
61
+ const kinds = [];
62
+ const read = (expression, base) => {
63
+ reads.push(...identifiers(expression, base).filter((each) => !bound.includes(each.name)));
64
+ };
65
+ let at = 0;
66
+ while (at < text.length) {
67
+ const char = text[at];
68
+ if (char === "{") {
69
+ const end = balanced(text, at);
70
+ const inner = text.slice(at + 1, end);
71
+ const block = BLOCK.exec(inner);
72
+ if (block === null) {
73
+ read(inner, at + 1);
74
+ }
75
+ else if (block[1] === "#each") {
76
+ // `{#each items as item (key)}`: the list is read where the block is; the key and the body see the name it binds, which is a binding, not a read of state.
77
+ const head = EACH.exec(inner);
78
+ if (head !== null) {
79
+ const items = inner.slice(block[0].length, head.index);
80
+ read(items, at + 1 + block[0].length);
81
+ bound.push(head[1]);
82
+ kinds.push("each");
83
+ if (head[2] !== undefined) {
84
+ read(head[2], at + 1 + head.index + head[0].indexOf("(") + 1);
85
+ }
86
+ }
87
+ else {
88
+ kinds.push("each");
89
+ }
90
+ }
91
+ else if (block[1] === "#if") {
92
+ kinds.push("if");
93
+ read(inner.slice(block[0].length), at + 1 + block[0].length);
94
+ }
95
+ else if (block[1] === ":else if") {
96
+ read(inner.slice(block[0].length), at + 1 + block[0].length);
97
+ }
98
+ else if (block[1] === "/each" || block[1] === "/if") {
99
+ if (kinds.pop() === "each") {
100
+ bound.pop();
101
+ }
102
+ }
103
+ at = end + 1;
104
+ }
105
+ else if (char === "<" && text[at + 1] === "/") {
106
+ at = text.indexOf(">", at) + 1 || text.length;
107
+ }
108
+ else if (char === "<" && /[a-z]/.test(text[at + 1] ?? "")) {
109
+ const name = /^[a-z][a-z0-9-]*/.exec(text.slice(at + 1))[0];
110
+ const attributes = [];
111
+ let cursor = at + 1 + name.length;
112
+ for (;;) {
113
+ while (/\s/.test(text[cursor] ?? "")) {
114
+ cursor += 1;
115
+ }
116
+ if (cursor >= text.length) {
117
+ break;
118
+ }
119
+ if (text.startsWith("/>", cursor)) {
120
+ cursor += 2;
121
+ break;
122
+ }
123
+ if (text[cursor] === ">") {
124
+ cursor += 1;
125
+ break;
126
+ }
127
+ const attribute = /^[A-Za-z_][\w.:-]*/.exec(text.slice(cursor));
128
+ if (attribute === null) {
129
+ cursor += 1;
130
+ continue;
131
+ }
132
+ const attributeAt = cursor;
133
+ cursor += attribute[0].length;
134
+ while (/\s/.test(text[cursor] ?? "")) {
135
+ cursor += 1;
136
+ }
137
+ if (text[cursor] !== "=") {
138
+ attributes.push({ name: attribute[0], string: true, text: "", at: attributeAt });
139
+ continue;
140
+ }
141
+ cursor += 1;
142
+ while (/\s/.test(text[cursor] ?? "")) {
143
+ cursor += 1;
144
+ }
145
+ if (text[cursor] === '"') {
146
+ let end = cursor + 1;
147
+ while (end < text.length && text[end] !== '"') {
148
+ end += text[end] === "\\" ? 2 : 1;
149
+ }
150
+ attributes.push({ name: attribute[0], string: true, text: text.slice(cursor + 1, end), at: attributeAt });
151
+ cursor = end + 1;
152
+ }
153
+ else if (text[cursor] === "{") {
154
+ const end = balanced(text, cursor);
155
+ read(text.slice(cursor + 1, end), cursor + 1);
156
+ attributes.push({ name: attribute[0], string: false, text: text.slice(cursor + 1, end).trim(), at: attributeAt });
157
+ cursor = end + 1;
158
+ }
159
+ }
160
+ elements.push({ tag: name, at, attributes });
161
+ at = cursor;
162
+ }
163
+ else {
164
+ at += 1;
165
+ }
166
+ }
167
+ return { reads, elements };
168
+ };
169
+ /** The first place a key `name:` is written in `src/main.ts`: where a state field or a command is declared. A text search, not a parse: it is for pointing, and falls back to the top of the file. */
170
+ export const locateKey = (text, name) => {
171
+ // The keys of a component's `props: { … }` and `events: { … }` are not state fields or commands: blank them out, keeping every offset.
172
+ let masked = text;
173
+ for (const contract of text.matchAll(/\b(?:props|events)\s*:\s*\{/g)) {
174
+ const open = contract.index + contract[0].length - 1;
175
+ let depth = 0;
176
+ let end = open;
177
+ for (; end < text.length; end += 1) {
178
+ if (text[end] === "{") {
179
+ depth += 1;
180
+ }
181
+ else if (text[end] === "}") {
182
+ depth -= 1;
183
+ if (depth === 0) {
184
+ break;
185
+ }
186
+ }
187
+ }
188
+ masked = masked.slice(0, open) + text.slice(open, end).replace(/[^\n]/g, " ") + masked.slice(end);
189
+ }
190
+ const found = new RegExp(`(^|[\\s{,(])(${name})\\s*:`, "m").exec(masked);
191
+ return found === null ? { file: ENTRY_FILE, line: 1, column: 1 } : placeIn(text, found.index + found[1].length, ENTRY_FILE);
192
+ };
193
+ const docLine = (docs, find) => {
194
+ const at = docs?.lines.findIndex(find) ?? -1;
195
+ return docs === undefined || at < 0 ? undefined : { file: docs.file, line: at + 1, column: 1 };
196
+ };
197
+ export const buildWiring = (root, author, compiled, docs) => {
198
+ const manifest = compiled.manifest;
199
+ const own = ownNames(author);
200
+ const mainText = (() => {
201
+ try {
202
+ return readFileSync(join(root, ENTRY_FILE), "utf8");
203
+ }
204
+ catch {
205
+ return "";
206
+ }
207
+ })();
208
+ const scanned = new Map(compiled.templates.map((template) => [template.component, { template, ...scan(template.source) }]));
209
+ const builtInFiles = new Set(compiled.templates.filter((template) => template.builtIn).map((template) => template.file));
210
+ const definitions = {};
211
+ const bindings = {};
212
+ const define = (path, place) => {
213
+ if (place !== undefined) {
214
+ definitions[path.join("/")] = place;
215
+ }
216
+ };
217
+ const placeOf = (component, offset) => {
218
+ const found = scanned.get(component);
219
+ return placeIn(found.template.source, offset, found.template.file);
220
+ };
221
+ const declarations = declarationsOf(root);
222
+ const sites = new Map(compiled.tree.map((node) => [node.name, declarations.get(node.file)]));
223
+ const declaredAt = (name) => {
224
+ const site = sites.get(name);
225
+ return site?.call ?? site?.imported;
226
+ };
227
+ const top = (component) => ({ file: scanned.get(component).template.file, line: 1, column: 1 });
228
+ // Where each component is used, and what is passed and handled.
229
+ const uses = [];
230
+ for (const [component, found] of scanned) {
231
+ for (const element of found.elements.filter((each) => scanned.has(each.tag))) {
232
+ uses.push({
233
+ at: placeOf(component, element.at),
234
+ tag: element.tag,
235
+ target: top(element.tag),
236
+ props: element.attributes
237
+ .filter((attribute) => !attribute.name.startsWith("on."))
238
+ .map((attribute) => ({ name: attribute.name, from: attribute.string ? JSON.stringify(attribute.text) : `{${attribute.text}}`, at: placeOf(component, attribute.at) })),
239
+ events: element.attributes
240
+ .filter((attribute) => attribute.name.startsWith("on."))
241
+ .map((attribute) => ({ name: attribute.name.slice(3), to: attribute.text, at: placeOf(component, attribute.at) })),
242
+ });
243
+ }
244
+ }
245
+ // What the layout reads or runs: only the layout's own template. A component's `theme` is its prop, a different name that happens to be spelt alike.
246
+ const readsOf = (name, call) => {
247
+ const seen = new Set();
248
+ return scanned
249
+ .get("layout")
250
+ .reads.filter((read) => read.name === name && read.call === call)
251
+ .map((read) => placeOf("layout", read.at))
252
+ .filter((place) => !seen.has(place.line) && seen.add(place.line));
253
+ };
254
+ // The layout: its names are your state, your commands, or the framework's.
255
+ define(["components", "layout"], top("layout"));
256
+ for (const name of Object.keys(manifest.components["layout"].scope)) {
257
+ define(["components", "layout", "scope", name], own.state.includes(name)
258
+ ? locateKey(mainText, name)
259
+ : docLine(docs, (line) => line.startsWith("|") && (line.split("|")[1] ?? "").includes(`\`${name}\``)));
260
+ }
261
+ for (const name of Object.keys(manifest.components["layout"].commands)) {
262
+ define(["components", "layout", "commands", name], own.commands.includes(name)
263
+ ? locateKey(mainText, name)
264
+ : docLine(docs, (line) => line.includes(`\`${name}\` is the framework`)));
265
+ }
266
+ // Components: declared where the template first uses the prop or raises the event, and bound where a caller writes it.
267
+ const components = [];
268
+ for (const { template } of scanned.values()) {
269
+ const name = template.component;
270
+ if (name === "layout") {
271
+ continue;
272
+ }
273
+ define(["components", name], top(name));
274
+ const entry = manifest.components[name];
275
+ const callers = uses.filter((use) => use.tag === name && !builtInFiles.has(use.at.file));
276
+ const reads = scanned.get(name).reads;
277
+ const props = Object.keys(entry?.props ?? {}).map((prop) => {
278
+ const first = reads.find((read) => read.name === prop && !read.call);
279
+ const at = first === undefined ? top(name) : placeOf(name, first.at);
280
+ const passedBy = callers.flatMap((use) => use.props.filter((passed) => passed.name === prop).map((passed) => ({ at: passed.at, from: passed.from })));
281
+ const declared = sites.get(name)?.member("props", prop) ?? declaredAt(name);
282
+ const everywhere = [...(first === undefined ? [] : [at]), ...passedBy.map((each) => each.at)];
283
+ define(["components", name, "props", prop], declared ?? at);
284
+ define(["components", name, "scope", prop], declared ?? at);
285
+ bindings[["components", name, "props", prop].join("/")] = everywhere;
286
+ bindings[["components", name, "scope", prop].join("/")] = everywhere;
287
+ return { name: prop, at, ...(declared === undefined ? {} : { declared }), passedBy };
288
+ });
289
+ const events = Object.keys(entry?.events ?? {}).map((event) => {
290
+ const first = reads.find((read) => read.name === event && read.call);
291
+ const at = first === undefined ? top(name) : placeOf(name, first.at);
292
+ const handledBy = callers.flatMap((use) => use.events.filter((handled) => handled.name === event).map((handled) => ({ at: handled.at, to: handled.to })));
293
+ const declared = sites.get(name)?.member("events", event) ?? declaredAt(name);
294
+ const everywhere = [...(first === undefined ? [] : [at]), ...handledBy.map((each) => each.at)];
295
+ define(["components", name, "events", event], declared ?? at);
296
+ define(["components", name, "commands", event], declared ?? at);
297
+ bindings[["components", name, "events", event].join("/")] = everywhere;
298
+ bindings[["components", name, "commands", event].join("/")] = everywhere;
299
+ return { name: event, at, ...(declared === undefined ? {} : { declared }), handledBy };
300
+ });
301
+ components.push({ name, file: template.file, props, events });
302
+ }
303
+ // The elements the framework draws: documented in its README, as one list.
304
+ const tags = docLine(docs, (line) => line.startsWith("Tags you may use:"));
305
+ for (const [tag, entry] of Object.entries(manifest.components)) {
306
+ if (scanned.has(tag) || tag.startsWith("mesh-")) {
307
+ continue;
308
+ }
309
+ define(["components", tag], tags);
310
+ for (const prop of Object.keys(entry.props)) {
311
+ define(["components", tag, "props", prop], tags);
312
+ }
313
+ for (const event of Object.keys(entry.events)) {
314
+ define(["components", tag, "events", event], tags);
315
+ }
316
+ }
317
+ return {
318
+ version: 1,
319
+ definitions,
320
+ bindings,
321
+ uses,
322
+ components,
323
+ tree: compiled.tree.map((node) => {
324
+ const declared = declaredAt(node.name);
325
+ return { name: node.name, file: node.file, ...(declared === undefined ? {} : { declared }), parents: node.parents, uses: node.uses };
326
+ }),
327
+ state: own.state.map((name) => ({ name, source: locateKey(mainText, name), readBy: readsOf(name, false) })),
328
+ commands: own.commands.map((name) => ({ name, source: locateKey(mainText, name), runBy: readsOf(name, true) })),
329
+ };
330
+ };
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "@valancex/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.11.0",
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
+ "keywords": [
6
+ "valance",
7
+ "cli",
8
+ "vlx",
9
+ "static-site",
10
+ "scaffold"
11
+ ],
12
+ "homepage": "https://github.com/ValanceX/Valance/tree/main/packages/cli#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/ValanceX/Valance/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/ValanceX/Valance.git",
19
+ "directory": "packages/cli"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Jack Miller <jackmiller0899@gmail.com>",
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "bin": {
28
+ "vlx": "./bin/vlx.js"
29
+ },
30
+ "sideEffects": false,
31
+ "exports": {
32
+ "./node": {
33
+ "types": "./dist/node.d.ts",
34
+ "default": "./dist/node.js"
35
+ },
36
+ "./schemas/*": "./schemas/*",
37
+ "./package.json": "./package.json"
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "bin",
42
+ "views",
43
+ "schemas",
44
+ "templates"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "provenance": true
49
+ },
50
+ "dependencies": {
51
+ "@valancex/mesh-compiler": "^0.11.0",
52
+ "effect": "^3.10.0",
53
+ "ink": "7.1.1",
54
+ "react": "19.2.8",
55
+ "vite": "7.3.6",
56
+ "@valancex/common": "^0.11.0",
57
+ "@valancex/valance": "^0.11.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^22.0.0",
61
+ "@types/react": "19.2.18",
62
+ "ink-testing-library": "4.0.0",
63
+ "playwright": "1.56.1",
64
+ "typescript": "^5.6.0",
65
+ "vitest": "^3.2.0"
66
+ },
67
+ "scripts": {
68
+ "build": "tsc -p tsconfig.json",
69
+ "typecheck": "tsc -p tsconfig.typecheck.json",
70
+ "test": "vitest run"
71
+ }
6
72
  }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://valancex.dev/schemas/valance.schema.json",
4
+ "title": "valance.json",
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
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["name"],
9
+ "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>." }
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://valancex.dev/schemas/valance.web.schema.json",
4
+ "title": "valance.web.json",
5
+ "description": "Settings for the web target: where the built site is served.",
6
+ "type": "object",
7
+ "additionalProperties": false,
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." }
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ # {{name}}
2
+
3
+ A VALANCE application for the web, made with `vlx init`.
4
+
5
+ ```console
6
+ $ {{run}} dev # serve it, and rebuild as you edit
7
+ $ {{run}} build # write the site to dist/
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 page around it; `src/main.ts` declares the components it uses (start reading there).
13
+ - `src/styles.css` is the look. `public/` is served as it is.
14
+ - `valance.json` names the application{{web_note}}.
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: Not found
3
+ ---
4
+
5
+ # Not found
6
+
7
+ There is no page at this address. [Back to the start](/).
@@ -0,0 +1,33 @@
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
+ ## Run it
10
+
11
+ ```
12
+ pnpm install
13
+ pnpm dev
14
+ ```
15
+
16
+ Open the address `vlx dev` prints. Edit this file and save: the page reloads with your change.
17
+
18
+ ## Build it
19
+
20
+ ```
21
+ pnpm build
22
+ ```
23
+
24
+ The finished site is in `dist/`. Anything that serves files can host it.
25
+
26
+ ## Where things are
27
+
28
+ 1. `content/` holds the pages.
29
+ 2. `src/layout.mprx` is the shell around every page.
30
+ 3. `src/styles.css` is the look.
31
+ 4. `public/` holds files that are served as they are, like the logo.
32
+
33
+ Next: [writing pages](./writing-pages.md).