@valancex/cli 0.13.0 → 0.15.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/README.md +33 -6
- package/dist/budgets.json +175 -0
- package/dist/build.d.ts +8 -0
- package/dist/build.js +38 -9
- package/dist/check.js +6 -1
- package/dist/cli.js +164 -6
- package/dist/compile.d.ts +8 -0
- package/dist/compile.js +135 -16
- package/dist/config.d.ts +12 -0
- package/dist/config.js +34 -1
- package/dist/contract.d.ts +18 -1
- package/dist/contract.js +92 -3
- package/dist/dev.d.ts +5 -0
- package/dist/dev.js +60 -26
- package/dist/emit.d.ts +3 -1
- package/dist/emit.js +56 -17
- package/dist/libraries.d.ts +41 -0
- package/dist/libraries.js +169 -0
- package/dist/library.d.ts +32 -0
- package/dist/library.js +333 -0
- package/dist/package.js +8 -4
- 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/theme.d.ts +41 -0
- package/dist/theme.js +84 -0
- package/dist/tree.d.ts +14 -2
- package/dist/tree.js +16 -1
- package/dist/ui/report.js +1 -1
- package/dist/ui/run.d.ts +1 -0
- package/dist/ui/run.js +13 -0
- package/dist/vite.js +17 -3
- package/package.json +13 -8
- package/schemas/valance.schema.json +76 -0
- package/templates/desktop/gitignore +3 -0
- package/templates/multi/src/web/layout.mprx +2 -2
- 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/README.md
CHANGED
|
@@ -153,7 +153,7 @@ Templates are MPRX. Conditional and repeated parts are blocks: `{#if cond}…{:e
|
|
|
153
153
|
| `prev`, `next` | `{ href, label }`, empty `href` when there is none |
|
|
154
154
|
| `viewport` | How much room the page has: `"compact"` (under 600 px), `"medium"` (600 to 839) or `"expanded"` (840 and up), kept current as the window changes. `{#if viewport == "compact"}` chooses markup for a phone. The server renders `expanded`; the browser corrects it as soon as it starts. |
|
|
155
155
|
|
|
156
|
-
Tags you may use: `shell` (`class`, `theme`), `div`, `header`, `footer`, `nav` (`class`, `label`), `aside`, `section`, `article`, `main` (`id`, `tabindex`), `h1`–`h3` (`id`), `
|
|
156
|
+
Tags you may use: `shell` (`class`, `theme`), `div`, `header`, `footer`, `nav` (`class`, `label`), `aside`, `section`, `article`, `main` (`id`, `tabindex`), `h1`–`h3` (`id`), `paragraph`, `blockquote`, `list`, `olist`, `listitem`, `pre`, `code`, `strong`, `em`, `span`, `image`, `a` (`href`), `link` (`href`, `class`, `current`), `button` (`class`, `pressed`, `on.click`), `toggle`, `checkbox`, `textfield`, `radiogroup`, `radio`, `label`, `status`, `alert` (the full list, with roles and names, is [the primitives](../../docs/use/primitives.md)), plus your components and the built-ins (`blocks`, `block`, `heading`, `list-block`, `list-items`, `runs`, `inline`). The tags `para`, `item` and `quote` are now `paragraph`, `listitem` and `blockquote` (the ARIA names, which the Desktop already used); a template that writes the old one is told the new one. A prop a tag declares must be written wherever the tag is used; that is why there are many narrow tags rather than a few wide ones. The attribute each prop becomes is in [`src/primitives.ts`](src/primitives.ts).
|
|
157
157
|
|
|
158
158
|
## The components
|
|
159
159
|
|
|
@@ -173,20 +173,22 @@ export default defineApp({
|
|
|
173
173
|
|
|
174
174
|
```ts
|
|
175
175
|
// src/theme-toggle.ts: the component states its own interface, where it is defined
|
|
176
|
-
import { component } from "@valancex/common";
|
|
176
|
+
import { component, field } from "@valancex/common";
|
|
177
177
|
import { Schema } from "effect";
|
|
178
178
|
import template from "./theme-toggle.mprx";
|
|
179
179
|
|
|
180
180
|
export default component(template, {
|
|
181
|
-
props: { theme: Schema.Literal("light", "dark") },
|
|
181
|
+
props: { theme: field(Schema.Literal("light", "dark"), "light") },
|
|
182
182
|
events: { toggle: Schema.Void },
|
|
183
183
|
});
|
|
184
184
|
```
|
|
185
185
|
|
|
186
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`.
|
|
187
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`).
|
|
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
|
|
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`. A **string-literal union is a closed set**: `Schema.Literal("info", "danger")` becomes the MESH type `"info" | "danger"`, and MESH reports any other literal written for the prop at the use (`unknown-variant`, with a did-you-mean when one is close); a number or boolean literal keeps its primitive type. A prop gets a **default** with `field(schema, value)` (`props: { tone: field(Schema.Literal("info", "danger"), "info") }`): a use of the component that leaves the prop out gets the value, the prop may then be left out, and TypeScript refuses a default that is not a member. A default is for a component with a template; on a primitive it is ignored. A schema with no MESH type, for example a union of a string literal and a number, or a default that is not a value of the prop's type, is a diagnostic at the declaration (`component-schema`). Both need MESH 0.14 or later (`@valancex/mesh-compiler` `^0.14.0`, which the packages depend on). 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.
|
|
189
189
|
- `override: [component(runs, { props: { spans: … } })]` replaces a built-in (`runs`, `heading-block`, …) with your template of the same name.
|
|
190
|
+
- **One MESH runtime.** `vlx build` (never `vlx dev`) stops with `runtime-duplicate` when the packages resolve more than one copy of `@valancex/mesh-runtime`: each copy is a 765 KB WebAssembly module (171 KB of brotli) that the site would ship and the browser fetch. The message names each copy, its version and who uses it, and says to add an `overrides` entry (npm; `pnpm.overrides`, or `resolutions` with yarn) so there is one. `@valancex/common`, `@valancex/valance` and `@valancex/nexus` accept the same range (`^0.12.0`) so a plain install has one.
|
|
191
|
+
- **One name is one component.** Two registered files with the same name (for example `a/disclosure.mprx` and `b/disclosure.mprx`, whether yours or from two libraries) are `template-duplicate`, and a registered component named like a built-in is `template-builtin` unless it is in `override`: neither silently replaces the other, and the first one listed does not win. Rename one of the files; qualified ids for a library's components (`acme-ui/disclosure`, decision K14) are not built yet. `override` is only for built-ins: for any other name it is `override-unknown`, and the component belongs in `components`.
|
|
190
192
|
- `layout` is optional; without it the built-in layout draws the page. Its template must be named `layout.mprx`.
|
|
191
193
|
|
|
192
194
|
### Children and slots
|
|
@@ -202,13 +204,38 @@ A component places the content its caller writes inside it with `<mesh-slot />`,
|
|
|
202
204
|
|
|
203
205
|
<!-- where it is used -->
|
|
204
206
|
<card>
|
|
205
|
-
<mesh-fill slot="head"><
|
|
206
|
-
<
|
|
207
|
+
<mesh-fill slot="head"><paragraph class="title">Title</paragraph></mesh-fill>
|
|
208
|
+
<paragraph class="body">The body goes to the unnamed slot.</paragraph>
|
|
207
209
|
</card>
|
|
208
210
|
```
|
|
209
211
|
|
|
210
212
|
The children are the caller's: `{x}` in them is the caller's `x`, never the card's. `vlx generate component` writes the single, unnamed slot for you. The rules (where a slot may be, what a fill is, the errors) are in MESH's [templates manual](https://github.com/ValanceX/Mesh/blob/main/docs/manual/templates.md#children-and-the-slot).
|
|
211
213
|
|
|
214
|
+
### Snippets as props
|
|
215
|
+
|
|
216
|
+
A prop can be a piece of template (MESH 0.14). Declare it with `snippet(…)`, the types of its parameters in order, and read it in the component's template with `{@render name(args)}`:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
// src/panel.ts and src/item-list.ts
|
|
220
|
+
component(panel, { props: { title: Schema.String, children: snippet() } });
|
|
221
|
+
component(itemList, { props: { items: Schema.Array(Item), row: snippet(Item, Schema.String) } });
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
<!-- src/panel.mprx -->
|
|
226
|
+
<section><strong>{title}</strong>{@render children()}</section>
|
|
227
|
+
|
|
228
|
+
<!-- where they are used: loose content is `children`; a {#snippet} that is a direct child is the prop of its name -->
|
|
229
|
+
<panel title="Totals"><span class="count">{count}</span></panel>
|
|
230
|
+
<item-list items={items} label="row">{#snippet row(item, tag)}<span class="row">{tag}:{item.name}</span>{/snippet}</item-list>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- `children` is reserved for the content between the tags: it must be `snippet()` (no parameters), and `children={…}` with content is `children-conflict`. A snippet is not data: it has no `field` default, is never optional, and is not a record field or an event's payload.
|
|
234
|
+
- A snippet reads the scope of the template that **wrote** it (`count` above is the layout's, not the panel's), so a node that renders one is redrawn when that scope changes though its own props did not.
|
|
235
|
+
- The arity and the types are checked: `snippet-arity-mismatch` in the composite's template when `{@render row(item)}` passes too few, and a member the parameter does not have is reported where the snippet is written.
|
|
236
|
+
- **Optional.** `snippet(…).optional()` (and `children: snippet().optional()`) is one an occurrence may leave out: it is `snippet<…>?` in the model, and an absent one draws nothing. Rendering it is `possibly-absent-snippet` unless it is inside the first branch of an `{#if has(name)}` (`&&` allowed), so `{#if has(header)}{@render header()}{/if}` is how a template uses one.
|
|
237
|
+
- Slots (`mesh-slot`, `mesh-fill`) keep working for a component that does not declare `children`.
|
|
238
|
+
|
|
212
239
|
### Typed template imports
|
|
213
240
|
|
|
214
241
|
`vlx sync` (and `check`, `dev` and `build`) write `.valance/types/<template>.d.mprx.ts`, so a component's declaration is checked against the names its template uses: `component(template, { props: { thme: … } })` is a compile-time error. This needs two options in your `tsconfig.json`:
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"about": "The ecosystem's budgets, as data (Bet 3; decisions W1 to W24). `project` is what `vlx build` holds an application to when its valance.json sets none (W14). `framework` is what the release check holds the reference applications and the runtime to (scripts/check-budgets.mjs, over the output of scripts/measure-footprint.mjs). Brotli bytes, because that is what travels (W2).",
|
|
4
|
+
"rule": "A ceiling is the baseline plus 5 percent, rounded up to the next 100 bytes (the next 10 under 1,000), then ratcheted down (W3). A ceiling above that needs `analysis`: the path of a size analysis that says why (W23a), and a decision row. The check refuses a raised ceiling without one.",
|
|
5
|
+
"project": {
|
|
6
|
+
"budgets": [
|
|
7
|
+
{
|
|
8
|
+
"type": "initial",
|
|
9
|
+
"warning": 105200,
|
|
10
|
+
"error": 210400
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "runtime",
|
|
14
|
+
"error": 192900
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"note": "W14: the framework's own start-up ceilings. `initial` warns above the largest reference application's JavaScript and CSS ceiling (docs-app) and fails at twice that; `runtime` is the WebAssembly runtime, the same for every application, so a growth of it fails the build of whoever upgrades."
|
|
18
|
+
},
|
|
19
|
+
"framework": {
|
|
20
|
+
"baseline": "docs/direction/baselines/2026-10-24-wp8-before.md",
|
|
21
|
+
"ceilings": [
|
|
22
|
+
{
|
|
23
|
+
"id": "counter.javascript",
|
|
24
|
+
"what": "Counter application, JavaScript, brotli",
|
|
25
|
+
"path": "counter.deduped.js.brotli",
|
|
26
|
+
"unit": "bytes",
|
|
27
|
+
"baseline": 93722,
|
|
28
|
+
"ceiling": 98500,
|
|
29
|
+
"previousCeiling": 92400,
|
|
30
|
+
"analysis": "docs/direction/evidence/measurement/size-2026-10-24.md"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "counter.css",
|
|
34
|
+
"what": "Counter application, CSS, brotli",
|
|
35
|
+
"path": "counter.deduped.css.brotli",
|
|
36
|
+
"unit": "bytes",
|
|
37
|
+
"baseline": 1053,
|
|
38
|
+
"ceiling": 1200
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "runtime.wasm",
|
|
42
|
+
"what": "The WebAssembly runtime, brotli",
|
|
43
|
+
"path": "counter.deduped.wasm.brotli",
|
|
44
|
+
"unit": "bytes",
|
|
45
|
+
"baseline": 183629,
|
|
46
|
+
"ceiling": 192900,
|
|
47
|
+
"previousCeiling": 179400,
|
|
48
|
+
"analysis": "docs/direction/evidence/measurement/size-2026-10-24.md"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "counter.transferred",
|
|
52
|
+
"what": "Counter application, everything the browser transfers, brotli",
|
|
53
|
+
"path": "browser.deduped.noThrottling.bytesTransferredBrotli",
|
|
54
|
+
"unit": "bytes",
|
|
55
|
+
"baseline": 279594,
|
|
56
|
+
"ceiling": 293600,
|
|
57
|
+
"previousCeiling": 274100,
|
|
58
|
+
"analysis": "docs/direction/evidence/measurement/size-2026-10-24.md"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "docsApp.javascript",
|
|
62
|
+
"what": "docs-app, JavaScript, brotli",
|
|
63
|
+
"path": "docsApp.js.brotli",
|
|
64
|
+
"unit": "bytes",
|
|
65
|
+
"baseline": 98520,
|
|
66
|
+
"ceiling": 103500,
|
|
67
|
+
"previousCeiling": 97400,
|
|
68
|
+
"analysis": "docs/direction/evidence/measurement/size-2026-10-24.md"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "docsApp.css",
|
|
72
|
+
"what": "docs-app, CSS, brotli",
|
|
73
|
+
"path": "docsApp.css.brotli",
|
|
74
|
+
"unit": "bytes",
|
|
75
|
+
"baseline": 1542,
|
|
76
|
+
"ceiling": 1700
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "docsApp.perPage",
|
|
80
|
+
"what": "Client JavaScript each added page costs, raw",
|
|
81
|
+
"path": "docsApp.perAddedPage.jsRawPerPage",
|
|
82
|
+
"unit": "bytes",
|
|
83
|
+
"baseline": 2471.5,
|
|
84
|
+
"ceiling": 2600,
|
|
85
|
+
"previousCeiling": 2600
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "counter.heap",
|
|
89
|
+
"what": "Counter, JavaScript heap after 200 updates",
|
|
90
|
+
"path": "browser.deduped.noThrottling.afterUpdates.jsHeapUsedBytes",
|
|
91
|
+
"unit": "bytes",
|
|
92
|
+
"baseline": 4575160,
|
|
93
|
+
"ceiling": 4630800,
|
|
94
|
+
"previousCeiling": 4630800,
|
|
95
|
+
"note": "W24: the 1,000-update heap gate stays off"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "counter.wasmMemory",
|
|
99
|
+
"what": "Counter, WebAssembly memory after 200 updates",
|
|
100
|
+
"path": "browser.deduped.noThrottling.afterUpdates.wasmMemoryBytes",
|
|
101
|
+
"unit": "bytes",
|
|
102
|
+
"baseline": 1310720,
|
|
103
|
+
"ceiling": 1376300,
|
|
104
|
+
"previousCeiling": 4885800,
|
|
105
|
+
"note": "ratcheted down: W21 fixed the growth, the memory is flat"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"id": "docsApp.wasmMemory",
|
|
109
|
+
"what": "docs-app, WebAssembly memory after 200 updates",
|
|
110
|
+
"path": "browser.docsApp.noThrottling.afterUpdates.wasmMemoryBytes",
|
|
111
|
+
"unit": "bytes",
|
|
112
|
+
"baseline": 2293760,
|
|
113
|
+
"ceiling": 2408500
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"exact": [
|
|
117
|
+
{
|
|
118
|
+
"id": "wire.patch",
|
|
119
|
+
"what": "Bytes a PORT is sent for one changed item of 1,000 rows, as patches",
|
|
120
|
+
"path": "wire.1000.patchBytes",
|
|
121
|
+
"unit": "bytes",
|
|
122
|
+
"max": 202
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"ratios": [
|
|
126
|
+
{
|
|
127
|
+
"id": "counter.interactiveAt",
|
|
128
|
+
"what": "Counter, time until a click is handled, unthrottled",
|
|
129
|
+
"path": "browser.deduped.noThrottling.interactiveAtMs",
|
|
130
|
+
"max": 2
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "counter.interactiveAt4x",
|
|
134
|
+
"what": "Counter, time until a click is handled, 4x CPU",
|
|
135
|
+
"path": "browser.deduped.cpu4x.interactiveAtMs",
|
|
136
|
+
"max": 2
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"id": "counter.firstPaint",
|
|
140
|
+
"what": "Counter, first paint, unthrottled",
|
|
141
|
+
"path": "browser.deduped.noThrottling.firstPaintMs",
|
|
142
|
+
"max": 2
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": "update.10000",
|
|
146
|
+
"what": "`update`, 10,000 items, one changed",
|
|
147
|
+
"path": "update.10000.updateMs",
|
|
148
|
+
"max": 1.5
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"id": "updateFromChanges.10000",
|
|
152
|
+
"what": "`update` from changes, 10,000 items",
|
|
153
|
+
"path": "update.10000.updateFromChangesMs",
|
|
154
|
+
"max": 1.5
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
"known": [
|
|
158
|
+
{
|
|
159
|
+
"id": "wire.wholeTree",
|
|
160
|
+
"what": "Bytes a PORT is sent for one changed item of 1,000 rows, whole tree",
|
|
161
|
+
"path": "wire.1000.wholeTreeBytes",
|
|
162
|
+
"reason": "the Desktop protocol sends the whole tree on every update; patches are Bet 3's own work item, not WP8",
|
|
163
|
+
"owner": "unassigned"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"id": "installability",
|
|
167
|
+
"what": "The newest published @valancex set, with the release being measured, installs together",
|
|
168
|
+
"path": "newestInstallsTogether.installs",
|
|
169
|
+
"expect": true,
|
|
170
|
+
"reason": "Was red from MESH 0.16 until VALANCE 0.14: NEXUS 0.14 and PORT 0.12 stopped their MESH peer range at 0.15. With --tree the release candidate's own tarballs stand in for the published VALANCE packages, so a release is held to installing beside the newest of the others before it is published (a published VALANCE, the previous release, would gate a release on its predecessor)",
|
|
171
|
+
"owner": "WP8"
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
}
|
package/dist/build.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { TargetId } from "./targets.js";
|
|
2
|
+
import type { BudgetResult, SizeReport } from "./size.js";
|
|
2
3
|
export interface Built {
|
|
3
4
|
readonly dir: string;
|
|
4
5
|
readonly files: ReadonlyArray<string>;
|
|
5
6
|
readonly pages: ReadonlyArray<string>;
|
|
7
|
+
/** The web build's start-up and after-start sets, measured, and each budget it was held to (a production build only). */
|
|
8
|
+
readonly size?: SizeReport;
|
|
9
|
+
readonly budgets?: ReadonlyArray<BudgetResult>;
|
|
10
|
+
/** The size report as the lines a terminal prints. */
|
|
11
|
+
readonly report?: ReadonlyArray<string>;
|
|
6
12
|
}
|
|
7
13
|
/** The file a pathname is written to in the site: `/` → `index.html`, `/guide/x` → `guide/x/index.html`, relative to the base. */
|
|
8
14
|
export declare const fileOf: (pathname: string, base: string) => string;
|
|
@@ -13,5 +19,7 @@ export interface BuildOptions {
|
|
|
13
19
|
readonly onStage?: (stage: BuildStage) => void;
|
|
14
20
|
/** Which of the project's targets to build; all of them by default. */
|
|
15
21
|
readonly targets?: ReadonlyArray<TargetId>;
|
|
22
|
+
/** Called with the size report as soon as the bundle is measured, before a budget error stops the build (a terminal prints it). */
|
|
23
|
+
readonly onReport?: (lines: ReadonlyArray<string>) => void;
|
|
16
24
|
}
|
|
17
25
|
export declare const build: (root: string, options?: BuildOptions) => Promise<Built>;
|
package/dist/build.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
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
2
|
// when the site has a published address). Everything is generated under `.valance/` first; `dist/` holds only what is built.
|
|
3
|
-
import { build as bundle } from "vite";
|
|
3
|
+
import { build as bundle, defaultServerConditions } from "vite";
|
|
4
|
+
import { CONDITION } from "./libraries.js";
|
|
4
5
|
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
@@ -10,6 +11,8 @@ import { headersFile } from "./security.js";
|
|
|
10
11
|
import { AppError } from "./diagnostics.js";
|
|
11
12
|
import { generateContent, generateEntries, generateProgram } from "./emit.js";
|
|
12
13
|
import { OUTPUT_DIR } from "./project.js";
|
|
14
|
+
import { check, DEFAULT_BUDGETS, failure, lines, measure } from "./size.js";
|
|
15
|
+
import { assertOneRuntime } from "./runtime-guard.js";
|
|
13
16
|
import { baseConfig, createLoader, SERVER_ENTRY, templates } from "./vite.js";
|
|
14
17
|
import { GENERATED_DIR } from "./project.js";
|
|
15
18
|
/** The file a pathname is written to in the site: `/` → `index.html`, `/guide/x` → `guide/x/index.html`, relative to the base. */
|
|
@@ -35,14 +38,17 @@ const buildDesktop = async (root) => {
|
|
|
35
38
|
logLevel: "warn",
|
|
36
39
|
plugins: [templates(root)],
|
|
37
40
|
cacheDir: join(root, GENERATED_DIR, "cache"),
|
|
38
|
-
|
|
41
|
+
// the desktop engine is one file: everything is bundled, and a library resolves by its `valance` condition (K4)
|
|
42
|
+
resolve: { conditions: [CONDITION, ...defaultServerConditions] },
|
|
43
|
+
ssr: { noExternal: true, target: "node", resolve: { conditions: [CONDITION, ...defaultServerConditions] } },
|
|
44
|
+
esbuild: { keepNames: true },
|
|
39
45
|
build: {
|
|
40
46
|
ssr: join(root, GENERATED_DIR, "desktop.ts"),
|
|
41
47
|
outDir: out,
|
|
42
48
|
emptyOutDir: true,
|
|
43
49
|
target: "node22",
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
// 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.
|
|
51
|
+
minify: "esbuild",
|
|
46
52
|
rollupOptions: {
|
|
47
53
|
output: {
|
|
48
54
|
format: "es",
|
|
@@ -61,7 +67,7 @@ const buildDesktop = async (root) => {
|
|
|
61
67
|
return { dir: out, files: ["desktop/engine.mjs", "desktop/mesh-runtime.wasm"], pages: [] };
|
|
62
68
|
};
|
|
63
69
|
/** 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) => {
|
|
70
|
+
const buildWeb = async (root, config, loader, content, out, prefix, stage, onReport) => {
|
|
65
71
|
// The browser bundle first: it empties its directory, and the documents are written into it after.
|
|
66
72
|
stage("bundle");
|
|
67
73
|
await bundle({ ...baseConfig(root, config, out), mode: "production" });
|
|
@@ -74,7 +80,22 @@ const buildWeb = async (root, config, loader, content, out, prefix, stage) => {
|
|
|
74
80
|
throw new AppError([{ file: out, line: 0, column: 0, code: "build-entry", message: "the bundle has no entry chunk" }]);
|
|
75
81
|
}
|
|
76
82
|
rmSync(join(dist, ".vite"), { recursive: true, force: true }); // the bundler's bookkeeping is not part of the site
|
|
77
|
-
|
|
83
|
+
// The WebAssembly runtime is in the start-up set (H7): the head starts compiling it while the page's script downloads (W11).
|
|
84
|
+
const wasm = Object.values(manifest).find((chunk) => chunk.file.endsWith(".wasm"))?.file;
|
|
85
|
+
const built = {
|
|
86
|
+
script: `${config.web.base}${entry.file}`,
|
|
87
|
+
styles: (entry.css ?? []).map((file) => `${config.web.base}${file}`),
|
|
88
|
+
...(wasm === undefined ? {} : { wasm: `${config.web.base}${wasm}` }),
|
|
89
|
+
};
|
|
90
|
+
// The size report: the start-up set and the after-start set, held to the project's budgets or the framework's (W9, W14). Production only.
|
|
91
|
+
const size = measure(dist, manifest, wasm);
|
|
92
|
+
const budgets = check(size, config.budgets ?? DEFAULT_BUDGETS);
|
|
93
|
+
const report = lines(size, budgets);
|
|
94
|
+
onReport(report);
|
|
95
|
+
const over = failure(budgets);
|
|
96
|
+
if (over !== undefined) {
|
|
97
|
+
throw over;
|
|
98
|
+
}
|
|
78
99
|
const files = [];
|
|
79
100
|
const write = (relative, text) => {
|
|
80
101
|
mkdirSync(dirname(join(dist, relative)), { recursive: true });
|
|
@@ -95,7 +116,7 @@ const buildWeb = async (root, config, loader, content, out, prefix, stage) => {
|
|
|
95
116
|
if (config.web.headers) {
|
|
96
117
|
write("_headers", headersFile(config));
|
|
97
118
|
}
|
|
98
|
-
return { dir: dist, files: files.map((file) => `${prefix}${file}`), pages };
|
|
119
|
+
return { dir: dist, files: files.map((file) => `${prefix}${file}`), pages, size, budgets, report };
|
|
99
120
|
}
|
|
100
121
|
};
|
|
101
122
|
export const build = async (root, options = {}) => {
|
|
@@ -115,6 +136,8 @@ export const build = async (root, options = {}) => {
|
|
|
115
136
|
},
|
|
116
137
|
]);
|
|
117
138
|
}
|
|
139
|
+
// Production only (`vlx dev` does not build through here): one copy of the MESH runtime, or the site ships its WebAssembly module more than once (W20).
|
|
140
|
+
assertOneRuntime(root);
|
|
118
141
|
generateEntries(root, config);
|
|
119
142
|
stage("content");
|
|
120
143
|
const content = generateContent(root, config);
|
|
@@ -125,7 +148,7 @@ export const build = async (root, options = {}) => {
|
|
|
125
148
|
await generateProgram(root, await loadAuthor(loader, root), config, selected);
|
|
126
149
|
// Each target writes to a directory of its own, whatever else the project has: dist/web/ and dist/desktop/.
|
|
127
150
|
if (selected.includes("web")) {
|
|
128
|
-
built.push(await buildWeb(root, config, loader, content, `${OUTPUT_DIR}/web`, "web/", stage));
|
|
151
|
+
built.push(await buildWeb(root, config, loader, content, `${OUTPUT_DIR}/web`, "web/", stage, options.onReport ?? (() => undefined)));
|
|
129
152
|
}
|
|
130
153
|
if (selected.includes("desktop")) {
|
|
131
154
|
stage("bundle");
|
|
@@ -135,5 +158,11 @@ export const build = async (root, options = {}) => {
|
|
|
135
158
|
finally {
|
|
136
159
|
await loader.close();
|
|
137
160
|
}
|
|
138
|
-
|
|
161
|
+
const web = built.find((each) => each.size !== undefined);
|
|
162
|
+
return {
|
|
163
|
+
dir: join(root, OUTPUT_DIR),
|
|
164
|
+
files: built.flatMap((each) => each.files),
|
|
165
|
+
pages: built.flatMap((each) => each.pages),
|
|
166
|
+
...(web?.size === undefined ? {} : { size: web.size, budgets: web.budgets, report: web.report }),
|
|
167
|
+
};
|
|
139
168
|
};
|
package/dist/check.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AppError } from "./diagnostics.js";
|
|
2
2
|
import { loadAuthor } from "./author.js";
|
|
3
3
|
import { loadConfig } from "./config.js";
|
|
4
|
+
import { librariesOf } from "./libraries.js";
|
|
4
5
|
import { generateContent, generateEntries, generateProgram } from "./emit.js";
|
|
5
6
|
import { createLoader } from "./vite.js";
|
|
6
7
|
const collect = async (into, work) => {
|
|
@@ -22,6 +23,8 @@ export const check = async (root) => {
|
|
|
22
23
|
if (config === undefined) {
|
|
23
24
|
return { diagnostics: problems, pages: 0, components: [] };
|
|
24
25
|
}
|
|
26
|
+
// a library whose marker is wrong is reported first: when the application cannot even be loaded, it is the likely reason (a package the build does not process)
|
|
27
|
+
problems.push(...librariesOf(root).problems);
|
|
25
28
|
generateEntries(root, config);
|
|
26
29
|
const content = await collect(problems, () => generateContent(root, config));
|
|
27
30
|
const server = await createLoader(root, config);
|
|
@@ -29,7 +32,9 @@ export const check = async (root) => {
|
|
|
29
32
|
const author = await collect(problems, () => loadAuthor(server, root));
|
|
30
33
|
const failedAuthor = problems.some((each) => each.file === "src/main.ts");
|
|
31
34
|
const components = failedAuthor ? undefined : await collect(problems, () => generateProgram(root, author, config));
|
|
32
|
-
|
|
35
|
+
// (the templates stage reports the same marker problems; once is enough)
|
|
36
|
+
const once = problems.filter((each, index) => problems.findIndex((other) => other.file === each.file && other.code === each.code && other.message === each.message) === index);
|
|
37
|
+
return { diagnostics: once, pages: content?.pages.length ?? 0, components: components ?? [] };
|
|
33
38
|
}
|
|
34
39
|
finally {
|
|
35
40
|
await server.close();
|