@valancex/cli 0.12.0 → 0.14.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 +69 -11
- 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/budgets.json +175 -0
- package/dist/build.d.ts +11 -0
- package/dist/build.js +120 -30
- package/dist/check.js +2 -2
- package/dist/cli.js +131 -17
- package/dist/compile.d.ts +11 -3
- package/dist/compile.js +123 -25
- package/dist/config.d.ts +13 -0
- package/dist/config.js +34 -1
- package/dist/contract.d.ts +18 -1
- package/dist/contract.js +85 -3
- package/dist/dev-desktop.d.ts +8 -0
- package/dist/dev-desktop.js +114 -0
- package/dist/dev.d.ts +8 -0
- package/dist/dev.js +91 -29
- package/dist/emit.d.ts +9 -4
- package/dist/emit.js +129 -28
- package/dist/init.js +14 -4
- package/dist/package.d.ts +18 -0
- package/dist/package.js +142 -0
- package/dist/project.js +1 -1
- package/dist/recording-guard.d.ts +5 -0
- package/dist/recording-guard.js +28 -0
- package/dist/replay.d.ts +15 -0
- package/dist/replay.js +59 -0
- package/dist/rules.d.ts +23 -0
- package/dist/rules.js +123 -0
- package/dist/runtime-guard.d.ts +11 -0
- package/dist/runtime-guard.js +62 -0
- package/dist/security.d.ts +2 -0
- package/dist/security.js +8 -1
- package/dist/size.d.ts +54 -0
- package/dist/size.js +153 -0
- package/dist/targets.d.ts +6 -1
- package/dist/targets.js +26 -4
- package/dist/theme.d.ts +35 -0
- package/dist/theme.js +44 -0
- package/dist/tree.d.ts +14 -2
- package/dist/tree.js +16 -1
- package/dist/ui/init.js +4 -4
- package/dist/ui/report.js +1 -1
- package/dist/ui/run.d.ts +4 -1
- package/dist/ui/run.js +18 -3
- package/dist/vite.d.ts +3 -2
- package/dist/vite.js +3 -3
- package/package.json +15 -7
- package/schemas/valance.desktop.schema.json +17 -0
- package/schemas/valance.schema.json +71 -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 +7 -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/templates/web/gitignore +3 -0
- package/templates/web/src/layout.mprx +2 -2
- package/views/block.mprx +1 -1
- package/views/layout.mprx +2 -2
- package/views/list-items.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/dev.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
// `vlx dev`: the project served from memory and from disk, rebuilt as its authored files change. Built on a Vite server for what Vite is good at (serving modules, style updates without a
|
|
2
|
+
// reload, the error overlay); everything else is this file's: each page is rendered on the server with the same code a build uses, so development shows what production will.
|
|
3
|
+
//
|
|
4
|
+
// What each change does:
|
|
5
|
+
//
|
|
6
|
+
// content/** the content is rebuilt; the browser reloads
|
|
7
|
+
// src/**/*.mprx, src/main.ts the templates are recompiled (the manifest derives from main.ts); the browser reloads
|
|
8
|
+
// src/**/*.css the style is replaced in the open page, no reload
|
|
9
|
+
// public/** the browser reloads
|
|
10
|
+
// adding or removing a stylesheet or src/main.ts the entries are rewritten; the browser reloads
|
|
11
|
+
// valance.json, valance.web.json the server restarts (the name and the base are fixed when it starts)
|
|
12
|
+
//
|
|
13
|
+
// A mistake in any of them leaves the last good build serving, and is shown in the terminal and as an overlay in the page, naming the file and line. Fixing it reloads the page.
|
|
14
|
+
import { recordingNotice } from "./recording-guard.js";
|
|
1
15
|
import { createServer } from "vite";
|
|
2
|
-
import {
|
|
16
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
17
|
+
import { dirname, relative, sep } from "node:path";
|
|
3
18
|
import { loadAuthor } from "./author.js";
|
|
4
19
|
import { loadConfig, PROJECT_FILE, WEB_FILE } from "./config.js";
|
|
5
20
|
import { AppError, format } from "./diagnostics.js";
|
|
21
|
+
import { devDesktop } from "./dev-desktop.js";
|
|
6
22
|
import { generateContent, generateEntries, generateProgram } from "./emit.js";
|
|
7
23
|
import { GENERATED_DIR, OUTPUT_DIR } from "./project.js";
|
|
8
24
|
import { baseConfig, SERVER_ENTRY } from "./vite.js";
|
|
@@ -50,7 +66,10 @@ const start = async (root, config, options, restart) => {
|
|
|
50
66
|
});
|
|
51
67
|
const failed = new Map();
|
|
52
68
|
let pending = Promise.resolve();
|
|
53
|
-
generateEntries(root);
|
|
69
|
+
generateEntries(root, config, { record: options.record !== undefined });
|
|
70
|
+
if (options.record !== undefined) {
|
|
71
|
+
recordingNotice(root, options.record, config).forEach(log);
|
|
72
|
+
}
|
|
54
73
|
const base = baseConfig(root, config);
|
|
55
74
|
const server = await createServer({
|
|
56
75
|
...base,
|
|
@@ -66,29 +85,45 @@ const start = async (root, config, options, restart) => {
|
|
|
66
85
|
...(base.plugins ?? []),
|
|
67
86
|
{
|
|
68
87
|
name: "valance",
|
|
69
|
-
configureServer: (vite) =>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
configureServer: (vite) => {
|
|
89
|
+
if (options.record !== undefined) {
|
|
90
|
+
const file = options.record;
|
|
91
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
92
|
+
// the page's recorder posts each line of its log here, in order
|
|
93
|
+
vite.middlewares.use("/__vlx/record", (request, response) => {
|
|
94
|
+
const chunks = [];
|
|
95
|
+
request.on("data", (chunk) => chunks.push(chunk));
|
|
96
|
+
request.on("end", () => {
|
|
97
|
+
appendFileSync(file, `${Buffer.concat(chunks).toString("utf8")}\n`);
|
|
98
|
+
response.statusCode = 204;
|
|
99
|
+
response.end();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return () => {
|
|
104
|
+
// After Vite's own middleware: modules, styles, public files and the client have had their chance. What is left is a page.
|
|
105
|
+
vite.middlewares.use((request, response, next) => {
|
|
106
|
+
void (async () => {
|
|
107
|
+
// Vite has already removed the base from `request.url`; the application's addresses include it.
|
|
108
|
+
const url = new URL(request.originalUrl ?? request.url ?? "/", "http://localhost");
|
|
109
|
+
if ((request.method !== "GET" && request.method !== "HEAD") || !url.pathname.startsWith(config.web.base) || /\.[A-Za-z0-9]+$/.test(url.pathname)) {
|
|
110
|
+
next();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const { render } = (await vite.ssrLoadModule(SERVER_ENTRY));
|
|
115
|
+
const { status, html } = await render(url.pathname, { script: `${config.web.base}${GENERATED_DIR}/client.ts`, styles: [] });
|
|
116
|
+
response.statusCode = status;
|
|
117
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
118
|
+
response.end(await vite.transformIndexHtml(url.pathname, html));
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
vite.ssrFixStacktrace(error);
|
|
122
|
+
next(error);
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
});
|
|
126
|
+
};
|
|
92
127
|
},
|
|
93
128
|
},
|
|
94
129
|
],
|
|
@@ -114,13 +149,13 @@ const start = async (root, config, options, restart) => {
|
|
|
114
149
|
};
|
|
115
150
|
const wasFailing = failed.size > 0;
|
|
116
151
|
if (stages.includes("entries")) {
|
|
117
|
-
generateEntries(root);
|
|
152
|
+
generateEntries(root, config, { record: options.record !== undefined });
|
|
118
153
|
}
|
|
119
154
|
if (stages.includes("content")) {
|
|
120
155
|
await run("content", () => generateContent(root, config));
|
|
121
156
|
}
|
|
122
157
|
if (stages.includes("program")) {
|
|
123
|
-
await run("templates", async () => generateProgram(root, await loadAuthor(server, root)));
|
|
158
|
+
await run("templates", async () => generateProgram(root, await loadAuthor(server, root), config, ["web"]));
|
|
124
159
|
}
|
|
125
160
|
const open = [...failed.values()].flat();
|
|
126
161
|
options.onStatus?.(open);
|
|
@@ -176,7 +211,7 @@ const start = async (root, config, options, restart) => {
|
|
|
176
211
|
// First build: a mistake here is reported like any other and the server still starts, so the overlay can show it.
|
|
177
212
|
for (const [name, work] of [
|
|
178
213
|
["content", () => generateContent(root, config)],
|
|
179
|
-
["templates", async () => generateProgram(root, await loadAuthor(server, root))],
|
|
214
|
+
["templates", async () => generateProgram(root, await loadAuthor(server, root), config, ["web"])],
|
|
180
215
|
]) {
|
|
181
216
|
try {
|
|
182
217
|
await work();
|
|
@@ -206,6 +241,33 @@ const start = async (root, config, options, restart) => {
|
|
|
206
241
|
};
|
|
207
242
|
/** Starts the development server for the project at `root`. */
|
|
208
243
|
export const dev = async (root, options = {}) => {
|
|
244
|
+
const config = loadConfig(root);
|
|
245
|
+
const target = options.target ?? (config.targets.length === 1 ? config.targets[0] : undefined);
|
|
246
|
+
if (target !== undefined && !config.targets.includes(target)) {
|
|
247
|
+
throw new AppError([
|
|
248
|
+
{ file: "valance.json", line: 0, column: 0, code: "dev-target", message: `this project is not built for ${target} (its targets: ${config.targets.join(", ")})` },
|
|
249
|
+
]);
|
|
250
|
+
}
|
|
251
|
+
// 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.
|
|
252
|
+
if (target === undefined) {
|
|
253
|
+
const web = await dev(root, { ...options, target: "web" });
|
|
254
|
+
const desktop = await devDesktop(root, options);
|
|
255
|
+
return {
|
|
256
|
+
url: web.url,
|
|
257
|
+
closed: desktop.closed,
|
|
258
|
+
settled: async () => {
|
|
259
|
+
await web.settled();
|
|
260
|
+
await desktop.settled();
|
|
261
|
+
},
|
|
262
|
+
close: async () => {
|
|
263
|
+
await desktop.close();
|
|
264
|
+
await web.close();
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (target === "desktop") {
|
|
269
|
+
return devDesktop(root, options);
|
|
270
|
+
}
|
|
209
271
|
let current;
|
|
210
272
|
const log = options.log ??
|
|
211
273
|
((line) => {
|
|
@@ -244,7 +306,7 @@ export const dev = async (root, options = {}) => {
|
|
|
244
306
|
}, 500);
|
|
245
307
|
timer.unref();
|
|
246
308
|
};
|
|
247
|
-
|
|
309
|
+
// (the configuration is read above)
|
|
248
310
|
current = await start(root, config, options, restart);
|
|
249
311
|
return {
|
|
250
312
|
get url() {
|
package/dist/emit.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
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
|
|
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, options?: {
|
|
17
|
+
readonly record?: boolean;
|
|
18
|
+
}) => boolean;
|
package/dist/emit.js
CHANGED
|
@@ -17,30 +17,82 @@ 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";
|
|
24
|
+
import { themeOf } from "./theme.js";
|
|
21
25
|
import { buildWiring } from "./wiring.js";
|
|
26
|
+
/** 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 is the library's layer, the first of the
|
|
27
|
+
* three (library, theme, application): a theme's gap replaces the default here, and a project's own styles, which are in no layer, win over both. */
|
|
28
|
+
const CORE_CSS = `/* Generated by \`valance\`. The web's realization of the portable core's layout components. */
|
|
29
|
+
@layer vlx-library, vlx-theme, vlx-app;
|
|
30
|
+
|
|
31
|
+
@layer vlx-library {
|
|
32
|
+
vlx-column {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
gap: 0.5rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
vlx-row {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: row;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 0.5rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Until the application has started (H5) a control the person can see does not yet do anything: say so, so they do not tap it again. */
|
|
46
|
+
[data-vlx="starting"] :is(button, input, select, textarea, summary, [role="button"], label) {
|
|
47
|
+
cursor: progress;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
/** The theme's stylesheet. A theme with a problem is reported by \`check\` (and refused by \`build\`); the page gets no theme meanwhile rather than a half of one. */
|
|
52
|
+
const themeStylesheet = (root) => themeOf(root).css ?? "/* The theme has a problem: run `vlx check`. */\n";
|
|
53
|
+
/** The stylesheets the web loads: every one under `src/` except those in another target's directory (`src/desktop/`). */
|
|
22
54
|
const stylesheets = (root) => existsSync(join(root, SOURCE_DIR))
|
|
23
55
|
? readdirSync(join(root, SOURCE_DIR), { recursive: true, encoding: "utf8" })
|
|
24
|
-
.
|
|
56
|
+
.map((file) => file.split(sep).join("/"))
|
|
57
|
+
.filter((file) => file.endsWith(".css") && !file.startsWith("desktop/"))
|
|
25
58
|
.sort()
|
|
26
59
|
.map((file) => `${SOURCE_DIR}/${file}`)
|
|
27
60
|
: [];
|
|
28
61
|
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.
|
|
62
|
+
const clientSource = (root, config, record = false) => `// Generated by \`valance\`. Do not edit: it is rewritten whenever the project changes.
|
|
30
63
|
// @ts-nocheck
|
|
31
64
|
import { start } from "@valancex/common/client";
|
|
32
65
|
import content from "./content.json";
|
|
33
|
-
import program from "
|
|
66
|
+
import program from "./${programFile(config, "web")}";
|
|
67
|
+
import "./theme.css";
|
|
68
|
+
import "./core.css";
|
|
34
69
|
${hasEntry(root) ? `import author from "../${ENTRY_FILE}";\n` : "const author = undefined;\n"}${stylesheets(root)
|
|
35
70
|
.map((file) => `import "../${file}";\n`)
|
|
36
71
|
.join("")}
|
|
37
|
-
await start({ content, program, author });
|
|
72
|
+
${record ? `// \`vlx dev --record\`: development only. A build rewrites this entry without it.\nconst { recording } = await import("@valancex/common/record");\nconst record = recording(program, ${JSON.stringify(config?.record.redact ?? { scope: [], commands: [] })});\n` : ""}await start({ content, program, author${record ? ", record" : ""} });
|
|
38
73
|
`;
|
|
39
|
-
const
|
|
74
|
+
const desktopSource = (root, config) => `// Generated by \`valance\`. Do not edit: it is rewritten whenever the project changes.
|
|
75
|
+
// @ts-nocheck
|
|
76
|
+
import { start } from "@valancex/common/desktop";
|
|
77
|
+
import theme from "./theme.desktop.json";
|
|
78
|
+
import { readFileSync } from "node:fs";
|
|
79
|
+
import { fileURLToPath } from "node:url";
|
|
80
|
+
import content from "./content.json";
|
|
81
|
+
import program from "./${programFile(config, "desktop")}";
|
|
82
|
+
${hasEntry(root) ? `import author from "../${ENTRY_FILE}";\n` : "const author = undefined;\n"}
|
|
83
|
+
// the MESH runtime's module sits beside the built engine (vlx build copies it there)
|
|
84
|
+
const wasm = new Uint8Array(readFileSync(new URL("./mesh-runtime.wasm", import.meta.url)));
|
|
85
|
+
// (and the project's public/ files, which images are drawn from)
|
|
86
|
+
const assets = fileURLToPath(new URL("./public/", import.meta.url));
|
|
87
|
+
await start({ content, program, author, wasm, assets, theme: theme ?? undefined });
|
|
88
|
+
// the window is closed: nothing the application left open keeps the engine alive
|
|
89
|
+
process.exit(0);
|
|
90
|
+
`;
|
|
91
|
+
const serverSource = (root, config) => `// Generated by \`valance\`. Do not edit: it is rewritten whenever the project changes.
|
|
40
92
|
// @ts-nocheck
|
|
41
93
|
import { createRenderer, pathsOf } from "@valancex/common/server";
|
|
42
94
|
import content from "./content.json";
|
|
43
|
-
import program from "
|
|
95
|
+
import program from "./${programFile(config, "web")}";
|
|
44
96
|
${hasEntry(root) ? `import author from "../${ENTRY_FILE}";\n` : "const author = undefined;\n"}
|
|
45
97
|
export const render = createRenderer({ content, program, author });
|
|
46
98
|
export const paths = () => pathsOf(content);
|
|
@@ -54,20 +106,52 @@ export const generateContent = (root, config) => {
|
|
|
54
106
|
writeContent(root, content);
|
|
55
107
|
return content;
|
|
56
108
|
};
|
|
57
|
-
/** The
|
|
58
|
-
export const
|
|
109
|
+
/** 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. */
|
|
110
|
+
export const programFile = (config, id) => ((config?.targets.length ?? 1) > 1 ? `program.${id}.json` : "program.json");
|
|
111
|
+
const manifestFile = (config, id) => ((config?.targets.length ?? 1) > 1 ? `manifest.${id}.json` : "manifest.json");
|
|
112
|
+
const wiringFile = (config, id) => ((config?.targets.length ?? 1) > 1 ? `wiring.${id}.json` : "wiring.json");
|
|
113
|
+
/** 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. */
|
|
114
|
+
export const generateProgram = async (root, author, config, only) => {
|
|
59
115
|
ensureGeneratedDir(root);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
116
|
+
const targets = (config?.targets ?? ["web"]).filter((id) => only === undefined || only.includes(id));
|
|
117
|
+
const compiledAll = [];
|
|
118
|
+
const problems = [];
|
|
119
|
+
for (const id of targets) {
|
|
120
|
+
const target = targetOf(id);
|
|
121
|
+
try {
|
|
122
|
+
compiledAll.push({ id, compiled: await compileViews(root, forTarget(author, id), target.vocabulary, target.views) });
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (!(error instanceof AppError)) {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
problems.push(...error.diagnostics);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// a theme the project writes is checked with the templates: a mistake in it is as much the project's as one in a layout
|
|
132
|
+
problems.push(...(themeOf(root).custom ? themeOf(root).diagnostics : []));
|
|
133
|
+
if (problems.length > 0) {
|
|
134
|
+
throw new AppError(problems);
|
|
135
|
+
}
|
|
67
136
|
const readme = join(PACKAGE_DIR, "README.md");
|
|
68
137
|
const docs = existsSync(readme) ? { file: relative(root, readme).split(sep).join("/"), lines: readFileSync(readme, "utf8").split("\n") } : undefined;
|
|
69
|
-
|
|
70
|
-
|
|
138
|
+
const editor = [];
|
|
139
|
+
for (const { id, compiled } of compiledAll) {
|
|
140
|
+
writeIfChanged(generated(root, programFile(config, id)), `${JSON.stringify(compiled.program)}\n`);
|
|
141
|
+
writeIfChanged(generated(root, manifestFile(config, id)), `${JSON.stringify(compiled.manifest, null, 2)}\n`);
|
|
142
|
+
// 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.
|
|
143
|
+
writeIfChanged(generated(root, wiringFile(config, id)), `${JSON.stringify(buildWiring(root, forTarget(author, id), compiled, docs), null, 2)}\n`);
|
|
144
|
+
editor.push({ id, model: `${GENERATED_DIR}/${manifestFile(config, id)}`, wiring: `${GENERATED_DIR}/${wiringFile(config, id)}`, components: compiled.files });
|
|
145
|
+
}
|
|
146
|
+
// 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
|
|
147
|
+
// of several targets the first one is what the editor reads (`model`, `wiring`), and `targets` lists each.
|
|
148
|
+
const first = editor[0];
|
|
149
|
+
// Only a pass over every target writes it (a pass over one, as `vlx dev --web` is, would leave the others out of it).
|
|
150
|
+
if (first !== undefined && editor.length === (config?.targets ?? ["web"]).length) {
|
|
151
|
+
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`);
|
|
152
|
+
}
|
|
153
|
+
writeTemplateTypes(root, compiledAll.map((each) => each.compiled));
|
|
154
|
+
return [...new Set(compiledAll.flatMap((each) => each.compiled.components))];
|
|
71
155
|
};
|
|
72
156
|
const union = (names) => (names.length === 0 ? "never" : names.map((name) => JSON.stringify(name)).join(" | "));
|
|
73
157
|
/** The declaration of one template's type: its props and events as the names TypeScript checks a contract's keys against. */
|
|
@@ -81,15 +165,16 @@ export default template;
|
|
|
81
165
|
*/
|
|
82
166
|
const typePath = (file) => `types/${file.replace(new RegExp(`^${SOURCE_DIR}/`), "").replace(/\.mprx$/, ".d.mprx.ts")}`;
|
|
83
167
|
/** 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;
|
|
168
|
+
const writeTemplateTypes = (root, all) => {
|
|
87
169
|
const written = new Set();
|
|
88
|
-
for (const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
170
|
+
for (const compiled of all) {
|
|
171
|
+
const components = compiled.manifest.components;
|
|
172
|
+
for (const template of compiled.templates.filter((each) => !each.builtIn)) {
|
|
173
|
+
const entry = components[template.component];
|
|
174
|
+
const path = typePath(template.file);
|
|
175
|
+
written.add(path);
|
|
176
|
+
writeIfChanged(generated(root, path), templateType(Object.keys(entry?.props ?? {}), Object.keys(entry?.events ?? {})));
|
|
177
|
+
}
|
|
93
178
|
}
|
|
94
179
|
const dir = generated(root, "types");
|
|
95
180
|
if (existsSync(dir)) {
|
|
@@ -100,8 +185,24 @@ const writeTemplateTypes = (root, compiled) => {
|
|
|
100
185
|
}
|
|
101
186
|
}
|
|
102
187
|
};
|
|
103
|
-
/** The entries; rewritten when `src/main.ts` or a stylesheet is added or removed. */
|
|
104
|
-
export const generateEntries = (root) => {
|
|
188
|
+
/** 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`). */
|
|
189
|
+
export const generateEntries = (root, config, options) => {
|
|
105
190
|
ensureGeneratedDir(root);
|
|
106
|
-
|
|
191
|
+
const targets = config?.targets ?? ["web"];
|
|
192
|
+
return [
|
|
193
|
+
...(targets.includes("web")
|
|
194
|
+
? [
|
|
195
|
+
writeIfChanged(generated(root, "core.css"), CORE_CSS),
|
|
196
|
+
writeIfChanged(generated(root, "theme.css"), themeStylesheet(root)),
|
|
197
|
+
writeIfChanged(generated(root, "client.ts"), clientSource(root, config, options?.record === true)),
|
|
198
|
+
writeIfChanged(generated(root, "server.ts"), serverSource(root, config)),
|
|
199
|
+
]
|
|
200
|
+
: []),
|
|
201
|
+
...(targets.includes("desktop")
|
|
202
|
+
? [
|
|
203
|
+
writeIfChanged(generated(root, "theme.desktop.json"), `${JSON.stringify(themeOf(root).desktop ?? null)}\n`),
|
|
204
|
+
writeIfChanged(generated(root, "desktop.ts"), desktopSource(root, config)),
|
|
205
|
+
]
|
|
206
|
+
: []),
|
|
207
|
+
].some(Boolean);
|
|
107
208
|
};
|
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",
|
|
@@ -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>;
|
package/dist/package.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// `vlx package`: the desktop target as something to hand to a person. The engine becomes one executable (Node's single executable application: the engine bundled to one file, with the MESH
|
|
2
|
+
// runtime's module inside as an asset, injected into a `node` binary), set beside the Desktop PORT's window program. Unsigned: signing and notarizing are not done here (the artifacts say so).
|
|
3
|
+
import { build as bundle } from "vite";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { delimiter, join } from "node:path";
|
|
8
|
+
import { checksums, tarGz, zip } from "./archive.js";
|
|
9
|
+
import { loadAuthor } from "./author.js";
|
|
10
|
+
import { build } from "./build.js";
|
|
11
|
+
import { loadConfig } from "./config.js";
|
|
12
|
+
import { AppError } from "./diagnostics.js";
|
|
13
|
+
import { hasEntry } from "./emit.js";
|
|
14
|
+
import { ENTRY_FILE, GENERATED_DIR, OUTPUT_DIR } from "./project.js";
|
|
15
|
+
import { programFile } from "./emit.js";
|
|
16
|
+
import { createLoader, templates } from "./vite.js";
|
|
17
|
+
export const PLATFORMS = ["linux-x64", "win32-x64"];
|
|
18
|
+
const hostPlatform = () => `${process.platform}-${process.arch}`;
|
|
19
|
+
const fail = (message) => {
|
|
20
|
+
throw new AppError([{ file: "valance.desktop.json", line: 0, column: 0, code: "package", message }]);
|
|
21
|
+
};
|
|
22
|
+
/** A program on the PATH, as the shell would find it. */
|
|
23
|
+
const onPath = (name) => (process.env["PATH"] ?? "")
|
|
24
|
+
.split(delimiter)
|
|
25
|
+
.map((dir) => join(dir, name))
|
|
26
|
+
.find((candidate) => existsSync(candidate));
|
|
27
|
+
/** The entry of the packaged engine: the generated desktop entry, with the MESH runtime's module read from the executable's own assets (a single file has nothing beside it). */
|
|
28
|
+
const seaSource = (root, config) => `// Generated by \`vlx package\`. Do not edit.
|
|
29
|
+
// @ts-nocheck
|
|
30
|
+
import { getRawAsset } from "node:sea";
|
|
31
|
+
import { dirname, join } from "node:path";
|
|
32
|
+
import { start } from "@valancex/common/desktop";
|
|
33
|
+
import content from "./content.json";
|
|
34
|
+
import program from "./${programFile(config, "desktop")}";
|
|
35
|
+
${hasEntry(root) ? `import author from "../${ENTRY_FILE}";\n` : "const author = undefined;\n"}
|
|
36
|
+
const wasm = new Uint8Array(getRawAsset("mesh-runtime.wasm"));
|
|
37
|
+
|
|
38
|
+
// the project's public/ files sit beside the executable
|
|
39
|
+
const assets = join(dirname(process.execPath), "public");
|
|
40
|
+
|
|
41
|
+
start({ content, program, author, wasm, assets }).then(
|
|
42
|
+
() => process.exit(0),
|
|
43
|
+
(error) => {
|
|
44
|
+
console.error("the engine stopped:", error);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
`;
|
|
49
|
+
/** Every file under `dir` as an archive entry inside `top/`, in a stable order (a program keeps its executable bit). */
|
|
50
|
+
const entriesOf = (dir, top, at = "") => readdirSync(join(dir, at))
|
|
51
|
+
.sort()
|
|
52
|
+
.flatMap((name) => {
|
|
53
|
+
const relative = at === "" ? name : `${at}/${name}`;
|
|
54
|
+
const file = join(dir, relative);
|
|
55
|
+
return statSync(file).isDirectory()
|
|
56
|
+
? entriesOf(dir, top, relative)
|
|
57
|
+
: [{ path: `${top}/${relative}`, data: readFileSync(file), mode: statSync(file).mode & 0o111 ? 0o755 : 0o644 }];
|
|
58
|
+
});
|
|
59
|
+
export const packageDesktop = async (root, options = {}) => {
|
|
60
|
+
const config = loadConfig(root);
|
|
61
|
+
if (!config.targets.includes("desktop")) {
|
|
62
|
+
return fail("this project has no desktop target (vlx generate target desktop)");
|
|
63
|
+
}
|
|
64
|
+
const platform = options.platform ?? hostPlatform();
|
|
65
|
+
if (!PLATFORMS.includes(platform)) {
|
|
66
|
+
return fail(`cannot package for ${platform}: the platforms are ${PLATFORMS.join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
const windows = platform.startsWith("win32");
|
|
69
|
+
const node = options.node ?? (platform === hostPlatform() ? process.execPath : undefined);
|
|
70
|
+
const window = options.window ?? process.env["VLX_WINDOW"] ?? onPath(windows ? "vlx-window.exe" : "vlx-window");
|
|
71
|
+
if (node === undefined) {
|
|
72
|
+
return fail(`packaging for ${platform} from ${hostPlatform()} needs a node binary of this version for ${platform}: --node <path>`);
|
|
73
|
+
}
|
|
74
|
+
if (window === undefined || !existsSync(window)) {
|
|
75
|
+
return fail(`no window program: build it from the PORT repository (cargo build --release --bin vlx-window) and give its path with --window <path> (or set VLX_WINDOW)`);
|
|
76
|
+
}
|
|
77
|
+
// The engine, built first: the content, the program and the entries are the project's as they are now.
|
|
78
|
+
await build(root, { targets: ["desktop"] });
|
|
79
|
+
const work = join(root, GENERATED_DIR, "package");
|
|
80
|
+
rmSync(work, { recursive: true, force: true });
|
|
81
|
+
mkdirSync(work, { recursive: true });
|
|
82
|
+
writeFileSync(join(root, GENERATED_DIR, "desktop.sea.ts"), seaSource(root, config));
|
|
83
|
+
// Loading the author's definition once checks it still loads; the bundle below is the one that ships.
|
|
84
|
+
const loader = await createLoader(root, config);
|
|
85
|
+
try {
|
|
86
|
+
await loadAuthor(loader, root);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await loader.close();
|
|
90
|
+
}
|
|
91
|
+
await bundle({
|
|
92
|
+
root,
|
|
93
|
+
configFile: false,
|
|
94
|
+
publicDir: false,
|
|
95
|
+
logLevel: "warn",
|
|
96
|
+
plugins: [templates(root)],
|
|
97
|
+
cacheDir: join(root, GENERATED_DIR, "cache"),
|
|
98
|
+
ssr: { noExternal: true, target: "node" },
|
|
99
|
+
esbuild: { keepNames: true },
|
|
100
|
+
build: {
|
|
101
|
+
ssr: join(root, GENERATED_DIR, "desktop.sea.ts"),
|
|
102
|
+
outDir: work,
|
|
103
|
+
emptyOutDir: false,
|
|
104
|
+
target: "node22",
|
|
105
|
+
// W10: the engine is minified (half the bytes; start-up measured no worse, 2026-10-24). Names are kept: errors and their `_tag` are read by name.
|
|
106
|
+
minify: "esbuild",
|
|
107
|
+
rollupOptions: { output: { format: "cjs", entryFileNames: "engine.cjs" } },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
const wasm = createRequire(createRequire(join(root, "package.json")).resolve("@valancex/common/package.json")).resolve("@valancex/mesh-runtime/mesh-runtime.wasm");
|
|
111
|
+
writeFileSync(join(work, "sea-config.json"), JSON.stringify({ main: join(work, "engine.cjs"), output: join(work, "engine.blob"), disableExperimentalSEAWarning: true, assets: { "mesh-runtime.wasm": wasm } }));
|
|
112
|
+
execFileSync(process.execPath, ["--experimental-sea-config", join(work, "sea-config.json")], { stdio: "pipe" });
|
|
113
|
+
const out = join(root, OUTPUT_DIR, "package", platform);
|
|
114
|
+
rmSync(out, { recursive: true, force: true });
|
|
115
|
+
mkdirSync(out, { recursive: true });
|
|
116
|
+
const engine = join(out, windows ? "vlx-engine.exe" : "vlx-engine");
|
|
117
|
+
const app = join(out, `${config.name.replace(/[^\w.-]+/g, "-")}${windows ? ".exe" : ""}`);
|
|
118
|
+
copyFileSync(node, engine);
|
|
119
|
+
chmodSync(engine, 0o755);
|
|
120
|
+
const injector = "postject"; // (no type declarations of its own)
|
|
121
|
+
const { inject } = (await import(injector));
|
|
122
|
+
await inject(engine, "NODE_SEA_BLOB", readFileSync(join(work, "engine.blob")), { sentinelFuse: "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2" });
|
|
123
|
+
copyFileSync(window, app);
|
|
124
|
+
chmodSync(app, 0o755);
|
|
125
|
+
// The files images are drawn from, beside the engine.
|
|
126
|
+
const publicDir = join(root, "public");
|
|
127
|
+
if (existsSync(publicDir)) {
|
|
128
|
+
cpSync(publicDir, join(out, "public"), { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
const files = [engine, app].map((file) => file.slice(out.length + 1));
|
|
131
|
+
const notes = ["unsigned: macOS notarization and Windows signing are not done here; an unsigned program draws a warning on first run"];
|
|
132
|
+
if (options.archive === true) {
|
|
133
|
+
const stem = `${config.name.replace(/[^\w.-]+/g, "-")}-${platform}`;
|
|
134
|
+
const archive = `${stem}.${windows ? "zip" : "tar.gz"}`;
|
|
135
|
+
const data = (windows ? zip : tarGz)(entriesOf(out, stem));
|
|
136
|
+
writeFileSync(join(root, OUTPUT_DIR, "package", archive), data);
|
|
137
|
+
writeFileSync(join(root, OUTPUT_DIR, "package", `${archive}.sha256`), checksums([{ name: archive, data }]));
|
|
138
|
+
files.push(`../${archive}`, `../${archive}.sha256`);
|
|
139
|
+
notes.push(`${archive} is the package as one file, with its SHA-256 beside it; the checksum says the file is the one that was made, not who made it (that is what signing is for)`);
|
|
140
|
+
}
|
|
141
|
+
return { dir: out, files, notes };
|
|
142
|
+
};
|
package/dist/project.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Where things are in a project. Authored: `valance.json`, `valance.web.json`, `content/`, `public/`, `src/`. Generated and never edited or committed: `.valance/`. The final site: `dist/`.
|
|
1
|
+
// Where things are in a project. Authored: `valance.json`, `valance.web.json`, `content/`, `public/`, `src/`. Generated and never edited or committed: `.valance/`. The final site: `dist/web/`.
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
export const GENERATED_DIR = ".valance";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Config } from "./config.js";
|
|
2
|
+
/** The lines `vlx dev --record` prints before it records: where, what the log holds, what is redacted, and whether git would commit it. */
|
|
3
|
+
export declare const recordingNotice: (root: string, file: string, config: Config) => ReadonlyArray<string>;
|
|
4
|
+
/** What `vlx replay` says when it is given a log that holds user data without being told it may read it. */
|
|
5
|
+
export declare const userDataRefusal: (file: string) => string;
|