@valancex/cli 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -9
  3. package/dist/add.d.ts +9 -1
  4. package/dist/add.js +68 -5
  5. package/dist/archive.d.ts +17 -0
  6. package/dist/archive.js +111 -0
  7. package/dist/args.js +2 -2
  8. package/dist/build.d.ts +3 -0
  9. package/dist/build.js +98 -29
  10. package/dist/check.js +2 -2
  11. package/dist/cli.js +59 -16
  12. package/dist/compile.d.ts +13 -4
  13. package/dist/compile.js +46 -19
  14. package/dist/config.d.ts +14 -0
  15. package/dist/config.js +51 -4
  16. package/dist/content.js +2 -1
  17. package/dist/dev-desktop.d.ts +8 -0
  18. package/dist/dev-desktop.js +114 -0
  19. package/dist/dev.d.ts +3 -0
  20. package/dist/dev.js +41 -6
  21. package/dist/emit.d.ts +7 -4
  22. package/dist/emit.js +105 -27
  23. package/dist/init.js +14 -4
  24. package/dist/markdown.js +2 -1
  25. package/dist/package.d.ts +18 -0
  26. package/dist/package.js +141 -0
  27. package/dist/project.js +1 -1
  28. package/dist/security.d.ts +4 -0
  29. package/dist/security.js +35 -0
  30. package/dist/targets.d.ts +9 -1
  31. package/dist/targets.js +28 -1
  32. package/dist/ui/init.js +4 -4
  33. package/dist/ui/run.d.ts +3 -1
  34. package/dist/ui/run.js +5 -3
  35. package/dist/vite.d.ts +3 -2
  36. package/dist/vite.js +3 -3
  37. package/package.json +7 -3
  38. package/schemas/valance.desktop.schema.json +17 -0
  39. package/schemas/valance.schema.json +22 -4
  40. package/schemas/valance.web.schema.json +46 -4
  41. package/templates/desktop/README.md +14 -0
  42. package/templates/desktop/content/404.md +7 -0
  43. package/templates/desktop/content/guide/getting-started.md +15 -0
  44. package/templates/desktop/content/index.md +14 -0
  45. package/templates/desktop/gitignore +4 -0
  46. package/templates/desktop/src/layout.mprx +23 -0
  47. package/templates/desktop/src/main.ts +15 -0
  48. package/templates/desktop/tsconfig.json +18 -0
  49. package/templates/multi/README.md +13 -0
  50. package/templates/multi/content/404.md +7 -0
  51. package/templates/multi/content/guide/getting-started.md +15 -0
  52. package/templates/multi/content/index.md +14 -0
  53. package/templates/multi/src/desktop/layout.mprx +21 -0
  54. package/templates/multi/src/main.ts +23 -0
  55. package/templates/multi/src/web/layout.mprx +27 -0
  56. package/templates/multi/src/web/styles.css +28 -0
  57. package/templates/multi/src/web/theme-toggle.mprx +1 -0
  58. package/templates/multi/src/web/theme-toggle.ts +10 -0
  59. package/templates/web/content/guide/getting-started.md +1 -1
  60. package/views/block.mprx +1 -1
  61. package/views-desktop/block.mprx +3 -0
  62. package/views-desktop/blocks.mprx +3 -0
  63. package/views-desktop/inline.mprx +3 -0
  64. package/views-desktop/layout.mprx +20 -0
  65. package/views-desktop/list-block.mprx +3 -0
  66. package/views-desktop/list-items.mprx +3 -0
  67. package/views-desktop/runs.mprx +3 -0
  68. /package/views/{heading.mprx → heading-block.mprx} +0 -0
package/dist/build.js CHANGED
@@ -1,48 +1,77 @@
1
- // `vlx build`: the project → a static site in `dist/`. The stages, in order: configuration, content, templates, the browser bundle, then one document per page (and a 404 page, a sitemap
2
- // when the site has a published address). Everything is generated under `.valance/` first; `dist/` holds only the site.
1
+ // `vlx build`: the project → a static site in `dist/web/`. The stages, in order: configuration, content, templates, the browser bundle, then one document per page (and a 404 page, a sitemap
2
+ // when the site has a published address). Everything is generated under `.valance/` first; `dist/` holds only what is built.
3
3
  import { build as bundle } from "vite";
4
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { createRequire } from "node:module";
5
6
  import { dirname, join } from "node:path";
6
7
  import { loadAuthor } from "./author.js";
7
8
  import { loadConfig } from "./config.js";
9
+ import { headersFile } from "./security.js";
8
10
  import { AppError } from "./diagnostics.js";
9
11
  import { generateContent, generateEntries, generateProgram } from "./emit.js";
10
12
  import { OUTPUT_DIR } from "./project.js";
11
- import { baseConfig, createLoader, SERVER_ENTRY } from "./vite.js";
13
+ import { baseConfig, createLoader, SERVER_ENTRY, templates } from "./vite.js";
14
+ import { GENERATED_DIR } from "./project.js";
12
15
  /** The file a pathname is written to in the site: `/` → `index.html`, `/guide/x` → `guide/x/index.html`, relative to the base. */
13
16
  export const fileOf = (pathname, base) => {
14
17
  const inside = pathname.slice(base.length).replace(/\/+$/, "");
15
18
  return inside === "" ? "index.html" : `${inside}/index.html`;
16
19
  };
17
20
  const sitemap = (content) => `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${content.pages.map((page) => ` <url><loc>${content.site.url}${content.site.base}${page.path.replace(/^\//, "")}</loc></url>`).join("\n")}\n</urlset>\n`;
18
- export const build = async (root, options = {}) => {
19
- const stage = options.onStage ?? (() => undefined);
20
- stage("configuration");
21
- const config = loadConfig(root);
22
- generateEntries(root);
23
- stage("content");
24
- const content = generateContent(root, config);
25
- const loader = await createLoader(root, config);
26
- let rendered;
27
- try {
28
- stage("templates");
29
- const author = await loadAuthor(loader, root);
30
- await generateProgram(root, author);
31
- // The browser bundle first: it empties dist/, and the documents are written into it after.
32
- stage("bundle");
33
- await bundle({ ...baseConfig(root, config), mode: "production" });
34
- rendered = (await loader.ssrLoadModule(SERVER_ENTRY));
35
- }
36
- catch (error) {
37
- await loader.close();
38
- throw error;
21
+ /** The MESH runtime's module, found the way the project's own dependency would be: through `@valancex/common`, which depends on it. */
22
+ const wasmOf = (root) => {
23
+ const fromRoot = createRequire(join(root, "package.json"));
24
+ const fromCommon = createRequire(fromRoot.resolve("@valancex/common/package.json"));
25
+ return fromCommon.resolve("@valancex/mesh-runtime/mesh-runtime.wasm");
26
+ };
27
+ /** The desktop build: the engine, one ES module with everything it needs bundled in, and the MESH runtime's module beside it, in `dist/desktop/`. The window is the Desktop PORT's. */
28
+ const buildDesktop = async (root) => {
29
+ const out = join(root, OUTPUT_DIR, "desktop");
30
+ rmSync(out, { recursive: true, force: true });
31
+ await bundle({
32
+ root,
33
+ configFile: false,
34
+ publicDir: false, // the site's public files are the web's
35
+ logLevel: "warn",
36
+ plugins: [templates(root)],
37
+ cacheDir: join(root, GENERATED_DIR, "cache"),
38
+ ssr: { noExternal: true, target: "node" },
39
+ build: {
40
+ ssr: join(root, GENERATED_DIR, "desktop.ts"),
41
+ outDir: out,
42
+ emptyOutDir: true,
43
+ target: "node22",
44
+ minify: false,
45
+ chunkSizeWarningLimit: 4000,
46
+ rollupOptions: {
47
+ output: {
48
+ format: "es",
49
+ entryFileNames: "engine.mjs",
50
+ banner: 'import { createRequire as __vlxRequire } from "node:module"; const require = __vlxRequire(import.meta.url);',
51
+ },
52
+ },
53
+ },
54
+ });
55
+ copyFileSync(wasmOf(root), join(out, "mesh-runtime.wasm"));
56
+ // The files images are drawn from, beside the engine (the site's `public/`, which the desktop's content refers to the same way).
57
+ const publicDir = join(root, "public");
58
+ if (existsSync(publicDir)) {
59
+ cpSync(publicDir, join(out, "public"), { recursive: true });
39
60
  }
40
- try {
41
- const dist = join(root, OUTPUT_DIR);
61
+ return { dir: out, files: ["desktop/engine.mjs", "desktop/mesh-runtime.wasm"], pages: [] };
62
+ };
63
+ /** The web build: the browser bundle into `out`, then one document per page. The loader is the one the templates were compiled with. */
64
+ const buildWeb = async (root, config, loader, content, out, prefix, stage) => {
65
+ // The browser bundle first: it empties its directory, and the documents are written into it after.
66
+ stage("bundle");
67
+ await bundle({ ...baseConfig(root, config, out), mode: "production" });
68
+ const rendered = (await loader.ssrLoadModule(SERVER_ENTRY));
69
+ {
70
+ const dist = join(root, out);
42
71
  const manifest = JSON.parse(readFileSync(join(dist, ".vite", "manifest.json"), "utf8"));
43
72
  const entry = Object.values(manifest).find((chunk) => chunk.isEntry === true);
44
73
  if (entry === undefined) {
45
- throw new AppError([{ file: OUTPUT_DIR, line: 0, column: 0, code: "build-entry", message: "the bundle has no entry chunk" }]);
74
+ throw new AppError([{ file: out, line: 0, column: 0, code: "build-entry", message: "the bundle has no entry chunk" }]);
46
75
  }
47
76
  rmSync(join(dist, ".vite"), { recursive: true, force: true }); // the bundler's bookkeeping is not part of the site
48
77
  const built = { script: `${config.web.base}${entry.file}`, styles: (entry.css ?? []).map((file) => `${config.web.base}${file}`) };
@@ -62,9 +91,49 @@ export const build = async (root, options = {}) => {
62
91
  if (config.web.url !== "") {
63
92
  write("sitemap.xml", sitemap(content));
64
93
  }
65
- return { dir: dist, files, pages };
94
+ // The headers to serve it with: the page is only as safe as the host makes it (src/security.ts).
95
+ if (config.web.headers) {
96
+ write("_headers", headersFile(config));
97
+ }
98
+ return { dir: dist, files: files.map((file) => `${prefix}${file}`), pages };
99
+ }
100
+ };
101
+ export const build = async (root, options = {}) => {
102
+ const stage = options.onStage ?? (() => undefined);
103
+ stage("configuration");
104
+ const config = loadConfig(root);
105
+ const selected = options.targets ?? config.targets;
106
+ const unknown = selected.filter((id) => !config.targets.includes(id));
107
+ if (unknown.length > 0) {
108
+ throw new AppError([
109
+ {
110
+ file: "valance.json",
111
+ line: 0,
112
+ column: 0,
113
+ code: "build-target",
114
+ message: `this project is not built for ${unknown.join(", ")} (its targets: ${config.targets.join(", ")}): add valance.${unknown[0]}.json`,
115
+ },
116
+ ]);
117
+ }
118
+ generateEntries(root, config);
119
+ stage("content");
120
+ const content = generateContent(root, config);
121
+ const loader = await createLoader(root, config);
122
+ const built = [];
123
+ try {
124
+ stage("templates");
125
+ await generateProgram(root, await loadAuthor(loader, root), config, selected);
126
+ // Each target writes to a directory of its own, whatever else the project has: dist/web/ and dist/desktop/.
127
+ if (selected.includes("web")) {
128
+ built.push(await buildWeb(root, config, loader, content, `${OUTPUT_DIR}/web`, "web/", stage));
129
+ }
130
+ if (selected.includes("desktop")) {
131
+ stage("bundle");
132
+ built.push(await buildDesktop(root));
133
+ }
66
134
  }
67
135
  finally {
68
136
  await loader.close();
69
137
  }
138
+ return { dir: join(root, OUTPUT_DIR), files: built.flatMap((each) => each.files), pages: built.flatMap((each) => each.pages) };
70
139
  };
package/dist/check.js CHANGED
@@ -22,13 +22,13 @@ export const check = async (root) => {
22
22
  if (config === undefined) {
23
23
  return { diagnostics: problems, pages: 0, components: [] };
24
24
  }
25
- generateEntries(root);
25
+ generateEntries(root, config);
26
26
  const content = await collect(problems, () => generateContent(root, config));
27
27
  const server = await createLoader(root, config);
28
28
  try {
29
29
  const author = await collect(problems, () => loadAuthor(server, root));
30
30
  const failedAuthor = problems.some((each) => each.file === "src/main.ts");
31
- const components = failedAuthor ? undefined : await collect(problems, () => generateProgram(root, author));
31
+ const components = failedAuthor ? undefined : await collect(problems, () => generateProgram(root, author, config));
32
32
  return { diagnostics: problems, pages: content?.pages.length ?? 0, components: components ?? [] };
33
33
  }
34
34
  finally {
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // vlx init [name] [--web ...] make a new application (asks what the flags do not say)
4
4
  // vlx dev [--port N] [--host H] serve the project and rebuild it as it changes
5
- // vlx build write the site to dist/
5
+ // vlx build write the site to dist/web/
6
6
  // vlx check [--json] find mistakes in authored files without building
7
7
  // vlx generate page|component <name> add a page, or a component registered in src/main.ts (alias: g)
8
8
  // vlx sync bring .valance/ (generated types, manifest, entries) up to date; runs by itself after install and generate
@@ -13,35 +13,41 @@ import { readFileSync } from "node:fs";
13
13
  import { pathToFileURL } from "node:url";
14
14
  import { INIT_HELP, parseInit } from "./args.js";
15
15
  import { build } from "./build.js";
16
- import { generateComponent, generatePage, GenerateError, KINDS } from "./add.js";
16
+ import { packageDesktop } from "./package.js";
17
+ import { generateComponent, generatePage, generateTarget, GenerateError, KINDS } from "./add.js";
17
18
  import { check } from "./check.js";
18
19
  import { findRoot } from "./config.js";
19
20
  import { localCli } from "./delegate.js";
20
21
  import { dev } from "./dev.js";
21
22
  import { AppError, format } from "./diagnostics.js";
22
23
  import { detectPackageManager, directoryFor, directoryProblem, execute, nameProblem, runCommand, titleOf } from "./init.js";
23
- import { TARGETS } from "./targets.js";
24
+ import { DEFAULT_TARGETS, TARGETS } from "./targets.js";
24
25
  import { canAsk, canDraw } from "./tty.js";
25
26
  const HELP = `vlx <command>
26
27
 
27
28
  init [name] [--web] make a new application
28
29
  dev [--port N] [--host H] serve it and rebuild as it changes
29
- build write the site to dist/
30
+ build write the site to dist/web/
30
31
  check [--json] find mistakes in your files without building
31
32
  generate page|component <name> add a page, or a component (alias: g)
33
+ generate target web|desktop add a target to the project
34
+ package --desktop the desktop target as one executable beside its window (unsigned)
32
35
  sync refresh the generated files (this runs by itself; see vlx help sync)
33
36
  help [command] say more about one command
34
37
 
35
38
  Run it in a project (a directory with a valance.json), or any directory inside one.
36
39
  In a terminal the commands draw an interface; --plain (or a pipe, or CI) prints plain text.
37
40
  `;
38
- const GENERATE_HELP = `vlx generate <page|component> <name> (alias: vlx g)
41
+ const GENERATE_HELP = `vlx generate <page|component|target> <name> (alias: vlx g)
39
42
 
40
43
  vlx generate page guide/getting-started content/guide/getting-started.md, with its front matter
41
44
  (--title "Getting started" to say the title)
42
45
  vlx generate component post-card src/post-card.mprx and src/post-card.ts, imported in
43
46
  src/main.ts and listed in its \`components\`. Write
44
47
  <post-card></post-card> in a template to use it.
48
+ vlx generate target desktop valance.desktop.json and a layout of its own in src/desktop/,
49
+ registered in \`targets\` of src/main.ts; the content, the state and
50
+ the commands are shared. Nothing the project has is moved.
45
51
 
46
52
  Nothing is overwritten: a name that is taken is refused.
47
53
  `;
@@ -49,9 +55,10 @@ const COMMAND_HELP = {
49
55
  init: INIT_HELP,
50
56
  generate: GENERATE_HELP,
51
57
  g: GENERATE_HELP,
52
- dev: "vlx dev [--port N] [--host H]\n\nServe the project and rebuild it as files change. A mistake leaves the last good build serving and shows in the terminal and in the page.\n",
53
- build: "vlx build\n\nWrite the site to dist/. Any host that serves files can serve it.\n",
58
+ dev: "vlx dev [--web|--desktop] [--port N] [--host H]\n\nServe the project (the web is served, a desktop project opens its window; a project of more than one target does both, or the one named) and rebuild it as files change. A mistake leaves the last good build serving and shows in the terminal and in the page.\n",
59
+ build: "vlx build [--web] [--desktop]\n\nBuild every target the project has (or the ones named). The web: the site, in dist/web/; any host that serves files can serve it. The desktop: the engine, in dist/desktop/.\n",
54
60
  sync: "vlx sync\n\nBring .valance/ up to date: the generated types for your templates (so your editor and tsc agree with them), the manifest and the entries. You rarely run it: a new project does it after install (the `prepare` script), `vlx generate` does it after it writes, and dev, build and check do it as they run. It never fails: mistakes in your files are reported by vlx check.\n",
61
+ package: "vlx package --desktop [--platform linux-x64|win32-x64] [--window <path>] [--node <path>] [--archive]\n\nBuild the desktop target and package it: the engine as one executable (Node's single executable application, with the MESH runtime inside) beside the Desktop PORT's window program, in dist/package/<platform>/. --window is the window program for that platform (default: VLX_WINDOW, else vlx-window on the PATH; build it from the PORT repository). For another platform, --node is a node binary of this version for it. --archive also writes the package as one file, a .tar.gz (Linux) or a .zip (Windows), with its SHA-256 beside it. The result is unsigned.\n",
55
62
  check: "vlx check [--json]\n\nFind mistakes in your files without building. --json prints them as { file, line, column, code, message } for editors and CI.\n",
56
63
  };
57
64
  /** Refreshes `.valance/` and says so in a line. Best effort: a mistake in the project is `check`'s to report, and a project with no `valance.json` (the install ran before init finished) is left alone. */
@@ -66,6 +73,8 @@ const sync = async (from) => {
66
73
  return `skipped (${error instanceof Error ? error.message.split("\n")[0] : String(error)})`;
67
74
  }
68
75
  };
76
+ /** The targets named by flags (`--web`, `--desktop`), in the order given. */
77
+ const targetFlags = (args) => TARGETS.map((each) => each.id).filter((id) => args.includes(`--${id}`));
69
78
  const option = (args, name) => {
70
79
  const at = args.indexOf(name);
71
80
  return at < 0 ? undefined : args[at + 1];
@@ -90,13 +99,13 @@ const runInit = async (args, cwd) => {
90
99
  return 2;
91
100
  }
92
101
  if (args.targets.length === 0 && !args.yes) {
93
- console.error(`vlx init: choose a target (${TARGETS.map((each) => `--${each.id}`).join(", ")}), or --yes for all of them`);
102
+ console.error(`vlx init: choose a target (${TARGETS.map((each) => `--${each.id}`).join(", ")}), or --yes for the web`);
94
103
  return 2;
95
104
  }
96
105
  const answers = {
97
106
  name: args.name,
98
107
  dir: directoryFor(args.name, cwd),
99
- targets: args.targets.length === 0 ? TARGETS.map((each) => each.id) : args.targets,
108
+ targets: args.targets.length === 0 ? DEFAULT_TARGETS : args.targets,
100
109
  web: { base: args.base ?? "", url: args.url ?? "" },
101
110
  packageManager: args.packageManager ?? detectPackageManager(),
102
111
  install: args.install ?? true,
@@ -128,7 +137,7 @@ export const main = async (args, cwd = process.cwd()) => {
128
137
  return 0;
129
138
  }
130
139
  // A global `vlx` runs the project's own (see `delegate.ts`).
131
- if (command === "dev" || command === "build" || command === "check" || command === "sync" || command === "generate" || command === "g") {
140
+ if (command === "dev" || command === "build" || command === "package" || command === "check" || command === "sync" || command === "generate" || command === "g") {
132
141
  const local = localCli(cwd);
133
142
  if (local !== undefined) {
134
143
  const theirs = (await import(pathToFileURL(local).href));
@@ -153,17 +162,32 @@ export const main = async (args, cwd = process.cwd()) => {
153
162
  case "dev": {
154
163
  const port = option(rest, "--port");
155
164
  const host = option(rest, "--host");
165
+ const picked = targetFlags(rest);
166
+ if (picked.length > 1) {
167
+ console.error("vlx dev: name one target, or none for all of them: vlx dev --web or vlx dev --desktop");
168
+ return 2;
169
+ }
170
+ const target = picked[0];
156
171
  if (draws) {
157
172
  const { drawDev } = await import("./ui/run.js");
158
- await drawDev(findRoot(cwd), { ...(port === undefined ? {} : { port: Number(port) }), ...(host === undefined ? {} : { host }) });
173
+ await drawDev(findRoot(cwd), {
174
+ ...(target === undefined ? {} : { target }),
175
+ ...(port === undefined ? {} : { port: Number(port) }),
176
+ ...(host === undefined ? {} : { host }),
177
+ });
159
178
  return 0;
160
179
  }
161
- const running = await dev(findRoot(cwd), { ...(port === undefined ? {} : { port: Number(port) }), ...(host === undefined ? {} : { host }) });
180
+ const running = await dev(findRoot(cwd), {
181
+ ...(target === undefined ? {} : { target }),
182
+ ...(port === undefined ? {} : { port: Number(port) }),
183
+ ...(host === undefined ? {} : { host }),
184
+ });
162
185
  console.log(`vlx dev: ${running.url}`);
163
- // Until interrupted.
186
+ // Until interrupted (or, for a desktop project, until the window is closed).
164
187
  await new Promise((resolve) => {
165
188
  process.once("SIGINT", resolve);
166
189
  process.once("SIGTERM", resolve);
190
+ void running.closed?.then(resolve);
167
191
  });
168
192
  await running.close();
169
193
  return 0;
@@ -171,14 +195,33 @@ export const main = async (args, cwd = process.cwd()) => {
171
195
  case "build": {
172
196
  if (draws) {
173
197
  const { drawBuild } = await import("./ui/run.js");
174
- await drawBuild(findRoot(cwd));
198
+ await drawBuild(findRoot(cwd), targetFlags(rest));
175
199
  return 0;
176
200
  }
177
201
  const started = Date.now();
178
- const built = await build(findRoot(cwd));
202
+ const flagged = targetFlags(rest);
203
+ const built = await build(findRoot(cwd), flagged.length === 0 ? {} : { targets: flagged });
179
204
  console.log(`vlx build: ${built.pages.length} pages, ${built.files.length} files → ${built.dir} (${((Date.now() - started) / 1000).toFixed(1)}s)`);
180
205
  return 0;
181
206
  }
207
+ case "package": {
208
+ if (!rest.includes("--desktop")) {
209
+ console.error("vlx package: say what: vlx package --desktop [--platform linux-x64|win32-x64] [--window <path>] [--node <path>] [--archive]");
210
+ return 2;
211
+ }
212
+ const platform = option(rest, "--platform");
213
+ const window = option(rest, "--window");
214
+ const node = option(rest, "--node");
215
+ const packaged = await packageDesktop(findRoot(cwd), {
216
+ ...(platform === undefined ? {} : { platform }),
217
+ ...(window === undefined ? {} : { window }),
218
+ ...(node === undefined ? {} : { node }),
219
+ ...(rest.includes("--archive") ? { archive: true } : {}),
220
+ });
221
+ console.log(`vlx package: ${packaged.files.join(", ")} → ${packaged.dir}`);
222
+ packaged.notes.forEach((note) => console.log(`vlx package: ${note}`));
223
+ return 0;
224
+ }
182
225
  case "check": {
183
226
  if (draws && !rest.includes("--json")) {
184
227
  const { drawCheck } = await import("./ui/run.js");
@@ -219,7 +262,7 @@ export const main = async (args, cwd = process.cwd()) => {
219
262
  }
220
263
  const root = findRoot(cwd);
221
264
  try {
222
- const made = kind === "page" ? generatePage(root, positional[1], title) : generateComponent(root, positional[1]);
265
+ const made = kind === "page" ? generatePage(root, positional[1], title) : kind === "target" ? generateTarget(root, positional[1]) : generateComponent(root, positional[1]);
223
266
  made.created.forEach((file) => console.log(`vlx generate: created ${file}`));
224
267
  made.changed.forEach((file) => console.log(`vlx generate: changed ${file}`));
225
268
  console.log(`vlx generate: ${await sync(root)}`);
package/dist/compile.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import type * as Valance from "@valancex/valance";
2
2
  import type { DefinedApp } from "@valancex/common";
3
3
  import type { Node } from "./tree.js";
4
+ import type { Vocabulary } from "@valancex/common/internal";
4
5
  export declare const SOURCE_DIR = "src";
5
6
  /** The component the page is drawn by. */
6
7
  export declare const ROOT = "layout";
7
8
  export declare const BUILT_IN: string;
9
+ /** The desktop target's built-in views: the same components as the web's, written in the desktop vocabulary. */
10
+ export declare const BUILT_IN_DESKTOP: string;
8
11
  /** The package directory: where its README, which documents the framework's own names, is. */
9
12
  export declare const PACKAGE_DIR: string;
10
13
  interface Template {
@@ -23,8 +26,14 @@ export interface Templates {
23
26
  * Built-in templates, then the author's. The author's are exactly the ones `src/main.ts` declares: `layout`, `components` and `override`. A file under `src/` that is not declared is an error, not
24
27
  * a guess; a registered component that no template writes is allowed (it is not part of the program until one does). Which components a template uses is read from the template. A name that is a built-in's replaces it only through `override`.
25
28
  */
26
- export declare const templatesOf: (root: string, author: DefinedApp | undefined) => Templates;
27
- export declare const manifestOf: (author: DefinedApp | undefined) => import("@valancex/valance/web/build").Manifest;
29
+ export declare const templatesOf: (root: string, author: DefinedApp | undefined, vocabulary?: Vocabulary, views?: string) => Templates;
30
+ export declare const manifestOf: (author: DefinedApp | undefined, vocabulary?: Vocabulary) => {
31
+ components: {
32
+ [x: string]: unknown;
33
+ };
34
+ version: 1;
35
+ types: Readonly<Record<string, never>>;
36
+ };
28
37
  export interface Compiled {
29
38
  readonly program: Valance.Program;
30
39
  /**
@@ -46,6 +55,6 @@ export interface Compiled {
46
55
  readonly builtIn: boolean;
47
56
  }>;
48
57
  }
49
- /** Compiles the application's templates. Fails with every diagnostic, each pointing into the file it is about. */
50
- export declare const compileViews: (root: string, author: DefinedApp | undefined) => Promise<Compiled>;
58
+ /** Compiles the application's templates against a target's vocabulary (the web's by default). Fails with every diagnostic, each pointing into the file it is about. */
59
+ export declare const compileViews: (root: string, author: DefinedApp | undefined, vocabulary?: Vocabulary, views?: string) => Promise<Compiled>;
51
60
  export {};
package/dist/compile.js CHANGED
@@ -12,12 +12,14 @@ import { declarationsOf, linked, nameOf, treeOf } from "./tree.js";
12
12
  import { tagsOf } from "./outline.js";
13
13
  import { entryOf } from "./contract.js";
14
14
  import { locateKey } from "./wiring.js";
15
- import { primitives } from "@valancex/common/internal";
15
+ import { namesOf, webVocabulary, WHOLE } from "@valancex/common/internal";
16
16
  import { ENTRY_FILE } from "./project.js";
17
17
  export const SOURCE_DIR = "src";
18
18
  /** The component the page is drawn by. */
19
19
  export const ROOT = "layout";
20
20
  export const BUILT_IN = fileURLToPath(new URL("../views/", import.meta.url));
21
+ /** The desktop target's built-in views: the same components as the web's, written in the desktop vocabulary. */
22
+ export const BUILT_IN_DESKTOP = fileURLToPath(new URL("../views-desktop/", import.meta.url));
21
23
  /** The package directory: where its README, which documents the framework's own names, is. */
22
24
  export const PACKAGE_DIR = fileURLToPath(new URL("../", import.meta.url));
23
25
  const mprxIn = (dir) => existsSync(dir)
@@ -37,11 +39,11 @@ const entryText = (root) => {
37
39
  * Built-in templates, then the author's. The author's are exactly the ones `src/main.ts` declares: `layout`, `components` and `override`. A file under `src/` that is not declared is an error, not
38
40
  * a guess; a registered component that no template writes is allowed (it is not part of the program until one does). Which components a template uses is read from the template. A name that is a built-in's replaces it only through `override`.
39
41
  */
40
- export const templatesOf = (root, author) => {
42
+ export const templatesOf = (root, author, vocabulary = webVocabulary, views = BUILT_IN) => {
41
43
  const found = new Map();
42
- for (const file of mprxIn(BUILT_IN)) {
44
+ for (const file of mprxIn(views)) {
43
45
  const component = basename(file, ".mprx");
44
- found.set(component, { component, path: `@valancex/cli/views/${file}`, source: readFileSync(join(BUILT_IN, file), "utf8"), builtIn: true });
46
+ found.set(component, { component, path: `@valancex/cli/${basename(views)}/${file}`, source: readFileSync(join(views, file), "utf8"), builtIn: true });
45
47
  }
46
48
  const layout = author?.layout;
47
49
  const overrides = author?.override ?? [];
@@ -77,7 +79,7 @@ export const templatesOf = (root, author) => {
77
79
  problem(place, "template-duplicate", `${files.get(node.name)} is also <${node.name}>: a component is named by its file, so the name must be unique`);
78
80
  continue;
79
81
  }
80
- if (node.name in primitives) {
82
+ if (namesOf(vocabulary).includes(node.name)) {
81
83
  problem(place, "template-primitive", `<${node.name}> is already a tag the framework draws as an element; name the file something else`);
82
84
  continue;
83
85
  }
@@ -92,10 +94,17 @@ export const templatesOf = (root, author) => {
92
94
  files.set(node.name, node.file);
93
95
  found.set(node.name, { component: node.name, path: node.file, source: readFileSync(join(root, node.file), "utf8"), builtIn: false });
94
96
  }
95
- // Every template under src/ must be somewhere in the tree.
97
+ // Every template under src/ must be somewhere in a tree: this target's, or another target's (a project for more than one target keeps each one's templates side by side).
98
+ const claimed = new Set(files.values());
99
+ const whole = (author?.[WHOLE] ?? author);
100
+ for (const parts of [whole, ...Object.values(whole?.targets ?? {})]) {
101
+ for (const node of treeOf(parts?.layout, parts?.override ?? [], parts?.components ?? [])) {
102
+ claimed.add(node.file);
103
+ }
104
+ }
96
105
  for (const file of mprxIn(join(root, SOURCE_DIR))) {
97
106
  const path = `${SOURCE_DIR}/${file}`;
98
- if (![...files.values()].includes(path)) {
107
+ if (!claimed.has(path)) {
99
108
  problems.push(diagnostic(path, 0, "template-unused", `${path} is not in the component tree: import it in ${ENTRY_FILE} and ${basename(file) === `${ROOT}.mprx` ? "pass it as `layout`" : "list it in `components`"}`));
100
109
  }
101
110
  }
@@ -124,11 +133,29 @@ const reachable = (all) => {
124
133
  }
125
134
  return all.filter((template) => seen.has(template.component));
126
135
  };
127
- export const manifestOf = (author) => manifest({
128
- primitives,
129
- scopes: { [ROOT]: scopeSchema(author) },
130
- commands: commandTable(author),
131
- });
136
+ export const manifestOf = (author, vocabulary = webVocabulary) => {
137
+ const derived = manifest({
138
+ // A web vocabulary is a table the manifest is derived from; any other is a manifest already, and its components are merged in as they are.
139
+ primitives: "tags" in vocabulary ? vocabulary.tags : {},
140
+ scopes: { [ROOT]: scopeSchema(author) },
141
+ commands: commandTable(author),
142
+ });
143
+ if (!("tags" in vocabulary)) {
144
+ return { ...derived, components: { ...vocabulary.components, ...derived.components } };
145
+ }
146
+ // The props the vocabulary says a template may leave out.
147
+ const components = { ...derived.components };
148
+ for (const [tag, names] of Object.entries(vocabulary.optional ?? {})) {
149
+ const entry = components[tag];
150
+ if (entry !== undefined) {
151
+ components[tag] = {
152
+ ...entry,
153
+ props: Object.fromEntries(Object.entries(entry.props).map(([name, prop]) => [name, names.includes(name) ? { ...prop, required: false } : prop])),
154
+ };
155
+ }
156
+ }
157
+ return { ...derived, components };
158
+ };
132
159
  const isManifestError = (error) => error instanceof Error && error._tag === "ManifestError";
133
160
  /**
134
161
  * The model the templates are checked against: the derived one (the tags, the layout's scope, the commands), and an entry for each component that declares its interface. A declared interface is
@@ -136,12 +163,12 @@ const isManifestError = (error) => error instanceof Error && error._tag === "Man
136
163
  * uses. MESH checks every use against it (an undeclared prop, a missing one, a wrong type, an undeclared event in a handler), at the use or in the template. A schema with no model type is a
137
164
  * diagnostic at the declaration, not a crash.
138
165
  */
139
- const modelOf = (root, author, tree) => {
166
+ const modelOf = (root, author, tree, vocabulary) => {
140
167
  const problems = [];
141
168
  const mainText = entryText(root);
142
169
  let derived;
143
170
  try {
144
- derived = manifestOf(author);
171
+ derived = manifestOf(author, vocabulary);
145
172
  }
146
173
  catch (error) {
147
174
  if (!isManifestError(error)) {
@@ -171,11 +198,11 @@ const modelOf = (root, author, tree) => {
171
198
  }
172
199
  return { ...derived, components: { ...derived.components, ...entries } };
173
200
  };
174
- /** Compiles the application's templates. Fails with every diagnostic, each pointing into the file it is about. */
175
- export const compileViews = async (root, author) => {
176
- const { all, tree } = templatesOf(root, author);
201
+ /** Compiles the application's templates against a target's vocabulary (the web's by default). Fails with every diagnostic, each pointing into the file it is about. */
202
+ export const compileViews = async (root, author, vocabulary = webVocabulary, views = BUILT_IN) => {
203
+ const { all, tree } = templatesOf(root, author, vocabulary, views);
177
204
  const used = reachable(all);
178
- const model = modelOf(root, author, tree);
205
+ const model = modelOf(root, author, tree, vocabulary);
179
206
  if (!used.some((template) => template.component === ROOT)) {
180
207
  throw new AppError([diagnostic(`${SOURCE_DIR}/${ROOT}.mprx`, 0, "layout-missing", "there is no layout")]);
181
208
  }
@@ -205,7 +232,7 @@ export const compileViews = async (root, author) => {
205
232
  templates: used.map((template) => ({
206
233
  component: template.component,
207
234
  file: template.builtIn
208
- ? relative(root, join(BUILT_IN, `${template.component}.mprx`))
235
+ ? relative(root, join(views, `${template.component}.mprx`))
209
236
  .split(sep)
210
237
  .join("/")
211
238
  : template.path,
package/dist/config.d.ts CHANGED
@@ -1,13 +1,27 @@
1
1
  import type { SiteInfo } from "@valancex/common/internal";
2
2
  export declare const PROJECT_FILE = "valance.json";
3
3
  export declare const WEB_FILE = "valance.web.json";
4
+ export declare const DESKTOP_FILE = "valance.desktop.json";
4
5
  /** The only version of both files there is. */
5
6
  export declare const CONFIG_VERSION = 1;
6
7
  export interface Config {
7
8
  readonly name: string;
9
+ /** The targets the project is built for: each one whose settings file it has (`valance.web.json`, `valance.desktop.json`), else the web alone. One application, one build per target. */
10
+ readonly targets: ReadonlyArray<"web" | "desktop">;
11
+ /** How the document's title is written from a page's title and the site's name: `{page}` and `{site}`. */
12
+ readonly title: string;
8
13
  readonly web: {
9
14
  readonly base: string;
10
15
  readonly url: string;
16
+ /** Write the security headers file beside the built site. */
17
+ readonly headers: boolean;
18
+ /** Announce a page change made in place to assistive technology. */
19
+ readonly announce: boolean;
20
+ };
21
+ /** The application's backend: the address its requests are made to (a path on the site's own origin, or an absolute https address), and, for `vlx dev`, where a path is proxied to. */
22
+ readonly api: {
23
+ readonly base: string;
24
+ readonly proxy: string;
11
25
  };
12
26
  }
13
27
  /** Reads and validates the project's configuration. Every problem in both files is reported together. */