@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/init.js ADDED
@@ -0,0 +1,185 @@
1
+ // `vlx init`: a new application on disk. This file is the work (names, what the questions mean, the files written, the install); the interface that asks the questions is `ui/init.tsx`, and
2
+ // answers can equally come from flags, so everything here takes plain answers and nothing here prints.
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { PROJECT_FILE, WEB_FILE } from "./config.js";
8
+ export const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
9
+ const PACKAGE = new URL("../", import.meta.url);
10
+ const TEMPLATES = new URL("../templates/", import.meta.url);
11
+ const own = () => JSON.parse(readFileSync(new URL("package.json", PACKAGE), "utf8"));
12
+ /** The package name an application gets: lowercase, URL-safe. */
13
+ export const packageName = (name) => {
14
+ const slug = name
15
+ .trim()
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9._-]+/g, "-")
18
+ .replace(/^[-._]+|[-._]+$/g, "");
19
+ return slug === "" ? "app" : slug;
20
+ };
21
+ /** A reason the name cannot be used, or `undefined`. The directory is checked separately (it may be a path). */
22
+ export const nameProblem = (name) => {
23
+ if (name.trim() === "") {
24
+ return "the application needs a name";
25
+ }
26
+ // oxlint-disable-next-line no-control-regex -- control characters are exactly what is refused
27
+ if (/[\u0000-\u001f"\\]/.test(name)) {
28
+ return "the name cannot contain quotes, backslashes or control characters";
29
+ }
30
+ return undefined;
31
+ };
32
+ /** The directory a name is made in: the name itself, written as a path, resolved from `cwd`. `my-app`, `./my-app` and `../x/my-app` are the directory; `My App` becomes `my-app`. */
33
+ export const directoryFor = (name, cwd) => {
34
+ const trimmed = name.trim();
35
+ return /[\\/]/.test(trimmed) || trimmed.startsWith(".") ? resolve(cwd, trimmed) : resolve(cwd, packageName(trimmed));
36
+ };
37
+ /** The title a name gives the site: a path's last part, as written. */
38
+ export const titleOf = (name) => basename(name.trim().replace(/[\\/]+$/, "")) || name.trim();
39
+ /** What is in the way of writing into `dir`: nothing (it is absent or empty), or a sentence. */
40
+ export const directoryProblem = (dir) => {
41
+ if (!existsSync(dir)) {
42
+ return undefined;
43
+ }
44
+ const entries = readdirSync(dir).filter((each) => each !== ".git");
45
+ return entries.length === 0 ? undefined : `${dir} is not empty`;
46
+ };
47
+ /** Which package manager started this: from the user agent npm, pnpm, yarn and bun set for what they run (`npm create`, `pnpm dlx`); `npm` otherwise. */
48
+ export const detectPackageManager = (env = process.env) => {
49
+ const name = (env["npm_config_user_agent"] ?? "").split("/")[0];
50
+ return PACKAGE_MANAGERS.find((each) => each === name) ?? "npm";
51
+ };
52
+ export const runCommand = (manager) => (manager === "npm" ? "npm run" : manager === "bun" ? "bun run" : manager);
53
+ const inCheckout = () => (own().dependencies["@valancex/common"] ?? "").startsWith("workspace:");
54
+ /** What `@valancex/cli` is in the new package.json. A published package is a range; run from a checkout of the repository, the package this command came from, by path (the only place it can be installed from). */
55
+ export const cliSpec = (manager, given) => {
56
+ if (given !== undefined) {
57
+ return given;
58
+ }
59
+ const manifest = own();
60
+ // Installed from the registry, a dependency on a sibling is a range; in a checkout of the repository it is still `workspace:`, and the package can only be installed from where it is.
61
+ if (!inCheckout()) {
62
+ return `^${manifest.version}`;
63
+ }
64
+ const path = fileURLToPath(PACKAGE).replace(/[\\/]$/, "");
65
+ return `${manager === "pnpm" ? "link" : "file"}:${path}`;
66
+ };
67
+ /**
68
+ * What `@valancex/common` is in the new package.json: the range this package depends on once published; inside the workspace (where that is `workspace:*`) the sibling package, by path, like
69
+ * `cliSpec`.
70
+ */
71
+ export const commonSpec = (manager) => {
72
+ if (!inCheckout()) {
73
+ return own().dependencies["@valancex/common"] ?? "*";
74
+ }
75
+ return `${manager === "pnpm" ? "link" : "file"}:${fileURLToPath(new URL("../common/", PACKAGE)).replace(/[\\/]$/, "")}`;
76
+ };
77
+ const mesh = () => own().dependencies["@valancex/mesh-compiler"] ?? "^0.11.0";
78
+ const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
79
+ const files = (dir, prefix = "") => readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? files(new URL(`${entry.name}/`, dir), `${prefix}${entry.name}/`) : [`${prefix}${entry.name}`]);
80
+ /** The name a template file has once written: npm drops `.gitignore` from a package, so it ships as `gitignore`. */
81
+ const written = (name) => (name === "gitignore" ? ".gitignore" : name);
82
+ /** Every file the answers make, without writing any. */
83
+ export const plan = (answers) => {
84
+ const out = new Map();
85
+ const title = titleOf(answers.name);
86
+ const web = answers.targets.includes("web");
87
+ const hasBase = answers.web.base !== "" && answers.web.base !== "/";
88
+ const settings = [hasBase ? `base ${answers.web.base}` : "", answers.web.url !== "" ? `url ${answers.web.url}` : ""].filter((each) => each !== "");
89
+ const tokens = {
90
+ name: title,
91
+ run: runCommand(answers.packageManager),
92
+ web_note: web && settings.length > 0 ? `, and \`${WEB_FILE}\` says where the web site is served (${settings.join(", ")})` : "",
93
+ };
94
+ const fill = (text) => text.replace(/\{\{(\w+)\}\}/g, (match, key) => tokens[key] ?? match);
95
+ if (web) {
96
+ const root = new URL("web/", TEMPLATES);
97
+ for (const file of files(root)) {
98
+ out.set(written(file), fill(readFileSync(new URL(file, root), "utf8")));
99
+ }
100
+ }
101
+ out.set(PROJECT_FILE, json({ $schema: "./node_modules/@valancex/cli/schemas/valance.schema.json", version: 1, name: title }));
102
+ if (web && (hasBase || answers.web.url !== "")) {
103
+ out.set(WEB_FILE, json({
104
+ $schema: "./node_modules/@valancex/cli/schemas/valance.web.schema.json",
105
+ version: 1,
106
+ ...(hasBase ? { base: answers.web.base } : {}),
107
+ ...(answers.web.url !== "" ? { url: answers.web.url } : {}),
108
+ }));
109
+ }
110
+ out.set("package.json", json({
111
+ name: packageName(basename(answers.dir)),
112
+ version: "0.0.0",
113
+ private: true,
114
+ type: "module",
115
+ scripts: { dev: "vlx dev", build: "vlx build", check: "vlx check", typecheck: "vlx sync && tsc --noEmit", prepare: "vlx sync" },
116
+ dependencies: { "@valancex/common": commonSpec(answers.packageManager), effect: own().dependencies["effect"] ?? "^3.10.0" },
117
+ devDependencies: { "@types/node": "^22.0.0", "@valancex/cli": cliSpec(answers.packageManager, answers.cli), "@valancex/mesh-lsp": mesh(), typescript: "^5.6.0" },
118
+ }));
119
+ return { files: out };
120
+ };
121
+ /** Writes the application. Throws if the directory has something in it. Returns the files written, relative and sorted. */
122
+ export const scaffold = (answers) => {
123
+ const problem = directoryProblem(answers.dir);
124
+ if (problem !== undefined) {
125
+ throw new Error(problem);
126
+ }
127
+ const { files: made } = plan(answers);
128
+ for (const [name, text] of made) {
129
+ const path = join(answers.dir, ...name.split("/"));
130
+ mkdirSync(dirname(path), { recursive: true });
131
+ writeFileSync(path, text);
132
+ }
133
+ return [...made.keys()].sort();
134
+ };
135
+ /** Runs `command args` in `cwd`, quietly. A `.cmd` shim on Windows needs a shell; nothing here takes user text as an argument. */
136
+ export const run = (command, args, cwd) => new Promise((done) => {
137
+ const child = spawn(command, [...args], { cwd, stdio: ["ignore", "ignore", "pipe"], shell: process.platform === "win32" });
138
+ let output = "";
139
+ child.stderr.on("data", (chunk) => {
140
+ output = (output + chunk.toString()).slice(-4000);
141
+ });
142
+ child.on("error", (error) => {
143
+ done({ ok: false, output: error.message });
144
+ });
145
+ child.on("close", (code) => {
146
+ done({ ok: code === 0, output: output.trim().split("\n").slice(-6).join("\n") });
147
+ });
148
+ });
149
+ export const install = (answers) => run(answers.packageManager, ["install"], answers.dir);
150
+ /** `git init` and a first commit's worth of staging is not ours to do: the repository is made, the author commits. */
151
+ export const initGit = (dir) => run("git", ["init", "--quiet"], dir);
152
+ /** Does everything the answers say, in order, and says how each step goes. Only writing the files can throw; an install or `git init` that fails is a note, not the end of the application. */
153
+ export const execute = async (answers, report = () => undefined) => {
154
+ const notes = [];
155
+ report("files", "start");
156
+ const made = scaffold(answers);
157
+ report("files", "done", `${made.length} files`);
158
+ let installed = false;
159
+ let git = false;
160
+ if (answers.install) {
161
+ report("install", "start");
162
+ const ran = await install(answers);
163
+ installed = ran.ok;
164
+ report("install", ran.ok ? "done" : "failed", ran.ok ? undefined : ran.output);
165
+ if (!ran.ok) {
166
+ notes.push(`the dependencies were not installed (${answers.packageManager} install failed); run it yourself in the new directory`);
167
+ }
168
+ }
169
+ else {
170
+ report("install", "skipped");
171
+ }
172
+ if (answers.git) {
173
+ report("git", "start");
174
+ const ran = await initGit(answers.dir);
175
+ git = ran.ok;
176
+ report("git", ran.ok ? "done" : "failed", ran.ok ? undefined : ran.output);
177
+ if (!ran.ok) {
178
+ notes.push("no git repository was made (git init failed)");
179
+ }
180
+ }
181
+ else {
182
+ report("git", "skipped");
183
+ }
184
+ return { files: made, installed, git, notes };
185
+ };
@@ -0,0 +1,20 @@
1
+ import type { Block, Heading, Span } from "@valancex/common/internal";
2
+ export interface LinkRef {
3
+ readonly href: string;
4
+ readonly line: number;
5
+ readonly image: boolean;
6
+ }
7
+ export interface Parsed {
8
+ readonly blocks: ReadonlyArray<Block>;
9
+ readonly outline: ReadonlyArray<Heading>;
10
+ /** Every link and image address, for the checks that need the other pages. */
11
+ readonly links: ReadonlyArray<LinkRef>;
12
+ /** The anchors of the page's headings. */
13
+ readonly anchors: ReadonlySet<string>;
14
+ }
15
+ export declare const slug: (text: string) => string;
16
+ export declare const plain: (spans: ReadonlyArray<Span>) => string;
17
+ /**
18
+ * Parses `text`. `file` and `offset` (the lines before `text` in the file, the front matter) only make diagnostics point at the right line.
19
+ */
20
+ export declare const parseMarkdown: (text: string, file: string, offset: number) => Parsed;
@@ -0,0 +1,185 @@
1
+ import { AppError, diagnostic } from "./diagnostics.js";
2
+ export const slug = (text) => text
3
+ .toLowerCase()
4
+ .normalize("NFKD")
5
+ .replace(/[̀-ͯ]/g, "")
6
+ .replace(/[^a-z0-9]+/g, "-")
7
+ .replace(/^-+|-+$/g, "");
8
+ const MARKS = /\*\*([^*]+)\*\*|`([^`]+)`|!\[[^\]]*\]\([^)\s]*\)|\[([^\]]+)\]\(([^)\s]+)\)|\*([^*\s][^*]*)\*|(?<![\w])_([^_\s][^_]*)_(?![\w])/g;
9
+ const empty = { level: 0, anchor: "", spans: [], items: [], ordered: false, lang: "", text: "", src: "", alt: "" };
10
+ export const plain = (spans) => spans.map((span) => span.text).join("");
11
+ /**
12
+ * Parses `text`. `file` and `offset` (the lines before `text` in the file, the front matter) only make diagnostics point at the right line.
13
+ */
14
+ export const parseMarkdown = (text, file, offset) => {
15
+ const lines = text.split("\n");
16
+ const blocks = [];
17
+ const links = [];
18
+ const problems = [];
19
+ const anchors = new Set();
20
+ const fail = (at, code, message) => {
21
+ problems.push(diagnostic(file, offset + at + 1, code, message));
22
+ };
23
+ const inline = (source, blockId, at) => {
24
+ const spans = [];
25
+ const add = (kind, value, href = "") => {
26
+ if (value !== "") {
27
+ spans.push({ id: `${blockId}.${spans.length}`, kind, text: value, href });
28
+ }
29
+ };
30
+ let from = 0;
31
+ for (let found = MARKS.exec(source); found !== null; found = MARKS.exec(source)) {
32
+ add("text", source.slice(from, found.index));
33
+ if (found[1] !== undefined) {
34
+ add("strong", found[1]);
35
+ }
36
+ else if (found[2] !== undefined) {
37
+ add("code", found[2]);
38
+ }
39
+ else if (found[3] !== undefined) {
40
+ add("link", found[3], found[4]);
41
+ links.push({ href: found[4], line: offset + at + 1, image: false });
42
+ }
43
+ else if (found[5] !== undefined || found[6] !== undefined) {
44
+ add("emphasis", found[5] ?? found[6]);
45
+ }
46
+ else {
47
+ fail(at, "md-inline-image", "an image must be on a line of its own");
48
+ }
49
+ from = found.index + found[0].length;
50
+ }
51
+ add("text", source.slice(from));
52
+ MARKS.lastIndex = 0;
53
+ return spans;
54
+ };
55
+ const next = () => `b${blocks.length}`;
56
+ let at = 0;
57
+ while (at < lines.length) {
58
+ const line = lines[at];
59
+ if (line.trim() === "") {
60
+ at += 1;
61
+ continue;
62
+ }
63
+ const fence = /^```\s*([\w+-]*)\s*$/.exec(line);
64
+ if (fence !== null) {
65
+ const start = at;
66
+ const body = [];
67
+ at += 1;
68
+ while (at < lines.length && !/^```\s*$/.test(lines[at])) {
69
+ body.push(lines[at]);
70
+ at += 1;
71
+ }
72
+ if (at >= lines.length) {
73
+ fail(start, "md-unclosed-fence", "this code fence is never closed");
74
+ break;
75
+ }
76
+ blocks.push({ ...empty, id: next(), kind: "code", lang: fence[1], text: body.join("\n") });
77
+ at += 1;
78
+ continue;
79
+ }
80
+ const heading = /^(#{1,6})\s+(.*\S)\s*$/.exec(line);
81
+ if (heading !== null) {
82
+ const level = heading[1].length;
83
+ const id = next();
84
+ if (level > 3) {
85
+ fail(at, "md-heading-level", "headings go from # to ###");
86
+ at += 1;
87
+ continue;
88
+ }
89
+ const spans = inline(heading[2], id, at);
90
+ const base = slug(plain(spans)) || "section";
91
+ let anchor = base;
92
+ for (let n = 2; anchors.has(anchor); n += 1) {
93
+ anchor = `${base}-${n}`;
94
+ }
95
+ anchors.add(anchor);
96
+ blocks.push({ ...empty, id, kind: "heading", level, anchor, spans });
97
+ at += 1;
98
+ continue;
99
+ }
100
+ const image = /^!\[([^\]]*)\]\(([^)\s]+)\)\s*$/.exec(line);
101
+ if (image !== null) {
102
+ blocks.push({ ...empty, id: next(), kind: "image", alt: image[1], src: image[2] });
103
+ links.push({ href: image[2], line: offset + at + 1, image: true });
104
+ at += 1;
105
+ continue;
106
+ }
107
+ if (/^\s*([-*]|\d+\.)\s+/.test(line)) {
108
+ const id = next();
109
+ const ordered = /^\s*\d+\./.test(line);
110
+ const items = [];
111
+ while (at < lines.length && lines[at].trim() !== "") {
112
+ const item = /^(\s*)([-*]|\d+\.)\s+(.*\S)\s*$/.exec(lines[at]);
113
+ if (item !== null) {
114
+ if (item[1] !== "") {
115
+ fail(at, "md-nested-list", "lists nest one level at most: this build draws a single level");
116
+ }
117
+ else if (/\d/.test(item[2]) !== ordered) {
118
+ fail(at, "md-mixed-list", "a list is either bulleted or numbered, not both");
119
+ }
120
+ items.push({ id: `${id}.i${items.length}`, spans: inline(item[3], `${id}.i${items.length}`, at) });
121
+ }
122
+ else if (/^\s+\S/.test(lines[at]) && items.length > 0) {
123
+ const last = items[items.length - 1];
124
+ const more = inline(lines[at].trim(), last.id, at);
125
+ items[items.length - 1] = {
126
+ id: last.id,
127
+ spans: [
128
+ ...last.spans,
129
+ { id: `${last.id}.c${last.spans.length}`, kind: "text", text: " ", href: "" },
130
+ ...more.map((span, n) => ({ ...span, id: `${last.id}.c${last.spans.length + n + 1}` })),
131
+ ],
132
+ };
133
+ }
134
+ else {
135
+ break;
136
+ }
137
+ at += 1;
138
+ }
139
+ blocks.push({ ...empty, id, kind: "list", ordered, items });
140
+ continue;
141
+ }
142
+ if (line.startsWith(">")) {
143
+ const id = next();
144
+ const body = [];
145
+ const first = at;
146
+ while (at < lines.length && lines[at].startsWith(">")) {
147
+ body.push(lines[at].replace(/^>\s?/, ""));
148
+ at += 1;
149
+ }
150
+ blocks.push({ ...empty, id, kind: "quote", spans: inline(body.join(" ").trim(), id, first) });
151
+ continue;
152
+ }
153
+ if (/^\s*</.test(line)) {
154
+ fail(at, "md-html", "raw HTML is not supported: use Markdown, or a component in your layout");
155
+ at += 1;
156
+ continue;
157
+ }
158
+ if (/^\s*\|/.test(line)) {
159
+ fail(at, "md-table", "tables are not supported yet");
160
+ at += 1;
161
+ continue;
162
+ }
163
+ if (/^(-{3,}|\*{3,}|_{3,}|={3,})\s*$/.test(line)) {
164
+ fail(at, "md-rule", "horizontal rules and setext headings are not supported: use # headings");
165
+ at += 1;
166
+ continue;
167
+ }
168
+ const id = next();
169
+ const first = at;
170
+ const body = [line.trim()];
171
+ at += 1;
172
+ while (at < lines.length && lines[at].trim() !== "" && !/^(#{1,6}\s|```|>|\s*([-*]|\d+\.)\s|!\[)/.test(lines[at])) {
173
+ body.push(lines[at].trim());
174
+ at += 1;
175
+ }
176
+ blocks.push({ ...empty, id, kind: "paragraph", spans: inline(body.join(" "), id, first) });
177
+ }
178
+ if (problems.length > 0) {
179
+ throw new AppError(problems);
180
+ }
181
+ const outline = blocks
182
+ .filter((block) => block.kind === "heading" && block.level >= 2)
183
+ .map((block) => ({ id: block.id, label: plain(block.spans), anchor: block.anchor, level: block.level }));
184
+ return { blocks, outline, links, anchors };
185
+ };
package/dist/node.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { build } from "./build.js";
2
+ export type { Built } from "./build.js";
3
+ export { check } from "./check.js";
4
+ export type { Checked } from "./check.js";
5
+ export { findRoot, loadConfig } from "./config.js";
6
+ export type { Config } from "./config.js";
7
+ export { dev } from "./dev.js";
8
+ export type { Dev, DevOptions } from "./dev.js";
9
+ export { AppError, format } from "./diagnostics.js";
10
+ export type { Diagnostic } from "./diagnostics.js";
package/dist/node.js ADDED
@@ -0,0 +1,6 @@
1
+ // The tooling as a library: what the `valance` command runs, for tests, scripts and other tools. Node only.
2
+ export { build } from "./build.js";
3
+ export { check } from "./check.js";
4
+ export { findRoot, loadConfig } from "./config.js";
5
+ export { dev } from "./dev.js";
6
+ export { AppError, format } from "./diagnostics.js";
@@ -0,0 +1,17 @@
1
+ export interface TagUse {
2
+ /** The tag, as written: `theme-toggle`, `button`, `mesh-slot`. */
3
+ readonly name: string;
4
+ /** Where the `<` is, from the start of the source. */
5
+ readonly offset: number;
6
+ }
7
+ export interface Outline {
8
+ /** Every element written, in order, repeats included. */
9
+ readonly tags: ReadonlyArray<TagUse>;
10
+ /** The names of the slots the template places (`""` is the default slot). */
11
+ readonly slots: ReadonlyArray<string>;
12
+ /** The slots its elements fill (`<mesh-fill slot="footer">`). */
13
+ readonly fills: ReadonlyArray<string>;
14
+ }
15
+ export declare const outline: (source: string) => Outline;
16
+ /** The distinct tag names a template writes, with the offset of the first. */
17
+ export declare const tagsOf: (source: string) => ReadonlyMap<string, number>;
@@ -0,0 +1,88 @@
1
+ // A read-only outline of a template's source: the tags it writes, and the slots it places and fills. It is a scanner, not the MESH parser: it knows only where an element starts, which is enough to
2
+ // answer "which components does this template use" without compiling it (the compile needs the answer to build the model it compiles against). Expressions in `{…}`, quoted values and comments
3
+ // are skipped, so `{a <b}` or `title="<x>"` is never read as a tag.
4
+ //
5
+ // MESH owns the language, and a parser in its package is where this belongs; until that exists, this is the one place in VALANCE that looks inside a template's text.
6
+ const NAME = /[a-z][a-z0-9-]*/y;
7
+ const LITERAL = (attribute) => new RegExp(`${attribute}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`);
8
+ /** The offset just past the `}` that closes the `{` at `open`; strings inside are respected. */
9
+ const afterBrace = (source, open) => {
10
+ let depth = 0;
11
+ for (let at = open; at < source.length; at += 1) {
12
+ const char = source[at];
13
+ if (char === '"' || char === "'" || char === "`") {
14
+ at += 1;
15
+ while (at < source.length && source[at] !== char) {
16
+ at += source[at] === "\\" ? 2 : 1;
17
+ }
18
+ }
19
+ else if (char === "{") {
20
+ depth += 1;
21
+ }
22
+ else if (char === "}" && --depth === 0) {
23
+ return at + 1;
24
+ }
25
+ }
26
+ return source.length;
27
+ };
28
+ /** The offset of the `>` that ends the start tag whose attributes begin at `from` (quotes and `{…}` respected), or the end of the source. */
29
+ const endOfTag = (source, from) => {
30
+ for (let at = from; at < source.length; at += 1) {
31
+ const char = source[at];
32
+ if (char === '"' || char === "'") {
33
+ at = source.indexOf(char, at + 1);
34
+ if (at < 0) {
35
+ return source.length;
36
+ }
37
+ }
38
+ else if (char === "{") {
39
+ at = afterBrace(source, at) - 1;
40
+ }
41
+ else if (char === ">") {
42
+ return at;
43
+ }
44
+ }
45
+ return source.length;
46
+ };
47
+ export const outline = (source) => {
48
+ const tags = [];
49
+ const slots = [];
50
+ const fills = [];
51
+ for (let at = 0; at < source.length; at += 1) {
52
+ const char = source[at];
53
+ if (char === "{") {
54
+ at = afterBrace(source, at) - 1;
55
+ }
56
+ else if (char === "<" && source.startsWith("<!--", at)) {
57
+ const end = source.indexOf("-->", at + 4);
58
+ at = end < 0 ? source.length : end + 2;
59
+ }
60
+ else if (char === "<") {
61
+ NAME.lastIndex = at + 1;
62
+ const name = NAME.exec(source)?.[0];
63
+ if (name === undefined) {
64
+ continue;
65
+ }
66
+ const end = endOfTag(source, at + 1 + name.length);
67
+ const start = source.slice(at, end);
68
+ tags.push({ name, offset: at });
69
+ if (name === "mesh-slot" || name === "mesh-fill") {
70
+ const attribute = LITERAL(name === "mesh-slot" ? "name" : "slot").exec(start);
71
+ (name === "mesh-slot" ? slots : fills).push(attribute?.[1] ?? attribute?.[2] ?? "");
72
+ }
73
+ // Past the tag's name: its attributes may hold `{…}`, which the next turn of the loop skips, but a `<` in a quoted value is not an element.
74
+ at = Math.max(at, end - 1);
75
+ }
76
+ }
77
+ return { tags, slots, fills };
78
+ };
79
+ /** The distinct tag names a template writes, with the offset of the first. */
80
+ export const tagsOf = (source) => {
81
+ const first = new Map();
82
+ for (const tag of outline(source).tags) {
83
+ if (!first.has(tag.name)) {
84
+ first.set(tag.name, tag.offset);
85
+ }
86
+ }
87
+ return first;
88
+ };
@@ -0,0 +1,8 @@
1
+ export declare const GENERATED_DIR = ".valance";
2
+ export declare const OUTPUT_DIR = "dist";
3
+ export declare const ENTRY_FILE = "src/main.ts";
4
+ export declare const generated: (root: string, name: string) => string;
5
+ /** Writes `text` unless the file already says exactly that: a watcher that sees a write should only see a change. Returns whether it wrote. */
6
+ export declare const writeIfChanged: (path: string, text: string) => boolean;
7
+ /** Keeps `.valance/` out of version control without asking the author to remember: a `.gitignore` inside it. */
8
+ export declare const ensureGeneratedDir: (root: string) => void;
@@ -0,0 +1,20 @@
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/`.
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ export const GENERATED_DIR = ".valance";
5
+ export const OUTPUT_DIR = "dist";
6
+ export const ENTRY_FILE = "src/main.ts";
7
+ export const generated = (root, name) => join(root, GENERATED_DIR, name);
8
+ /** Writes `text` unless the file already says exactly that: a watcher that sees a write should only see a change. Returns whether it wrote. */
9
+ export const writeIfChanged = (path, text) => {
10
+ if (existsSync(path) && readFileSync(path, "utf8") === text) {
11
+ return false;
12
+ }
13
+ mkdirSync(dirname(path), { recursive: true });
14
+ writeFileSync(path, text);
15
+ return true;
16
+ };
17
+ /** Keeps `.valance/` out of version control without asking the author to remember: a `.gitignore` inside it. */
18
+ export const ensureGeneratedDir = (root) => {
19
+ writeIfChanged(generated(root, ".gitignore"), "# Generated by `valance`. Never edited, never committed.\n*\n");
20
+ };
@@ -0,0 +1,9 @@
1
+ export interface Target {
2
+ /** The flag (`--web`) and the name in `valance.<id>.json`. */
3
+ readonly id: "web";
4
+ readonly label: string;
5
+ readonly hint: string;
6
+ }
7
+ export declare const TARGETS: ReadonlyArray<Target>;
8
+ export type TargetId = Target["id"];
9
+ export declare const isTarget: (name: string) => name is TargetId;
@@ -0,0 +1,2 @@
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)" }];
2
+ export const isTarget = (name) => TARGETS.some((each) => each.id === name);
package/dist/tree.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { Schema } from "effect";
2
+ import type { Component } from "@valancex/common";
3
+ export interface Place {
4
+ readonly file: string;
5
+ readonly line: number;
6
+ readonly column: number;
7
+ }
8
+ export interface Node {
9
+ /** The tag: the template's file name without `.mprx`. */
10
+ readonly name: string;
11
+ /** The template, relative to the project. */
12
+ readonly file: string;
13
+ /** The interface the component declares: present when it declares one (an omitted side is none), absent when its contract is inferred from the template and where it is used. */
14
+ readonly interface?: {
15
+ readonly props: Readonly<Record<string, Schema.Schema<any, any, never>>>;
16
+ readonly events: Readonly<Record<string, Schema.Schema<any, any, never>>>;
17
+ };
18
+ /** The declared components its template writes. */
19
+ readonly uses: ReadonlyArray<string>;
20
+ /** Who uses it: the components whose templates write its tag; `"(application)"` for the layout and for an override. */
21
+ readonly parents: ReadonlyArray<string>;
22
+ }
23
+ export declare const APPLICATION = "(application)";
24
+ export declare const nameOf: (file: string) => string;
25
+ /** Every component the definition declares, once each by name: `layout`, each `override`, then each of `components`. Who uses whom is read from the templates (`linked`). */
26
+ export declare const treeOf: (layout: Component | undefined, overrides: ReadonlyArray<Component>, components?: ReadonlyArray<Component>) => ReadonlyArray<Node>;
27
+ /**
28
+ * The tree with who uses whom filled in: `usesOf` gives the tags each component's template writes. A component uses the declared components its template writes (not itself); the layout and each
29
+ * override are also the application's.
30
+ */
31
+ export declare const linked: (nodes: ReadonlyArray<Node>, layout: Component | undefined, overrides: ReadonlyArray<Component>, usesOf: (name: string) => ReadonlyArray<string>) => ReadonlyArray<Node>;
32
+ export declare const placeIn: (text: string, offset: number, file: string) => Place;
33
+ export interface Site {
34
+ /** The file the site is in, relative to the project. */
35
+ readonly file: string;
36
+ /** The `import x from "./the.mprx"` line. */
37
+ readonly imported?: Place;
38
+ /** The `component(x, …)` call that defines the component. */
39
+ readonly call?: Place;
40
+ /** Where a prop or an event is declared in that call: its key in `props: { … }` or `events: { … }`. */
41
+ readonly member: (kind: "props" | "events", name: string) => Place | undefined;
42
+ }
43
+ /**
44
+ * Where each template's component is defined: the `component(template, …)` call in the project's TypeScript, wherever it is written (the entry file, or a file of its own beside the template), and the
45
+ * keys of its contract. A text search for pointing, not a parse: what it cannot find it leaves out, and callers fall back to the template.
46
+ */
47
+ export declare const declarationsOf: (root: string) => ReadonlyMap<string, Site>;