volaro 0.0.2 → 0.1.0-alpha.3
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 +114 -22
- package/bin/vl.js +466 -28
- package/compiler/SOURCE_INFO.json +6 -0
- package/compiler/SOURCE_REV +1 -0
- package/compiler/validator/vlcheck/__init__.py +10 -0
- package/compiler/validator/vlcheck/__main__.py +128 -0
- package/compiler/validator/vlcheck/ast_nodes.py +397 -0
- package/compiler/validator/vlcheck/checks.py +1227 -0
- package/compiler/validator/vlcheck/diagnostics.py +88 -0
- package/compiler/validator/vlcheck/lexer.py +343 -0
- package/compiler/validator/vlcheck/parser.py +1638 -0
- package/compiler/validator/vlcheck/project_config.py +97 -0
- package/compiler/validator/vlcheck/resolve.py +849 -0
- package/compiler/validator/vlcheck/test_ids.py +98 -0
- package/compiler/vlbuild/styling/README.md +39 -0
- package/compiler/vlbuild/styling/build-css.mjs +103 -0
- package/compiler/vlbuild/styling/package-lock.json +1254 -0
- package/compiler/vlbuild/styling/package.json +15 -0
- package/compiler/vlbuild/styling/test-build-css.mjs +69 -0
- package/compiler/vlbuild/vlbuild/__init__.py +16 -0
- package/compiler/vlbuild/vlbuild/__main__.py +246 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.css +165 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.js +1291 -0
- package/compiler/vlbuild/vlbuild/emit.py +2023 -0
- package/compiler/vlbuild/vlbuild/server_emit.py +1551 -0
- package/compiler/vlbuild/vlbuild/static_assets.py +83 -0
- package/compiler/vlbuild/vlbuild/style_config.py +347 -0
- package/compiler/vlbuild/vlbuild/styling.py +39 -0
- package/examples/station.vl +2 -2
- package/language/crib.md +131 -10
- package/language/spec.md +209 -7
- package/language/supported.md +185 -0
- package/lib/env.js +107 -0
- package/package.json +19 -2
- package/scripts/record-provenance.mjs +51 -0
- package/scripts/selftest.mjs +54 -0
- package/scripts/sync-compiler.sh +52 -0
package/README.md
CHANGED
|
@@ -1,46 +1,138 @@
|
|
|
1
1
|
# Volaro
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Limited alpha, published as `0.1.0-alpha.1` under the `alpha` dist-tag** —
|
|
4
|
+
`npm install volaro@alpha`. Not `latest` (that still resolves to an earlier
|
|
5
|
+
`0.0.x` placeholder) — always specify `@alpha` or the exact version.
|
|
4
6
|
|
|
5
7
|
Volaro is an experimental application language intended for AI authoring and
|
|
6
|
-
human review. This package
|
|
7
|
-
|
|
8
|
+
human review. This package ships the language reference **and a working
|
|
9
|
+
compiler for a supported subset**: `volaro check`, `volaro build`,
|
|
10
|
+
`volaro dev`.
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
The command is **`volaro`** (canonical). **`vl` is a compatibility alias** for
|
|
13
|
+
the same binary. Run it via your project's `npm run …` scripts or
|
|
14
|
+
`npx --no-install volaro …` inside the project — a bare `npx volaro` / `npx vl`
|
|
15
|
+
from elsewhere may resolve an unrelated package.
|
|
16
|
+
|
|
17
|
+
## What you need
|
|
18
|
+
|
|
19
|
+
- **Python 3.10+** — runs the compiler, which is bundled inside this package
|
|
20
|
+
(`compiler/`). `volaro` finds `python3` on your PATH; set `VOLARO_PYTHON` to
|
|
21
|
+
choose a different interpreter. A missing or too-old Python is reported with
|
|
22
|
+
the fix, not a stack trace.
|
|
23
|
+
- **Node.js** — runs the `volaro` command and the dev server. The minimum
|
|
24
|
+
depends on what you build:
|
|
25
|
+
|
|
26
|
+
| App shape | Node | Verified in CI |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Minimal single-page (view only): `check` / `build` / static `dev` | **≥ 18** | 18, 20, 22, 24 (packaging matrix) |
|
|
29
|
+
| Full-stack: a `service` backed by a `model` → generated `server.js` (`node:sqlite`) | **≥ 22.5** | 22, 24 (packaging matrix, full-stack `dev`) |
|
|
30
|
+
| Password-auth: the generated server's built-in Argon2 | **≥ 24.7** | 24 only (`./verify.sh` browser journey) |
|
|
31
|
+
|
|
32
|
+
A green packaging matrix establishes the minimal-app row across 18–24; it
|
|
33
|
+
does **not** by itself establish the full-stack or auth rows on 18/20.
|
|
34
|
+
|
|
35
|
+
### Operating systems
|
|
36
|
+
|
|
37
|
+
| OS | Status |
|
|
38
|
+
|---|---|
|
|
39
|
+
| **Linux** | **Verified** — CI (`verify` + `packaging` matrix) and an isolated-install test |
|
|
40
|
+
| **macOS** | **Unverified** — never run. No known blocker, but no evidence. |
|
|
41
|
+
| **Windows** | **Unverified** — the `npm.cmd` / `PYTHONPATH` handling is present but untested; the packaging test scripts are `bash`. |
|
|
42
|
+
|
|
43
|
+
Do not treat this as cross-platform support. Only Linux has been tested.
|
|
44
|
+
|
|
45
|
+
No global install, no repository checkout, and no network access are needed
|
|
46
|
+
once the package is installed.
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
volaro check <path>... # validate .vl source (full resolver), exit non-zero on error
|
|
52
|
+
volaro build app.vl # transpile to ./build (index.html + app.js + runtime)
|
|
53
|
+
volaro build app.vl -o dist --release
|
|
54
|
+
volaro dev # build ./app.vl, serve http://127.0.0.1:5173, rebuild on change
|
|
55
|
+
volaro crib # the authoring reference (write from this)
|
|
56
|
+
volaro supported # what THIS version accepts (the shipped-feature guide)
|
|
57
|
+
volaro spec # the full language design (a superset of this build)
|
|
58
|
+
volaro example sensors # a worked example (also: station)
|
|
59
|
+
volaro version | volaro help
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`volaro dev` serves a static bundle for a single-page app. If the app compiles
|
|
63
|
+
to a full-stack server (`server.js`), it runs that instead and needs
|
|
64
|
+
Node 22.5+ for `node:sqlite`.
|
|
65
|
+
|
|
66
|
+
## Scaffold a project
|
|
10
67
|
|
|
11
68
|
```bash
|
|
12
|
-
|
|
13
|
-
npx volaro spec
|
|
14
|
-
npx volaro example sensors
|
|
15
|
-
npx volaro example station
|
|
69
|
+
npm create volaro@alpha my-app # the separate create-volaro package
|
|
16
70
|
```
|
|
17
71
|
|
|
18
|
-
|
|
72
|
+
(`@alpha` matters — `npm create volaro` with no tag resolves `latest`, which
|
|
73
|
+
is still an earlier `0.0.x` placeholder, not this build.)
|
|
74
|
+
|
|
75
|
+
It writes one single-page starter, a `volaro.json`, and `package.json` scripts
|
|
76
|
+
(`dev` / `build` / `check`) that call `volaro`.
|
|
19
77
|
|
|
20
78
|
## Scope and evidence
|
|
21
79
|
|
|
22
|
-
The project-authored twelve-feature corpus records roughly 3.00× source
|
|
23
|
-
against selected baselines
|
|
80
|
+
The project-authored twelve-feature corpus records roughly 3.00× source
|
|
81
|
+
density against selected baselines — not a general productivity result.
|
|
24
82
|
Security and accessibility checks cover a tested prototype subset, not a
|
|
25
|
-
universal guarantee. Independent human-readability validation
|
|
83
|
+
universal guarantee. Independent human-readability validation is outstanding.
|
|
26
84
|
|
|
27
|
-
|
|
85
|
+
**MVP scope is single-page.** Routing, nested layouts and multi-page starters
|
|
86
|
+
are later work; `volaro build` / `volaro dev` target one entry file.
|
|
28
87
|
|
|
29
|
-
|
|
30
|
-
The repository has a working compiler for a supported subset, but distributing
|
|
31
|
-
that toolchain is separate work. Styling and project creation are not supplied
|
|
32
|
-
by this reference CLI.
|
|
88
|
+
## Not included
|
|
33
89
|
|
|
34
|
-
|
|
35
|
-
|
|
90
|
+
Migrations, deployable production output, a specified stdlib surface, and
|
|
91
|
+
escape hatches to npm packages (email, payments, storage) are not here. Styled
|
|
92
|
+
builds (`theme` / `recipe`) additionally need a one-time
|
|
93
|
+
`npm ci` inside `compiler/vlbuild/styling`; the single-page starter does not
|
|
94
|
+
use them.
|
|
36
95
|
|
|
37
|
-
|
|
96
|
+
**Form controls:** the element set is `text` inputs plus `button` / `link` —
|
|
97
|
+
there is no `checkbox`, `radio`, `select`, `textarea`, or `disabled` attribute.
|
|
98
|
+
Model a toggle as a `button` reading a `state` bool. `variant:` is a literal
|
|
99
|
+
style name, not a computed expression. `if` used as an expression is binary
|
|
100
|
+
(`if c a else b`); `match` and block `if` are statements only. A module-level
|
|
101
|
+
`fn` is not available to a view. **`volaro supported` and `volaro crib` are
|
|
102
|
+
authoritative** for what this build accepts — `volaro spec` describes the
|
|
103
|
+
wider language design, not this subset.
|
|
38
104
|
|
|
39
105
|
## Local testing
|
|
40
106
|
|
|
41
|
-
|
|
42
|
-
|
|
107
|
+
```bash
|
|
108
|
+
npm pack # runs prepack -> vendors compiler/ -> volaro-*.tgz
|
|
109
|
+
npm test # scripts/selftest.mjs: check + build in a temp dir
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The legacy `volara` npm package is unrelated to this release.
|
|
43
113
|
|
|
44
114
|
## License
|
|
45
115
|
|
|
46
116
|
MIT
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
## Local assets
|
|
120
|
+
|
|
121
|
+
Put public files in `assets/` beside your entry `.vl` file and use
|
|
122
|
+
`img src:"assets/logo.svg" alt:"Logo"`. Build and dev copy them to the browser
|
|
123
|
+
bundle. Dev watches changes; refresh the browser after a rebuild. Missing
|
|
124
|
+
literal asset references fail the build. Output `assets/` is compiler-owned.
|
|
125
|
+
|
|
126
|
+
## Package provenance
|
|
127
|
+
|
|
128
|
+
`compiler/SOURCE_REV` records the source commit. `compiler/SOURCE_INFO.json`
|
|
129
|
+
adds its origin, Git dirty status where available, and a SHA-256 digest of
|
|
130
|
+
compiler and CLI/documentation content. A package version alone does not
|
|
131
|
+
identify a local candidate; preserve the tarball hash as well.
|
|
132
|
+
|
|
133
|
+
Packaging a Git export uses `.volaro-source-rev`, expanded by `git archive`.
|
|
134
|
+
For other exported source, set `VOLARO_SOURCE_REV` to the full source commit
|
|
135
|
+
hash before `npm pack`. Missing/invalid revisions fail packaging; an explicit
|
|
136
|
+
revision cannot override a different checkout HEAD. Export metadata identifies
|
|
137
|
+
the base revision, not an assurance that someone has not modified the export;
|
|
138
|
+
the content hash distinguishes modified candidates. Nothing is published by packing.
|
package/bin/vl.js
CHANGED
|
@@ -1,34 +1,472 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// The `volaro` command (alias: `vl`): a thin Node wrapper around the Volaro compiler, which
|
|
3
|
+
// ships as Python sources under ../compiler and is invoked as a subprocess.
|
|
4
|
+
//
|
|
5
|
+
// volaro check <path>... validate .vl source (lexer / parser / resolver)
|
|
6
|
+
// volaro build <file.vl> transpile to a runnable bundle
|
|
7
|
+
// volaro dev [file.vl] build, serve on localhost, rebuild on change
|
|
8
|
+
// volaro crib | volaro spec print the language reference
|
|
9
|
+
// volaro example <name> print a worked example
|
|
10
|
+
// volaro version | volaro help
|
|
11
|
+
//
|
|
12
|
+
// Python is located and version-checked up front with an actionable message
|
|
13
|
+
// (see ../lib/env.js). Nothing here depends on the Volaro repository layout.
|
|
14
|
+
|
|
15
|
+
import { readFileSync, existsSync, statSync, lstatSync, readdirSync } from "node:fs";
|
|
3
16
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
17
|
+
import { dirname, join, resolve as resolvePath, extname, relative, sep, delimiter } from "node:path";
|
|
18
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
19
|
+
import { createServer } from "node:http";
|
|
20
|
+
import { resolvePython, nodeCanRunServer, VolaroEnvError, MIN_NODE_FOR_SERVER } from "../lib/env.js";
|
|
5
21
|
|
|
6
22
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
23
|
const read = (p) => readFileSync(join(root, p), "utf8");
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
const pkg = JSON.parse(read("package.json"));
|
|
25
|
+
|
|
26
|
+
// The canonical command is `volaro`; `vl` is a compatibility alias. Messages
|
|
27
|
+
// use whichever name this process was invoked as, defaulting to `volaro`.
|
|
28
|
+
const CLI = (() => {
|
|
29
|
+
const b = (process.argv[1] || "").split(/[\\/]/).pop() || "";
|
|
30
|
+
return b === "vl" || b === "vl.js" ? "vl" : "volaro";
|
|
31
|
+
})();
|
|
32
|
+
|
|
33
|
+
const COMPILER = join(root, "compiler");
|
|
34
|
+
const VALIDATOR_PP = join(COMPILER, "validator");
|
|
35
|
+
const VLBUILD_PP = join(COMPILER, "vlbuild");
|
|
36
|
+
|
|
37
|
+
function fail(msg, code = 1) {
|
|
38
|
+
process.stderr.write((msg.endsWith("\n") ? msg : msg + "\n"));
|
|
39
|
+
process.exit(code);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function haveCompiler() {
|
|
43
|
+
return existsSync(join(VALIDATOR_PP, "vlcheck", "__main__.py")) &&
|
|
44
|
+
existsSync(join(VLBUILD_PP, "vlbuild", "__main__.py"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function python() {
|
|
48
|
+
if (!haveCompiler()) {
|
|
49
|
+
fail(
|
|
50
|
+
`${CLI}: the bundled compiler is missing from this package.\n` +
|
|
51
|
+
"If you are working inside the Volaro repo, run:\n" +
|
|
52
|
+
" bash packaging/volaro/scripts/sync-compiler.sh\n" +
|
|
53
|
+
"An installed copy from `npm pack` / a published release always includes it.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return resolvePython();
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err instanceof VolaroEnvError) fail(`${CLI}: ` + err.message);
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Shared Python-invocation setup for both the streaming and capturing paths.
|
|
65
|
+
function pyInvocation(pp, moduleName, moduleArgs) {
|
|
66
|
+
const py = python();
|
|
67
|
+
const env = {
|
|
68
|
+
...process.env,
|
|
69
|
+
PYTHONPATH: [pp, VALIDATOR_PP, process.env.PYTHONPATH].filter(Boolean).join(delimiter),
|
|
70
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
71
|
+
};
|
|
72
|
+
return { cmd: py.cmd, argv: ["-B", "-m", moduleName, ...moduleArgs], env };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Run a Python module, streaming its output, and exit with its status.
|
|
76
|
+
function runPython(pp, moduleName, moduleArgs) {
|
|
77
|
+
const { cmd, argv, env } = pyInvocation(pp, moduleName, moduleArgs);
|
|
78
|
+
const child = spawn(cmd, argv, { stdio: "inherit", env });
|
|
79
|
+
child.on("error", (e) => fail(`${CLI}: failed to start Python (${cmd}): ${e.message}`));
|
|
80
|
+
child.on("exit", (code, signal) => process.exit(signal ? 1 : code ?? 0));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function pyCapture(pp, moduleName, moduleArgs) {
|
|
84
|
+
const { cmd, argv, env } = pyInvocation(pp, moduleName, moduleArgs);
|
|
85
|
+
return spawnSync(cmd, argv, { encoding: "utf8", env });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- subcommands -----------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
function cmdCheck(args) {
|
|
91
|
+
if (args.length === 0) fail("usage: volaro check <file-or-dir>... [--release] [--config PATH]");
|
|
92
|
+
// Always resolve: a bare structural pass is weaker than the real check.
|
|
93
|
+
runPython(VALIDATOR_PP, "vlcheck", ["--resolve", ...args]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseBuildArgs(args) {
|
|
97
|
+
const out = { source: null, o: null, data: null, release: false, config: null };
|
|
98
|
+
for (let i = 0; i < args.length; i++) {
|
|
99
|
+
const a = args[i];
|
|
100
|
+
if (a === "-o" || a === "--out") out.o = args[++i];
|
|
101
|
+
else if (a === "--data") out.data = args[++i];
|
|
102
|
+
else if (a === "--config") out.config = args[++i];
|
|
103
|
+
else if (a === "--release") out.release = true;
|
|
104
|
+
else if (a.startsWith("-")) fail(`volaro build: unknown option ${a}`);
|
|
105
|
+
else if (!out.source) out.source = a;
|
|
106
|
+
else fail(`volaro build: unexpected argument ${a}`);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveEntry(explicit) {
|
|
112
|
+
if (explicit) {
|
|
113
|
+
if (!existsSync(explicit)) fail(`${CLI}: no such file: ${explicit}`);
|
|
114
|
+
return resolvePath(explicit);
|
|
115
|
+
}
|
|
116
|
+
for (const c of ["app.vl", join("src", "app.vl")]) {
|
|
117
|
+
if (existsSync(c)) return resolvePath(c);
|
|
118
|
+
}
|
|
119
|
+
fail(
|
|
120
|
+
"volaro: no entry file given and no app.vl found in this directory.\n" +
|
|
121
|
+
"Pass one explicitly: volaro build path/to/app.vl",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function cmdBuild(args) {
|
|
126
|
+
const b = parseBuildArgs(args);
|
|
127
|
+
const source = resolveEntry(b.source);
|
|
128
|
+
const outDir = b.o ? resolvePath(b.o) : resolvePath("build");
|
|
129
|
+
const pyArgs = [source, "-o", outDir];
|
|
130
|
+
if (b.data) pyArgs.push("--data", resolvePath(b.data));
|
|
131
|
+
if (b.config) pyArgs.push("--config", resolvePath(b.config));
|
|
132
|
+
if (b.release) pyArgs.push("--release");
|
|
133
|
+
runPython(VLBUILD_PP, "vlbuild", pyArgs);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Synchronous single build for `volaro dev`; returns { ok, server } or exits on env error.
|
|
137
|
+
function buildOnce(source, outDir, extra = []) {
|
|
138
|
+
const r = pyCapture(VLBUILD_PP, "vlbuild", [source, "-o", outDir, ...extra]);
|
|
139
|
+
if (r.error) fail(`${CLI}: failed to start Python: ${r.error.message}`);
|
|
140
|
+
if (r.stdout) process.stdout.write(r.stdout);
|
|
141
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
142
|
+
return { ok: r.status === 0, server: existsSync(join(outDir, "server.js")) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const MIME = {
|
|
146
|
+
".html": "text/html; charset=utf-8",
|
|
147
|
+
".js": "text/javascript; charset=utf-8",
|
|
148
|
+
".css": "text/css; charset=utf-8",
|
|
149
|
+
".json": "application/json; charset=utf-8",
|
|
150
|
+
".svg": "image/svg+xml",
|
|
151
|
+
".png": "image/png",
|
|
152
|
+
".jpg": "image/jpeg",
|
|
153
|
+
".jpeg": "image/jpeg",
|
|
154
|
+
".webp": "image/webp",
|
|
155
|
+
".gif": "image/gif",
|
|
156
|
+
".ico": "image/x-icon",
|
|
157
|
+
".woff2": "font/woff2",
|
|
158
|
+
".map": "application/json",
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function staticServer(dir, port) {
|
|
162
|
+
const base = resolvePath(dir);
|
|
163
|
+
const srv = createServer((req, res) => {
|
|
164
|
+
let rel = decodeURIComponent((req.url || "/").split("?")[0]);
|
|
165
|
+
if (rel.endsWith("/")) rel += "index.html";
|
|
166
|
+
const target = resolvePath(join(base, rel));
|
|
167
|
+
// Contain to `base`: an exact match, or a path that starts with `base` +
|
|
168
|
+
// separator. A bare `startsWith(base)` would also accept a sibling like
|
|
169
|
+
// `<base>-notes/…`.
|
|
170
|
+
if (target !== base && !target.startsWith(base + sep)) {
|
|
171
|
+
res.writeHead(403).end("forbidden");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!existsSync(target) || statSync(target).isDirectory()) {
|
|
175
|
+
res.writeHead(404, { "content-type": "text/html; charset=utf-8" });
|
|
176
|
+
res.end("<!doctype html><meta charset=utf-8><title>404</title><p>Not found. The dev server serves the built bundle in <code>" + dir + "</code>.");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
res.writeHead(200, { "content-type": MIME[extname(target)] || "application/octet-stream", "cache-control": "no-store" });
|
|
180
|
+
res.end(readFileSync(target));
|
|
181
|
+
});
|
|
182
|
+
srv.on("error", (e) => {
|
|
183
|
+
if (e.code === "EADDRINUSE") fail(`volaro dev: port ${port} is already in use. Pass --port <n>.`);
|
|
184
|
+
fail(`volaro dev: ${e.message}`);
|
|
185
|
+
});
|
|
186
|
+
srv.listen(port, "127.0.0.1");
|
|
187
|
+
return srv;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Snapshot every .vl file under dir (bounded depth) as path -> "mtime:size".
|
|
191
|
+
// Polling beats fs.watch here: it is identical across platforms and editors
|
|
192
|
+
// (rename-replace, truncate-write and append all show up), which fs.watch is
|
|
193
|
+
// not, and the cost is trivial for a project-sized tree.
|
|
194
|
+
function scanVl(dir, depth = 6, acc = new Map(), excluded = null) {
|
|
195
|
+
let entries;
|
|
196
|
+
try {
|
|
197
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
198
|
+
} catch {
|
|
199
|
+
return acc;
|
|
200
|
+
}
|
|
201
|
+
for (const e of entries) {
|
|
202
|
+
if (e.name === "node_modules" || e.name === ".git" || e.name.startsWith(".")) continue;
|
|
203
|
+
const p = join(dir, e.name);
|
|
204
|
+
if (resolvePath(p) === excluded) continue;
|
|
205
|
+
if (e.isDirectory()) {
|
|
206
|
+
if (depth > 0) scanVl(p, depth - 1, acc, excluded);
|
|
207
|
+
} else if (extname(e.name) === ".vl") {
|
|
208
|
+
try {
|
|
209
|
+
const s = statSync(p);
|
|
210
|
+
acc.set(p, `${s.mtimeMs}:${s.size}`);
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return acc;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function snapEqual(a, b) {
|
|
218
|
+
if (a.size !== b.size) return false;
|
|
219
|
+
for (const [k, v] of a) if (b.get(k) !== v) return false;
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Poll watchDir for .vl changes and call onChange (debounced by the interval).
|
|
224
|
+
// Returns a close() that stops polling.
|
|
225
|
+
function watchVl(watchDir, onChange, outDir) {
|
|
226
|
+
const snapshot = () => {
|
|
227
|
+
const acc = scanVl(watchDir, 6, new Map(), resolvePath(outDir));
|
|
228
|
+
const scanAssets = (dir) => {
|
|
229
|
+
try {
|
|
230
|
+
const s = lstatSync(dir);
|
|
231
|
+
acc.set(dir, `${s.mtimeMs}:${s.ctimeMs}:${s.size}`);
|
|
232
|
+
if (s.isDirectory() && !s.isSymbolicLink()) {
|
|
233
|
+
for (const name of readdirSync(dir)) scanAssets(join(dir, name));
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
};
|
|
237
|
+
scanAssets(join(watchDir, "assets"));
|
|
238
|
+
return acc;
|
|
239
|
+
};
|
|
240
|
+
let prev = snapshot();
|
|
241
|
+
const iv = setInterval(() => {
|
|
242
|
+
const now = snapshot();
|
|
243
|
+
if (!snapEqual(prev, now)) {
|
|
244
|
+
prev = now;
|
|
245
|
+
onChange();
|
|
246
|
+
}
|
|
247
|
+
}, 250);
|
|
248
|
+
iv.unref();
|
|
249
|
+
return () => clearInterval(iv);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function cmdDev(args) {
|
|
253
|
+
let source = null,
|
|
254
|
+
port = 5173,
|
|
255
|
+
outDir = null;
|
|
256
|
+
for (let i = 0; i < args.length; i++) {
|
|
257
|
+
const a = args[i];
|
|
258
|
+
if (a === "--port" || a === "-p") port = Number(args[++i]);
|
|
259
|
+
else if (a === "-o" || a === "--out") outDir = args[++i];
|
|
260
|
+
else if (a.startsWith("-")) fail(`volaro dev: unknown option ${a}`);
|
|
261
|
+
else if (!source) source = a;
|
|
262
|
+
}
|
|
263
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) fail("volaro dev: --port must be 1..65535");
|
|
264
|
+
const entry = resolveEntry(source);
|
|
265
|
+
const out = outDir ? resolvePath(outDir) : resolvePath("build");
|
|
266
|
+
const watchDir = dirname(entry);
|
|
267
|
+
|
|
268
|
+
process.stdout.write(`volaro dev: building ${relative(process.cwd(), entry) || entry}\n`);
|
|
269
|
+
const first = buildOnce(entry, out);
|
|
270
|
+
if (!first.ok) fail("volaro dev: initial build failed. Fix the errors above and re-run.");
|
|
271
|
+
|
|
272
|
+
let shuttingDown = false;
|
|
273
|
+
let stopWatch = () => {};
|
|
274
|
+
|
|
275
|
+
// ---- full-stack: run server.js, restart it on a successful rebuild ----
|
|
276
|
+
if (first.server) {
|
|
277
|
+
if (!nodeCanRunServer()) {
|
|
278
|
+
fail(
|
|
279
|
+
`volaro dev: this app builds a full-stack server (server.js), which needs Node ` +
|
|
280
|
+
`${MIN_NODE_FOR_SERVER.join(".")}+ for node:sqlite. You have ${process.versions.node}.`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const serverPath = join(out, "server.js");
|
|
284
|
+
let child = null;
|
|
285
|
+
let restarting = false;
|
|
286
|
+
|
|
287
|
+
const startServer = () => {
|
|
288
|
+
child = spawn(process.execPath, [serverPath], {
|
|
289
|
+
stdio: "inherit",
|
|
290
|
+
env: { ...process.env, PORT: String(port) },
|
|
291
|
+
});
|
|
292
|
+
child.on("exit", (code, signal) => {
|
|
293
|
+
if (shuttingDown || restarting) return;
|
|
294
|
+
// the server died on its own (e.g. a port clash it reported to stderr)
|
|
295
|
+
stopWatch();
|
|
296
|
+
process.exit(signal ? 1 : code ?? 0);
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const restartServer = () =>
|
|
301
|
+
new Promise((done) => {
|
|
302
|
+
if (!child) return done();
|
|
303
|
+
restarting = true;
|
|
304
|
+
child.once("exit", () => {
|
|
305
|
+
restarting = false;
|
|
306
|
+
done();
|
|
307
|
+
});
|
|
308
|
+
child.kill("SIGTERM");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
process.stdout.write(`volaro dev: full-stack app — starting ${relative(process.cwd(), serverPath) || serverPath}\n`);
|
|
312
|
+
startServer();
|
|
313
|
+
|
|
314
|
+
stopWatch = watchVl(watchDir, async () => {
|
|
315
|
+
process.stdout.write("volaro dev: change detected — rebuilding\n");
|
|
316
|
+
const r = buildOnce(entry, out);
|
|
317
|
+
if (!r.ok) {
|
|
318
|
+
process.stdout.write("volaro dev: build failed — server left running on the last good build\n");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
await restartServer();
|
|
322
|
+
startServer();
|
|
323
|
+
process.stdout.write("volaro dev: server restarted\n");
|
|
324
|
+
}, out);
|
|
325
|
+
|
|
326
|
+
const bye = () => {
|
|
327
|
+
if (shuttingDown) return;
|
|
328
|
+
shuttingDown = true;
|
|
329
|
+
stopWatch();
|
|
330
|
+
if (child) child.kill("SIGTERM");
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
if (child) child.kill("SIGKILL");
|
|
333
|
+
process.exit(0);
|
|
334
|
+
}, 2000).unref();
|
|
335
|
+
if (child) child.once("exit", () => process.exit(0));
|
|
336
|
+
else process.exit(0);
|
|
337
|
+
};
|
|
338
|
+
process.on("SIGINT", bye);
|
|
339
|
+
process.on("SIGTERM", bye);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---- static single-page: serve the bundle, rebuild on change ----
|
|
344
|
+
const srv = staticServer(out, port);
|
|
345
|
+
process.stdout.write(`volaro dev: serving http://127.0.0.1:${port} (Ctrl+C to stop)\n`);
|
|
346
|
+
|
|
347
|
+
stopWatch = watchVl(watchDir, () => {
|
|
348
|
+
process.stdout.write("volaro dev: change detected — rebuilding\n");
|
|
349
|
+
const r = buildOnce(entry, out);
|
|
350
|
+
process.stdout.write(r.ok ? "volaro dev: rebuilt\n" : "volaro dev: build failed — keeping last good bundle\n");
|
|
351
|
+
}, out);
|
|
352
|
+
|
|
353
|
+
const bye = () => {
|
|
354
|
+
if (shuttingDown) return;
|
|
355
|
+
shuttingDown = true;
|
|
356
|
+
stopWatch();
|
|
357
|
+
srv.close(() => process.exit(0));
|
|
358
|
+
setTimeout(() => process.exit(0), 1000).unref();
|
|
359
|
+
};
|
|
360
|
+
process.on("SIGINT", bye);
|
|
361
|
+
process.on("SIGTERM", bye);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function cmdReference(which) {
|
|
365
|
+
if (which === "spec") {
|
|
366
|
+
// `volaro-language-spec.md` is the whole-language DESIGN — a superset of
|
|
367
|
+
// what this build accepts. `supported.md` is the version-specific
|
|
368
|
+
// shipped-feature guide; `crib.md` is the authoring reference.
|
|
369
|
+
process.stderr.write(
|
|
370
|
+
`note: \`${CLI} spec\` is the full language design — a SUPERSET of this build.\n` +
|
|
371
|
+
` \`${CLI} supported\` is what version ${pkg.version} actually accepts;\n` +
|
|
372
|
+
` \`${CLI} crib\` is the authoring reference.\n\n`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const file = which === "supported" ? "supported" : which;
|
|
376
|
+
// `supported.md`'s own title carries this build's exact version as a
|
|
377
|
+
// `{{VERSION}}` placeholder rather than a hardcoded string -- found stale
|
|
378
|
+
// (still reading a prior release's version after a version bump, since
|
|
379
|
+
// nothing kept it in sync) in the Test_003 benchmark's packaged-docs
|
|
380
|
+
// finding. Substituted from `package.json` at print time so it can never
|
|
381
|
+
// drift again, the same way the "supported"-vs-"spec" note above already
|
|
382
|
+
// reads `pkg.version` live rather than a copy-pasted string.
|
|
383
|
+
process.stdout.write(read(`language/${file}.md`).replace("{{VERSION}}", pkg.version));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function cmdExample(name) {
|
|
387
|
+
if (name === "sensors" || name === "station") process.stdout.write(read(`examples/${name}.vl`));
|
|
388
|
+
else fail("usage: volaro example sensors|station");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const HELP = `Volaro ${pkg.version} — an application language for AI authoring and human review.
|
|
392
|
+
|
|
393
|
+
USAGE
|
|
394
|
+
volaro <command> [args] (installed as both \`volaro\` and \`vl\`;
|
|
395
|
+
\`volaro\` is the canonical name)
|
|
396
|
+
|
|
397
|
+
COMMANDS
|
|
398
|
+
check <path>... Validate .vl source. Runs the full resolver, not just
|
|
399
|
+
a structural pass. Exit non-zero on any error.
|
|
400
|
+
build <file.vl> Transpile to a runnable bundle (default: ./build).
|
|
401
|
+
-o <dir> output directory
|
|
402
|
+
--data <file> data.json to bundle
|
|
403
|
+
--release fail on unresolved placeholders (e.g. img alt_todo:)
|
|
404
|
+
dev [file.vl] Build, serve on http://127.0.0.1:5173, and rebuild
|
|
405
|
+
when a .vl file changes. Defaults to ./app.vl. A
|
|
406
|
+
full-stack build runs server.js instead (Node 22.5+)
|
|
407
|
+
and restarts it after each successful rebuild.
|
|
408
|
+
--port <n> dev server port (passed to server.js as $PORT)
|
|
409
|
+
crib The authoring reference (write from this).
|
|
410
|
+
supported What version ${pkg.version} actually accepts.
|
|
411
|
+
spec The full language design (a SUPERSET of this build).
|
|
412
|
+
example <name> Print a worked example: sensors | station.
|
|
413
|
+
version Print the version.
|
|
414
|
+
help This message.
|
|
415
|
+
|
|
416
|
+
The compiler is Python and ships inside this package. Set VOLARO_PYTHON to
|
|
417
|
+
choose the interpreter if \`python3\` is not the one you want.
|
|
418
|
+
|
|
419
|
+
SHIPPED SUBSET: single page (no router); text \`input\` / \`button\` / \`link\`
|
|
420
|
+
only (no checkbox/select/textarea/disabled — a toggle is a \`button\` + \`state\`,
|
|
421
|
+
not a semantic checkbox); \`if\` as an expression is binary; \`match\` is a
|
|
422
|
+
statement; a module-level \`fn\` cannot be called from a view. \`volaro crib\` +
|
|
423
|
+
\`volaro supported\` are authoritative for this build; \`volaro spec\` is the wider
|
|
424
|
+
design, not what it accepts.
|
|
425
|
+
|
|
426
|
+
Run commands via your project's \`npm run …\` scripts or \`npx --no-install
|
|
427
|
+
volaro …\` inside the project. A bare \`npx volaro …\` from elsewhere may fetch
|
|
428
|
+
the wrong thing.
|
|
429
|
+
|
|
430
|
+
TO AN AI AGENT: run \`volaro crib\` and load its output — that is the complete
|
|
431
|
+
authoring reference. Then write source, run \`volaro check\`, and repair from the
|
|
432
|
+
diagnostics until it is clean.`;
|
|
433
|
+
|
|
434
|
+
// ---- dispatch ------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
437
|
+
switch (cmd) {
|
|
438
|
+
case "check":
|
|
439
|
+
cmdCheck(rest);
|
|
440
|
+
break;
|
|
441
|
+
case "build":
|
|
442
|
+
cmdBuild(rest);
|
|
443
|
+
break;
|
|
444
|
+
case "dev":
|
|
445
|
+
cmdDev(rest);
|
|
446
|
+
break;
|
|
447
|
+
case "crib":
|
|
448
|
+
cmdReference("crib");
|
|
449
|
+
break;
|
|
450
|
+
case "spec":
|
|
451
|
+
cmdReference("spec");
|
|
452
|
+
break;
|
|
453
|
+
case "supported":
|
|
454
|
+
cmdReference("supported");
|
|
455
|
+
break;
|
|
456
|
+
case "example":
|
|
457
|
+
cmdExample(rest[0]);
|
|
458
|
+
break;
|
|
459
|
+
case "version":
|
|
460
|
+
case "--version":
|
|
461
|
+
case "-v":
|
|
462
|
+
process.stdout.write(pkg.version + "\n");
|
|
463
|
+
break;
|
|
464
|
+
case undefined:
|
|
465
|
+
case "help":
|
|
466
|
+
case "--help":
|
|
467
|
+
case "-h":
|
|
468
|
+
process.stdout.write(HELP + "\n");
|
|
469
|
+
break;
|
|
470
|
+
default:
|
|
471
|
+
fail(`${CLI}: unknown command "${cmd}". Run \`${CLI} help\`.`, 2);
|
|
34
472
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
a450e6fe6549b8c7df43ba343793ab2e52758a78
|