@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jack Miller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -66,6 +66,16 @@ Then it writes the project (the same files as `examples/docs-app`: a layout, a c
|
|
|
66
66
|
|
|
67
67
|
Without a terminal (a pipe, CI) it cannot ask, so it needs a name and a target: `vlx init my-app --web`.
|
|
68
68
|
|
|
69
|
+
## A desktop application: `vlx init --desktop`
|
|
70
|
+
|
|
71
|
+
A project has the targets whose settings file it has; `vlx init my-app --web --desktop` makes both, and `defineApp`'s `targets` gives each its templates (how it was designed and what it found: `docs/application-model/MULTI-TARGET.md` and `MULTI-TARGET-PLAN.md`). `vlx init my-app --desktop` makes the same project as for the web (Markdown in `content/`, a layout, state and commands in `src/main.ts`) with `valance.desktop.json` in place of `valance.web.json`. Its templates are written against the Desktop PORT's vocabulary (`column`, `row`, `heading`, `text`, `button`, `checkbox`, `textfield`, and the content layer: `paragraph`, `list`, `listitem`, `blockquote`, `emphasis`, …), not the web's tags: `<div>` in a desktop project is an error from `vlx check`.
|
|
72
|
+
|
|
73
|
+
`vlx build` writes the engine to `dist/desktop/`: `engine.mjs` (everything bundled; run it with Node) and `mesh-runtime.wasm` beside it. The engine speaks the host protocol on stdin and stdout; the window that runs it is the Desktop PORT's (`valancex-port-desktop`). `vlx package --desktop` packs the engine into one executable (Node's single executable application, with the MESH runtime inside) and sets it beside the window program (`--window <path>`, or `VLX_WINDOW`, or `vlx-window` on the PATH) in `dist/package/<platform>/`. `--archive` also writes the package as one reproducible file, a `.tar.gz` (Linux) or a `.zip` (Windows), with its SHA-256 beside it. **Unsigned**: signing and notarization are not done by `vlx`. For another platform, `--platform win32-x64 --node <that platform's node> --window <that platform's window>`. `vlx dev` builds the engine, opens the Desktop PORT's window on it (the `vlx-window` program, installed with `cargo install --git https://github.com/ValanceX/Port valancex-port-desktop --bin vlx-window`, found on the PATH or at `VLX_WINDOW`), and when a file changes it builds again and tells the running window to start the engine again in place (the window keeps its position and size). Closing the window ends `vlx dev`.
|
|
74
|
+
|
|
75
|
+
## Adding a target: `vlx generate target desktop`
|
|
76
|
+
|
|
77
|
+
A web project becomes a project of two targets without moving anything: `vlx generate target desktop` writes `valance.desktop.json` (and `valance.web.json`, which a project needs once it has another target's), a layout of its own in `src/desktop/`, and registers it in `targets` in `src/main.ts`. The layout the project has stays the default for a target with none of its own. **The site moves**: a project of one web target builds to `dist/`, one of several to `dist/<target>/` (`dist/web/`, `dist/desktop/`), so a deploy step that serves `dist/` is pointed at `dist/web/` when the second target is added (the command says so). See `docs/application-model/MULTI-TARGET.md`.
|
|
78
|
+
|
|
69
79
|
## Adding to an application: `vlx generate`
|
|
70
80
|
|
|
71
81
|
```console
|
|
@@ -90,7 +100,7 @@ Try it locally: [`docs/application-model/TRY-IT.md`](../../docs/application-mode
|
|
|
90
100
|
and a `package.json` that depends on `@valancex/cli` and has `"dev": "vlx dev"`, `"build": "vlx build"`.
|
|
91
101
|
2. Add pages: **`content/index.md`** is the first page (`# Notes` and some text). Every other `.md` file under `content/` is a page.
|
|
92
102
|
3. `pnpm dev`. Open the address it prints. Edit a Markdown file and save: the page reloads with your change.
|
|
93
|
-
4. `pnpm build`. The site is in `dist/`; any host that serves files can serve it.
|
|
103
|
+
4. `pnpm build`. The site is in `dist/web/`; any host that serves files can serve it.
|
|
94
104
|
|
|
95
105
|
That is a complete application. It uses a built-in layout. Everything below is how you take over a part of it.
|
|
96
106
|
|
|
@@ -107,7 +117,7 @@ That is a complete application. It uses a built-in layout. Everything below is h
|
|
|
107
117
|
| `src/**/*.css` | Styles. Every stylesheet is loaded. | you |
|
|
108
118
|
| `src/main.ts` | Optional until you own a template. `defineApp`: the `layout`, your `components`, state of your own and the commands that change it. | you |
|
|
109
119
|
| `.valance/` | Generated: content, compiled templates, the entries, and for editors `manifest.json` (what a template may use) and `editor.json` (which file is which component). Git-ignored by a `.gitignore` inside it. | the tools |
|
|
110
|
-
| `dist/` | The built site. | the tools |
|
|
120
|
+
| `dist/web/` | The built site. | the tools |
|
|
111
121
|
|
|
112
122
|
Settings are validated, unknown keys are errors, and editors can use the JSON Schemas in `schemas/` (`"$schema": "./node_modules/@valancex/cli/schemas/valance.schema.json"`).
|
|
113
123
|
|
|
@@ -176,7 +186,7 @@ export default component(template, {
|
|
|
176
186
|
- `import x from "./x.mprx"` names a template file; `component(x, …)` defines it. The tag is the file's name (`theme-toggle.mprx` is `<theme-toggle>`). Register it once, in `components`.
|
|
177
187
|
- **Which components a template uses is read from the template.** Write `<theme-toggle … />` in the layout (or in another component) and it is used; there is no list of uses to keep in step. A registered component no template writes yet is not part of the program, and is not an error. A file under `src/` that is not registered is an error (`template-unused`).
|
|
178
188
|
- **The contract.** `props` maps a prop name to its Effect Schema and `events` an event name to its payload Schema (`Schema.Void` is no payload). Declare either and the contract is exactly what you wrote (an omitted side is none), overriding inference; declare neither and MESH infers it from the template and where it is used. Schemas are converted by the same rules as state; a prop is optional when its schema admits `undefined`, and props have no defaults. A schema with no MESH type is a diagnostic at the declaration (`component-schema`). MESH reports an undeclared or missing prop, a prop of the wrong type and a handler for an undeclared event at the use. The layout takes no contract (`layout-contract`). That declared interface is where an editor takes you from `on.toggle=` or `theme=` at a use, and where *Find All References* starts.
|
|
179
|
-
- `override: [component(runs, { props: { spans: … } })]` replaces a built-in (`runs`, `heading`, …) with your template of the same name.
|
|
189
|
+
- `override: [component(runs, { props: { spans: … } })]` replaces a built-in (`runs`, `heading-block`, …) with your template of the same name.
|
|
180
190
|
- `layout` is optional; without it the built-in layout draws the page. Its template must be named `layout.mprx`.
|
|
181
191
|
|
|
182
192
|
### Children and slots
|
|
@@ -229,6 +239,27 @@ export default defineApp({
|
|
|
229
239
|
|
|
230
240
|
A state field is its type and its default in one declaration. Hand-building `state: { schema, initial }` is still accepted; giving both forms is the same definition error as a clash. `theme` is now a name the layout reads (`<shell theme={theme}>`) and `toggleTheme()` a command it runs (`on.click={toggleTheme()}`). A component raises an event (`on.click={toggle()}`, declared in `events`) and its user binds it (`<theme-toggle on.toggle={toggleTheme()} />`). A state field may not reuse a framework name (`site`, `title`, `blocks`…), and `go` is the framework's command. Both are errors that say so.
|
|
231
241
|
|
|
242
|
+
### Values worked out, kept, and moved between
|
|
243
|
+
|
|
244
|
+
`defineApp` has more than state and commands. All of it is the same on every target:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
export default defineApp({
|
|
248
|
+
state: { items: field(Schema.Array(Item), []), theme: field(Schema.Literal("system", "light", "dark"), "system") },
|
|
249
|
+
// Worked out from the state, with their types, for a template to read: `{summary}`.
|
|
250
|
+
derived: (derive) => ({ summary: derive(Schema.String, (state) => `${state.items.length} items`) }),
|
|
251
|
+
// Kept between runs through the platform's `Store` (the page's storage; a file in the user's data directory on the desktop).
|
|
252
|
+
// "navigation" keeps the pages visited, where the engine keeps them (a window; a page's list is the browser's).
|
|
253
|
+
persist: ["items", "theme", "navigation"],
|
|
254
|
+
// What the window asks to look like (`"light"`, `"dark"` or `"system"`); the layout reads `colorScheme`, the system's.
|
|
255
|
+
appearance: (state) => state.theme,
|
|
256
|
+
// Asked before `go`, `back` and `forward` (the router's `Guard`): `false` stops the move, `redirect(page)` sends it elsewhere.
|
|
257
|
+
guard: ({ to, kind }) => to.name !== "/private",
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The framework's own names are `page`, `trail`, `cursor`, `viewport`, `colorScheme`, `canGoBack` and `canGoForward` (state a layout reads) and the commands `go`, `back`, `forward`, `resize`, `colorScheme` and `persist`; an application's own state and commands cannot take them. A text field reports `change`, `submit` (Enter) and `cancel` (Escape). `Store` and `Open` (send a person to an address outside the application: a new tab, or the system's opener) are capabilities in `@valancex/common`. How navigation is modelled across a page, a window and a phone: `docs/application-model/NAVIGATION.md`.
|
|
262
|
+
|
|
232
263
|
## The document's title
|
|
233
264
|
|
|
234
265
|
A page's title is its front-matter `title` (else its first heading), written with the project's format in `valance.json`:
|
|
@@ -320,7 +351,7 @@ A mistake leaves the last good build serving and appears in the terminal and as
|
|
|
320
351
|
$ pnpm add -D @valancex/mesh-lsp
|
|
321
352
|
```
|
|
322
353
|
|
|
323
|
-
The VS Code extension in [`editors/vscode`](../../editors/vscode)
|
|
354
|
+
The VS Code extension (**VALANCE**, in [`editors/vscode`](../../editors/vscode)) starts it, highlights `.mprx`, and shows `vlx check` mistakes in Markdown, config and `main.ts`. Neovim and Helix can use the same two files ([MESH's editor guide](https://github.com/ValanceX/Mesh/blob/main/docs/guides/editor-setup.md)).
|
|
324
355
|
|
|
325
356
|
## Diagnostics
|
|
326
357
|
|
|
@@ -331,5 +362,5 @@ Every mistake the tools can attribute to a file has one shape: `{ file, line, co
|
|
|
331
362
|
- The whole content model ships in the page bundle. Fine for tens of pages; a per-page payload is the known answer for hundreds (the documentation verifier does it).
|
|
332
363
|
- The Markdown dialect is a subset; no syntax highlighting, tables, callouts, tabs, search, versions or languages (the verifier has these as stand-ins).
|
|
333
364
|
- The tag table is fixed, and it is the web's: `class`, `theme`, `tabindex`, `label`, `current`, `pressed`, `id` and `loading` are web hooks (a build against a vocabulary without them rejects the built-in views). An application cannot declare new primitives.
|
|
334
|
-
-
|
|
365
|
+
- The web target is delivered as a static site, with no server rendering command and no `preview` command; the desktop target needs the Desktop PORT's window built from its source (it is not yet published). A third target (a phone) does not exist.
|
|
335
366
|
- Updates are reloads, not hot swaps (except styles).
|
package/dist/add.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import type { TargetId } from "./targets.js";
|
|
2
|
+
export declare const KINDS: readonly ["page", "component", "target"];
|
|
2
3
|
export type Kind = (typeof KINDS)[number];
|
|
3
4
|
export interface Generated {
|
|
4
5
|
/** Files written, relative to the project. */
|
|
@@ -18,3 +19,10 @@ export declare const componentProblem: (name: string, root?: string) => string |
|
|
|
18
19
|
/** `main.ts` with `ident` added to `components`, or `undefined` when its definition is not written in the plain form this can edit. */
|
|
19
20
|
export declare const register: (text: string, ident: string, name: string) => string | undefined;
|
|
20
21
|
export declare const generateComponent: (root: string, name: string) => Generated;
|
|
22
|
+
/**
|
|
23
|
+
* `main.ts` with a `targets` entry for `id`: its layout, from `src/<id>/layout.mprx`. `undefined` when the definition is not written in the plain form this can edit (or already has `targets`).
|
|
24
|
+
* The layout the project already has stays the default for every target that has none of its own, so nothing is moved.
|
|
25
|
+
*/
|
|
26
|
+
export declare const registerTarget: (text: string, id: TargetId) => string | undefined;
|
|
27
|
+
/** Adds a target to the project: its settings file, and a starting layout of its own in `src/<id>/`, registered in `src/main.ts`. Nothing the project has is moved or changed but `src/main.ts`. */
|
|
28
|
+
export declare const generateTarget: (root: string, given: string) => Generated;
|
package/dist/add.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
// Nothing here overwrites a file: a name that is taken is refused.
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { BUILT_IN, ROOT, SOURCE_DIR } from "./compile.js";
|
|
5
|
+
import { BUILT_IN, BUILT_IN_DESKTOP, ROOT, SOURCE_DIR } from "./compile.js";
|
|
6
|
+
import { DESKTOP_FILE, loadConfig, WEB_FILE } from "./config.js";
|
|
7
|
+
import { isTarget } from "./targets.js";
|
|
6
8
|
import { ENTRY_FILE } from "./project.js";
|
|
7
9
|
import { primitives } from "@valancex/common/internal";
|
|
8
|
-
export const KINDS = ["page", "component"];
|
|
10
|
+
export const KINDS = ["page", "component", "target"];
|
|
9
11
|
export class GenerateError extends Error {
|
|
10
12
|
}
|
|
11
13
|
const words = (name) => name.replace(/[-_]+/g, " ").replace(/^./, (first) => first.toUpperCase());
|
|
@@ -40,9 +42,7 @@ export const generatePage = (root, given, title) => {
|
|
|
40
42
|
return { created: [file], changed: [], notes: [] };
|
|
41
43
|
};
|
|
42
44
|
// ---- a component -------------------------------------------------------------------------------------------------------------------------------------------
|
|
43
|
-
const builtIns = () => new Set(readdirSync(BUILT_IN)
|
|
44
|
-
.filter((file) => file.endsWith(".mprx"))
|
|
45
|
-
.map((file) => file.slice(0, -".mprx".length)));
|
|
45
|
+
const builtIns = () => new Set([...readdirSync(BUILT_IN), ...readdirSync(BUILT_IN_DESKTOP)].filter((file) => file.endsWith(".mprx")).map((file) => file.slice(0, -".mprx".length)));
|
|
46
46
|
export const componentProblem = (name, root) => {
|
|
47
47
|
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name)) {
|
|
48
48
|
return `"${name}" cannot be a tag: use lowercase words joined by - (callout, post-card)`;
|
|
@@ -141,3 +141,66 @@ export const generateComponent = (root, name) => {
|
|
|
141
141
|
notes.push(`use it by writing <${name}></${name}> in a template`);
|
|
142
142
|
return { created, changed, notes };
|
|
143
143
|
};
|
|
144
|
+
// ---- a target ----------------------------------------------------------------------------------------------------------------------------------------------
|
|
145
|
+
/**
|
|
146
|
+
* `main.ts` with a `targets` entry for `id`: its layout, from `src/<id>/layout.mprx`. `undefined` when the definition is not written in the plain form this can edit (or already has `targets`).
|
|
147
|
+
* The layout the project already has stays the default for every target that has none of its own, so nothing is moved.
|
|
148
|
+
*/
|
|
149
|
+
export const registerTarget = (text, id) => {
|
|
150
|
+
const define = /defineApp\(\s*\{/.exec(text);
|
|
151
|
+
if (define === null || /\btargets\s*:/.test(text)) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const ident = `${id}Layout`;
|
|
155
|
+
const at = define.index + define[0].length;
|
|
156
|
+
let edited = `${text.slice(0, at)}\n targets: { ${id}: { layout: component(${ident}) } },${text.slice(at)}`;
|
|
157
|
+
edited = withImport(edited, `import ${ident} from "./${id}/layout.mprx";`);
|
|
158
|
+
return /import\s*\{[^}]*\bcomponent\b[^}]*\}\s*from\s*"@valancex\/common"/.test(edited) ? edited : withImport(edited, 'import { component } from "@valancex/common";');
|
|
159
|
+
};
|
|
160
|
+
/** Adds a target to the project: its settings file, and a starting layout of its own in `src/<id>/`, registered in `src/main.ts`. Nothing the project has is moved or changed but `src/main.ts`. */
|
|
161
|
+
export const generateTarget = (root, given) => {
|
|
162
|
+
if (!isTarget(given)) {
|
|
163
|
+
throw new GenerateError(`"${given}" is not a target: the targets are web and desktop (vlx generate target desktop)`);
|
|
164
|
+
}
|
|
165
|
+
const id = given;
|
|
166
|
+
const config = loadConfig(root);
|
|
167
|
+
if (config.targets.includes(id)) {
|
|
168
|
+
throw new GenerateError(`this project already has the ${id} target`);
|
|
169
|
+
}
|
|
170
|
+
const settings = id === "web" ? WEB_FILE : DESKTOP_FILE;
|
|
171
|
+
const layout = `${SOURCE_DIR}/${id}/layout.mprx`;
|
|
172
|
+
const entry = join(root, ENTRY_FILE);
|
|
173
|
+
if (existsSync(join(root, layout))) {
|
|
174
|
+
throw new GenerateError(`${layout} already exists`);
|
|
175
|
+
}
|
|
176
|
+
const marker = (target) => `${JSON.stringify({ $schema: `./node_modules/@valancex/cli/schemas/valance.${target}.schema.json`, version: 1 }, null, 2)}\n`;
|
|
177
|
+
const created = [];
|
|
178
|
+
// A project is for the web when it has no settings file at all; once it has another target's, the web needs its own to stay a target.
|
|
179
|
+
if (id === "desktop" && !existsSync(join(root, WEB_FILE))) {
|
|
180
|
+
write(root, WEB_FILE, marker("web"));
|
|
181
|
+
created.push(WEB_FILE);
|
|
182
|
+
}
|
|
183
|
+
write(root, settings, marker(id));
|
|
184
|
+
created.push(settings);
|
|
185
|
+
// The framework's own layout for the target is the starting point: the page's shell, the sections and the previous and next pages.
|
|
186
|
+
write(root, layout, readFileSync(join(id === "web" ? BUILT_IN : BUILT_IN_DESKTOP, "layout.mprx"), "utf8"));
|
|
187
|
+
created.push(layout);
|
|
188
|
+
const changed = [];
|
|
189
|
+
const notes = [];
|
|
190
|
+
if (!existsSync(entry)) {
|
|
191
|
+
write(root, ENTRY_FILE, `import { component, defineApp } from "@valancex/common";\nimport ${id}Layout from "./${id}/layout.mprx";\n\nexport default defineApp({\n targets: { ${id}: { layout: component(${id}Layout) } },\n});\n`);
|
|
192
|
+
created.push(ENTRY_FILE);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const edited = registerTarget(readFileSync(entry, "utf8"), id);
|
|
196
|
+
if (edited === undefined) {
|
|
197
|
+
notes.push(`add it to ${ENTRY_FILE} yourself: import ${id}Layout from "./${id}/layout.mprx"; and give defineApp \`targets: { ${id}: { layout: component(${id}Layout) } }\``);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
writeFileSync(entry, edited);
|
|
201
|
+
changed.push(ENTRY_FILE);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
notes.push(`the ${id} target is drawn with its own vocabulary: write its templates in ${SOURCE_DIR}/${id}/, and the state, the commands and the content are shared`);
|
|
205
|
+
return { created, changed, notes };
|
|
206
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Entry {
|
|
2
|
+
/** Path inside the archive, `/` separated, no leading slash. */
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly data: Buffer;
|
|
5
|
+
/** Unix permission bits (0o755 for a program, 0o644 for data). */
|
|
6
|
+
readonly mode: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const crc32: (data: Uint8Array) => number;
|
|
9
|
+
/** A gzipped tar of `entries`, in the order given. */
|
|
10
|
+
export declare const tarGz: (entries: ReadonlyArray<Entry>) => Buffer;
|
|
11
|
+
/** A zip of `entries` (deflated), with each entry's Unix mode kept so a program stays executable where the unzipper honors it. */
|
|
12
|
+
export declare const zip: (entries: ReadonlyArray<Entry>) => Buffer;
|
|
13
|
+
/** `sha256sum`'s format: the hex digest, two spaces, the file name; one line per file. */
|
|
14
|
+
export declare const checksums: (files: ReadonlyArray<{
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly data: Buffer;
|
|
17
|
+
}>) => string;
|
package/dist/archive.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Archives for `vlx package --archive`: a .tar.gz (Linux) and a .zip (Windows), written here with Node's own zlib so nothing has to be installed, and written the same every time (no clock, no owner)
|
|
2
|
+
// so the same files make the same bytes and a checksum means something.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { deflateRawSync, gzipSync } from "node:zlib";
|
|
5
|
+
/** CRC-32 (the zip checksum), here and not `zlib.crc32`: that is Node 22.2 and later, and `vlx` also runs under the Node an editor ships (VS Code 1.90 has 20.9). */
|
|
6
|
+
const TABLE = Uint32Array.from({ length: 256 }, (_, n) => {
|
|
7
|
+
let value = n;
|
|
8
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
9
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
10
|
+
}
|
|
11
|
+
return value >>> 0;
|
|
12
|
+
});
|
|
13
|
+
export const crc32 = (data) => {
|
|
14
|
+
let crc = 0xffffffff;
|
|
15
|
+
for (const byte of data) {
|
|
16
|
+
crc = TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
17
|
+
}
|
|
18
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
19
|
+
};
|
|
20
|
+
const octal = (value, width) => value.toString(8).padStart(width - 1, "0") + "\0";
|
|
21
|
+
/** One ustar header (512 bytes). A path longer than 100 bytes is split at a `/` into prefix and name, as ustar does. */
|
|
22
|
+
const tarHeader = (path, size, mode) => {
|
|
23
|
+
const bytes = Buffer.byteLength(path);
|
|
24
|
+
let name = path;
|
|
25
|
+
let prefix = "";
|
|
26
|
+
if (bytes > 100) {
|
|
27
|
+
const cut = path.lastIndexOf("/", path.length - 1);
|
|
28
|
+
const split = path.slice(0, cut);
|
|
29
|
+
const rest = path.slice(cut + 1);
|
|
30
|
+
if (cut < 0 || Buffer.byteLength(rest) > 100 || Buffer.byteLength(split) > 155) {
|
|
31
|
+
throw new Error(`the path is too long for an archive: ${path}`);
|
|
32
|
+
}
|
|
33
|
+
prefix = split;
|
|
34
|
+
name = rest;
|
|
35
|
+
}
|
|
36
|
+
const header = Buffer.alloc(512);
|
|
37
|
+
header.write(name, 0, "utf8");
|
|
38
|
+
header.write(octal(mode & 0o777, 8), 100, "ascii");
|
|
39
|
+
header.write(octal(0, 8), 108, "ascii"); // uid
|
|
40
|
+
header.write(octal(0, 8), 116, "ascii"); // gid
|
|
41
|
+
header.write(octal(size, 12), 124, "ascii");
|
|
42
|
+
header.write(octal(0, 12), 136, "ascii"); // mtime: the epoch, so the same files are the same bytes
|
|
43
|
+
header.write(" ", 148, "ascii"); // the checksum is summed over this field as spaces
|
|
44
|
+
header.write("0", 156, "ascii"); // a regular file
|
|
45
|
+
header.write("ustar\0" + "00", 257, "ascii");
|
|
46
|
+
header.write(prefix, 345, "utf8");
|
|
47
|
+
const sum = header.reduce((total, byte) => total + byte, 0);
|
|
48
|
+
header.write(octal(sum, 7) + " ", 148, "ascii");
|
|
49
|
+
return header;
|
|
50
|
+
};
|
|
51
|
+
/** A gzipped tar of `entries`, in the order given. */
|
|
52
|
+
export const tarGz = (entries) => {
|
|
53
|
+
const parts = [];
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
parts.push(tarHeader(entry.path, entry.data.length, entry.mode), entry.data, Buffer.alloc((512 - (entry.data.length % 512)) % 512));
|
|
56
|
+
}
|
|
57
|
+
parts.push(Buffer.alloc(1024)); // two empty blocks end an archive
|
|
58
|
+
return gzipSync(Buffer.concat(parts), { level: 9, mtime: 0 });
|
|
59
|
+
};
|
|
60
|
+
const DOS_TIME = 0; // 00:00:00
|
|
61
|
+
const DOS_DATE = (1 << 5) | 1; // 1980-01-01, the earliest a zip can say
|
|
62
|
+
/** A zip of `entries` (deflated), with each entry's Unix mode kept so a program stays executable where the unzipper honors it. */
|
|
63
|
+
export const zip = (entries) => {
|
|
64
|
+
const local = [];
|
|
65
|
+
const central = [];
|
|
66
|
+
let offset = 0;
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const name = Buffer.from(entry.path, "utf8");
|
|
69
|
+
const packed = deflateRawSync(entry.data, { level: 9 });
|
|
70
|
+
const crc = crc32(entry.data);
|
|
71
|
+
const head = Buffer.alloc(30);
|
|
72
|
+
head.writeUInt32LE(0x04034b50, 0);
|
|
73
|
+
head.writeUInt16LE(20, 4); // version needed
|
|
74
|
+
head.writeUInt16LE(0x0800, 6); // names are UTF-8
|
|
75
|
+
head.writeUInt16LE(8, 8); // deflate
|
|
76
|
+
head.writeUInt16LE(DOS_TIME, 10);
|
|
77
|
+
head.writeUInt16LE(DOS_DATE, 12);
|
|
78
|
+
head.writeUInt32LE(crc, 14);
|
|
79
|
+
head.writeUInt32LE(packed.length, 18);
|
|
80
|
+
head.writeUInt32LE(entry.data.length, 22);
|
|
81
|
+
head.writeUInt16LE(name.length, 26);
|
|
82
|
+
head.writeUInt16LE(0, 28);
|
|
83
|
+
local.push(head, name, packed);
|
|
84
|
+
const entryCentral = Buffer.alloc(46);
|
|
85
|
+
entryCentral.writeUInt32LE(0x02014b50, 0);
|
|
86
|
+
entryCentral.writeUInt16LE((3 << 8) | 20, 4); // made by Unix, version 2.0
|
|
87
|
+
entryCentral.writeUInt16LE(20, 6);
|
|
88
|
+
entryCentral.writeUInt16LE(0x0800, 8);
|
|
89
|
+
entryCentral.writeUInt16LE(8, 10);
|
|
90
|
+
entryCentral.writeUInt16LE(DOS_TIME, 12);
|
|
91
|
+
entryCentral.writeUInt16LE(DOS_DATE, 14);
|
|
92
|
+
entryCentral.writeUInt32LE(crc, 16);
|
|
93
|
+
entryCentral.writeUInt32LE(packed.length, 20);
|
|
94
|
+
entryCentral.writeUInt32LE(entry.data.length, 24);
|
|
95
|
+
entryCentral.writeUInt16LE(name.length, 28);
|
|
96
|
+
entryCentral.writeUInt32LE(((0o100000 | (entry.mode & 0o777)) << 16) >>> 0, 38); // external attributes: a Unix regular file and its mode
|
|
97
|
+
entryCentral.writeUInt32LE(offset, 42);
|
|
98
|
+
central.push(entryCentral, name);
|
|
99
|
+
offset += head.length + name.length + packed.length;
|
|
100
|
+
}
|
|
101
|
+
const directory = Buffer.concat(central);
|
|
102
|
+
const end = Buffer.alloc(22);
|
|
103
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
104
|
+
end.writeUInt16LE(entries.length, 8);
|
|
105
|
+
end.writeUInt16LE(entries.length, 10);
|
|
106
|
+
end.writeUInt32LE(directory.length, 12);
|
|
107
|
+
end.writeUInt32LE(offset, 16);
|
|
108
|
+
return Buffer.concat([...local, directory, end]);
|
|
109
|
+
};
|
|
110
|
+
/** `sha256sum`'s format: the hex digest, two spaces, the file name; one line per file. */
|
|
111
|
+
export const checksums = (files) => files.map((file) => `${createHash("sha256").update(file.data).digest("hex")} ${file.name}\n`).join("");
|
package/dist/args.js
CHANGED
|
@@ -4,8 +4,8 @@ export const INIT_HELP = `vlx init [app-name] [options]
|
|
|
4
4
|
|
|
5
5
|
Make a new application. Without options it asks what it needs to know.
|
|
6
6
|
|
|
7
|
-
--web
|
|
8
|
-
-y, --yes answer every question with its default (
|
|
7
|
+
--web, --desktop the targets: the web, a desktop window, or both (a flag per target: ${TARGETS.map((each) => `--${each.id}`).join(", ")})
|
|
8
|
+
-y, --yes answer every question with its default (the web)
|
|
9
9
|
--pm <npm|pnpm|yarn|bun> the package manager (default: the one that ran this, else npm)
|
|
10
10
|
--no-install do not install the dependencies
|
|
11
11
|
--no-git do not run git init
|
package/dist/build.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TargetId } from "./targets.js";
|
|
1
2
|
export interface Built {
|
|
2
3
|
readonly dir: string;
|
|
3
4
|
readonly files: ReadonlyArray<string>;
|
|
@@ -10,5 +11,7 @@ export type BuildStage = "configuration" | "content" | "templates" | "bundle" |
|
|
|
10
11
|
export interface BuildOptions {
|
|
11
12
|
/** Called as each stage begins. */
|
|
12
13
|
readonly onStage?: (stage: BuildStage) => void;
|
|
14
|
+
/** Which of the project's targets to build; all of them by default. */
|
|
15
|
+
readonly targets?: ReadonlyArray<TargetId>;
|
|
13
16
|
}
|
|
14
17
|
export declare const build: (root: string, options?: BuildOptions) => Promise<Built>;
|
package/dist/build.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// `vlx build`: the project → a static site in `dist/`. The stages, in order: configuration, content, templates, the browser bundle, then one document per page (and a 404 page, a sitemap
|
|
2
|
-
// when the site has a published address). Everything is generated under `.valance/` first; `dist/` holds only
|
|
1
|
+
// `vlx build`: the project → a static site in `dist/web/`. The stages, in order: configuration, content, templates, the browser bundle, then one document per page (and a 404 page, a sitemap
|
|
2
|
+
// when the site has a published address). Everything is generated under `.valance/` first; `dist/` holds only what is built.
|
|
3
3
|
import { build as bundle } from "vite";
|
|
4
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
7
|
import { loadAuthor } from "./author.js";
|
|
7
8
|
import { loadConfig } from "./config.js";
|
|
@@ -9,41 +10,68 @@ import { headersFile } from "./security.js";
|
|
|
9
10
|
import { AppError } from "./diagnostics.js";
|
|
10
11
|
import { generateContent, generateEntries, generateProgram } from "./emit.js";
|
|
11
12
|
import { OUTPUT_DIR } from "./project.js";
|
|
12
|
-
import { baseConfig, createLoader, SERVER_ENTRY } from "./vite.js";
|
|
13
|
+
import { baseConfig, createLoader, SERVER_ENTRY, templates } from "./vite.js";
|
|
14
|
+
import { GENERATED_DIR } from "./project.js";
|
|
13
15
|
/** The file a pathname is written to in the site: `/` → `index.html`, `/guide/x` → `guide/x/index.html`, relative to the base. */
|
|
14
16
|
export const fileOf = (pathname, base) => {
|
|
15
17
|
const inside = pathname.slice(base.length).replace(/\/+$/, "");
|
|
16
18
|
return inside === "" ? "index.html" : `${inside}/index.html`;
|
|
17
19
|
};
|
|
18
20
|
const sitemap = (content) => `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${content.pages.map((page) => ` <url><loc>${content.site.url}${content.site.base}${page.path.replace(/^\//, "")}</loc></url>`).join("\n")}\n</urlset>\n`;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
/** The MESH runtime's module, found the way the project's own dependency would be: through `@valancex/common`, which depends on it. */
|
|
22
|
+
const wasmOf = (root) => {
|
|
23
|
+
const fromRoot = createRequire(join(root, "package.json"));
|
|
24
|
+
const fromCommon = createRequire(fromRoot.resolve("@valancex/common/package.json"));
|
|
25
|
+
return fromCommon.resolve("@valancex/mesh-runtime/mesh-runtime.wasm");
|
|
26
|
+
};
|
|
27
|
+
/** The desktop build: the engine, one ES module with everything it needs bundled in, and the MESH runtime's module beside it, in `dist/desktop/`. The window is the Desktop PORT's. */
|
|
28
|
+
const buildDesktop = async (root) => {
|
|
29
|
+
const out = join(root, OUTPUT_DIR, "desktop");
|
|
30
|
+
rmSync(out, { recursive: true, force: true });
|
|
31
|
+
await bundle({
|
|
32
|
+
root,
|
|
33
|
+
configFile: false,
|
|
34
|
+
publicDir: false, // the site's public files are the web's
|
|
35
|
+
logLevel: "warn",
|
|
36
|
+
plugins: [templates(root)],
|
|
37
|
+
cacheDir: join(root, GENERATED_DIR, "cache"),
|
|
38
|
+
ssr: { noExternal: true, target: "node" },
|
|
39
|
+
build: {
|
|
40
|
+
ssr: join(root, GENERATED_DIR, "desktop.ts"),
|
|
41
|
+
outDir: out,
|
|
42
|
+
emptyOutDir: true,
|
|
43
|
+
target: "node22",
|
|
44
|
+
minify: false,
|
|
45
|
+
chunkSizeWarningLimit: 4000,
|
|
46
|
+
rollupOptions: {
|
|
47
|
+
output: {
|
|
48
|
+
format: "es",
|
|
49
|
+
entryFileNames: "engine.mjs",
|
|
50
|
+
banner: 'import { createRequire as __vlxRequire } from "node:module"; const require = __vlxRequire(import.meta.url);',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
copyFileSync(wasmOf(root), join(out, "mesh-runtime.wasm"));
|
|
56
|
+
// The files images are drawn from, beside the engine (the site's `public/`, which the desktop's content refers to the same way).
|
|
57
|
+
const publicDir = join(root, "public");
|
|
58
|
+
if (existsSync(publicDir)) {
|
|
59
|
+
cpSync(publicDir, join(out, "public"), { recursive: true });
|
|
40
60
|
}
|
|
41
|
-
|
|
42
|
-
|
|
61
|
+
return { dir: out, files: ["desktop/engine.mjs", "desktop/mesh-runtime.wasm"], pages: [] };
|
|
62
|
+
};
|
|
63
|
+
/** The web build: the browser bundle into `out`, then one document per page. The loader is the one the templates were compiled with. */
|
|
64
|
+
const buildWeb = async (root, config, loader, content, out, prefix, stage) => {
|
|
65
|
+
// The browser bundle first: it empties its directory, and the documents are written into it after.
|
|
66
|
+
stage("bundle");
|
|
67
|
+
await bundle({ ...baseConfig(root, config, out), mode: "production" });
|
|
68
|
+
const rendered = (await loader.ssrLoadModule(SERVER_ENTRY));
|
|
69
|
+
{
|
|
70
|
+
const dist = join(root, out);
|
|
43
71
|
const manifest = JSON.parse(readFileSync(join(dist, ".vite", "manifest.json"), "utf8"));
|
|
44
72
|
const entry = Object.values(manifest).find((chunk) => chunk.isEntry === true);
|
|
45
73
|
if (entry === undefined) {
|
|
46
|
-
throw new AppError([{ file:
|
|
74
|
+
throw new AppError([{ file: out, line: 0, column: 0, code: "build-entry", message: "the bundle has no entry chunk" }]);
|
|
47
75
|
}
|
|
48
76
|
rmSync(join(dist, ".vite"), { recursive: true, force: true }); // the bundler's bookkeeping is not part of the site
|
|
49
77
|
const built = { script: `${config.web.base}${entry.file}`, styles: (entry.css ?? []).map((file) => `${config.web.base}${file}`) };
|
|
@@ -67,9 +95,45 @@ export const build = async (root, options = {}) => {
|
|
|
67
95
|
if (config.web.headers) {
|
|
68
96
|
write("_headers", headersFile(config));
|
|
69
97
|
}
|
|
70
|
-
return { dir: dist, files, pages };
|
|
98
|
+
return { dir: dist, files: files.map((file) => `${prefix}${file}`), pages };
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
export const build = async (root, options = {}) => {
|
|
102
|
+
const stage = options.onStage ?? (() => undefined);
|
|
103
|
+
stage("configuration");
|
|
104
|
+
const config = loadConfig(root);
|
|
105
|
+
const selected = options.targets ?? config.targets;
|
|
106
|
+
const unknown = selected.filter((id) => !config.targets.includes(id));
|
|
107
|
+
if (unknown.length > 0) {
|
|
108
|
+
throw new AppError([
|
|
109
|
+
{
|
|
110
|
+
file: "valance.json",
|
|
111
|
+
line: 0,
|
|
112
|
+
column: 0,
|
|
113
|
+
code: "build-target",
|
|
114
|
+
message: `this project is not built for ${unknown.join(", ")} (its targets: ${config.targets.join(", ")}): add valance.${unknown[0]}.json`,
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
generateEntries(root, config);
|
|
119
|
+
stage("content");
|
|
120
|
+
const content = generateContent(root, config);
|
|
121
|
+
const loader = await createLoader(root, config);
|
|
122
|
+
const built = [];
|
|
123
|
+
try {
|
|
124
|
+
stage("templates");
|
|
125
|
+
await generateProgram(root, await loadAuthor(loader, root), config, selected);
|
|
126
|
+
// Each target writes to a directory of its own, whatever else the project has: dist/web/ and dist/desktop/.
|
|
127
|
+
if (selected.includes("web")) {
|
|
128
|
+
built.push(await buildWeb(root, config, loader, content, `${OUTPUT_DIR}/web`, "web/", stage));
|
|
129
|
+
}
|
|
130
|
+
if (selected.includes("desktop")) {
|
|
131
|
+
stage("bundle");
|
|
132
|
+
built.push(await buildDesktop(root));
|
|
133
|
+
}
|
|
71
134
|
}
|
|
72
135
|
finally {
|
|
73
136
|
await loader.close();
|
|
74
137
|
}
|
|
138
|
+
return { dir: join(root, OUTPUT_DIR), files: built.flatMap((each) => each.files), pages: built.flatMap((each) => each.pages) };
|
|
75
139
|
};
|
package/dist/check.js
CHANGED
|
@@ -22,13 +22,13 @@ export const check = async (root) => {
|
|
|
22
22
|
if (config === undefined) {
|
|
23
23
|
return { diagnostics: problems, pages: 0, components: [] };
|
|
24
24
|
}
|
|
25
|
-
generateEntries(root);
|
|
25
|
+
generateEntries(root, config);
|
|
26
26
|
const content = await collect(problems, () => generateContent(root, config));
|
|
27
27
|
const server = await createLoader(root, config);
|
|
28
28
|
try {
|
|
29
29
|
const author = await collect(problems, () => loadAuthor(server, root));
|
|
30
30
|
const failedAuthor = problems.some((each) => each.file === "src/main.ts");
|
|
31
|
-
const components = failedAuthor ? undefined : await collect(problems, () => generateProgram(root, author));
|
|
31
|
+
const components = failedAuthor ? undefined : await collect(problems, () => generateProgram(root, author, config));
|
|
32
32
|
return { diagnostics: problems, pages: content?.pages.length ?? 0, components: components ?? [] };
|
|
33
33
|
}
|
|
34
34
|
finally {
|