@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.
- package/LICENSE +21 -0
- package/README.md +36 -5
- package/dist/add.d.ts +9 -1
- package/dist/add.js +68 -5
- package/dist/archive.d.ts +17 -0
- package/dist/archive.js +111 -0
- package/dist/args.js +2 -2
- package/dist/build.d.ts +3 -0
- package/dist/build.js +93 -29
- package/dist/check.js +2 -2
- package/dist/cli.js +59 -16
- package/dist/compile.d.ts +11 -3
- package/dist/compile.js +42 -15
- package/dist/config.d.ts +3 -0
- package/dist/config.js +4 -0
- package/dist/dev-desktop.d.ts +8 -0
- package/dist/dev-desktop.js +114 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +33 -5
- package/dist/emit.d.ts +7 -4
- package/dist/emit.js +105 -27
- package/dist/init.js +14 -4
- package/dist/package.d.ts +18 -0
- package/dist/package.js +141 -0
- package/dist/project.js +1 -1
- package/dist/targets.d.ts +6 -1
- package/dist/targets.js +26 -4
- package/dist/ui/init.js +4 -4
- package/dist/ui/run.d.ts +3 -1
- package/dist/ui/run.js +5 -3
- package/dist/vite.d.ts +3 -2
- package/dist/vite.js +3 -3
- package/package.json +6 -3
- package/schemas/valance.desktop.schema.json +17 -0
- package/templates/desktop/README.md +14 -0
- package/templates/desktop/content/404.md +7 -0
- package/templates/desktop/content/guide/getting-started.md +15 -0
- package/templates/desktop/content/index.md +14 -0
- package/templates/desktop/gitignore +4 -0
- package/templates/desktop/src/layout.mprx +23 -0
- package/templates/desktop/src/main.ts +15 -0
- package/templates/desktop/tsconfig.json +18 -0
- package/templates/multi/README.md +13 -0
- package/templates/multi/content/404.md +7 -0
- package/templates/multi/content/guide/getting-started.md +15 -0
- package/templates/multi/content/index.md +14 -0
- package/templates/multi/src/desktop/layout.mprx +21 -0
- package/templates/multi/src/main.ts +23 -0
- package/templates/multi/src/web/layout.mprx +27 -0
- package/templates/multi/src/web/styles.css +28 -0
- package/templates/multi/src/web/theme-toggle.mprx +1 -0
- package/templates/multi/src/web/theme-toggle.ts +10 -0
- package/templates/web/content/guide/getting-started.md +1 -1
- package/views/block.mprx +1 -1
- package/views-desktop/block.mprx +3 -0
- package/views-desktop/blocks.mprx +3 -0
- package/views-desktop/inline.mprx +3 -0
- package/views-desktop/layout.mprx +20 -0
- package/views-desktop/list-block.mprx +3 -0
- package/views-desktop/list-items.mprx +3 -0
- package/views-desktop/runs.mprx +3 -0
- /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 {
|
|
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\
|
|
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
|
|
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 ?
|
|
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), {
|
|
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), {
|
|
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
|
|
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) =>
|
|
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(
|
|
44
|
+
for (const file of mprxIn(views)) {
|
|
43
45
|
const component = basename(file, ".mprx");
|
|
44
|
-
found.set(component, { component, path: `@valancex/cli
|
|
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
|
|
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
|
|
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 (!
|
|
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) =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
9
|
-
export declare const
|
|
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;
|