@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/config.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { AppError, diagnostic, lineOf } from "./diagnostics.js";
|
|
7
|
+
import { DEFAULT_TITLE } from "@valancex/common/internal";
|
|
7
8
|
export const PROJECT_FILE = "valance.json";
|
|
8
9
|
export const WEB_FILE = "valance.web.json";
|
|
10
|
+
export const DESKTOP_FILE = "valance.desktop.json";
|
|
9
11
|
/** The only version of both files there is. */
|
|
10
12
|
export const CONFIG_VERSION = 1;
|
|
11
13
|
const read = (root, file) => {
|
|
@@ -60,6 +62,35 @@ const url = (value) => {
|
|
|
60
62
|
return "must be an absolute address such as https://example.com";
|
|
61
63
|
}
|
|
62
64
|
};
|
|
65
|
+
const title = (value) => typeof value === "string" && value.includes("{page}") && value.length <= 120 && !/[{}]/.test(value.replaceAll("{page}", "").replaceAll("{site}", ""))
|
|
66
|
+
? undefined
|
|
67
|
+
: 'must contain "{page}" and may use "{site}", for example "{page} · {site}" (at most 120 characters)';
|
|
68
|
+
const flag = (value) => (typeof value === "boolean" ? undefined : "must be true or false");
|
|
69
|
+
const api = (value) => {
|
|
70
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
71
|
+
return 'must be an object such as { "base": "/api" }';
|
|
72
|
+
}
|
|
73
|
+
const given = value;
|
|
74
|
+
const unknown = Object.keys(given).filter((key) => key !== "base" && key !== "proxy");
|
|
75
|
+
if (unknown.length > 0) {
|
|
76
|
+
return `has unknown settings ${unknown.map((key) => `"${key}"`).join(", ")} (known: base, proxy)`;
|
|
77
|
+
}
|
|
78
|
+
if (typeof given["base"] !== "string" || !/^(\/([\w.~-]+\/?)*|https:\/\/[^\s/]+(\/[^\s]*)?)$/.test(given["base"]) || given["base"].endsWith("//")) {
|
|
79
|
+
return '"base" must be a path on the site ("/api") or an absolute https address ("https://api.example.com")';
|
|
80
|
+
}
|
|
81
|
+
if (given["proxy"] !== undefined) {
|
|
82
|
+
try {
|
|
83
|
+
const target = new URL(String(given["proxy"]));
|
|
84
|
+
if (!target.protocol.startsWith("http") || !given["base"].startsWith("/")) {
|
|
85
|
+
return '"proxy" is an http(s) address that a path "base" is forwarded to while you run vlx dev';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return '"proxy" must be an absolute address such as http://localhost:3000';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
};
|
|
63
94
|
/** Reads and validates the project's configuration. Every problem in both files is reported together. */
|
|
64
95
|
export const loadConfig = (root) => {
|
|
65
96
|
const project = read(root, PROJECT_FILE);
|
|
@@ -67,17 +98,33 @@ export const loadConfig = (root) => {
|
|
|
67
98
|
throw new AppError([diagnostic(PROJECT_FILE, 0, "config-missing", `there is no ${PROJECT_FILE} in ${root}`)]);
|
|
68
99
|
}
|
|
69
100
|
const web = read(root, WEB_FILE);
|
|
101
|
+
const desktop = read(root, DESKTOP_FILE);
|
|
70
102
|
const problems = [
|
|
71
|
-
...check(
|
|
72
|
-
...
|
|
103
|
+
...(desktop === undefined ? [] : check(DESKTOP_FILE, desktop.text, desktop.value, { $schema: schema, version }, [])),
|
|
104
|
+
...check(PROJECT_FILE, project.text, project.value, { $schema: schema, version, name: text, title }, ["name"]),
|
|
105
|
+
...(web === undefined ? [] : check(WEB_FILE, web.text, web.value, { $schema: schema, version, base, url, headers: flag, announce: flag, api }, [])),
|
|
73
106
|
];
|
|
74
107
|
if (problems.length > 0) {
|
|
75
108
|
throw new AppError(problems);
|
|
76
109
|
}
|
|
77
110
|
const given = (web?.value ?? {});
|
|
78
|
-
|
|
111
|
+
const named = project.value;
|
|
112
|
+
return {
|
|
113
|
+
name: named.name,
|
|
114
|
+
targets: desktop === undefined ? ["web"] : web === undefined ? ["desktop"] : ["web", "desktop"],
|
|
115
|
+
title: named.title ?? DEFAULT_TITLE,
|
|
116
|
+
web: { base: given.base ?? "/", url: given.url ?? "", headers: given.headers ?? true, announce: given.announce ?? true },
|
|
117
|
+
api: { base: given.api?.base ?? "", proxy: given.api?.proxy ?? "" },
|
|
118
|
+
};
|
|
79
119
|
};
|
|
80
|
-
export const siteInfo = (config) => ({
|
|
120
|
+
export const siteInfo = (config) => ({
|
|
121
|
+
name: config.name,
|
|
122
|
+
base: config.web.base,
|
|
123
|
+
url: config.web.url,
|
|
124
|
+
title: config.title,
|
|
125
|
+
api: config.api.base,
|
|
126
|
+
announce: config.web.announce,
|
|
127
|
+
});
|
|
81
128
|
/**
|
|
82
129
|
* The nearest directory at or above `from` that holds a `valance.json`, as its real path. The bundler names a file by its real path, so a project reached through a symlink, or by a short
|
|
83
130
|
* Windows name (`RUNNER~1`), must be called the same thing here or a file is not the file the bundler means.
|
package/dist/content.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// discovery, front matter, Markdown, navigation order, and a check of every link and image against the pages and `public/` that exist.
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { join, posix } from "node:path";
|
|
5
|
+
import { siteInfo } from "./config.js";
|
|
5
6
|
import { AppError, diagnostic } from "./diagnostics.js";
|
|
6
7
|
import { parseMarkdown, plain } from "./markdown.js";
|
|
7
8
|
export const CONTENT_DIR = "content";
|
|
@@ -188,6 +189,6 @@ export const readContent = (root, config) => {
|
|
|
188
189
|
throw new AppError(problems);
|
|
189
190
|
}
|
|
190
191
|
const ordered = [...pages].sort((a, b) => (a.section === "" ? 0 : 1) - (b.section === "" ? 0 : 1) || a.section.localeCompare(b.section) || a.order - b.order || a.path.localeCompare(b.path));
|
|
191
|
-
return { site:
|
|
192
|
+
return { site: siteInfo(config), pages: ordered, ...(fallback === undefined ? {} : { notFound: fallback }) };
|
|
192
193
|
};
|
|
193
194
|
export const sectionTitle = titleCase;
|
|
@@ -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,11 +51,18 @@ 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,
|
|
57
|
-
server: {
|
|
58
|
+
server: {
|
|
59
|
+
port: options.port ?? 5173,
|
|
60
|
+
host: options.host ?? "127.0.0.1",
|
|
61
|
+
strictPort: false,
|
|
62
|
+
fs: { allow: [root] },
|
|
63
|
+
// The backend, reached through the page's own origin: no CORS settings to write (valance.web.json `api`).
|
|
64
|
+
...(config.api.proxy === "" || !config.api.base.startsWith("/") ? {} : { proxy: { [config.api.base.replace(/\/$/, "")]: { target: config.api.proxy, changeOrigin: true } } }),
|
|
65
|
+
},
|
|
58
66
|
plugins: [
|
|
59
67
|
...(base.plugins ?? []),
|
|
60
68
|
{
|
|
@@ -107,13 +115,13 @@ const start = async (root, config, options, restart) => {
|
|
|
107
115
|
};
|
|
108
116
|
const wasFailing = failed.size > 0;
|
|
109
117
|
if (stages.includes("entries")) {
|
|
110
|
-
generateEntries(root);
|
|
118
|
+
generateEntries(root, config);
|
|
111
119
|
}
|
|
112
120
|
if (stages.includes("content")) {
|
|
113
121
|
await run("content", () => generateContent(root, config));
|
|
114
122
|
}
|
|
115
123
|
if (stages.includes("program")) {
|
|
116
|
-
await run("templates", async () => generateProgram(root, await loadAuthor(server, root)));
|
|
124
|
+
await run("templates", async () => generateProgram(root, await loadAuthor(server, root), config, ["web"]));
|
|
117
125
|
}
|
|
118
126
|
const open = [...failed.values()].flat();
|
|
119
127
|
options.onStatus?.(open);
|
|
@@ -169,7 +177,7 @@ const start = async (root, config, options, restart) => {
|
|
|
169
177
|
// First build: a mistake here is reported like any other and the server still starts, so the overlay can show it.
|
|
170
178
|
for (const [name, work] of [
|
|
171
179
|
["content", () => generateContent(root, config)],
|
|
172
|
-
["templates", async () => generateProgram(root, await loadAuthor(server, root))],
|
|
180
|
+
["templates", async () => generateProgram(root, await loadAuthor(server, root), config, ["web"])],
|
|
173
181
|
]) {
|
|
174
182
|
try {
|
|
175
183
|
await work();
|
|
@@ -199,6 +207,33 @@ const start = async (root, config, options, restart) => {
|
|
|
199
207
|
};
|
|
200
208
|
/** Starts the development server for the project at `root`. */
|
|
201
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
|
+
}
|
|
202
237
|
let current;
|
|
203
238
|
const log = options.log ??
|
|
204
239
|
((line) => {
|
|
@@ -237,7 +272,7 @@ export const dev = async (root, options = {}) => {
|
|
|
237
272
|
}, 500);
|
|
238
273
|
timer.unref();
|
|
239
274
|
};
|
|
240
|
-
|
|
275
|
+
// (the configuration is read above)
|
|
241
276
|
current = await start(root, config, options, restart);
|
|
242
277
|
return {
|
|
243
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;
|
package/dist/emit.js
CHANGED
|
@@ -17,30 +17,67 @@ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
|
17
17
|
import { join, relative, sep } from "node:path";
|
|
18
18
|
import { compileViews, PACKAGE_DIR, SOURCE_DIR } from "./compile.js";
|
|
19
19
|
import { readContent } from "./content.js";
|
|
20
|
+
import { targetOf } from "./targets.js";
|
|
21
|
+
import { forTarget } from "@valancex/common/internal";
|
|
22
|
+
import { AppError } from "./diagnostics.js";
|
|
20
23
|
import { ENTRY_FILE, ensureGeneratedDir, GENERATED_DIR, generated, writeIfChanged } from "./project.js";
|
|
21
24
|
import { buildWiring } from "./wiring.js";
|
|
25
|
+
/** The layout of the portable core on the web: `column` and `row` are custom elements, and this is what they are (`@valancex/common`'s web vocabulary). It comes first, so a project's own styles win. */
|
|
26
|
+
const CORE_CSS = `/* Generated by \`valance\`. The web's realization of the portable core's layout components. */
|
|
27
|
+
vlx-column {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: 0.5rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
vlx-row {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: row;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 0.5rem;
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
/** The stylesheets the web loads: every one under `src/` except those in another target's directory (`src/desktop/`). */
|
|
22
41
|
const stylesheets = (root) => existsSync(join(root, SOURCE_DIR))
|
|
23
42
|
? readdirSync(join(root, SOURCE_DIR), { recursive: true, encoding: "utf8" })
|
|
24
|
-
.
|
|
43
|
+
.map((file) => file.split(sep).join("/"))
|
|
44
|
+
.filter((file) => file.endsWith(".css") && !file.startsWith("desktop/"))
|
|
25
45
|
.sort()
|
|
26
46
|
.map((file) => `${SOURCE_DIR}/${file}`)
|
|
27
47
|
: [];
|
|
28
48
|
export const hasEntry = (root) => existsSync(join(root, ENTRY_FILE));
|
|
29
|
-
const clientSource = (root) => `// Generated by \`valance\`. Do not edit: it is rewritten whenever the project changes.
|
|
49
|
+
const clientSource = (root, config) => `// Generated by \`valance\`. Do not edit: it is rewritten whenever the project changes.
|
|
30
50
|
// @ts-nocheck
|
|
31
51
|
import { start } from "@valancex/common/client";
|
|
32
52
|
import content from "./content.json";
|
|
33
|
-
import program from "
|
|
53
|
+
import program from "./${programFile(config, "web")}";
|
|
54
|
+
import "./core.css";
|
|
34
55
|
${hasEntry(root) ? `import author from "../${ENTRY_FILE}";\n` : "const author = undefined;\n"}${stylesheets(root)
|
|
35
56
|
.map((file) => `import "../${file}";\n`)
|
|
36
57
|
.join("")}
|
|
37
58
|
await start({ content, program, author });
|
|
38
59
|
`;
|
|
39
|
-
const
|
|
60
|
+
const desktopSource = (root, config) => `// Generated by \`valance\`. Do not edit: it is rewritten whenever the project changes.
|
|
61
|
+
// @ts-nocheck
|
|
62
|
+
import { start } from "@valancex/common/desktop";
|
|
63
|
+
import { readFileSync } from "node:fs";
|
|
64
|
+
import { fileURLToPath } from "node:url";
|
|
65
|
+
import content from "./content.json";
|
|
66
|
+
import program from "./${programFile(config, "desktop")}";
|
|
67
|
+
${hasEntry(root) ? `import author from "../${ENTRY_FILE}";\n` : "const author = undefined;\n"}
|
|
68
|
+
// the MESH runtime's module sits beside the built engine (vlx build copies it there)
|
|
69
|
+
const wasm = new Uint8Array(readFileSync(new URL("./mesh-runtime.wasm", import.meta.url)));
|
|
70
|
+
// (and the project's public/ files, which images are drawn from)
|
|
71
|
+
const assets = fileURLToPath(new URL("./public/", import.meta.url));
|
|
72
|
+
await start({ content, program, author, wasm, assets });
|
|
73
|
+
// the window is closed: nothing the application left open keeps the engine alive
|
|
74
|
+
process.exit(0);
|
|
75
|
+
`;
|
|
76
|
+
const serverSource = (root, config) => `// Generated by \`valance\`. Do not edit: it is rewritten whenever the project changes.
|
|
40
77
|
// @ts-nocheck
|
|
41
78
|
import { createRenderer, pathsOf } from "@valancex/common/server";
|
|
42
79
|
import content from "./content.json";
|
|
43
|
-
import program from "
|
|
80
|
+
import program from "./${programFile(config, "web")}";
|
|
44
81
|
${hasEntry(root) ? `import author from "../${ENTRY_FILE}";\n` : "const author = undefined;\n"}
|
|
45
82
|
export const render = createRenderer({ content, program, author });
|
|
46
83
|
export const paths = () => pathsOf(content);
|
|
@@ -54,20 +91,50 @@ export const generateContent = (root, config) => {
|
|
|
54
91
|
writeContent(root, content);
|
|
55
92
|
return content;
|
|
56
93
|
};
|
|
57
|
-
/** The
|
|
58
|
-
export const
|
|
94
|
+
/** 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. */
|
|
95
|
+
export const programFile = (config, id) => ((config?.targets.length ?? 1) > 1 ? `program.${id}.json` : "program.json");
|
|
96
|
+
const manifestFile = (config, id) => ((config?.targets.length ?? 1) > 1 ? `manifest.${id}.json` : "manifest.json");
|
|
97
|
+
const wiringFile = (config, id) => ((config?.targets.length ?? 1) > 1 ? `wiring.${id}.json` : "wiring.json");
|
|
98
|
+
/** 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. */
|
|
99
|
+
export const generateProgram = async (root, author, config, only) => {
|
|
59
100
|
ensureGeneratedDir(root);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
const targets = (config?.targets ?? ["web"]).filter((id) => only === undefined || only.includes(id));
|
|
102
|
+
const compiledAll = [];
|
|
103
|
+
const problems = [];
|
|
104
|
+
for (const id of targets) {
|
|
105
|
+
const target = targetOf(id);
|
|
106
|
+
try {
|
|
107
|
+
compiledAll.push({ id, compiled: await compileViews(root, forTarget(author, id), target.vocabulary, target.views) });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!(error instanceof AppError)) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
problems.push(...error.diagnostics);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (problems.length > 0) {
|
|
117
|
+
throw new AppError(problems);
|
|
118
|
+
}
|
|
67
119
|
const readme = join(PACKAGE_DIR, "README.md");
|
|
68
120
|
const docs = existsSync(readme) ? { file: relative(root, readme).split(sep).join("/"), lines: readFileSync(readme, "utf8").split("\n") } : undefined;
|
|
69
|
-
|
|
70
|
-
|
|
121
|
+
const editor = [];
|
|
122
|
+
for (const { id, compiled } of compiledAll) {
|
|
123
|
+
writeIfChanged(generated(root, programFile(config, id)), `${JSON.stringify(compiled.program)}\n`);
|
|
124
|
+
writeIfChanged(generated(root, manifestFile(config, id)), `${JSON.stringify(compiled.manifest, null, 2)}\n`);
|
|
125
|
+
// Where every name came from and what uses it, at both ends, for the editor. The framework's own names point into the README that ships with the package.
|
|
126
|
+
writeIfChanged(generated(root, wiringFile(config, id)), `${JSON.stringify(buildWiring(root, forTarget(author, id), compiled, docs), null, 2)}\n`);
|
|
127
|
+
editor.push({ id, model: `${GENERATED_DIR}/${manifestFile(config, id)}`, wiring: `${GENERATED_DIR}/${wiringFile(config, id)}`, components: compiled.files });
|
|
128
|
+
}
|
|
129
|
+
// The one file an editor reads to know the project: where the manifest is, and which file is the template of which component. The conventions stay here, not copied into every editor. For a project
|
|
130
|
+
// of several targets the first one is what the editor reads (`model`, `wiring`), and `targets` lists each.
|
|
131
|
+
const first = editor[0];
|
|
132
|
+
// Only a pass over every target writes it (a pass over one, as `vlx dev --web` is, would leave the others out of it).
|
|
133
|
+
if (first !== undefined && editor.length === (config?.targets ?? ["web"]).length) {
|
|
134
|
+
writeIfChanged(generated(root, "editor.json"), `${JSON.stringify({ version: 1, model: first.model, components: first.components, wiring: first.wiring, ...(editor.length > 1 ? { targets: Object.fromEntries(editor.map((each) => [each.id, { model: each.model, wiring: each.wiring, components: each.components }])) } : {}) }, null, 2)}\n`);
|
|
135
|
+
}
|
|
136
|
+
writeTemplateTypes(root, compiledAll.map((each) => each.compiled));
|
|
137
|
+
return [...new Set(compiledAll.flatMap((each) => each.compiled.components))];
|
|
71
138
|
};
|
|
72
139
|
const union = (names) => (names.length === 0 ? "never" : names.map((name) => JSON.stringify(name)).join(" | "));
|
|
73
140
|
/** The declaration of one template's type: its props and events as the names TypeScript checks a contract's keys against. */
|
|
@@ -81,15 +148,16 @@ export default template;
|
|
|
81
148
|
*/
|
|
82
149
|
const typePath = (file) => `types/${file.replace(new RegExp(`^${SOURCE_DIR}/`), "").replace(/\.mprx$/, ".d.mprx.ts")}`;
|
|
83
150
|
/** Writes the type of each of the author's templates, and removes the type of one that is gone. */
|
|
84
|
-
const writeTemplateTypes = (root,
|
|
85
|
-
const components = compiled.manifest
|
|
86
|
-
.components;
|
|
151
|
+
const writeTemplateTypes = (root, all) => {
|
|
87
152
|
const written = new Set();
|
|
88
|
-
for (const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
153
|
+
for (const compiled of all) {
|
|
154
|
+
const components = compiled.manifest.components;
|
|
155
|
+
for (const template of compiled.templates.filter((each) => !each.builtIn)) {
|
|
156
|
+
const entry = components[template.component];
|
|
157
|
+
const path = typePath(template.file);
|
|
158
|
+
written.add(path);
|
|
159
|
+
writeIfChanged(generated(root, path), templateType(Object.keys(entry?.props ?? {}), Object.keys(entry?.events ?? {})));
|
|
160
|
+
}
|
|
93
161
|
}
|
|
94
162
|
const dir = generated(root, "types");
|
|
95
163
|
if (existsSync(dir)) {
|
|
@@ -100,8 +168,18 @@ const writeTemplateTypes = (root, compiled) => {
|
|
|
100
168
|
}
|
|
101
169
|
}
|
|
102
170
|
};
|
|
103
|
-
/** The entries; rewritten when `src/main.ts` or a stylesheet is added or removed. */
|
|
104
|
-
export const generateEntries = (root) => {
|
|
171
|
+
/** 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`). */
|
|
172
|
+
export const generateEntries = (root, config) => {
|
|
105
173
|
ensureGeneratedDir(root);
|
|
106
|
-
|
|
174
|
+
const targets = config?.targets ?? ["web"];
|
|
175
|
+
return [
|
|
176
|
+
...(targets.includes("web")
|
|
177
|
+
? [
|
|
178
|
+
writeIfChanged(generated(root, "core.css"), CORE_CSS),
|
|
179
|
+
writeIfChanged(generated(root, "client.ts"), clientSource(root, config)),
|
|
180
|
+
writeIfChanged(generated(root, "server.ts"), serverSource(root, config)),
|
|
181
|
+
]
|
|
182
|
+
: []),
|
|
183
|
+
...(targets.includes("desktop") ? [writeIfChanged(generated(root, "desktop.ts"), desktopSource(root, config))] : []),
|
|
184
|
+
].some(Boolean);
|
|
107
185
|
};
|
package/dist/init.js
CHANGED
|
@@ -4,7 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { PROJECT_FILE, WEB_FILE } from "./config.js";
|
|
7
|
+
import { DESKTOP_FILE, PROJECT_FILE, WEB_FILE } from "./config.js";
|
|
8
8
|
export const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
9
9
|
const PACKAGE = new URL("../", import.meta.url);
|
|
10
10
|
const TEMPLATES = new URL("../templates/", import.meta.url);
|
|
@@ -84,6 +84,7 @@ export const plan = (answers) => {
|
|
|
84
84
|
const out = new Map();
|
|
85
85
|
const title = titleOf(answers.name);
|
|
86
86
|
const web = answers.targets.includes("web");
|
|
87
|
+
const desktop = answers.targets.includes("desktop");
|
|
87
88
|
const hasBase = answers.web.base !== "" && answers.web.base !== "/";
|
|
88
89
|
const settings = [hasBase ? `base ${answers.web.base}` : "", answers.web.url !== "" ? `url ${answers.web.url}` : ""].filter((each) => each !== "");
|
|
89
90
|
const tokens = {
|
|
@@ -92,14 +93,20 @@ export const plan = (answers) => {
|
|
|
92
93
|
web_note: web && settings.length > 0 ? `, and \`${WEB_FILE}\` says where the web site is served (${settings.join(", ")})` : "",
|
|
93
94
|
};
|
|
94
95
|
const fill = (text) => text.replace(/\{\{(\w+)\}\}/g, (match, key) => tokens[key] ?? match);
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
// A project for more than one target keeps what they share (the content, the state and the commands) and gives each its own templates (`src/web/`, `src/desktop/`): the "multi" template, on
|
|
97
|
+
// top of the web's files that are not templates.
|
|
98
|
+
const layers = web && desktop ? ["web", "multi"] : web ? ["web"] : desktop ? ["desktop"] : [];
|
|
99
|
+
for (const target of layers) {
|
|
100
|
+
const root = new URL(`${target}/`, TEMPLATES);
|
|
97
101
|
for (const file of files(root)) {
|
|
102
|
+
if (target === "web" && layers.includes("multi") && (file.startsWith("src/") || file.startsWith("content/") || file === "README.md")) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
98
105
|
out.set(written(file), fill(readFileSync(new URL(file, root), "utf8")));
|
|
99
106
|
}
|
|
100
107
|
}
|
|
101
108
|
out.set(PROJECT_FILE, json({ $schema: "./node_modules/@valancex/cli/schemas/valance.schema.json", version: 1, name: title }));
|
|
102
|
-
if (web && (hasBase || answers.web.url !== "")) {
|
|
109
|
+
if (web && (hasBase || answers.web.url !== "" || desktop)) {
|
|
103
110
|
out.set(WEB_FILE, json({
|
|
104
111
|
$schema: "./node_modules/@valancex/cli/schemas/valance.web.schema.json",
|
|
105
112
|
version: 1,
|
|
@@ -107,6 +114,9 @@ export const plan = (answers) => {
|
|
|
107
114
|
...(answers.web.url !== "" ? { url: answers.web.url } : {}),
|
|
108
115
|
}));
|
|
109
116
|
}
|
|
117
|
+
if (desktop) {
|
|
118
|
+
out.set(DESKTOP_FILE, json({ $schema: "./node_modules/@valancex/cli/schemas/valance.desktop.schema.json", version: 1 }));
|
|
119
|
+
}
|
|
110
120
|
out.set("package.json", json({
|
|
111
121
|
name: packageName(basename(answers.dir)),
|
|
112
122
|
version: "0.0.0",
|
package/dist/markdown.js
CHANGED
|
@@ -5,7 +5,8 @@ export const slug = (text) => text
|
|
|
5
5
|
.replace(/[̀-ͯ]/g, "")
|
|
6
6
|
.replace(/[^a-z0-9]+/g, "-")
|
|
7
7
|
.replace(/^-+|-+$/g, "");
|
|
8
|
-
|
|
8
|
+
// A link's address may hold one level of balanced parentheses (`/a_(b)`, `javascript:alert(1)`), so it is read whole: a check that names it quotes all of it.
|
|
9
|
+
const MARKS = /\*\*([^*]+)\*\*|`([^`]+)`|!\[[^\]]*\]\((?:[^()\s]|\([^()\s]*\))*\)|\[([^\]]+)\]\(((?:[^()\s]|\([^()\s]*\))+)\)|\*([^*\s][^*]*)\*|(?<![\w])_([^_\s][^_]*)_(?![\w])/g;
|
|
9
10
|
const empty = { level: 0, anchor: "", spans: [], items: [], ordered: false, lang: "", text: "", src: "", alt: "" };
|
|
10
11
|
export const plain = (spans) => spans.map((span) => span.text).join("");
|
|
11
12
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const PLATFORMS: readonly ["linux-x64", "win32-x64"];
|
|
2
|
+
export type Platform = (typeof PLATFORMS)[number];
|
|
3
|
+
export interface PackageOptions {
|
|
4
|
+
/** The platform the package is for; by default the one this runs on. */
|
|
5
|
+
readonly platform?: string;
|
|
6
|
+
/** The Desktop PORT's window program for that platform (`vlx-window`); by default `VLX_WINDOW`, else `vlx-window` on the PATH. */
|
|
7
|
+
readonly window?: string;
|
|
8
|
+
/** A `node` binary of the same version for that platform; by default the one running this, which is only right for the platform this runs on. */
|
|
9
|
+
readonly node?: string;
|
|
10
|
+
/** Also write the package as one file: a `.tar.gz` for Linux, a `.zip` for Windows, with its SHA-256 beside it. */
|
|
11
|
+
readonly archive?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface Packaged {
|
|
14
|
+
readonly dir: string;
|
|
15
|
+
readonly files: ReadonlyArray<string>;
|
|
16
|
+
readonly notes: ReadonlyArray<string>;
|
|
17
|
+
}
|
|
18
|
+
export declare const packageDesktop: (root: string, options?: PackageOptions) => Promise<Packaged>;
|