@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.
- package/LICENSE +21 -0
- package/README.md +111 -9
- 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 +98 -29
- package/dist/check.js +2 -2
- package/dist/cli.js +59 -16
- package/dist/compile.d.ts +13 -4
- package/dist/compile.js +46 -19
- package/dist/config.d.ts +14 -0
- package/dist/config.js +51 -4
- package/dist/content.js +2 -1
- 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 +41 -6
- package/dist/emit.d.ts +7 -4
- package/dist/emit.js +105 -27
- package/dist/init.js +14 -4
- package/dist/markdown.js +2 -1
- package/dist/package.d.ts +18 -0
- package/dist/package.js +141 -0
- package/dist/project.js +1 -1
- package/dist/security.d.ts +4 -0
- package/dist/security.js +35 -0
- package/dist/targets.d.ts +9 -1
- package/dist/targets.js +28 -1
- 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 +7 -3
- package/schemas/valance.desktop.schema.json +17 -0
- package/schemas/valance.schema.json +22 -4
- package/schemas/valance.web.schema.json +46 -4
- 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/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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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
|
@@ -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) =>
|
|
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 {
|
|
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(
|
|
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) => {
|
|
|
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) => {
|
|
|
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) =>
|
|
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
|
|
@@ -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(
|
|
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. */
|