@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/dev.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { Diagnostic } from "./diagnostics.js";
2
+ export interface DevOptions {
3
+ readonly port?: number;
4
+ readonly host?: string;
5
+ /** Called with each report (a rebuild, a mistake); defaults to the console. */
6
+ readonly log?: (line: string) => void;
7
+ /** Called with every mistake still open, after the first build and after each rebuild: empty when the project is sound. */
8
+ readonly onStatus?: (open: ReadonlyArray<Diagnostic>) => void;
9
+ }
10
+ export interface Dev {
11
+ readonly url: string;
12
+ /** Resolves when a change currently being handled has been applied. For tests and tools. */
13
+ readonly settled: () => Promise<void>;
14
+ readonly close: () => Promise<void>;
15
+ }
16
+ type Stage = "content" | "program" | "entries" | "reload" | "restart";
17
+ /** What a changed file (relative to the project, `/` separated) asks for. `event` is what happened to it: a stylesheet's edit is the page's to apply, its arrival or removal changes the entry. */
18
+ export declare const stagesFor: (file: string, event?: "change" | "add" | "unlink") => ReadonlyArray<Stage>;
19
+ /** Starts the development server for the project at `root`. */
20
+ export declare const dev: (root: string, options?: DevOptions) => Promise<Dev>;
21
+ export {};
package/dist/dev.js ADDED
@@ -0,0 +1,250 @@
1
+ import { createServer } from "vite";
2
+ import { relative, sep } from "node:path";
3
+ import { loadAuthor } from "./author.js";
4
+ import { loadConfig, PROJECT_FILE, WEB_FILE } from "./config.js";
5
+ import { AppError, format } from "./diagnostics.js";
6
+ import { generateContent, generateEntries, generateProgram } from "./emit.js";
7
+ import { GENERATED_DIR, OUTPUT_DIR } from "./project.js";
8
+ import { baseConfig, SERVER_ENTRY } from "./vite.js";
9
+ /** How long files must be quiet before a change is applied (ms). */
10
+ const QUIET = 60;
11
+ /** What a changed file (relative to the project, `/` separated) asks for. `event` is what happened to it: a stylesheet's edit is the page's to apply, its arrival or removal changes the entry. */
12
+ export const stagesFor = (file, event = "change") => {
13
+ if (file === PROJECT_FILE || file === WEB_FILE) {
14
+ return ["restart"];
15
+ }
16
+ if (file.startsWith("content/")) {
17
+ return ["content"];
18
+ }
19
+ if (file.startsWith("public/")) {
20
+ return ["reload"];
21
+ }
22
+ if (file === "src/main.ts") {
23
+ return event === "change" ? ["program"] : ["entries", "program"];
24
+ }
25
+ if (file.startsWith("src/") && file.endsWith(".mprx")) {
26
+ return ["program"];
27
+ }
28
+ if (file.startsWith("src/") && file.endsWith(".css")) {
29
+ return event === "change" ? [] : ["entries"];
30
+ } // an edit is style-updated in the open page by the bundler
31
+ return [];
32
+ };
33
+ const overlay = (diagnostics) => {
34
+ const first = diagnostics[0];
35
+ return {
36
+ type: "error",
37
+ err: {
38
+ message: diagnostics.map(format).join("\n"),
39
+ stack: "",
40
+ plugin: "valance",
41
+ id: first.file,
42
+ loc: { file: first.file, line: first.line, column: first.column },
43
+ },
44
+ };
45
+ };
46
+ const start = async (root, config, options, restart) => {
47
+ const log = options.log ??
48
+ ((line) => {
49
+ console.log(line);
50
+ });
51
+ const failed = new Map();
52
+ let pending = Promise.resolve();
53
+ generateEntries(root);
54
+ const base = baseConfig(root, config);
55
+ const server = await createServer({
56
+ ...base,
57
+ server: { port: options.port ?? 5173, host: options.host ?? "127.0.0.1", strictPort: false, fs: { allow: [root] } },
58
+ plugins: [
59
+ ...(base.plugins ?? []),
60
+ {
61
+ name: "valance",
62
+ configureServer: (vite) => () => {
63
+ // After Vite's own middleware: modules, styles, public files and the client have had their chance. What is left is a page.
64
+ vite.middlewares.use((request, response, next) => {
65
+ void (async () => {
66
+ // Vite has already removed the base from `request.url`; the application's addresses include it.
67
+ const url = new URL(request.originalUrl ?? request.url ?? "/", "http://localhost");
68
+ if ((request.method !== "GET" && request.method !== "HEAD") || !url.pathname.startsWith(config.web.base) || /\.[A-Za-z0-9]+$/.test(url.pathname)) {
69
+ next();
70
+ return;
71
+ }
72
+ try {
73
+ const { render } = (await vite.ssrLoadModule(SERVER_ENTRY));
74
+ const { status, html } = await render(url.pathname, { script: `${config.web.base}${GENERATED_DIR}/client.ts`, styles: [] });
75
+ response.statusCode = status;
76
+ response.setHeader("content-type", "text/html; charset=utf-8");
77
+ response.end(await vite.transformIndexHtml(url.pathname, html));
78
+ }
79
+ catch (error) {
80
+ vite.ssrFixStacktrace(error);
81
+ next(error);
82
+ }
83
+ })();
84
+ });
85
+ },
86
+ },
87
+ ],
88
+ });
89
+ await server.listen();
90
+ const rebuild = async (stages) => {
91
+ const run = async (name, work) => {
92
+ try {
93
+ await work();
94
+ if (failed.delete(name)) {
95
+ log(`vlx: ${name} is fine again`);
96
+ }
97
+ }
98
+ catch (error) {
99
+ if (!(error instanceof AppError)) {
100
+ throw error;
101
+ }
102
+ failed.set(name, error.diagnostics);
103
+ error.diagnostics.forEach((each) => {
104
+ log(format(each));
105
+ });
106
+ }
107
+ };
108
+ const wasFailing = failed.size > 0;
109
+ if (stages.includes("entries")) {
110
+ generateEntries(root);
111
+ }
112
+ if (stages.includes("content")) {
113
+ await run("content", () => generateContent(root, config));
114
+ }
115
+ if (stages.includes("program")) {
116
+ await run("templates", async () => generateProgram(root, await loadAuthor(server, root)));
117
+ }
118
+ const open = [...failed.values()].flat();
119
+ options.onStatus?.(open);
120
+ // A generated file that changes reloads the page by itself (the bundler watches `.valance/`). What it cannot know: a fix that rebuilds to the same files still has to clear the overlay,
121
+ // and a public file is not part of any module.
122
+ if (open.length > 0) {
123
+ server.ws.send(overlay(open));
124
+ }
125
+ else if (wasFailing || stages.includes("reload")) {
126
+ server.ws.send({ type: "full-reload", path: "*" });
127
+ }
128
+ };
129
+ // Changes are applied once the files have been quiet for a moment: an editor that saves twice, or a tool that writes several files, is one change, and the file is read after the last write.
130
+ // (The watcher itself drops a repeated event for the same file inside ~50 ms, so reading at once could read the first write and never hear about the second.)
131
+ const queued = new Map();
132
+ let timer;
133
+ const flush = () => {
134
+ const batch = [...queued];
135
+ queued.clear();
136
+ const stages = new Set(batch.flatMap(([file, event]) => stagesFor(file, event)));
137
+ if (stages.size === 0) {
138
+ return;
139
+ }
140
+ log(`vlx: ${batch.map(([file]) => file).join(", ")} changed`);
141
+ pending = pending
142
+ .then(async () => {
143
+ if (stages.has("restart")) {
144
+ await restart();
145
+ return;
146
+ }
147
+ await rebuild([...stages]);
148
+ })
149
+ .catch((error) => {
150
+ log(`vlx: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
151
+ });
152
+ };
153
+ const onChange = (event) => (path) => {
154
+ const file = relative(root, path).split(sep).join("/");
155
+ if (file.startsWith(`${GENERATED_DIR}/`) ||
156
+ file.startsWith(`${OUTPUT_DIR}/`) ||
157
+ file.startsWith("node_modules/") ||
158
+ file.startsWith("..") ||
159
+ stagesFor(file, event).length === 0) {
160
+ return;
161
+ }
162
+ queued.set(file, event);
163
+ clearTimeout(timer);
164
+ timer = setTimeout(flush, QUIET);
165
+ };
166
+ for (const event of ["change", "add", "unlink"]) {
167
+ server.watcher.on(event, onChange(event));
168
+ }
169
+ // First build: a mistake here is reported like any other and the server still starts, so the overlay can show it.
170
+ for (const [name, work] of [
171
+ ["content", () => generateContent(root, config)],
172
+ ["templates", async () => generateProgram(root, await loadAuthor(server, root))],
173
+ ]) {
174
+ try {
175
+ await work();
176
+ }
177
+ catch (error) {
178
+ if (!(error instanceof AppError)) {
179
+ throw error;
180
+ }
181
+ failed.set(name, error.diagnostics);
182
+ error.diagnostics.forEach((each) => {
183
+ log(format(each));
184
+ });
185
+ }
186
+ }
187
+ options.onStatus?.([...failed.values()].flat());
188
+ // The server's own address already includes the base.
189
+ const address = server.resolvedUrls?.local[0] ?? `http://${options.host ?? "127.0.0.1"}:${options.port ?? 5173}${config.web.base}`;
190
+ return {
191
+ server,
192
+ url: address,
193
+ settled: async () => {
194
+ await new Promise((resolve) => setTimeout(resolve, QUIET * 2));
195
+ await pending;
196
+ },
197
+ close: () => server.close(),
198
+ };
199
+ };
200
+ /** Starts the development server for the project at `root`. */
201
+ export const dev = async (root, options = {}) => {
202
+ let current;
203
+ const log = options.log ??
204
+ ((line) => {
205
+ console.log(line);
206
+ });
207
+ const restart = async () => {
208
+ log("vlx: the configuration changed; restarting");
209
+ await current.close();
210
+ try {
211
+ const config = loadConfig(root);
212
+ current = await start(root, config, { ...options, port: portOf(current) }, restart);
213
+ log(`vlx: serving ${current.url}`);
214
+ }
215
+ catch (error) {
216
+ if (!(error instanceof AppError)) {
217
+ throw error;
218
+ }
219
+ error.diagnostics.forEach((each) => {
220
+ log(format(each));
221
+ });
222
+ log("vlx: fix the configuration and save it; the server is stopped until then");
223
+ waitForConfig();
224
+ }
225
+ };
226
+ const waitForConfig = () => {
227
+ // The server is down, so nothing is watching: poll the two files until the configuration loads.
228
+ const timer = setInterval(() => {
229
+ try {
230
+ loadConfig(root);
231
+ }
232
+ catch {
233
+ return;
234
+ }
235
+ clearInterval(timer);
236
+ void restart();
237
+ }, 500);
238
+ timer.unref();
239
+ };
240
+ const config = loadConfig(root);
241
+ current = await start(root, config, options, restart);
242
+ return {
243
+ get url() {
244
+ return current.url;
245
+ },
246
+ settled: () => current.settled(),
247
+ close: () => current.close(),
248
+ };
249
+ };
250
+ const portOf = (running) => Number(new URL(running.url).port);
@@ -0,0 +1,16 @@
1
+ export interface Diagnostic {
2
+ readonly file: string;
3
+ readonly line: number;
4
+ readonly column: number;
5
+ readonly code: string;
6
+ readonly message: string;
7
+ }
8
+ /** A failure that carries the diagnostics it is made of. */
9
+ export declare class AppError extends Error {
10
+ readonly diagnostics: ReadonlyArray<Diagnostic>;
11
+ constructor(diagnostics: ReadonlyArray<Diagnostic>);
12
+ }
13
+ export declare const format: ({ file, line, column, code, message }: Diagnostic) => string;
14
+ export declare const diagnostic: (file: string, line: number, code: string, message: string, column?: number) => Diagnostic;
15
+ /** The 1-based line of the first occurrence of `needle` in `source` (1 when absent), for a diagnostic about a JSON key. */
16
+ export declare const lineOf: (source: string, needle: string) => number;
@@ -0,0 +1,16 @@
1
+ /** A failure that carries the diagnostics it is made of. */
2
+ export class AppError extends Error {
3
+ diagnostics;
4
+ constructor(diagnostics) {
5
+ super(diagnostics.map(format).join("\n"));
6
+ this.name = "AppError";
7
+ this.diagnostics = diagnostics;
8
+ }
9
+ }
10
+ export const format = ({ file, line, column, code, message }) => `${file}${line > 0 ? `:${line}${column > 0 ? `:${column}` : ""}` : ""} ${code}: ${message}`;
11
+ export const diagnostic = (file, line, code, message, column = 0) => ({ file, line, column, code, message });
12
+ /** The 1-based line of the first occurrence of `needle` in `source` (1 when absent), for a diagnostic about a JSON key. */
13
+ export const lineOf = (source, needle) => {
14
+ const at = source.indexOf(needle);
15
+ return at < 0 ? 1 : source.slice(0, at).split("\n").length;
16
+ };
package/dist/emit.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Config } from "./config.js";
2
+ import type { DefinedApp } from "@valancex/common";
3
+ import type { Content } from "@valancex/common/internal";
4
+ export declare const hasEntry: (root: string) => boolean;
5
+ export declare const writeContent: (root: string, content: Content) => boolean;
6
+ /** The content stage alone: it does not need the author's code. */
7
+ export declare const generateContent: (root: string, config: Config) => Content;
8
+ /** The templates stage: needs the author's definition (the manifest derives from its state and commands). */
9
+ export declare const generateProgram: (root: string, author: DefinedApp | undefined) => Promise<ReadonlyArray<string>>;
10
+ /** The declaration of one template's type: its props and events as the names TypeScript checks a contract's keys against. */
11
+ export declare const templateType: (props: ReadonlyArray<string>, events: ReadonlyArray<string>) => string;
12
+ /** The entries; rewritten when `src/main.ts` or a stylesheet is added or removed. */
13
+ export declare const generateEntries: (root: string) => boolean;
package/dist/emit.js ADDED
@@ -0,0 +1,107 @@
1
+ // Build time, in Node: turns the authored files into the generated ones under `.valance/`.
2
+ //
3
+ // content.json the content model (from content/)
4
+ // program.json the compiled templates (from src/*.mprx and the built-in views)
5
+ // manifest.json every tag, prop, scope name and command a template may use, inferred components included (for editors; nothing reads it back)
6
+ // editor.json where the manifest is and which file is which component (for editors)
7
+ // wiring.json where every name a template uses comes from, and what uses it, at both ends (for the editor: definitions and CodeLens)
8
+ // types/ for each of the author's templates (`types/theme-toggle.d.mprx.ts`), a type for its props and events (from the compiled model, declared or inferred), for TypeScript. With
9
+ // `"allowArbitraryExtensions": true` and `"rootDirs": ["src", ".valance/types"]` in the project's tsconfig.json, importing a template gives that type, and a `component(…)`
10
+ // contract that disagrees with the template's keys is a compile-time error. Without them templates are untyped. It follows the last check, dev or build run, and is stale
11
+ // until the next one.
12
+ // client.ts the browser entry: imports the above, the author's `src/main.ts` and every stylesheet under src/
13
+ // server.ts the server entry: renders a pathname to a document
14
+ //
15
+ // The two entries are real files, not virtual modules, so an author can read exactly what runs.
16
+ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
17
+ import { join, relative, sep } from "node:path";
18
+ import { compileViews, PACKAGE_DIR, SOURCE_DIR } from "./compile.js";
19
+ import { readContent } from "./content.js";
20
+ import { ENTRY_FILE, ensureGeneratedDir, GENERATED_DIR, generated, writeIfChanged } from "./project.js";
21
+ import { buildWiring } from "./wiring.js";
22
+ const stylesheets = (root) => existsSync(join(root, SOURCE_DIR))
23
+ ? readdirSync(join(root, SOURCE_DIR), { recursive: true, encoding: "utf8" })
24
+ .filter((file) => file.endsWith(".css"))
25
+ .sort()
26
+ .map((file) => `${SOURCE_DIR}/${file}`)
27
+ : [];
28
+ export const hasEntry = (root) => existsSync(join(root, ENTRY_FILE));
29
+ const clientSource = (root) => `// Generated by \`valance\`. Do not edit: it is rewritten whenever the project changes.
30
+ // @ts-nocheck
31
+ import { start } from "@valancex/common/client";
32
+ import content from "./content.json";
33
+ import program from "./program.json";
34
+ ${hasEntry(root) ? `import author from "../${ENTRY_FILE}";\n` : "const author = undefined;\n"}${stylesheets(root)
35
+ .map((file) => `import "../${file}";\n`)
36
+ .join("")}
37
+ await start({ content, program, author });
38
+ `;
39
+ const serverSource = (root) => `// Generated by \`valance\`. Do not edit: it is rewritten whenever the project changes.
40
+ // @ts-nocheck
41
+ import { createRenderer, pathsOf } from "@valancex/common/server";
42
+ import content from "./content.json";
43
+ import program from "./program.json";
44
+ ${hasEntry(root) ? `import author from "../${ENTRY_FILE}";\n` : "const author = undefined;\n"}
45
+ export const render = createRenderer({ content, program, author });
46
+ export const paths = () => pathsOf(content);
47
+ export { content };
48
+ `;
49
+ export const writeContent = (root, content) => writeIfChanged(generated(root, "content.json"), `${JSON.stringify(content, null, 2)}\n`);
50
+ /** The content stage alone: it does not need the author's code. */
51
+ export const generateContent = (root, config) => {
52
+ ensureGeneratedDir(root);
53
+ const content = readContent(root, config);
54
+ writeContent(root, content);
55
+ return content;
56
+ };
57
+ /** The templates stage: needs the author's definition (the manifest derives from its state and commands). */
58
+ export const generateProgram = async (root, author) => {
59
+ ensureGeneratedDir(root);
60
+ const compiled = await compileViews(root, author);
61
+ writeIfChanged(generated(root, "program.json"), `${JSON.stringify(compiled.program)}\n`);
62
+ writeIfChanged(generated(root, "manifest.json"), `${JSON.stringify(compiled.manifest, null, 2)}\n`);
63
+ // The one file an editor reads to know the project: where the manifest is, and which file is the template of which component. The conventions stay here, not copied into every editor.
64
+ writeIfChanged(generated(root, "editor.json"), `${JSON.stringify({ version: 1, model: `${GENERATED_DIR}/manifest.json`, components: compiled.files, wiring: `${GENERATED_DIR}/wiring.json` }, null, 2)}\n`);
65
+ writeTemplateTypes(root, compiled);
66
+ // Where every name came from and what uses it, at both ends, for the editor. The framework's own names point into the README that ships with the package.
67
+ const readme = join(PACKAGE_DIR, "README.md");
68
+ const docs = existsSync(readme) ? { file: relative(root, readme).split(sep).join("/"), lines: readFileSync(readme, "utf8").split("\n") } : undefined;
69
+ writeIfChanged(generated(root, "wiring.json"), `${JSON.stringify(buildWiring(root, author, compiled, docs), null, 2)}\n`);
70
+ return compiled.components;
71
+ };
72
+ const union = (names) => (names.length === 0 ? "never" : names.map((name) => JSON.stringify(name)).join(" | "));
73
+ /** The declaration of one template's type: its props and events as the names TypeScript checks a contract's keys against. */
74
+ export const templateType = (props, events) => `// Generated by \`vlx\`. Do not edit: it is rewritten by every check, dev or build run, and is stale until the next one.
75
+ declare const template: import("@valancex/common").Template<${union(props)}, ${union(events)}>;
76
+ export default template;
77
+ `;
78
+ /**
79
+ * Where a template's type is written: under `.valance/types/`, named so that `rootDirs` makes it the declaration of the template beside which it would sit, and `allowArbitraryExtensions` reads it
80
+ * as the declaration of a `.mprx` file (`src/a/b.mprx` → `types/a/b.d.mprx.ts`).
81
+ */
82
+ const typePath = (file) => `types/${file.replace(new RegExp(`^${SOURCE_DIR}/`), "").replace(/\.mprx$/, ".d.mprx.ts")}`;
83
+ /** Writes the type of each of the author's templates, and removes the type of one that is gone. */
84
+ const writeTemplateTypes = (root, compiled) => {
85
+ const components = compiled.manifest
86
+ .components;
87
+ const written = new Set();
88
+ for (const template of compiled.templates.filter((each) => !each.builtIn)) {
89
+ const entry = components[template.component];
90
+ const path = typePath(template.file);
91
+ written.add(path);
92
+ writeIfChanged(generated(root, path), templateType(Object.keys(entry?.props ?? {}), Object.keys(entry?.events ?? {})));
93
+ }
94
+ const dir = generated(root, "types");
95
+ if (existsSync(dir)) {
96
+ for (const file of readdirSync(dir, { recursive: true, encoding: "utf8" }).filter((each) => each.endsWith(".d.mprx.ts"))) {
97
+ if (!written.has(`types/${file.split(sep).join("/")}`)) {
98
+ rmSync(join(dir, file), { force: true });
99
+ }
100
+ }
101
+ }
102
+ };
103
+ /** The entries; rewritten when `src/main.ts` or a stylesheet is added or removed. */
104
+ export const generateEntries = (root) => {
105
+ ensureGeneratedDir(root);
106
+ return [writeIfChanged(generated(root, "client.ts"), clientSource(root)), writeIfChanged(generated(root, "server.ts"), serverSource(root))].some(Boolean);
107
+ };
package/dist/init.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ import type { TargetId } from "./targets.js";
2
+ export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
3
+ export declare const PACKAGE_MANAGERS: ReadonlyArray<PackageManager>;
4
+ export interface Answers {
5
+ /** The application's name, as people read it (the site's title). */
6
+ readonly name: string;
7
+ /** Where it is written: absolute. */
8
+ readonly dir: string;
9
+ readonly targets: ReadonlyArray<TargetId>;
10
+ /** The web target's settings; an empty `base` or `url` is the default. */
11
+ readonly web: {
12
+ readonly base: string;
13
+ readonly url: string;
14
+ };
15
+ readonly packageManager: PackageManager;
16
+ readonly install: boolean;
17
+ readonly git: boolean;
18
+ /** What `@valancex/cli` is in the new project's package.json; `undefined` is "this package, as it is installed". */
19
+ readonly cli?: string;
20
+ }
21
+ /** The package name an application gets: lowercase, URL-safe. */
22
+ export declare const packageName: (name: string) => string;
23
+ /** A reason the name cannot be used, or `undefined`. The directory is checked separately (it may be a path). */
24
+ export declare const nameProblem: (name: string) => string | undefined;
25
+ /** 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`. */
26
+ export declare const directoryFor: (name: string, cwd: string) => string;
27
+ /** The title a name gives the site: a path's last part, as written. */
28
+ export declare const titleOf: (name: string) => string;
29
+ /** What is in the way of writing into `dir`: nothing (it is absent or empty), or a sentence. */
30
+ export declare const directoryProblem: (dir: string) => string | undefined;
31
+ /** 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. */
32
+ export declare const detectPackageManager: (env?: NodeJS.ProcessEnv) => PackageManager;
33
+ export declare const runCommand: (manager: PackageManager) => string;
34
+ /** 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). */
35
+ export declare const cliSpec: (manager: PackageManager, given?: string) => string;
36
+ /**
37
+ * 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
38
+ * `cliSpec`.
39
+ */
40
+ export declare const commonSpec: (manager: PackageManager) => string;
41
+ export interface Plan {
42
+ /** Relative path (`/` separated) → text. */
43
+ readonly files: ReadonlyMap<string, string>;
44
+ }
45
+ /** Every file the answers make, without writing any. */
46
+ export declare const plan: (answers: Answers) => Plan;
47
+ /** Writes the application. Throws if the directory has something in it. Returns the files written, relative and sorted. */
48
+ export declare const scaffold: (answers: Answers) => ReadonlyArray<string>;
49
+ export interface Ran {
50
+ readonly ok: boolean;
51
+ /** What the command printed to stderr, the last lines of it: why it failed. */
52
+ readonly output: string;
53
+ }
54
+ /** Runs `command args` in `cwd`, quietly. A `.cmd` shim on Windows needs a shell; nothing here takes user text as an argument. */
55
+ export declare const run: (command: string, args: ReadonlyArray<string>, cwd: string) => Promise<Ran>;
56
+ export declare const install: (answers: Pick<Answers, "dir" | "packageManager">) => Promise<Ran>;
57
+ /** `git init` and a first commit's worth of staging is not ours to do: the repository is made, the author commits. */
58
+ export declare const initGit: (dir: string) => Promise<Ran>;
59
+ export type Step = "files" | "install" | "git";
60
+ export type Progress = "start" | "done" | "skipped" | "failed";
61
+ export interface Outcome {
62
+ readonly files: ReadonlyArray<string>;
63
+ readonly installed: boolean;
64
+ readonly git: boolean;
65
+ /** Why an install or `git init` did not happen, for the person to read. */
66
+ readonly notes: ReadonlyArray<string>;
67
+ }
68
+ /** 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. */
69
+ export declare const execute: (answers: Answers, report?: (step: Step, progress: Progress, detail?: string) => void) => Promise<Outcome>;