@valancex/cli 0.12.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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -5
  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 +93 -29
  10. package/dist/check.js +2 -2
  11. package/dist/cli.js +59 -16
  12. package/dist/compile.d.ts +11 -3
  13. package/dist/compile.js +42 -15
  14. package/dist/config.d.ts +3 -0
  15. package/dist/config.js +4 -0
  16. package/dist/dev-desktop.d.ts +8 -0
  17. package/dist/dev-desktop.js +114 -0
  18. package/dist/dev.d.ts +3 -0
  19. package/dist/dev.js +33 -5
  20. package/dist/emit.d.ts +7 -4
  21. package/dist/emit.js +105 -27
  22. package/dist/init.js +14 -4
  23. package/dist/package.d.ts +18 -0
  24. package/dist/package.js +141 -0
  25. package/dist/project.js +1 -1
  26. package/dist/targets.d.ts +6 -1
  27. package/dist/targets.js +26 -4
  28. package/dist/ui/init.js +4 -4
  29. package/dist/ui/run.d.ts +3 -1
  30. package/dist/ui/run.js +5 -3
  31. package/dist/vite.d.ts +3 -2
  32. package/dist/vite.js +3 -3
  33. package/package.json +6 -3
  34. package/schemas/valance.desktop.schema.json +17 -0
  35. package/templates/desktop/README.md +14 -0
  36. package/templates/desktop/content/404.md +7 -0
  37. package/templates/desktop/content/guide/getting-started.md +15 -0
  38. package/templates/desktop/content/index.md +14 -0
  39. package/templates/desktop/gitignore +4 -0
  40. package/templates/desktop/src/layout.mprx +23 -0
  41. package/templates/desktop/src/main.ts +15 -0
  42. package/templates/desktop/tsconfig.json +18 -0
  43. package/templates/multi/README.md +13 -0
  44. package/templates/multi/content/404.md +7 -0
  45. package/templates/multi/content/guide/getting-started.md +15 -0
  46. package/templates/multi/content/index.md +14 -0
  47. package/templates/multi/src/desktop/layout.mprx +21 -0
  48. package/templates/multi/src/main.ts +23 -0
  49. package/templates/multi/src/web/layout.mprx +27 -0
  50. package/templates/multi/src/web/styles.css +28 -0
  51. package/templates/multi/src/web/theme-toggle.mprx +1 -0
  52. package/templates/multi/src/web/theme-toggle.ts +10 -0
  53. package/templates/web/content/guide/getting-started.md +1 -1
  54. package/views/block.mprx +1 -1
  55. package/views-desktop/block.mprx +3 -0
  56. package/views-desktop/blocks.mprx +3 -0
  57. package/views-desktop/inline.mprx +3 -0
  58. package/views-desktop/layout.mprx +20 -0
  59. package/views-desktop/list-block.mprx +3 -0
  60. package/views-desktop/list-items.mprx +3 -0
  61. package/views-desktop/runs.mprx +3 -0
  62. /package/views/{heading.mprx → heading-block.mprx} +0 -0
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
@@ -6,6 +6,8 @@ export declare const SOURCE_DIR = "src";
6
6
  /** The component the page is drawn by. */
7
7
  export declare const ROOT = "layout";
8
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;
9
11
  /** The package directory: where its README, which documents the framework's own names, is. */
10
12
  export declare const PACKAGE_DIR: string;
11
13
  interface Template {
@@ -24,8 +26,14 @@ export interface Templates {
24
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
25
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`.
26
28
  */
27
- export declare const templatesOf: (root: string, author: DefinedApp | undefined, vocabulary?: Vocabulary) => Templates;
28
- export declare const manifestOf: (author: DefinedApp | undefined, vocabulary?: Vocabulary) => 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
+ };
29
37
  export interface Compiled {
30
38
  readonly program: Valance.Program;
31
39
  /**
@@ -48,5 +56,5 @@ export interface Compiled {
48
56
  }>;
49
57
  }
50
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. */
51
- export declare const compileViews: (root: string, author: DefinedApp | undefined, vocabulary?: Vocabulary) => Promise<Compiled>;
59
+ export declare const compileViews: (root: string, author: DefinedApp | undefined, vocabulary?: Vocabulary, views?: string) => Promise<Compiled>;
52
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 { webVocabulary } 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, vocabulary = webVocabulary) => {
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, vocabulary = webVocabulary) => {
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 vocabulary.tags) {
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, vocabulary = webVocabulary) => {
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, vocabulary = webVocabulary) => manifest({
128
- primitives: vocabulary.tags,
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
@@ -172,8 +199,8 @@ const modelOf = (root, author, tree, vocabulary) => {
172
199
  return { ...derived, components: { ...derived.components, ...entries } };
173
200
  };
174
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. */
175
- export const compileViews = async (root, author, vocabulary = webVocabulary) => {
176
- const { all, tree } = templatesOf(root, author, vocabulary);
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
205
  const model = modelOf(root, author, tree, vocabulary);
179
206
  if (!used.some((template) => template.component === ROOT)) {
@@ -205,7 +232,7 @@ export const compileViews = async (root, author, vocabulary = webVocabulary) =>
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,10 +1,13 @@
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">;
8
11
  /** How the document's title is written from a page's title and the site's name: `{page}` and `{site}`. */
9
12
  readonly title: string;
10
13
  readonly web: {
package/dist/config.js CHANGED
@@ -7,6 +7,7 @@ import { AppError, diagnostic, lineOf } from "./diagnostics.js";
7
7
  import { DEFAULT_TITLE } from "@valancex/common/internal";
8
8
  export const PROJECT_FILE = "valance.json";
9
9
  export const WEB_FILE = "valance.web.json";
10
+ export const DESKTOP_FILE = "valance.desktop.json";
10
11
  /** The only version of both files there is. */
11
12
  export const CONFIG_VERSION = 1;
12
13
  const read = (root, file) => {
@@ -97,7 +98,9 @@ export const loadConfig = (root) => {
97
98
  throw new AppError([diagnostic(PROJECT_FILE, 0, "config-missing", `there is no ${PROJECT_FILE} in ${root}`)]);
98
99
  }
99
100
  const web = read(root, WEB_FILE);
101
+ const desktop = read(root, DESKTOP_FILE);
100
102
  const problems = [
103
+ ...(desktop === undefined ? [] : check(DESKTOP_FILE, desktop.text, desktop.value, { $schema: schema, version }, [])),
101
104
  ...check(PROJECT_FILE, project.text, project.value, { $schema: schema, version, name: text, title }, ["name"]),
102
105
  ...(web === undefined ? [] : check(WEB_FILE, web.text, web.value, { $schema: schema, version, base, url, headers: flag, announce: flag, api }, [])),
103
106
  ];
@@ -108,6 +111,7 @@ export const loadConfig = (root) => {
108
111
  const named = project.value;
109
112
  return {
110
113
  name: named.name,
114
+ targets: desktop === undefined ? ["web"] : web === undefined ? ["desktop"] : ["web", "desktop"],
111
115
  title: named.title ?? DEFAULT_TITLE,
112
116
  web: { base: given.base ?? "/", url: given.url ?? "", headers: given.headers ?? true, announce: given.announce ?? true },
113
117
  api: { base: given.api?.base ?? "", proxy: given.api?.proxy ?? "" },
@@ -0,0 +1,8 @@
1
+ import type { Dev, DevOptions } from "./dev.js";
2
+ /** Whether a changed file (relative to the project) is one the application is made of. */
3
+ export declare const mattersToTheEngine: (file: string) => boolean;
4
+ export interface DesktopDev extends Dev {
5
+ /** Resolves when the window is closed by the person (not by a rebuild). */
6
+ readonly closed: Promise<void>;
7
+ }
8
+ export declare const devDesktop: (root: string, options?: DevOptions) => Promise<DesktopDev>;
@@ -0,0 +1,114 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { watch } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { build } from "./build.js";
7
+ import { AppError, format } from "./diagnostics.js";
8
+ /** How long files must be quiet before a change is built (ms). */
9
+ const QUIET = 150;
10
+ /** Whether a changed file (relative to the project) is one the application is made of. */
11
+ export const mattersToTheEngine = (file) => {
12
+ const name = file.split("\\").join("/");
13
+ return /^(content|src|public)\//.test(name) || /^valance(\.\w+)?\.json$/.test(name);
14
+ };
15
+ export const devDesktop = async (root, options = {}) => {
16
+ const log = options.log ?? ((line) => void console.log(line));
17
+ const program = process.env["VLX_WINDOW"] ?? "vlx-window";
18
+ let window;
19
+ let stopping = false;
20
+ let working = Promise.resolve();
21
+ let timer;
22
+ let leave = () => undefined;
23
+ const closed = new Promise((resolve) => {
24
+ leave = resolve;
25
+ });
26
+ const reloadDir = mkdtempSync(join(tmpdir(), "vlx-reload-"));
27
+ const reloadFile = join(reloadDir, "reload");
28
+ let reloads = 0;
29
+ writeFileSync(reloadFile, "0");
30
+ /** Tells a running window the engine was rebuilt: a new modification time on the file it watches. */
31
+ const touch = () => {
32
+ reloads += 1;
33
+ writeFileSync(reloadFile, String(reloads));
34
+ };
35
+ const stopWindow = async () => {
36
+ const running = window;
37
+ window = undefined;
38
+ if (running !== undefined && running.exitCode === null) {
39
+ const exited = new Promise((resolve) => running.once("exit", () => resolve()));
40
+ running.kill();
41
+ await exited;
42
+ }
43
+ };
44
+ const openWindow = (engine) => {
45
+ const child = spawn(program, [], { stdio: "inherit", env: { ...process.env, VLX_ENGINE: `${process.execPath} ${engine}`, VLX_RELOAD: reloadFile } });
46
+ window = child;
47
+ child.once("error", (error) => {
48
+ window = undefined;
49
+ log(error.code === "ENOENT"
50
+ ? `vlx: no window program "${program}": install it with \`cargo install --git https://github.com/ValanceX/Port valancex-port-desktop --bin vlx-window\`, or name it in VLX_WINDOW`
51
+ : `vlx: the window could not start: ${error.message}`);
52
+ });
53
+ child.once("exit", () => {
54
+ if (window === child && !stopping) {
55
+ window = undefined;
56
+ log("vlx: the window was closed");
57
+ leave();
58
+ }
59
+ });
60
+ };
61
+ const rebuild = async () => {
62
+ try {
63
+ const built = await build(root, { targets: ["desktop"] });
64
+ options.onStatus?.([]);
65
+ if (stopping) {
66
+ return;
67
+ }
68
+ if (window !== undefined && window.exitCode === null) {
69
+ touch();
70
+ log("vlx: built; the window restarts the engine");
71
+ }
72
+ else {
73
+ openWindow(join(built.dir, "desktop", "engine.mjs"));
74
+ log("vlx: built; the window is open");
75
+ }
76
+ }
77
+ catch (error) {
78
+ if (error instanceof AppError) {
79
+ options.onStatus?.(error.diagnostics);
80
+ error.diagnostics.forEach((each) => log(format(each)));
81
+ }
82
+ else {
83
+ log(`vlx: the build failed: ${error instanceof Error ? error.message : String(error)}`);
84
+ }
85
+ }
86
+ };
87
+ const queue = () => {
88
+ working = working.then(rebuild);
89
+ };
90
+ const watcher = watch(root, { recursive: true }, (_event, file) => {
91
+ if (file !== null && mattersToTheEngine(file)) {
92
+ clearTimeout(timer);
93
+ timer = setTimeout(queue, QUIET);
94
+ }
95
+ });
96
+ queue();
97
+ await working;
98
+ return {
99
+ url: "window",
100
+ closed,
101
+ settled: async () => {
102
+ await working;
103
+ },
104
+ close: async () => {
105
+ stopping = true;
106
+ clearTimeout(timer);
107
+ watcher.close();
108
+ await working;
109
+ await stopWindow();
110
+ rmSync(reloadDir, { recursive: true, force: true });
111
+ leave();
112
+ },
113
+ };
114
+ };
package/dist/dev.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { Diagnostic } from "./diagnostics.js";
2
+ import type { TargetId } from "./targets.js";
2
3
  export interface DevOptions {
4
+ /** Which target to serve, for a project of more than one. */
5
+ readonly target?: TargetId;
3
6
  readonly port?: number;
4
7
  readonly host?: string;
5
8
  /** Called with each report (a rebuild, a mistake); defaults to the console. */
package/dist/dev.js CHANGED
@@ -3,6 +3,7 @@ import { relative, sep } from "node:path";
3
3
  import { loadAuthor } from "./author.js";
4
4
  import { loadConfig, PROJECT_FILE, WEB_FILE } from "./config.js";
5
5
  import { AppError, format } from "./diagnostics.js";
6
+ import { devDesktop } from "./dev-desktop.js";
6
7
  import { generateContent, generateEntries, generateProgram } from "./emit.js";
7
8
  import { GENERATED_DIR, OUTPUT_DIR } from "./project.js";
8
9
  import { baseConfig, SERVER_ENTRY } from "./vite.js";
@@ -50,7 +51,7 @@ const start = async (root, config, options, restart) => {
50
51
  });
51
52
  const failed = new Map();
52
53
  let pending = Promise.resolve();
53
- generateEntries(root);
54
+ generateEntries(root, config);
54
55
  const base = baseConfig(root, config);
55
56
  const server = await createServer({
56
57
  ...base,
@@ -114,13 +115,13 @@ const start = async (root, config, options, restart) => {
114
115
  };
115
116
  const wasFailing = failed.size > 0;
116
117
  if (stages.includes("entries")) {
117
- generateEntries(root);
118
+ generateEntries(root, config);
118
119
  }
119
120
  if (stages.includes("content")) {
120
121
  await run("content", () => generateContent(root, config));
121
122
  }
122
123
  if (stages.includes("program")) {
123
- await run("templates", async () => generateProgram(root, await loadAuthor(server, root)));
124
+ await run("templates", async () => generateProgram(root, await loadAuthor(server, root), config, ["web"]));
124
125
  }
125
126
  const open = [...failed.values()].flat();
126
127
  options.onStatus?.(open);
@@ -176,7 +177,7 @@ const start = async (root, config, options, restart) => {
176
177
  // First build: a mistake here is reported like any other and the server still starts, so the overlay can show it.
177
178
  for (const [name, work] of [
178
179
  ["content", () => generateContent(root, config)],
179
- ["templates", async () => generateProgram(root, await loadAuthor(server, root))],
180
+ ["templates", async () => generateProgram(root, await loadAuthor(server, root), config, ["web"])],
180
181
  ]) {
181
182
  try {
182
183
  await work();
@@ -206,6 +207,33 @@ const start = async (root, config, options, restart) => {
206
207
  };
207
208
  /** Starts the development server for the project at `root`. */
208
209
  export const dev = async (root, options = {}) => {
210
+ const config = loadConfig(root);
211
+ const target = options.target ?? (config.targets.length === 1 ? config.targets[0] : undefined);
212
+ if (target !== undefined && !config.targets.includes(target)) {
213
+ throw new AppError([
214
+ { file: "valance.json", line: 0, column: 0, code: "dev-target", message: `this project is not built for ${target} (its targets: ${config.targets.join(", ")})` },
215
+ ]);
216
+ }
217
+ // A project of more than one target, and none named: serve the web and open the desktop's window, each as it would be alone.
218
+ if (target === undefined) {
219
+ const web = await dev(root, { ...options, target: "web" });
220
+ const desktop = await devDesktop(root, options);
221
+ return {
222
+ url: web.url,
223
+ closed: desktop.closed,
224
+ settled: async () => {
225
+ await web.settled();
226
+ await desktop.settled();
227
+ },
228
+ close: async () => {
229
+ await desktop.close();
230
+ await web.close();
231
+ },
232
+ };
233
+ }
234
+ if (target === "desktop") {
235
+ return devDesktop(root, options);
236
+ }
209
237
  let current;
210
238
  const log = options.log ??
211
239
  ((line) => {
@@ -244,7 +272,7 @@ export const dev = async (root, options = {}) => {
244
272
  }, 500);
245
273
  timer.unref();
246
274
  };
247
- const config = loadConfig(root);
275
+ // (the configuration is read above)
248
276
  current = await start(root, config, options, restart);
249
277
  return {
250
278
  get url() {
package/dist/emit.d.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import type { Config } from "./config.js";
2
+ import type { TargetId } from "./targets.js";
2
3
  import type { DefinedApp } from "@valancex/common";
3
4
  import type { Content } from "@valancex/common/internal";
4
5
  export declare const hasEntry: (root: string) => boolean;
5
6
  export declare const writeContent: (root: string, content: Content) => boolean;
6
7
  /** The content stage alone: it does not need the author's code. */
7
8
  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>>;
9
+ /** The file a target's compiled program is in: `program.json` for a project of one target (as it always was), `program.<target>.json` for one of several. */
10
+ export declare const programFile: (config: Config | undefined, id: TargetId) => string;
11
+ /** The templates stage: needs the author's definition (the manifest derives from its state and commands). One program per target; the state, the commands and the content are shared. */
12
+ export declare const generateProgram: (root: string, author: DefinedApp | undefined, config?: Config, only?: ReadonlyArray<TargetId>) => Promise<ReadonlyArray<string>>;
10
13
  /** The declaration of one template's type: its props and events as the names TypeScript checks a contract's keys against. */
11
14
  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;
15
+ /** The entries; rewritten when `src/main.ts` or a stylesheet is added or removed. Each target has its own: the web's two (`client.ts`, `server.ts`), the desktop's one (`desktop.ts`). */
16
+ export declare const generateEntries: (root: string, config?: Config) => boolean;