@valancex/cli 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -5
- package/dist/add.d.ts +9 -1
- package/dist/add.js +68 -5
- package/dist/archive.d.ts +17 -0
- package/dist/archive.js +111 -0
- package/dist/args.js +2 -2
- package/dist/build.d.ts +3 -0
- package/dist/build.js +93 -29
- package/dist/check.js +2 -2
- package/dist/cli.js +59 -16
- package/dist/compile.d.ts +11 -3
- package/dist/compile.js +42 -15
- package/dist/config.d.ts +3 -0
- package/dist/config.js +4 -0
- package/dist/dev-desktop.d.ts +8 -0
- package/dist/dev-desktop.js +114 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +33 -5
- package/dist/emit.d.ts +7 -4
- package/dist/emit.js +105 -27
- package/dist/init.js +14 -4
- package/dist/package.d.ts +18 -0
- package/dist/package.js +141 -0
- package/dist/project.js +1 -1
- package/dist/targets.d.ts +6 -1
- package/dist/targets.js +26 -4
- package/dist/ui/init.js +4 -4
- package/dist/ui/run.d.ts +3 -1
- package/dist/ui/run.js +5 -3
- package/dist/vite.d.ts +3 -2
- package/dist/vite.js +3 -3
- package/package.json +6 -3
- package/schemas/valance.desktop.schema.json +17 -0
- package/templates/desktop/README.md +14 -0
- package/templates/desktop/content/404.md +7 -0
- package/templates/desktop/content/guide/getting-started.md +15 -0
- package/templates/desktop/content/index.md +14 -0
- package/templates/desktop/gitignore +4 -0
- package/templates/desktop/src/layout.mprx +23 -0
- package/templates/desktop/src/main.ts +15 -0
- package/templates/desktop/tsconfig.json +18 -0
- package/templates/multi/README.md +13 -0
- package/templates/multi/content/404.md +7 -0
- package/templates/multi/content/guide/getting-started.md +15 -0
- package/templates/multi/content/index.md +14 -0
- package/templates/multi/src/desktop/layout.mprx +21 -0
- package/templates/multi/src/main.ts +23 -0
- package/templates/multi/src/web/layout.mprx +27 -0
- package/templates/multi/src/web/styles.css +28 -0
- package/templates/multi/src/web/theme-toggle.mprx +1 -0
- package/templates/multi/src/web/theme-toggle.ts +10 -0
- package/templates/web/content/guide/getting-started.md +1 -1
- package/views/block.mprx +1 -1
- package/views-desktop/block.mprx +3 -0
- package/views-desktop/blocks.mprx +3 -0
- package/views-desktop/inline.mprx +3 -0
- package/views-desktop/layout.mprx +20 -0
- package/views-desktop/list-block.mprx +3 -0
- package/views-desktop/list-items.mprx +3 -0
- package/views-desktop/runs.mprx +3 -0
- /package/views/{heading.mprx → heading-block.mprx} +0 -0
package/dist/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",
|
|
@@ -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,141 @@
|
|
|
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
|
+
build: {
|
|
100
|
+
ssr: join(root, GENERATED_DIR, "desktop.sea.ts"),
|
|
101
|
+
outDir: work,
|
|
102
|
+
emptyOutDir: false,
|
|
103
|
+
target: "node22",
|
|
104
|
+
minify: false,
|
|
105
|
+
chunkSizeWarningLimit: 4000,
|
|
106
|
+
rollupOptions: { output: { format: "cjs", entryFileNames: "engine.cjs" } },
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const wasm = createRequire(createRequire(join(root, "package.json")).resolve("@valancex/common/package.json")).resolve("@valancex/mesh-runtime/mesh-runtime.wasm");
|
|
110
|
+
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 } }));
|
|
111
|
+
execFileSync(process.execPath, ["--experimental-sea-config", join(work, "sea-config.json")], { stdio: "pipe" });
|
|
112
|
+
const out = join(root, OUTPUT_DIR, "package", platform);
|
|
113
|
+
rmSync(out, { recursive: true, force: true });
|
|
114
|
+
mkdirSync(out, { recursive: true });
|
|
115
|
+
const engine = join(out, windows ? "vlx-engine.exe" : "vlx-engine");
|
|
116
|
+
const app = join(out, `${config.name.replace(/[^\w.-]+/g, "-")}${windows ? ".exe" : ""}`);
|
|
117
|
+
copyFileSync(node, engine);
|
|
118
|
+
chmodSync(engine, 0o755);
|
|
119
|
+
const injector = "postject"; // (no type declarations of its own)
|
|
120
|
+
const { inject } = (await import(injector));
|
|
121
|
+
await inject(engine, "NODE_SEA_BLOB", readFileSync(join(work, "engine.blob")), { sentinelFuse: "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2" });
|
|
122
|
+
copyFileSync(window, app);
|
|
123
|
+
chmodSync(app, 0o755);
|
|
124
|
+
// The files images are drawn from, beside the engine.
|
|
125
|
+
const publicDir = join(root, "public");
|
|
126
|
+
if (existsSync(publicDir)) {
|
|
127
|
+
cpSync(publicDir, join(out, "public"), { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
const files = [engine, app].map((file) => file.slice(out.length + 1));
|
|
130
|
+
const notes = ["unsigned: macOS notarization and Windows signing are not done here; an unsigned program draws a warning on first run"];
|
|
131
|
+
if (options.archive === true) {
|
|
132
|
+
const stem = `${config.name.replace(/[^\w.-]+/g, "-")}-${platform}`;
|
|
133
|
+
const archive = `${stem}.${windows ? "zip" : "tar.gz"}`;
|
|
134
|
+
const data = (windows ? zip : tarGz)(entriesOf(out, stem));
|
|
135
|
+
writeFileSync(join(root, OUTPUT_DIR, "package", archive), data);
|
|
136
|
+
writeFileSync(join(root, OUTPUT_DIR, "package", `${archive}.sha256`), checksums([{ name: archive, data }]));
|
|
137
|
+
files.push(`../${archive}`, `../${archive}.sha256`);
|
|
138
|
+
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)`);
|
|
139
|
+
}
|
|
140
|
+
return { dir: out, files, notes };
|
|
141
|
+
};
|
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";
|
package/dist/targets.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { Vocabulary } from "@valancex/common/internal";
|
|
2
2
|
export interface Target {
|
|
3
3
|
/** The flag (`--web`) and the name in `valance.<id>.json`. */
|
|
4
|
-
readonly id: "web";
|
|
4
|
+
readonly id: "web" | "desktop";
|
|
5
5
|
readonly label: string;
|
|
6
6
|
readonly hint: string;
|
|
7
7
|
/** The tags its templates may write. A project builds against its target's vocabulary. */
|
|
8
8
|
readonly vocabulary: Vocabulary;
|
|
9
|
+
/** Where the framework's own templates for this target are (the layout and the views that draw a page's blocks): a project's own `layout` and `override`s replace them by name. */
|
|
10
|
+
readonly views: string;
|
|
9
11
|
}
|
|
10
12
|
export declare const TARGETS: ReadonlyArray<Target>;
|
|
13
|
+
export declare const targetOf: (id: TargetId) => Target;
|
|
11
14
|
export type TargetId = Target["id"];
|
|
12
15
|
export declare const isTarget: (name: string) => name is TargetId;
|
|
16
|
+
/** What `--yes` makes: the web alone is the one that needs nothing else installed. */
|
|
17
|
+
export declare const DEFAULT_TARGETS: ReadonlyArray<TargetId>;
|
package/dist/targets.js
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
|
-
// The targets an application can be developed for
|
|
2
|
-
// here (and its template under `templates
|
|
3
|
-
import { webVocabulary } from "@valancex/common/internal";
|
|
1
|
+
// The targets an application can be developed for: the web, and a desktop window. The wizard, `vlx init` and the flags are written against this list, so a target that arrives later is one more
|
|
2
|
+
// entry here (and its template under `templates/`, its built-in views, its vocabulary), not a change to them.
|
|
3
|
+
import { fromManifests, webVocabulary } from "@valancex/common/internal";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { BUILT_IN, BUILT_IN_DESKTOP } from "./compile.js";
|
|
7
|
+
/**
|
|
8
|
+
* The Desktop PORT's vocabulary: its components as data (`vocabulary.json`: columns, rows, text, a button, a checkbox, a text field; `content.json`: what Markdown produces), read from the
|
|
9
|
+
* PORT's own package (`@valancex/port-desktop-vocabulary`), not from a copy kept here.
|
|
10
|
+
*/
|
|
11
|
+
const desktopVocabulary = () => {
|
|
12
|
+
const need = createRequire(import.meta.url);
|
|
13
|
+
const read = (name) => JSON.parse(readFileSync(need.resolve(`@valancex/port-desktop-vocabulary/${name}`), "utf8"));
|
|
14
|
+
return fromManifests("desktop", read("vocabulary.json"), read("content.json"));
|
|
15
|
+
};
|
|
4
16
|
export const TARGETS = [
|
|
5
|
-
{ id: "web", label: "Web", hint: "a website: Markdown pages and templates, built to a static site (settings in valance.web.json)", vocabulary: webVocabulary },
|
|
17
|
+
{ id: "web", label: "Web", hint: "a website: Markdown pages and templates, built to a static site (settings in valance.web.json)", vocabulary: webVocabulary, views: BUILT_IN },
|
|
18
|
+
{
|
|
19
|
+
id: "desktop",
|
|
20
|
+
label: "Desktop",
|
|
21
|
+
hint: "a native window on Linux and Windows: Markdown pages and templates, drawn by the Desktop PORT (settings in valance.desktop.json)",
|
|
22
|
+
vocabulary: desktopVocabulary(),
|
|
23
|
+
views: BUILT_IN_DESKTOP,
|
|
24
|
+
},
|
|
6
25
|
];
|
|
26
|
+
export const targetOf = (id) => TARGETS.find((each) => each.id === id);
|
|
7
27
|
export const isTarget = (name) => TARGETS.some((each) => each.id === name);
|
|
28
|
+
/** What `--yes` makes: the web alone is the one that needs nothing else installed. */
|
|
29
|
+
export const DEFAULT_TARGETS = ["web"];
|
package/dist/ui/init.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Box, Text, useApp } from "ink";
|
|
|
4
4
|
import { useEffect, useRef, useState } from "react";
|
|
5
5
|
import { relative } from "node:path";
|
|
6
6
|
import { directoryFor, directoryProblem, execute as defaultExecute, nameProblem, PACKAGE_MANAGERS, runCommand, titleOf } from "../init.js";
|
|
7
|
-
import { TARGETS } from "../targets.js";
|
|
7
|
+
import { DEFAULT_TARGETS, TARGETS } from "../targets.js";
|
|
8
8
|
import { Spinner } from "./Spinner.js";
|
|
9
9
|
import { Answered, Confirm, MultiSelect, Select, TextPrompt } from "./prompts.js";
|
|
10
10
|
const ORDER = ["name", "targets", "base", "url", "pm", "install", "git"];
|
|
@@ -75,7 +75,7 @@ export const Wizard = ({ args, cwd, detected, version, execute = defaultExecute,
|
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
finished.current = true;
|
|
78
|
-
const targets = draft.targets.length === 0 ?
|
|
78
|
+
const targets = draft.targets.length === 0 ? DEFAULT_TARGETS : draft.targets;
|
|
79
79
|
const name = draft.name ?? "my-app";
|
|
80
80
|
const answers = {
|
|
81
81
|
name,
|
|
@@ -107,10 +107,10 @@ export const Wizard = ({ args, cwd, detected, version, execute = defaultExecute,
|
|
|
107
107
|
}
|
|
108
108
|
}, [outcome, failure]);
|
|
109
109
|
const question = running ? undefined : ORDER[at];
|
|
110
|
-
const picked = draft.targets.length === 0 ?
|
|
110
|
+
const picked = draft.targets.length === 0 ? DEFAULT_TARGETS : draft.targets;
|
|
111
111
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: "cyan", children: "vlx init" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 VALANCE ", version] })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [answered.map(([label, value]) => (_jsx(Answered, { label: label, value: value }, label))), question === "name" ? (_jsx(TextPrompt, { label: "What is the application called?", hint: "a name, or a path: it is made in a directory of that name", initial: "my-app", validate: (value) => nameProblem(value) ?? directoryProblem(directoryFor(value, cwd)), onSubmit: (value) => {
|
|
112
112
|
answer({ name: value.trim() }, "Application", `${titleOf(value)} → ${relative(cwd, directoryFor(value, cwd)) || "."}`);
|
|
113
|
-
} })) : null, question === "targets" ? (_jsx(MultiSelect, { label: "Which targets is it for?", choices: TARGETS.map((each) => ({ value: each.id, label: each.label, hint: each.hint })), initial:
|
|
113
|
+
} })) : null, question === "targets" ? (_jsx(MultiSelect, { label: "Which targets is it for?", hint: "one application, built for each", choices: TARGETS.map((each) => ({ value: each.id, label: each.label, hint: each.hint })), initial: DEFAULT_TARGETS, onSubmit: (values) => {
|
|
114
114
|
answer({ targets: values }, "Targets", values.map((id) => TARGETS.find((each) => each.id === id)?.label ?? id).join(", "));
|
|
115
115
|
} })) : null, question === "base" ? (_jsx(TextPrompt, { label: "Web: where is the site served?", hint: "a base path: / or /docs/", initial: "/", validate: (value) => (BASE.test(value) ? undefined : "a path that starts and ends with /, like / or /docs/"), onSubmit: (value) => {
|
|
116
116
|
answer({ base: value }, "Base path", value);
|
package/dist/ui/run.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TargetId } from "../targets.js";
|
|
1
2
|
import type { InitArgs } from "../args.js";
|
|
2
3
|
import type { Checked } from "../check.js";
|
|
3
4
|
import type { Diagnostic } from "../diagnostics.js";
|
|
@@ -6,10 +7,11 @@ import type { Result } from "./init.js";
|
|
|
6
7
|
import type { Built } from "../build.js";
|
|
7
8
|
export declare const drawInit: (args: InitArgs, cwd: string, detected: PackageManager, version: string) => Promise<Result>;
|
|
8
9
|
export declare const drawCheck: (root: string) => Promise<Checked>;
|
|
9
|
-
export declare const drawBuild: (root: string) => Promise<Built>;
|
|
10
|
+
export declare const drawBuild: (root: string, targets?: ReadonlyArray<TargetId>) => Promise<Built>;
|
|
10
11
|
/** Mistakes that stopped a command, drawn the way `check` draws them. */
|
|
11
12
|
export declare const drawDiagnostics: (diagnostics: ReadonlyArray<Diagnostic>) => Promise<void>;
|
|
12
13
|
export declare const drawDev: (root: string, options: {
|
|
14
|
+
readonly target?: TargetId;
|
|
13
15
|
readonly port?: number;
|
|
14
16
|
readonly host?: string;
|
|
15
17
|
}) => Promise<void>;
|
package/dist/ui/run.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
// Putting the views on the terminal. Ink and React are loaded only here, and only when a command is going to draw: a command that prints plain text never pays for them.
|
|
3
2
|
import { render } from "ink";
|
|
4
3
|
import { build as runBuild } from "../build.js";
|
|
5
4
|
import { check as runCheck } from "../check.js";
|
|
@@ -33,11 +32,14 @@ export const drawCheck = async (root) => {
|
|
|
33
32
|
await shown.waitUntilExit();
|
|
34
33
|
}
|
|
35
34
|
};
|
|
36
|
-
export const drawBuild = async (root) => {
|
|
35
|
+
export const drawBuild = async (root, targets = []) => {
|
|
37
36
|
const started = Date.now();
|
|
38
37
|
const shown = show(_jsx(BuildProgress, { current: "configuration" }));
|
|
39
38
|
try {
|
|
40
|
-
const built = await runBuild(root, {
|
|
39
|
+
const built = await runBuild(root, {
|
|
40
|
+
...(targets.length === 0 ? {} : { targets }),
|
|
41
|
+
onStage: ((stage) => shown.rerender(_jsx(BuildProgress, { current: stage }))),
|
|
42
|
+
});
|
|
41
43
|
shown.rerender(_jsx(BuildResult, { built: built, seconds: (Date.now() - started) / 1000 }));
|
|
42
44
|
return built;
|
|
43
45
|
}
|
package/dist/vite.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { InlineConfig, ViteDevServer } from "vite";
|
|
1
|
+
import type { InlineConfig, Plugin, ViteDevServer } from "vite";
|
|
2
2
|
import type { Config } from "./config.js";
|
|
3
3
|
export declare const CLIENT_ENTRY = ".valance/client.ts";
|
|
4
4
|
export declare const SERVER_ENTRY = "/.valance/server.ts";
|
|
5
|
-
export declare const
|
|
5
|
+
export declare const templates: (root: string) => Plugin;
|
|
6
|
+
export declare const baseConfig: (root: string, config: Config, out?: string) => InlineConfig;
|
|
6
7
|
/** A Vite server that serves nothing: it loads the author's TypeScript and the generated server entry in Node. Used by build and check. */
|
|
7
8
|
export declare const createLoader: (root: string, config: Config) => Promise<ViteDevServer>;
|
package/dist/vite.js
CHANGED
|
@@ -20,7 +20,7 @@ logger.warnOnce = (message, options) => {
|
|
|
20
20
|
};
|
|
21
21
|
// `import layout from "./layout.mprx"` in `src/main.ts` is the author naming a template. The import gives the file's address and nothing else: the compiler reads the file itself, and the
|
|
22
22
|
// browser bundle must not carry template text it has already been compiled from.
|
|
23
|
-
const templates = (root) => ({
|
|
23
|
+
export const templates = (root) => ({
|
|
24
24
|
name: "vlx-templates",
|
|
25
25
|
enforce: "pre",
|
|
26
26
|
load(id) {
|
|
@@ -30,7 +30,7 @@ const templates = (root) => ({
|
|
|
30
30
|
// The module is only the file's address, so a change to the template changes nothing the browser holds; the rebuild (dev.ts) recompiles it and reloads once, with the overlay if it fails.
|
|
31
31
|
handleHotUpdate: ({ file }) => (file.endsWith(".mprx") ? [] : undefined),
|
|
32
32
|
});
|
|
33
|
-
export const baseConfig = (root, config) => ({
|
|
33
|
+
export const baseConfig = (root, config, out = OUTPUT_DIR) => ({
|
|
34
34
|
root,
|
|
35
35
|
configFile: false,
|
|
36
36
|
base: config.web.base,
|
|
@@ -41,7 +41,7 @@ export const baseConfig = (root, config) => ({
|
|
|
41
41
|
cacheDir: join(root, GENERATED_DIR, "cache"),
|
|
42
42
|
customLogger: logger,
|
|
43
43
|
clearScreen: false,
|
|
44
|
-
build: { outDir: join(root,
|
|
44
|
+
build: { outDir: join(root, out), emptyOutDir: true, target: "esnext", manifest: true, chunkSizeWarningLimit: 1500, rollupOptions: { input: join(root, CLIENT_ENTRY) } },
|
|
45
45
|
// The MESH runtime refers to node:fs/promises on a path the browser never takes.
|
|
46
46
|
optimizeDeps: { exclude: ["@valancex/mesh-runtime", "@valancex/cli", "@valancex/common"] },
|
|
47
47
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valancex/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "vlx, the VALANCE command: make an application (vlx init), serve it as you edit (vlx dev), build it to a static site (vlx build), check it (vlx check). Content, layout, components and styles in, a website out.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"valance",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"dist",
|
|
41
41
|
"bin",
|
|
42
42
|
"views",
|
|
43
|
+
"views-desktop",
|
|
43
44
|
"schemas",
|
|
44
45
|
"templates"
|
|
45
46
|
],
|
|
@@ -49,12 +50,14 @@
|
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
52
|
"@valancex/mesh-compiler": "^0.11.0",
|
|
53
|
+
"@valancex/port-desktop-vocabulary": "^0.6.0",
|
|
52
54
|
"effect": "^3.10.0",
|
|
53
55
|
"ink": "7.1.1",
|
|
56
|
+
"postject": "1.0.0-alpha.6",
|
|
54
57
|
"react": "19.2.8",
|
|
55
58
|
"vite": "7.3.6",
|
|
56
|
-
"@valancex/
|
|
57
|
-
"@valancex/
|
|
59
|
+
"@valancex/valance": "^0.13.0",
|
|
60
|
+
"@valancex/common": "^0.13.0"
|
|
58
61
|
},
|
|
59
62
|
"devDependencies": {
|
|
60
63
|
"@types/node": "^22.0.0",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://valancex.dev/schemas/valance.desktop.schema.json",
|
|
4
|
+
"title": "valance.desktop.json",
|
|
5
|
+
"description": "Marks the project as a desktop one: it is built to an engine that a native window runs. It has no settings yet.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string"
|
|
11
|
+
},
|
|
12
|
+
"version": {
|
|
13
|
+
"const": 1,
|
|
14
|
+
"description": "The version of this file's format."
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|