@valancex/cli 0.0.0-stage → 0.11.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 +263 -2
- package/bin/vlx.js +4 -0
- package/dist/add.d.ts +20 -0
- package/dist/add.js +143 -0
- package/dist/args.d.ts +20 -0
- package/dist/args.js +78 -0
- package/dist/author.d.ts +4 -0
- package/dist/author.js +30 -0
- package/dist/build.d.ts +14 -0
- package/dist/build.js +70 -0
- package/dist/check.d.ts +8 -0
- package/dist/check.js +37 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +280 -0
- package/dist/compile.d.ts +51 -0
- package/dist/compile.js +217 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +99 -0
- package/dist/content.d.ts +12 -0
- package/dist/content.js +193 -0
- package/dist/contract.d.ts +31 -0
- package/dist/contract.js +45 -0
- package/dist/delegate.d.ts +2 -0
- package/dist/delegate.js +23 -0
- package/dist/dev.d.ts +21 -0
- package/dist/dev.js +250 -0
- package/dist/diagnostics.d.ts +16 -0
- package/dist/diagnostics.js +16 -0
- package/dist/emit.d.ts +13 -0
- package/dist/emit.js +107 -0
- package/dist/init.d.ts +69 -0
- package/dist/init.js +185 -0
- package/dist/markdown.d.ts +20 -0
- package/dist/markdown.js +185 -0
- package/dist/node.d.ts +10 -0
- package/dist/node.js +6 -0
- package/dist/outline.d.ts +17 -0
- package/dist/outline.js +88 -0
- package/dist/project.d.ts +8 -0
- package/dist/project.js +20 -0
- package/dist/targets.d.ts +9 -0
- package/dist/targets.js +2 -0
- package/dist/tree.d.ts +47 -0
- package/dist/tree.js +149 -0
- package/dist/tty.d.ts +13 -0
- package/dist/tty.js +4 -0
- package/dist/ui/Spinner.d.ts +3 -0
- package/dist/ui/Spinner.js +16 -0
- package/dist/ui/init.d.ts +22 -0
- package/dist/ui/init.js +137 -0
- package/dist/ui/prompts.d.ts +39 -0
- package/dist/ui/prompts.js +98 -0
- package/dist/ui/report.d.ts +31 -0
- package/dist/ui/report.js +30 -0
- package/dist/ui/run.d.ts +15 -0
- package/dist/ui/run.js +75 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +49 -0
- package/dist/wiring.d.ts +108 -0
- package/dist/wiring.js +330 -0
- package/package.json +69 -3
- package/schemas/valance.schema.json +14 -0
- package/schemas/valance.web.schema.json +14 -0
- package/templates/web/README.md +14 -0
- package/templates/web/content/404.md +7 -0
- package/templates/web/content/guide/getting-started.md +33 -0
- package/templates/web/content/guide/writing-pages.md +21 -0
- package/templates/web/content/index.md +16 -0
- package/templates/web/gitignore +4 -0
- package/templates/web/public/logo.svg +1 -0
- package/templates/web/src/layout.mprx +27 -0
- package/templates/web/src/main.ts +20 -0
- package/templates/web/src/styles.css +28 -0
- package/templates/web/src/theme-toggle.mprx +1 -0
- package/templates/web/src/theme-toggle.ts +10 -0
- package/templates/web/tsconfig.json +18 -0
- package/views/block.mprx +3 -0
- package/views/blocks.mprx +3 -0
- package/views/heading.mprx +3 -0
- package/views/inline.mprx +3 -0
- package/views/layout.mprx +26 -0
- package/views/list-block.mprx +5 -0
- package/views/list-items.mprx +3 -0
- package/views/runs.mprx +3 -0
package/README.md
CHANGED
|
@@ -1,3 +1,264 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @valancex/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Pre-1.0.** Like VALANCE itself, the commands and conventions may change between minor releases; each change is in the [release notes](../../docs/releases/). The design record is [`docs/application-model/`](../../docs/application-model/).
|
|
4
|
+
|
|
5
|
+
`vlx` is the way into VALANCE: it makes an application, serves it as you edit, builds it, checks it and adds to it. The application model it runs: you write **content**, a **layout**, **components** and **styles**; `vlx dev` serves them as you edit and `vlx build` produces a website. You do not write a compiler step, a manifest, a server, a hydration script or a bundler config.
|
|
6
|
+
|
|
7
|
+
```console
|
|
8
|
+
$ vlx init my-app --web # make a new application (asks what you leave out)
|
|
9
|
+
$ vlx dev # serve the project, rebuild as files change
|
|
10
|
+
$ vlx build # write the site to dist/
|
|
11
|
+
$ vlx check # find mistakes in your files without building (--json for tools)
|
|
12
|
+
$ vlx generate component post-card # add a component (or: generate page guide/intro)
|
|
13
|
+
$ vlx sync # refresh the generated files (it runs by itself; rarely typed)
|
|
14
|
+
$ vlx help # every command; `vlx help <command>` says more
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The code an application's `src/main.ts` imports (`defineApp`, `component`, `field`) is a separate package, [`@valancex/common`](../common), which also holds the runtime a built application runs: the command line builds your application and is not part of it, so a project has `@valancex/cli` as a dev dependency.
|
|
18
|
+
|
|
19
|
+
## Install `vlx` once, use it everywhere
|
|
20
|
+
|
|
21
|
+
`vlx` is a global command, like Angular's `ng`:
|
|
22
|
+
|
|
23
|
+
```console
|
|
24
|
+
$ npm install -g @valancex/cli # or: pnpm add -g @valancex/cli
|
|
25
|
+
$ vlx --version
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
From a clone of this repository instead (after `pnpm install && pnpm build`):
|
|
29
|
+
|
|
30
|
+
```console
|
|
31
|
+
$ cd packages/cli
|
|
32
|
+
$ pnpm link --global # or: npm link (pnpm needs PNPM_HOME on your PATH: `pnpm setup`)
|
|
33
|
+
$ vlx --version
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Either puts a `vlx` shim in your package manager's global bin directory, which has to be on your `PATH` (`npm config get prefix` shows npm's; on Windows the shim is `vlx.cmd` and works in PowerShell and cmd).
|
|
37
|
+
|
|
38
|
+
Inside a project, a global `vlx` does not build with its own copy: **`dev`, `build` and `check` run the `@valancex/cli` installed in that project** (the version its `package.json` names, with the MESH compiler and runtime that go with it), so the project builds the same wherever it is run from. `init` and `--help` need no project and are the global command's own. A project with no local install is run by the global one.
|
|
39
|
+
|
|
40
|
+
## Making an application: `vlx init`
|
|
41
|
+
|
|
42
|
+
```console
|
|
43
|
+
$ vlx init my-app --web # the web target, every other question asked
|
|
44
|
+
$ vlx init # asks for the name and the targets too
|
|
45
|
+
$ vlx init my-app --yes # no questions: every target, every default
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
In a terminal it asks (Ink), skipping whatever a flag already answered:
|
|
49
|
+
|
|
50
|
+
1. **the name** (`my-app`, or a path; it is made in a directory of that name, which must be empty or absent)
|
|
51
|
+
2. **the targets**: a multi-select of every target VALANCE supports (Space toggles, Enter confirms). Today that is the **web**; a target that arrives later is one more line in the list and one more flag
|
|
52
|
+
3. **web settings**: the base path (`/`, or `/docs/`) and where it will be published (optional; adds canonical links and a sitemap)
|
|
53
|
+
4. **the package manager** (npm, pnpm, yarn or bun; the one that ran `vlx` is preselected), whether to **install** the dependencies, and whether to **make a git repository**
|
|
54
|
+
|
|
55
|
+
Then it writes the project (the same files as `examples/docs-app`: a layout, a component with its stated contract, a stylesheet, two pages, typed template imports configured in `tsconfig.json`), installs, runs `git init`, and prints what to type next. The project depends on `@valancex/common` and `effect` (what `src/main.ts` imports) and has `@valancex/cli` as a dev dependency. An install that fails (no network) is a note, not an error: the project is written and the next step says what to run.
|
|
56
|
+
|
|
57
|
+
| Option | |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `--web` | develop for the web (one flag per target) |
|
|
60
|
+
| `-y`, `--yes` | take every default, ask nothing (all targets) |
|
|
61
|
+
| `--pm <npm\|pnpm\|yarn\|bun>` | the package manager |
|
|
62
|
+
| `--no-install`, `--no-git` | skip those steps |
|
|
63
|
+
| `--base <path>`, `--url <address>` | the web target's settings |
|
|
64
|
+
| `--cli <spec>` | what to write for `@valancex/cli` in `package.json` (default: `^<version>`; from a clone of this repository, the path of this package) |
|
|
65
|
+
| `--plain` | no interface: plain text and never a question |
|
|
66
|
+
|
|
67
|
+
Without a terminal (a pipe, CI) it cannot ask, so it needs a name and a target: `vlx init my-app --web`.
|
|
68
|
+
|
|
69
|
+
## Adding to an application: `vlx generate`
|
|
70
|
+
|
|
71
|
+
```console
|
|
72
|
+
$ vlx generate page guide/getting-started # content/guide/getting-started.md, with its front matter
|
|
73
|
+
$ vlx generate component post-card # src/post-card.mprx and src/post-card.ts, registered in src/main.ts
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`g` is short for `generate`. Nothing is overwritten: a name that is taken is refused. A component is made with a stated, empty contract (`props: {}`, `events: {}`) and a template that places its children (`<mesh-slot />`); it is imported in `src/main.ts` and listed in `components` (a `src/main.ts` that is not there is made). If `src/main.ts` is not written in the plain form `defineApp({ … })` the command cannot edit it, leaves it alone, and says what to add. Write `<post-card></post-card>` in a template to use the component: until a template writes its tag it is not part of the program.
|
|
77
|
+
|
|
78
|
+
## What the terminal shows
|
|
79
|
+
|
|
80
|
+
In a terminal `dev`, `build`, `check` and `init` draw an interface with [Ink](https://github.com/vadimdemedes/ink): `check` groups mistakes by file with line, code and message; `build` shows its stages and ends with what it wrote; `dev` shows the address, whether the project is sound, the mistakes that are open (the last good build keeps serving) and the latest changes. Anywhere else (a pipe, `CI`, `TERM=dumb`, `check --json`, or `--plain`) they print the plain text they always have, so scripts and editors read the same output as before.
|
|
81
|
+
|
|
82
|
+
Try it locally: [`docs/application-model/TRY-IT.md`](../../docs/application-model/TRY-IT.md). The worked example is [`examples/docs-app`](../../examples/docs-app): about 200 lines of authored files.
|
|
83
|
+
|
|
84
|
+
## How do I create an application, run it, and change a page?
|
|
85
|
+
|
|
86
|
+
1. A project is a directory with a **`valance.json`**:
|
|
87
|
+
```json
|
|
88
|
+
{ "version": 1, "name": "Notes" }
|
|
89
|
+
```
|
|
90
|
+
and a `package.json` that depends on `@valancex/cli` and has `"dev": "vlx dev"`, `"build": "vlx build"`.
|
|
91
|
+
2. Add pages: **`content/index.md`** is the first page (`# Notes` and some text). Every other `.md` file under `content/` is a page.
|
|
92
|
+
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.
|
|
94
|
+
|
|
95
|
+
That is a complete application. It uses a built-in layout. Everything below is how you take over a part of it.
|
|
96
|
+
|
|
97
|
+
## The project
|
|
98
|
+
|
|
99
|
+
| Path | What it is | Edited by |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `valance.json` | The project: its `name`. | you |
|
|
102
|
+
| `valance.web.json` | The web target: `base` (where it is served, default `/`) and `url` (where it is published; adds canonical links and `sitemap.xml`). Optional. | you |
|
|
103
|
+
| `content/**/*.md` | The pages. A file's path is its address. | you |
|
|
104
|
+
| `public/**` | Files served as they are (`public/logo.svg` is `/logo.svg`). | you |
|
|
105
|
+
| `src/layout.mprx` | The shell around every page. Optional: without a `layout` in `src/main.ts` a default ships. | you |
|
|
106
|
+
| `src/*.mprx` | Your components, each named by its file (`src/callout.mprx` is `<callout>`). Each is imported and registered in `src/main.ts`; a file nothing registers is an error. | you |
|
|
107
|
+
| `src/**/*.css` | Styles. Every stylesheet is loaded. | you |
|
|
108
|
+
| `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
|
+
| `.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 |
|
|
111
|
+
|
|
112
|
+
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
|
+
|
|
114
|
+
## Content
|
|
115
|
+
|
|
116
|
+
```md
|
|
117
|
+
---
|
|
118
|
+
title: Writing pages # else the first # heading
|
|
119
|
+
description: Shown in search results and link previews.
|
|
120
|
+
order: 2 # position among its siblings (default 1000)
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
# Writing pages
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- `content/index.md` → `/`, `content/guide/a.md` → `/guide/a`, `content/guide/index.md` → `/guide`. The first directory is the page's *section* in navigation. Pages sort by section, then `order`, then path.
|
|
127
|
+
- `content/404.md` (optional) is the page shown for an address that is not a page.
|
|
128
|
+
- **Markdown**: `#`–`###` headings, paragraphs, bulleted and numbered lists (one level), fenced code, `>` quotes, an image on its own line, and `**strong**`, `*emphasis*`, `` `code` ``, `[links](…)`. Anything else (raw HTML, tables, nested lists…) is an error naming the file and line, not something quietly mangled.
|
|
129
|
+
- **Links** are checked: `/guide/a`, `/guide/a#heading`, `./a.md`, `../index.md`, `#heading`, `https://…`, `mailto:`. A link that leads nowhere, or to a heading that does not exist, fails the build with `content/x.md:12 link-broken: …`. Addresses are written under the site's `base` for you.
|
|
130
|
+
- **Images**: `` names a file in `public/` and is checked.
|
|
131
|
+
|
|
132
|
+
## Layout and components
|
|
133
|
+
|
|
134
|
+
Templates are MPRX. Conditional and repeated parts are blocks: `{#if cond}…{:else if cond}…{:else}…{/if}` and `{#each items as item (item.id)}…{/each}` (the key is required; a branch or body is one element, several are wrapped in a fragment; `mesh-if`, `mesh-each`, `mesh-switch`, `mesh-case` and `mesh-default` are retired and rejected with the block to write). A layout reads these names (the framework's) and any field of your state:
|
|
135
|
+
|
|
136
|
+
| Name | Is |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `site`, `home` | The application's name; the first page's address |
|
|
139
|
+
| `title`, `description`, `found` | The current page; `found` is false on the not-found page |
|
|
140
|
+
| `sections` | `[{ id, title, items: [{ id, href, label, current }] }]` navigation; `current` is `"page"` or `"false"` |
|
|
141
|
+
| `hasToc`, `toc` | The `##`/`###` headings of this page: `[{ id, href, label, level }]` |
|
|
142
|
+
| `blocks` | The page body. `<blocks items={blocks} />` draws it |
|
|
143
|
+
| `prev`, `next` | `{ href, label }`, empty `href` when there is none |
|
|
144
|
+
|
|
145
|
+
Tags you may use: `shell` (`class`, `theme`), `div`, `header`, `footer`, `nav` (`class`, `label`), `aside`, `section`, `article`, `main` (`id`, `tabindex`), `h1`–`h3` (`id`), `para`, `quote`, `list`, `olist`, `item`, `pre`, `code`, `strong`, `em`, `span`, `image`, `a` (`href`), `link` (`href`, `class`, `current`), `button` (`class`, `pressed`, `on.click`), plus your components and the built-ins (`blocks`, `block`, `heading`, `list-block`, `list-items`, `runs`, `inline`). 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).
|
|
146
|
+
|
|
147
|
+
## The components
|
|
148
|
+
|
|
149
|
+
Your application is the layout and the components you register in `src/main.ts`. Nothing under `src/` is part of it by being there.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// src/main.ts
|
|
153
|
+
import { component, defineApp } from "@valancex/common";
|
|
154
|
+
import layout from "./layout.mprx";
|
|
155
|
+
import themeToggle from "./theme-toggle.js";
|
|
156
|
+
|
|
157
|
+
export default defineApp({
|
|
158
|
+
layout: component(layout),
|
|
159
|
+
components: [themeToggle],
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// src/theme-toggle.ts: the component states its own interface, where it is defined
|
|
165
|
+
import { component } from "@valancex/common";
|
|
166
|
+
import { Schema } from "effect";
|
|
167
|
+
import template from "./theme-toggle.mprx";
|
|
168
|
+
|
|
169
|
+
export default component(template, {
|
|
170
|
+
props: { theme: Schema.Literal("light", "dark") },
|
|
171
|
+
events: { toggle: Schema.Void },
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- `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`.
|
|
176
|
+
- **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`).
|
|
177
|
+
- **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.
|
|
178
|
+
- `override: [component(runs, { props: { spans: … } })]` replaces a built-in (`runs`, `heading`, …) with your template of the same name.
|
|
179
|
+
- `layout` is optional; without it the built-in layout draws the page. Its template must be named `layout.mprx`.
|
|
180
|
+
|
|
181
|
+
### Children and slots
|
|
182
|
+
|
|
183
|
+
A component places the content its caller writes inside it with `<mesh-slot />`, and may name several places:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
<!-- src/card.mprx -->
|
|
187
|
+
<div class="card">
|
|
188
|
+
<mesh-slot name="head" />
|
|
189
|
+
<mesh-slot />
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- where it is used -->
|
|
193
|
+
<card>
|
|
194
|
+
<mesh-fill slot="head"><para class="title">Title</para></mesh-fill>
|
|
195
|
+
<para class="body">The body goes to the unnamed slot.</para>
|
|
196
|
+
</card>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
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).
|
|
200
|
+
|
|
201
|
+
### Typed template imports
|
|
202
|
+
|
|
203
|
+
`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`:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{ "compilerOptions": { "allowArbitraryExtensions": true, "rootDirs": ["src", ".valance/types"] } }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Without them the import is untyped and nothing else changes. The types are regenerated by `vlx sync`, which runs by itself: after `install` (a new project's `prepare` script), after `vlx generate`, and as `dev`, `build` and `check` run; a project's `typecheck` script runs it first. You run it yourself only after changing a template with none of those running (an editor open without `vlx dev`, a template edited in another tool). Not covered: value-level agreement between a template and its component, literal values, exposing derived values, and props or events nothing uses.
|
|
210
|
+
|
|
211
|
+
Nothing is loaded lazily yet (a page ships one program).
|
|
212
|
+
|
|
213
|
+
## State and commands
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// src/main.ts
|
|
217
|
+
import { defineApp, field } from "@valancex/common";
|
|
218
|
+
import { Schema } from "effect";
|
|
219
|
+
|
|
220
|
+
export default defineApp({
|
|
221
|
+
// (and `layout`, if you own one: see above)
|
|
222
|
+
state: { theme: field(Schema.Literal("light", "dark"), "light") },
|
|
223
|
+
commands: (command) => ({
|
|
224
|
+
toggleTheme: command((state) => ({ ...state, theme: state.theme === "light" ? "dark" : "light" })),
|
|
225
|
+
}),
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
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.
|
|
230
|
+
|
|
231
|
+
## What `vlx dev` does when you save
|
|
232
|
+
|
|
233
|
+
| You change | It does |
|
|
234
|
+
|---|---|
|
|
235
|
+
| `content/**` | rebuilds the content, reloads the page |
|
|
236
|
+
| `src/*.mprx`, `src/main.ts` | recompiles the templates, reloads the page |
|
|
237
|
+
| `src/**/*.css` | updates the style in the open page, no reload |
|
|
238
|
+
| `public/**` | reloads |
|
|
239
|
+
| a new or removed stylesheet or `src/main.ts` | rewrites the entry, reloads |
|
|
240
|
+
| `valance.json`, `valance.web.json` | restarts the server (the name and the base are fixed at startup) |
|
|
241
|
+
|
|
242
|
+
A mistake leaves the last good build serving and appears in the terminal and as an overlay in the page, as `file:line:column code: message`. Fixing it reloads the page. There is no hot swap of the compiled templates yet: a reload is the correct, reliable update, and the browser keeps its address.
|
|
243
|
+
|
|
244
|
+
## Editor support
|
|
245
|
+
|
|
246
|
+
`valance` writes `.valance/manifest.json` (what a template may use, your own components included) and `.valance/editor.json` (which file is which component), so an editor can run MESH's language server on your project without configuration. Add the server to the project, in the version of your MESH:
|
|
247
|
+
|
|
248
|
+
```console
|
|
249
|
+
$ pnpm add -D @valancex/mesh-lsp
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The VS Code extension in [`editors/vscode`](../../editors/vscode) (not published yet) 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)).
|
|
253
|
+
|
|
254
|
+
## Diagnostics
|
|
255
|
+
|
|
256
|
+
Every mistake the tools can attribute to a file has one shape: `{ file, line, column, code, message }`. `vlx check --json` prints them for editors and CI; codes are stable (`link-broken`, `md-html`, `front-unknown`, `config-unknown`, `template-unused`, `entry-export`, and the MESH compiler's own for templates).
|
|
257
|
+
|
|
258
|
+
## Limits (this cycle)
|
|
259
|
+
|
|
260
|
+
- 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).
|
|
261
|
+
- The Markdown dialect is a subset; no syntax highlighting, tables, callouts, tabs, search, versions or languages (the verifier has these as stand-ins).
|
|
262
|
+
- The tag table is fixed. An application cannot declare new primitives.
|
|
263
|
+
- Only the web target exists, delivered as a static site. No server rendering command, no `preview` command.
|
|
264
|
+
- Updates are reloads, not hot swaps (except styles).
|
package/bin/vlx.js
ADDED
package/dist/add.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const KINDS: readonly ["page", "component"];
|
|
2
|
+
export type Kind = (typeof KINDS)[number];
|
|
3
|
+
export interface Generated {
|
|
4
|
+
/** Files written, relative to the project. */
|
|
5
|
+
readonly created: ReadonlyArray<string>;
|
|
6
|
+
/** Files changed, relative to the project. */
|
|
7
|
+
readonly changed: ReadonlyArray<string>;
|
|
8
|
+
/** What is left to do by hand, in words. */
|
|
9
|
+
readonly notes: ReadonlyArray<string>;
|
|
10
|
+
}
|
|
11
|
+
export declare class GenerateError extends Error {
|
|
12
|
+
}
|
|
13
|
+
/** `guide/getting-started`, from what was typed: `/guide/getting-started`, `guide/getting-started.md`. */
|
|
14
|
+
export declare const pageName: (given: string) => string;
|
|
15
|
+
export declare const pageProblem: (name: string) => string | undefined;
|
|
16
|
+
export declare const generatePage: (root: string, given: string, title?: string) => Generated;
|
|
17
|
+
export declare const componentProblem: (name: string, root?: string) => string | undefined;
|
|
18
|
+
/** `main.ts` with `ident` added to `components`, or `undefined` when its definition is not written in the plain form this can edit. */
|
|
19
|
+
export declare const register: (text: string, ident: string, name: string) => string | undefined;
|
|
20
|
+
export declare const generateComponent: (root: string, name: string) => Generated;
|
package/dist/add.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// `vlx generate`: the files for a new page or component, and, for a component, its registration in `src/main.ts`. Like `init`, this file is the work and prints nothing; `cli.ts` says what happened.
|
|
2
|
+
// Nothing here overwrites a file: a name that is taken is refused.
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { BUILT_IN, ROOT, SOURCE_DIR } from "./compile.js";
|
|
6
|
+
import { ENTRY_FILE } from "./project.js";
|
|
7
|
+
import { primitives } from "@valancex/common/internal";
|
|
8
|
+
export const KINDS = ["page", "component"];
|
|
9
|
+
export class GenerateError extends Error {
|
|
10
|
+
}
|
|
11
|
+
const words = (name) => name.replace(/[-_]+/g, " ").replace(/^./, (first) => first.toUpperCase());
|
|
12
|
+
const camel = (name) => name.replace(/-([a-z0-9])/g, (_, next) => next.toUpperCase());
|
|
13
|
+
const write = (root, file, text) => {
|
|
14
|
+
mkdirSync(dirname(join(root, file)), { recursive: true });
|
|
15
|
+
writeFileSync(join(root, file), text);
|
|
16
|
+
};
|
|
17
|
+
// ---- a page ------------------------------------------------------------------------------------------------------------------------------------------------
|
|
18
|
+
/** `guide/getting-started`, from what was typed: `/guide/getting-started`, `guide/getting-started.md`. */
|
|
19
|
+
export const pageName = (given) => given
|
|
20
|
+
.trim()
|
|
21
|
+
.replace(/^\/+|\/+$/g, "")
|
|
22
|
+
.replace(/\.md$/, "");
|
|
23
|
+
export const pageProblem = (name) => name === ""
|
|
24
|
+
? "the page needs a path: vlx generate page guide/getting-started"
|
|
25
|
+
: name.split("/").every((segment) => /^[A-Za-z0-9][\w-]*$/.test(segment))
|
|
26
|
+
? undefined
|
|
27
|
+
: `"${name}" cannot be a page's address: use words joined by - or _, and / between sections (guide/getting-started)`;
|
|
28
|
+
export const generatePage = (root, given, title) => {
|
|
29
|
+
const name = pageName(given);
|
|
30
|
+
const problem = pageProblem(name);
|
|
31
|
+
if (problem !== undefined) {
|
|
32
|
+
throw new GenerateError(problem);
|
|
33
|
+
}
|
|
34
|
+
const file = `content/${name}.md`;
|
|
35
|
+
if (existsSync(join(root, file))) {
|
|
36
|
+
throw new GenerateError(`${file} already exists`);
|
|
37
|
+
}
|
|
38
|
+
const heading = title ?? words(name.split("/").pop());
|
|
39
|
+
write(root, file, `---\ntitle: ${heading}\n---\n\n# ${heading}\n\nWrite here.\n`);
|
|
40
|
+
return { created: [file], changed: [], notes: [] };
|
|
41
|
+
};
|
|
42
|
+
// ---- a component -------------------------------------------------------------------------------------------------------------------------------------------
|
|
43
|
+
const builtIns = () => new Set(readdirSync(BUILT_IN)
|
|
44
|
+
.filter((file) => file.endsWith(".mprx"))
|
|
45
|
+
.map((file) => file.slice(0, -".mprx".length)));
|
|
46
|
+
export const componentProblem = (name, root) => {
|
|
47
|
+
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name)) {
|
|
48
|
+
return `"${name}" cannot be a tag: use lowercase words joined by - (callout, post-card)`;
|
|
49
|
+
}
|
|
50
|
+
if (name === ROOT) {
|
|
51
|
+
return `<${ROOT}> is the page's shell; it is made by vlx init, and changed in ${SOURCE_DIR}/${ROOT}.mprx`;
|
|
52
|
+
}
|
|
53
|
+
if (name in primitives) {
|
|
54
|
+
return `<${name}> is already a tag the framework draws as an element; name the component something else`;
|
|
55
|
+
}
|
|
56
|
+
if (builtIns().has(name)) {
|
|
57
|
+
return `<${name}> is a built-in component; to replace it, write src/${name}.mprx and list it in \`override\` in ${ENTRY_FILE}`;
|
|
58
|
+
}
|
|
59
|
+
return root !== undefined && (existsSync(join(root, SOURCE_DIR, `${name}.mprx`)) || existsSync(join(root, SOURCE_DIR, `${name}.ts`)))
|
|
60
|
+
? `src/${name}.mprx or src/${name}.ts already exists`
|
|
61
|
+
: undefined;
|
|
62
|
+
};
|
|
63
|
+
const TEMPLATE = (name) => `<div class="${name}">\n <mesh-slot />\n</div>\n`;
|
|
64
|
+
const DEFINITION = (name) => `import { component } from "@valancex/common";
|
|
65
|
+
import template from "./${name}.mprx";
|
|
66
|
+
|
|
67
|
+
// The interface of <${name}>, stated once, where it is defined: the props its template reads and the events it raises, each with its type (an Effect Schema, from "effect"; Schema.Void is no
|
|
68
|
+
// payload). Delete both to let the interface be inferred from the template instead.
|
|
69
|
+
export default component(template, {
|
|
70
|
+
props: {},
|
|
71
|
+
events: {},
|
|
72
|
+
});
|
|
73
|
+
`;
|
|
74
|
+
const NEW_MAIN = (ident, name) => `import { defineApp } from "@valancex/common";
|
|
75
|
+
import ${ident} from "./${name}.js";
|
|
76
|
+
|
|
77
|
+
export default defineApp({
|
|
78
|
+
components: [${ident}],
|
|
79
|
+
});
|
|
80
|
+
`;
|
|
81
|
+
/** The `import` line, after the last import already there. */
|
|
82
|
+
const withImport = (text, line) => {
|
|
83
|
+
const imports = [...text.matchAll(/^import\b[^;]*;[^\S\n]*$/gm)];
|
|
84
|
+
const last = imports[imports.length - 1];
|
|
85
|
+
return last === undefined ? `${line}\n${text}` : `${text.slice(0, last.index + last[0].length)}\n${line}${text.slice(last.index + last[0].length)}`;
|
|
86
|
+
};
|
|
87
|
+
/** `main.ts` with `ident` added to `components`, or `undefined` when its definition is not written in the plain form this can edit. */
|
|
88
|
+
export const register = (text, ident, name) => {
|
|
89
|
+
const line = `import ${ident} from "./${name}.js";`;
|
|
90
|
+
const list = /components\s*:\s*\[/.exec(text);
|
|
91
|
+
if (list !== undefined && list !== null) {
|
|
92
|
+
const open = list.index + list[0].length;
|
|
93
|
+
const close = text.indexOf("]", open);
|
|
94
|
+
if (close < 0 || /[[\]]/.test(text.slice(open, close))) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const inside = text.slice(open, close);
|
|
98
|
+
const items = inside.trim().replace(/,$/, "");
|
|
99
|
+
const multiline = inside.includes("\n");
|
|
100
|
+
const next = multiline
|
|
101
|
+
? `${inside.replace(/\s*$/, "").replace(/,?$/, ",")}\n${/^\s*/.exec(inside.split("\n")[1] ?? " ")[0]}${ident},\n${/[^\n]*$/.exec(inside)[0]}`
|
|
102
|
+
: items === ""
|
|
103
|
+
? ident
|
|
104
|
+
: `${items}, ${ident}`;
|
|
105
|
+
return withImport(`${text.slice(0, open)}${next}${text.slice(close)}`, line);
|
|
106
|
+
}
|
|
107
|
+
const define = /defineApp\(\s*\{/.exec(text);
|
|
108
|
+
if (define === null) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const at = define.index + define[0].length;
|
|
112
|
+
return withImport(`${text.slice(0, at)}\n components: [${ident}],${text.slice(at)}`, line);
|
|
113
|
+
};
|
|
114
|
+
export const generateComponent = (root, name) => {
|
|
115
|
+
const problem = componentProblem(name, root);
|
|
116
|
+
if (problem !== undefined) {
|
|
117
|
+
throw new GenerateError(problem);
|
|
118
|
+
}
|
|
119
|
+
const ident = camel(name);
|
|
120
|
+
const created = [`${SOURCE_DIR}/${name}.mprx`, `${SOURCE_DIR}/${name}.ts`];
|
|
121
|
+
const entry = join(root, ENTRY_FILE);
|
|
122
|
+
const notes = [];
|
|
123
|
+
const changed = [];
|
|
124
|
+
write(root, created[0], TEMPLATE(name));
|
|
125
|
+
write(root, created[1], DEFINITION(name));
|
|
126
|
+
if (!existsSync(entry)) {
|
|
127
|
+
write(root, ENTRY_FILE, NEW_MAIN(ident, name));
|
|
128
|
+
created.push(ENTRY_FILE);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const text = readFileSync(entry, "utf8");
|
|
132
|
+
const edited = new RegExp(`\\b${ident}\\b`).test(text) ? undefined : register(text, ident, name);
|
|
133
|
+
if (edited === undefined) {
|
|
134
|
+
notes.push(`add it to ${ENTRY_FILE} yourself: import ${ident} from "./${name}.js"; and list ${ident} in the \`components\` of defineApp`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
writeFileSync(entry, edited);
|
|
138
|
+
changed.push(ENTRY_FILE);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
notes.push(`use it by writing <${name}></${name}> in a template`);
|
|
142
|
+
return { created, changed, notes };
|
|
143
|
+
};
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PackageManager } from "./init.js";
|
|
2
|
+
import type { TargetId } from "./targets.js";
|
|
3
|
+
export interface InitArgs {
|
|
4
|
+
readonly name?: string;
|
|
5
|
+
/** The targets named by flags (`--web`); empty means "ask". */
|
|
6
|
+
readonly targets: ReadonlyArray<TargetId>;
|
|
7
|
+
/** Take the default for every question that has one, and ask none. */
|
|
8
|
+
readonly yes: boolean;
|
|
9
|
+
/** Never draw the interface, even in a terminal. */
|
|
10
|
+
readonly plain: boolean;
|
|
11
|
+
readonly packageManager?: PackageManager;
|
|
12
|
+
readonly install?: boolean;
|
|
13
|
+
readonly git?: boolean;
|
|
14
|
+
readonly base?: string;
|
|
15
|
+
readonly url?: string;
|
|
16
|
+
readonly cli?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const INIT_HELP: string;
|
|
19
|
+
/** `args` after `init`. Returns the reading, or the sentence that says what is wrong with it. */
|
|
20
|
+
export declare const parseInit: (args: ReadonlyArray<string>) => InitArgs | string;
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { PACKAGE_MANAGERS } from "./init.js";
|
|
2
|
+
import { isTarget, TARGETS } from "./targets.js";
|
|
3
|
+
export const INIT_HELP = `vlx init [app-name] [options]
|
|
4
|
+
|
|
5
|
+
Make a new application. Without options it asks what it needs to know.
|
|
6
|
+
|
|
7
|
+
--web develop for the web (a flag per target: ${TARGETS.map((each) => `--${each.id}`).join(", ")})
|
|
8
|
+
-y, --yes answer every question with its default (all targets)
|
|
9
|
+
--pm <npm|pnpm|yarn|bun> the package manager (default: the one that ran this, else npm)
|
|
10
|
+
--no-install do not install the dependencies
|
|
11
|
+
--no-git do not run git init
|
|
12
|
+
--base <path> the web site's base path, e.g. /docs/ (default /)
|
|
13
|
+
--url <address> where the web site is published, e.g. https://example.com
|
|
14
|
+
--cli <spec> what to write for @valancex/cli in package.json
|
|
15
|
+
--plain no interface: plain text, never a prompt
|
|
16
|
+
`;
|
|
17
|
+
/** `args` after `init`. Returns the reading, or the sentence that says what is wrong with it. */
|
|
18
|
+
export const parseInit = (args) => {
|
|
19
|
+
const targets = [];
|
|
20
|
+
const positional = [];
|
|
21
|
+
const out = { yes: false, plain: false };
|
|
22
|
+
for (let at = 0; at < args.length; at += 1) {
|
|
23
|
+
const arg = args[at];
|
|
24
|
+
const value = () => {
|
|
25
|
+
at += 1;
|
|
26
|
+
return args[at];
|
|
27
|
+
};
|
|
28
|
+
if (arg === "-y" || arg === "--yes") {
|
|
29
|
+
out.yes = true;
|
|
30
|
+
}
|
|
31
|
+
else if (arg === "--plain") {
|
|
32
|
+
out.plain = true;
|
|
33
|
+
}
|
|
34
|
+
else if (arg === "--no-install") {
|
|
35
|
+
out.install = false;
|
|
36
|
+
}
|
|
37
|
+
else if (arg === "--install") {
|
|
38
|
+
out.install = true;
|
|
39
|
+
}
|
|
40
|
+
else if (arg === "--no-git") {
|
|
41
|
+
out.git = false;
|
|
42
|
+
}
|
|
43
|
+
else if (arg === "--git") {
|
|
44
|
+
out.git = true;
|
|
45
|
+
}
|
|
46
|
+
else if (arg === "--pm") {
|
|
47
|
+
const manager = value();
|
|
48
|
+
const found = PACKAGE_MANAGERS.find((each) => each === manager);
|
|
49
|
+
if (found === undefined) {
|
|
50
|
+
return `--pm needs one of ${PACKAGE_MANAGERS.join(", ")}`;
|
|
51
|
+
}
|
|
52
|
+
out.packageManager = found;
|
|
53
|
+
}
|
|
54
|
+
else if (arg === "--base" || arg === "--url" || arg === "--cli") {
|
|
55
|
+
const given = value();
|
|
56
|
+
if (given === undefined || given.startsWith("--")) {
|
|
57
|
+
return `${arg} needs a value`;
|
|
58
|
+
}
|
|
59
|
+
out[arg.slice(2)] = given;
|
|
60
|
+
}
|
|
61
|
+
else if (arg.startsWith("--") && isTarget(arg.slice(2))) {
|
|
62
|
+
const id = arg.slice(2);
|
|
63
|
+
if (!targets.includes(id)) {
|
|
64
|
+
targets.push(id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (arg.startsWith("-")) {
|
|
68
|
+
return `unknown option "${arg}"`;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
positional.push(arg);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (positional.length > 1) {
|
|
75
|
+
return `one name, not ${positional.length}: "${positional.join(" ")}" (quote a name with spaces)`;
|
|
76
|
+
}
|
|
77
|
+
return { yes: false, plain: false, ...out, targets, ...(positional[0] === undefined ? {} : { name: positional[0] }) };
|
|
78
|
+
};
|
package/dist/author.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ViteDevServer } from "vite";
|
|
2
|
+
import type { DefinedApp } from "@valancex/common";
|
|
3
|
+
/** The author's definition, or nothing when there is no `src/main.ts`. A mistake in the file is a diagnostic about the file. */
|
|
4
|
+
export declare const loadAuthor: (server: ViteDevServer, root: string) => Promise<DefinedApp | undefined>;
|
package/dist/author.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { checkDefinition } from "@valancex/common/internal";
|
|
2
|
+
import { AppError, diagnostic } from "./diagnostics.js";
|
|
3
|
+
import { APP } from "@valancex/common";
|
|
4
|
+
import { hasEntry } from "./emit.js";
|
|
5
|
+
import { ENTRY_FILE } from "./project.js";
|
|
6
|
+
/** The author's definition, or nothing when there is no `src/main.ts`. A mistake in the file is a diagnostic about the file. */
|
|
7
|
+
export const loadAuthor = async (server, root) => {
|
|
8
|
+
if (!hasEntry(root)) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
let loaded;
|
|
12
|
+
try {
|
|
13
|
+
loaded = await server.ssrLoadModule(`/${ENTRY_FILE}`);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
const at = error.loc;
|
|
17
|
+
throw new AppError([diagnostic(ENTRY_FILE, at?.line ?? 0, "entry-load", error instanceof Error ? error.message.split("\n")[0] : String(error), at?.column ?? 0)]);
|
|
18
|
+
}
|
|
19
|
+
const definition = loaded["default"];
|
|
20
|
+
if (definition === undefined || definition[APP] !== true) {
|
|
21
|
+
throw new AppError([diagnostic(ENTRY_FILE, 1, "entry-export", `${ENTRY_FILE} must export default defineApp({ … }) from "@valancex/common"`)]);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
checkDefinition(definition);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new AppError([diagnostic(ENTRY_FILE, 1, "entry-definition", error instanceof Error ? error.message.replace(/^src\/main\.ts: /, "") : String(error))]);
|
|
28
|
+
}
|
|
29
|
+
return definition;
|
|
30
|
+
};
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface Built {
|
|
2
|
+
readonly dir: string;
|
|
3
|
+
readonly files: ReadonlyArray<string>;
|
|
4
|
+
readonly pages: ReadonlyArray<string>;
|
|
5
|
+
}
|
|
6
|
+
/** The file a pathname is written to in the site: `/` → `index.html`, `/guide/x` → `guide/x/index.html`, relative to the base. */
|
|
7
|
+
export declare const fileOf: (pathname: string, base: string) => string;
|
|
8
|
+
/** The stages of a build, in order, as `onStage` reports them. */
|
|
9
|
+
export type BuildStage = "configuration" | "content" | "templates" | "bundle" | "pages";
|
|
10
|
+
export interface BuildOptions {
|
|
11
|
+
/** Called as each stage begins. */
|
|
12
|
+
readonly onStage?: (stage: BuildStage) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare const build: (root: string, options?: BuildOptions) => Promise<Built>;
|