volaro 0.0.2 → 0.1.0-alpha.10
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 +175 -22
- package/bin/vl.js +829 -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 +197 -0
- package/compiler/validator/vlcheck/ast_nodes.py +457 -0
- package/compiler/validator/vlcheck/benchmark_signal.py +86 -0
- package/compiler/validator/vlcheck/checks.py +3193 -0
- package/compiler/validator/vlcheck/diagnostics.py +91 -0
- package/compiler/validator/vlcheck/elements.py +1260 -0
- package/compiler/validator/vlcheck/layoutcompose.py +412 -0
- package/compiler/validator/vlcheck/lexer.py +384 -0
- package/compiler/validator/vlcheck/pagemanifest.py +389 -0
- package/compiler/validator/vlcheck/pageroutes.py +468 -0
- package/compiler/validator/vlcheck/parser.py +1842 -0
- package/compiler/validator/vlcheck/project_config.py +97 -0
- package/compiler/validator/vlcheck/resolve.py +1085 -0
- package/compiler/validator/vlcheck/routes_cli.py +172 -0
- package/compiler/validator/vlcheck/test_ids.py +99 -0
- package/compiler/vlbuild/styling/README.md +48 -0
- package/compiler/vlbuild/styling/build-css.mjs +181 -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 +149 -0
- package/compiler/vlbuild/vlbuild/__init__.py +16 -0
- package/compiler/vlbuild/vlbuild/__main__.py +376 -0
- package/compiler/vlbuild/vlbuild/assets/vlrouter.js +740 -0
- package/compiler/vlbuild/vlbuild/assets/vlrouter.min.js +2 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.css +289 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.js +1648 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.min.css +2 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.min.js +2 -0
- package/compiler/vlbuild/vlbuild/emit.py +3732 -0
- package/compiler/vlbuild/vlbuild/pages_build.py +508 -0
- package/compiler/vlbuild/vlbuild/project.py +338 -0
- package/compiler/vlbuild/vlbuild/runtime_assets.py +41 -0
- package/compiler/vlbuild/vlbuild/server_emit.py +2149 -0
- package/compiler/vlbuild/vlbuild/static_assets.py +130 -0
- package/compiler/vlbuild/vlbuild/style_config.py +414 -0
- package/compiler/vlbuild/vlbuild/styling.py +41 -0
- package/examples/station.vl +2 -2
- package/language/crib.md +632 -24
- package/language/spec.md +722 -19
- package/language/supported.md +711 -0
- package/lib/env.js +107 -0
- package/package.json +19 -2
- package/scripts/record-provenance.mjs +51 -0
- package/scripts/selftest.mjs +108 -0
- package/scripts/sync-compiler.sh +64 -0
package/bin/vl.js
CHANGED
|
@@ -1,34 +1,835 @@
|
|
|
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 routes [project] print discovered pages and the page-server route manifest
|
|
9
|
+
// volaro crib | volaro spec print the language reference
|
|
10
|
+
// volaro example <name> print a worked example
|
|
11
|
+
// volaro version | volaro help
|
|
12
|
+
//
|
|
13
|
+
// Python is located and version-checked up front with an actionable message
|
|
14
|
+
// (see ../lib/env.js). Nothing here depends on the Volaro repository layout.
|
|
15
|
+
|
|
16
|
+
import { appendFileSync, readFileSync, existsSync, statSync, lstatSync, readdirSync } from "node:fs";
|
|
3
17
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
18
|
+
import { dirname, join, resolve as resolvePath, extname, relative, sep, delimiter } from "node:path";
|
|
19
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
20
|
+
import { createServer } from "node:http";
|
|
21
|
+
import { resolvePython, nodeCanRunServer, VolaroEnvError, MIN_NODE_FOR_SERVER } from "../lib/env.js";
|
|
5
22
|
|
|
6
23
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
24
|
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
|
-
|
|
25
|
+
const pkg = JSON.parse(read("package.json"));
|
|
26
|
+
|
|
27
|
+
// The canonical command is `volaro`; `vl` is a compatibility alias. Messages
|
|
28
|
+
// use whichever name this process was invoked as, defaulting to `volaro`.
|
|
29
|
+
const CLI = (() => {
|
|
30
|
+
const b = (process.argv[1] || "").split(/[\\/]/).pop() || "";
|
|
31
|
+
return b === "vl" || b === "vl.js" ? "vl" : "volaro";
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
const COMPILER = join(root, "compiler");
|
|
35
|
+
const VALIDATOR_PP = join(COMPILER, "validator");
|
|
36
|
+
const VLBUILD_PP = join(COMPILER, "vlbuild");
|
|
37
|
+
|
|
38
|
+
function fail(msg, code = 1) {
|
|
39
|
+
process.stderr.write((msg.endsWith("\n") ? msg : msg + "\n"));
|
|
40
|
+
process.exit(code);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function haveCompiler() {
|
|
44
|
+
return existsSync(join(VALIDATOR_PP, "vlcheck", "__main__.py")) &&
|
|
45
|
+
existsSync(join(VLBUILD_PP, "vlbuild", "__main__.py"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function python() {
|
|
49
|
+
if (!haveCompiler()) {
|
|
50
|
+
fail(
|
|
51
|
+
`${CLI}: the bundled compiler is missing from this package.\n` +
|
|
52
|
+
"If you are working inside the Volaro repo, run:\n" +
|
|
53
|
+
" bash packaging/volaro/scripts/sync-compiler.sh\n" +
|
|
54
|
+
"An installed copy from `npm pack` / a published release always includes it.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return resolvePython();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err instanceof VolaroEnvError) fail(`${CLI}: ` + err.message);
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Shared Python-invocation setup for both the streaming and capturing paths.
|
|
66
|
+
function pyInvocation(pp, moduleName, moduleArgs) {
|
|
67
|
+
const py = python();
|
|
68
|
+
const env = {
|
|
69
|
+
...process.env,
|
|
70
|
+
PYTHONPATH: [pp, VALIDATOR_PP, process.env.PYTHONPATH].filter(Boolean).join(delimiter),
|
|
71
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
72
|
+
// Diagnostics and build summaries contain non-ASCII (box drawing, arrows).
|
|
73
|
+
// Without this, Windows encodes a redirected stdout with the legacy code
|
|
74
|
+
// page and the compiler dies with UnicodeEncodeError; the capturing path
|
|
75
|
+
// below also decodes the child's output as UTF-8.
|
|
76
|
+
PYTHONIOENCODING: "utf-8",
|
|
77
|
+
};
|
|
78
|
+
return { cmd: py.cmd, argv: ["-B", "-m", moduleName, ...moduleArgs], env };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function elapsedMs(start) {
|
|
82
|
+
return Number(process.hrtime.bigint() - start) / 1e6;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function roundedMs(value) {
|
|
86
|
+
return Math.round(value * 1000) / 1000;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function benchmarkRecords(path, runId) {
|
|
90
|
+
if (!existsSync(path)) return [];
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
const st = statSync(path);
|
|
94
|
+
if (st.isDirectory()) {
|
|
95
|
+
fail(`${CLI}: benchmark log ${path} is a directory, not a file`);
|
|
96
|
+
}
|
|
97
|
+
raw = readFileSync(path, "utf8");
|
|
98
|
+
} catch (err) {
|
|
99
|
+
fail(`${CLI}: cannot read benchmark log ${path}: ${err.message}`);
|
|
100
|
+
}
|
|
101
|
+
return raw
|
|
102
|
+
.split(/\r?\n/)
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.map((line, index) => {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(line);
|
|
107
|
+
} catch {
|
|
108
|
+
fail(`${CLI}: invalid JSONL in benchmark log ${path} at line ${index + 1}: ` +
|
|
109
|
+
`${JSON.stringify(line.length > 80 ? line.slice(0, 80) + "…" : line)}`);
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
.filter((row) => row.schema === "volaro.compile-timing.v1" && row.run_id === runId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function appendBenchmark(path, record) {
|
|
116
|
+
try {
|
|
117
|
+
appendFileSync(path, JSON.stringify(record) + "\n", { encoding: "utf8", flag: "a" });
|
|
118
|
+
} catch (err) {
|
|
119
|
+
fail(`${CLI}: cannot append benchmark log ${path}: ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ensureBenchmarkLog(path) {
|
|
124
|
+
try {
|
|
125
|
+
appendFileSync(path, "", { encoding: "utf8", flag: "a" });
|
|
126
|
+
} catch (err) {
|
|
127
|
+
fail(`${CLI}: cannot open benchmark log ${path}: ${err.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function compilerProvenance() {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(readFileSync(join(COMPILER, "SOURCE_INFO.json"), "utf8"));
|
|
134
|
+
} catch {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseBenchmarkArgs(args, command) {
|
|
140
|
+
let log = null;
|
|
141
|
+
let runId = null;
|
|
142
|
+
let logCount = 0;
|
|
143
|
+
let runCount = 0;
|
|
144
|
+
const compilerArgs = [];
|
|
145
|
+
for (let i = 0; i < args.length; i++) {
|
|
146
|
+
const arg = args[i];
|
|
147
|
+
if (arg === "--benchmark-log" || arg === "--benchmark-run") {
|
|
148
|
+
if (i + 1 >= args.length || args[i + 1].startsWith("--")) {
|
|
149
|
+
fail(`${CLI} ${command}: ${arg} requires a value`, 2);
|
|
150
|
+
}
|
|
151
|
+
if (arg === "--benchmark-log") {
|
|
152
|
+
log = args[++i];
|
|
153
|
+
logCount++;
|
|
154
|
+
} else {
|
|
155
|
+
runId = args[++i];
|
|
156
|
+
runCount++;
|
|
157
|
+
}
|
|
158
|
+
} else if (arg.startsWith("--benchmark-log=")) {
|
|
159
|
+
log = arg.slice("--benchmark-log=".length);
|
|
160
|
+
logCount++;
|
|
161
|
+
} else if (arg.startsWith("--benchmark-run=")) {
|
|
162
|
+
runId = arg.slice("--benchmark-run=".length);
|
|
163
|
+
runCount++;
|
|
164
|
+
} else {
|
|
165
|
+
compilerArgs.push(arg);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if ((log === null) !== (runId === null)) {
|
|
169
|
+
fail(`${CLI} ${command}: --benchmark-log and --benchmark-run must be supplied together`, 2);
|
|
170
|
+
}
|
|
171
|
+
if (logCount > 1 || runCount > 1) {
|
|
172
|
+
fail(`${CLI} ${command}: benchmark options may be supplied only once`, 2);
|
|
173
|
+
}
|
|
174
|
+
if (log !== null && log.trim() === "") {
|
|
175
|
+
fail(`${CLI} ${command}: --benchmark-log must not be empty`, 2);
|
|
176
|
+
}
|
|
177
|
+
if (runId !== null && runId.trim() === "") {
|
|
178
|
+
fail(`${CLI} ${command}: --benchmark-run must not be empty`, 2);
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
args: compilerArgs,
|
|
182
|
+
benchmark: log === null ? null : { log: resolvePath(log), runId },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Four possible outcomes for a FAILED attempt (never called for exitCode 0).
|
|
187
|
+
// This is the entire classification: it never inspects captured stdout/
|
|
188
|
+
// stderr TEXT, only (a) whether the OS delivered a kill signal and (b)
|
|
189
|
+
// the compiler's own structural report of what kind of stop it made (see
|
|
190
|
+
// `vlcheck/benchmark_signal.py`'s module docstring). "Any output happened"
|
|
191
|
+
// is deliberately NOT a diagnostic signal on its own -- an internal
|
|
192
|
+
// traceback is output too.
|
|
193
|
+
function classifyAttempt(signal, benchmarkSignal, firstOutputMs) {
|
|
194
|
+
if (signal) return "operational_failure"; // killed by the OS, never a diagnostic
|
|
195
|
+
if (benchmarkSignal) {
|
|
196
|
+
if (benchmarkSignal.kind === "crash") return "operational_failure";
|
|
197
|
+
if (benchmarkSignal.kind === "diagnostic") {
|
|
198
|
+
// Each module's OWN reserved "diagnostics found in valid input" code
|
|
199
|
+
// is exactly 1 (see vlcheck/vlbuild __main__.py); any other code the
|
|
200
|
+
// compiler intentionally stopped with (a usage/configuration error,
|
|
201
|
+
// for instance) is a real, honest stop but not a SOURCE diagnostic.
|
|
202
|
+
return benchmarkSignal.exit_code === 1 ? "source_diagnostic" : "operational_failure";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// No structured signal reached us at all: the compiler crashed before
|
|
206
|
+
// reaching its own signal-emission wrapper (an import-time failure, an
|
|
207
|
+
// interpreter that never started, a segfault) or the side channel was
|
|
208
|
+
// lost. Distinguished only by whether anything was captured on stdout/
|
|
209
|
+
// stderr, not by reading it.
|
|
210
|
+
return firstOutputMs === null ? "silent_failure" : "unknown_failure";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function writeBenchmarkResult(benchmark, command, compilerArgv, code, signal, startedAt,
|
|
214
|
+
startedMono, firstOutputMs, benchmarkSignal) {
|
|
215
|
+
const finishedEpochMs = Date.now();
|
|
216
|
+
const durationMs = elapsedMs(startedMono);
|
|
217
|
+
const prior = benchmarkRecords(benchmark.log, benchmark.runId);
|
|
218
|
+
if (prior.some((row) => row.record === "summary")) {
|
|
219
|
+
fail(`${CLI}: benchmark run ${JSON.stringify(benchmark.runId)} is already complete`, 2);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const attempts = prior.filter((row) => row.record === "attempt");
|
|
223
|
+
const exitCode = signal ? 1 : code ?? 0;
|
|
224
|
+
const provenance = compilerProvenance();
|
|
225
|
+
const classification = exitCode === 0 ? null : classifyAttempt(signal, benchmarkSignal, firstOutputMs);
|
|
226
|
+
const attempt = {
|
|
227
|
+
schema: "volaro.compile-timing.v1",
|
|
228
|
+
record: "attempt",
|
|
229
|
+
run_id: benchmark.runId,
|
|
230
|
+
attempt: attempts.length + 1,
|
|
231
|
+
command,
|
|
232
|
+
compiler_argv: compilerArgv,
|
|
233
|
+
compiler_version: pkg.version,
|
|
234
|
+
compiler_revision: provenance.revision || null,
|
|
235
|
+
compiler_content_sha256: provenance.content_sha256 || null,
|
|
236
|
+
started_at: startedAt,
|
|
237
|
+
finished_at: new Date(finishedEpochMs).toISOString(),
|
|
238
|
+
exit_code: exitCode,
|
|
239
|
+
signal: signal || null,
|
|
240
|
+
outcome: exitCode === 0 ? "success" : "failure",
|
|
241
|
+
classification,
|
|
242
|
+
duration_ms: roundedMs(durationMs),
|
|
243
|
+
// Raw fact, any classification: did output arrive, and when -- kept
|
|
244
|
+
// even for a crash/silent/unknown failure as the "available output
|
|
245
|
+
// metadata" a human investigating an invalid run needs.
|
|
246
|
+
first_output_ms: firstOutputMs === null ? null : roundedMs(firstOutputMs),
|
|
247
|
+
// Only ever populated for a genuine source diagnostic -- T-fail's
|
|
248
|
+
// whole point is "how long until the compiler told you about YOUR
|
|
249
|
+
// bug", which an operational/silent/unknown failure never did.
|
|
250
|
+
first_diagnostic_ms: classification === "source_diagnostic" && firstOutputMs !== null
|
|
251
|
+
? roundedMs(firstOutputMs) : null,
|
|
252
|
+
crash_exception_type: benchmarkSignal && benchmarkSignal.kind === "crash"
|
|
253
|
+
? benchmarkSignal.exception_type : null,
|
|
254
|
+
crash_exception_message: benchmarkSignal && benchmarkSignal.kind === "crash"
|
|
255
|
+
? benchmarkSignal.exception_message : null,
|
|
256
|
+
};
|
|
257
|
+
appendBenchmark(benchmark.log, attempt);
|
|
258
|
+
|
|
259
|
+
if (exitCode !== 0) return;
|
|
260
|
+
|
|
261
|
+
const allAttempts = [...attempts, attempt];
|
|
262
|
+
const failures = allAttempts.filter((row) => row.outcome === "failure");
|
|
263
|
+
if (failures.length === 0) {
|
|
264
|
+
appendBenchmark(benchmark.log, {
|
|
265
|
+
schema: "volaro.compile-timing.v1",
|
|
266
|
+
record: "summary",
|
|
267
|
+
run_id: benchmark.runId,
|
|
268
|
+
command,
|
|
269
|
+
outcome: "clean",
|
|
270
|
+
timing_complete: true,
|
|
271
|
+
timing_incomplete_reason: null,
|
|
272
|
+
clock_anomaly: false,
|
|
273
|
+
attempt_count: 1,
|
|
274
|
+
failed_attempts: 0,
|
|
275
|
+
t_clean_ms: attempt.duration_ms,
|
|
276
|
+
t_fail_ms: null,
|
|
277
|
+
t_repair_ms: null,
|
|
278
|
+
t_recompile_ms: null,
|
|
279
|
+
t_recover_ms: null,
|
|
280
|
+
compiler_overhead_ms: null,
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Wall-clock gaps between separate process invocations, computed from
|
|
286
|
+
// each process's own Date.now()-derived timestamps -- vulnerable to a
|
|
287
|
+
// backward system clock step (an NTP sync) between two attempts. A
|
|
288
|
+
// negative gap is never clamped to zero and reported as if it were a
|
|
289
|
+
// real, small repair time: that would silently corrupt the median/p90
|
|
290
|
+
// aggregation STAGE3-PROTOCOL.md §6.1 defines over this data. Instead
|
|
291
|
+
// the whole run is marked invalid for recovery-time comparison, with
|
|
292
|
+
// every raw timestamp still preserved untouched in the attempts above.
|
|
293
|
+
let repairMs = 0;
|
|
294
|
+
let clockAnomaly = false;
|
|
295
|
+
for (let i = 1; i < allAttempts.length; i++) {
|
|
296
|
+
const previous = allAttempts[i - 1];
|
|
297
|
+
if (previous.outcome === "failure") {
|
|
298
|
+
const gap = Date.parse(allAttempts[i].started_at) - Date.parse(previous.finished_at);
|
|
299
|
+
if (gap < 0) clockAnomaly = true;
|
|
300
|
+
repairMs += gap;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const first = allAttempts[0];
|
|
304
|
+
const recoverMs = finishedEpochMs - Date.parse(first.started_at);
|
|
305
|
+
if (recoverMs < 0) clockAnomaly = true;
|
|
306
|
+
|
|
307
|
+
const firstIsDiagnostic = first.classification === "source_diagnostic";
|
|
308
|
+
const failMs = firstIsDiagnostic ? first.first_diagnostic_ms : null;
|
|
309
|
+
const timingComplete = firstIsDiagnostic && !clockAnomaly;
|
|
310
|
+
const compilerOverheadMs = timingComplete
|
|
311
|
+
? recoverMs - failMs - repairMs - attempt.duration_ms
|
|
312
|
+
: null;
|
|
313
|
+
|
|
314
|
+
let timingIncompleteReason = null;
|
|
315
|
+
if (!firstIsDiagnostic) {
|
|
316
|
+
timingIncompleteReason = `the original failing attempt was classified ` +
|
|
317
|
+
`${JSON.stringify(first.classification)}, not "source_diagnostic" -- ` +
|
|
318
|
+
`t_fail_ms and compiler_overhead_ms are not valid recovery-time samples for this run`;
|
|
319
|
+
} else if (clockAnomaly) {
|
|
320
|
+
timingIncompleteReason = "a wall-clock timestamp in this run predates an earlier one " +
|
|
321
|
+
"(a backward system clock step between two compiler invocations) -- " +
|
|
322
|
+
"t_repair_ms/t_recover_ms/compiler_overhead_ms are not valid recovery-time samples for this run";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
appendBenchmark(benchmark.log, {
|
|
326
|
+
schema: "volaro.compile-timing.v1",
|
|
327
|
+
record: "summary",
|
|
328
|
+
run_id: benchmark.runId,
|
|
329
|
+
command,
|
|
330
|
+
outcome: "recovered",
|
|
331
|
+
timing_complete: timingComplete,
|
|
332
|
+
timing_incomplete_reason: timingIncompleteReason,
|
|
333
|
+
clock_anomaly: clockAnomaly,
|
|
334
|
+
attempt_count: allAttempts.length,
|
|
335
|
+
failed_attempts: failures.length,
|
|
336
|
+
t_clean_ms: null,
|
|
337
|
+
t_fail_ms: failMs,
|
|
338
|
+
t_repair_ms: clockAnomaly ? null : roundedMs(repairMs),
|
|
339
|
+
t_recompile_ms: attempt.duration_ms,
|
|
340
|
+
t_recover_ms: clockAnomaly ? null : roundedMs(recoverMs),
|
|
341
|
+
compiler_overhead_ms: timingComplete ? roundedMs(Math.max(0, compilerOverheadMs)) : null,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Run a Python module, streaming its output, and exit with its status.
|
|
346
|
+
function runPython(pp, moduleName, moduleArgs, benchmark = null, command = moduleName) {
|
|
347
|
+
const { cmd, argv, env } = pyInvocation(pp, moduleName, moduleArgs);
|
|
348
|
+
if (!benchmark) {
|
|
349
|
+
const child = spawn(cmd, argv, { stdio: "inherit", env });
|
|
350
|
+
child.on("error", (e) => fail(`${CLI}: failed to start Python (${cmd}): ${e.message}`));
|
|
351
|
+
child.on("exit", (code, signal) => process.exit(signal ? 1 : code ?? 0));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
ensureBenchmarkLog(benchmark.log);
|
|
356
|
+
const existing = benchmarkRecords(benchmark.log, benchmark.runId);
|
|
357
|
+
if (existing.some((row) => row.record === "summary")) {
|
|
358
|
+
fail(`${CLI}: benchmark run ${JSON.stringify(benchmark.runId)} is already complete`, 2);
|
|
359
|
+
}
|
|
360
|
+
if (existing.some((row) => row.record === "attempt" && row.command !== command)) {
|
|
361
|
+
fail(`${CLI}: benchmark run ${JSON.stringify(benchmark.runId)} cannot mix check and build`, 2);
|
|
362
|
+
}
|
|
363
|
+
const compilerArgv = [moduleName, ...moduleArgs];
|
|
364
|
+
if (existing.some((row) => row.record === "attempt" &&
|
|
365
|
+
JSON.stringify(row.compiler_argv) !== JSON.stringify(compilerArgv))) {
|
|
366
|
+
fail(`${CLI}: benchmark run ${JSON.stringify(benchmark.runId)} must reuse the exact compiler command`, 2);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const startedEpochMs = Date.now();
|
|
370
|
+
const startedAt = new Date(startedEpochMs).toISOString();
|
|
371
|
+
const startedMono = process.hrtime.bigint();
|
|
372
|
+
let firstOutputMs = null;
|
|
373
|
+
// fd 3: a private side channel the compiler writes its own structural
|
|
374
|
+
// diagnostic/crash signal to (see vlcheck/benchmark_signal.py) -- never
|
|
375
|
+
// stdout/stderr, so ordinary output forwarded to the terminal below is
|
|
376
|
+
// untouched by this. VOLARO_BENCHMARK_SIGNAL_FD tells the compiler that
|
|
377
|
+
// channel exists; it is a documented no-op when unset, which is exactly
|
|
378
|
+
// every non-benchmark invocation.
|
|
379
|
+
const child = spawn(cmd, argv, {
|
|
380
|
+
stdio: ["inherit", "pipe", "pipe", "pipe"],
|
|
381
|
+
env: { ...env, PYTHONUNBUFFERED: "1", VOLARO_BENCHMARK_SIGNAL_FD: "3" },
|
|
382
|
+
});
|
|
383
|
+
const forward = (stream, target) => stream.on("data", (chunk) => {
|
|
384
|
+
if (firstOutputMs === null) firstOutputMs = elapsedMs(startedMono);
|
|
385
|
+
target.write(chunk);
|
|
386
|
+
});
|
|
387
|
+
forward(child.stdout, process.stdout);
|
|
388
|
+
forward(child.stderr, process.stderr);
|
|
389
|
+
const signalChunks = [];
|
|
390
|
+
child.stdio[3].on("data", (chunk) => signalChunks.push(chunk));
|
|
391
|
+
child.on("error", (e) => fail(`${CLI}: failed to start Python (${cmd}): ${e.message}`));
|
|
392
|
+
child.on("close", (code, signal) => {
|
|
393
|
+
let benchmarkSignal = null;
|
|
394
|
+
const signalRaw = Buffer.concat(signalChunks).toString("utf8").trim();
|
|
395
|
+
if (signalRaw) {
|
|
396
|
+
// The compiler writes exactly one JSON line and closes the stream
|
|
397
|
+
// (benchmark_signal.py); take the first line defensively rather than
|
|
398
|
+
// assuming that held.
|
|
399
|
+
try {
|
|
400
|
+
benchmarkSignal = JSON.parse(signalRaw.split(/\r?\n/)[0]);
|
|
401
|
+
} catch {
|
|
402
|
+
benchmarkSignal = null; // malformed signal -> falls through to unknown_failure
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
writeBenchmarkResult(benchmark, command, compilerArgv, code, signal, startedAt,
|
|
406
|
+
startedMono, firstOutputMs, benchmarkSignal);
|
|
407
|
+
process.exit(signal ? 1 : code ?? 0);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function pyCapture(pp, moduleName, moduleArgs) {
|
|
412
|
+
const { cmd, argv, env } = pyInvocation(pp, moduleName, moduleArgs);
|
|
413
|
+
return spawnSync(cmd, argv, { encoding: "utf8", env });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ---- subcommands -----------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
function cmdCheck(args) {
|
|
419
|
+
const parsed = parseBenchmarkArgs(args, "check");
|
|
420
|
+
if (parsed.args.length === 0) fail("usage: volaro check <file-or-dir>... [--release] [--config PATH]");
|
|
421
|
+
// Always resolve: a bare structural pass is weaker than the real check.
|
|
422
|
+
runPython(VALIDATOR_PP, "vlcheck", ["--resolve", ...parsed.args], parsed.benchmark, "check");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function parseBuildArgs(args) {
|
|
426
|
+
const out = { source: null, o: null, data: null, release: false, config: null };
|
|
427
|
+
for (let i = 0; i < args.length; i++) {
|
|
428
|
+
const a = args[i];
|
|
429
|
+
if (a === "-o" || a === "--out") out.o = args[++i];
|
|
430
|
+
else if (a === "--data") out.data = args[++i];
|
|
431
|
+
else if (a === "--config") out.config = args[++i];
|
|
432
|
+
else if (a === "--release") out.release = true;
|
|
433
|
+
else if (a.startsWith("-")) fail(`volaro build: unknown option ${a}`);
|
|
434
|
+
else if (!out.source) out.source = a;
|
|
435
|
+
else fail(`volaro build: unexpected argument ${a}`);
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function resolveEntry(explicit) {
|
|
441
|
+
if (explicit) {
|
|
442
|
+
if (!existsSync(explicit)) fail(`${CLI}: no such file: ${explicit}`);
|
|
443
|
+
return resolvePath(explicit);
|
|
444
|
+
}
|
|
445
|
+
for (const c of ["app.vl", join("src", "app.vl")]) {
|
|
446
|
+
if (existsSync(c)) return resolvePath(c);
|
|
447
|
+
}
|
|
448
|
+
fail(
|
|
449
|
+
"volaro: no entry file given and no app.vl found in this directory.\n" +
|
|
450
|
+
"Pass one explicitly: volaro build path/to/app.vl",
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function cmdBuild(args) {
|
|
455
|
+
const parsed = parseBenchmarkArgs(args, "build");
|
|
456
|
+
const b = parseBuildArgs(parsed.args);
|
|
457
|
+
const source = resolveEntry(b.source);
|
|
458
|
+
const outDir = b.o ? resolvePath(b.o) : resolvePath("build");
|
|
459
|
+
const pyArgs = [source, "-o", outDir];
|
|
460
|
+
if (b.data) pyArgs.push("--data", resolvePath(b.data));
|
|
461
|
+
if (b.config) pyArgs.push("--config", resolvePath(b.config));
|
|
462
|
+
if (b.release) pyArgs.push("--release");
|
|
463
|
+
runPython(VLBUILD_PP, "vlbuild", pyArgs, parsed.benchmark, "build");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Synchronous single build for `volaro dev`; returns { ok, server } or exits on env error.
|
|
467
|
+
function buildOnce(source, outDir, extra = []) {
|
|
468
|
+
const r = pyCapture(VLBUILD_PP, "vlbuild", [source, "-o", outDir, ...extra]);
|
|
469
|
+
if (r.error) fail(`${CLI}: failed to start Python: ${r.error.message}`);
|
|
470
|
+
if (r.stdout) process.stdout.write(r.stdout);
|
|
471
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
472
|
+
return { ok: r.status === 0, server: existsSync(join(outDir, "server.js")) };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const MIME = {
|
|
476
|
+
".html": "text/html; charset=utf-8",
|
|
477
|
+
".js": "text/javascript; charset=utf-8",
|
|
478
|
+
".css": "text/css; charset=utf-8",
|
|
479
|
+
".json": "application/json; charset=utf-8",
|
|
480
|
+
".svg": "image/svg+xml",
|
|
481
|
+
".png": "image/png",
|
|
482
|
+
".jpg": "image/jpeg",
|
|
483
|
+
".jpeg": "image/jpeg",
|
|
484
|
+
".webp": "image/webp",
|
|
485
|
+
".gif": "image/gif",
|
|
486
|
+
".ico": "image/x-icon",
|
|
487
|
+
".woff2": "font/woff2",
|
|
488
|
+
".map": "application/json",
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
function staticServer(dir, port) {
|
|
492
|
+
const base = resolvePath(dir);
|
|
493
|
+
const srv = createServer((req, res) => {
|
|
494
|
+
let rel = decodeURIComponent((req.url || "/").split("?")[0]);
|
|
495
|
+
if (rel.endsWith("/")) rel += "index.html";
|
|
496
|
+
const target = resolvePath(join(base, rel));
|
|
497
|
+
// Contain to `base`: an exact match, or a path that starts with `base` +
|
|
498
|
+
// separator. A bare `startsWith(base)` would also accept a sibling like
|
|
499
|
+
// `<base>-notes/…`.
|
|
500
|
+
if (target !== base && !target.startsWith(base + sep)) {
|
|
501
|
+
res.writeHead(403).end("forbidden");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (!existsSync(target) || statSync(target).isDirectory()) {
|
|
505
|
+
res.writeHead(404, { "content-type": "text/html; charset=utf-8" });
|
|
506
|
+
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>.");
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
res.writeHead(200, { "content-type": MIME[extname(target)] || "application/octet-stream", "cache-control": "no-store" });
|
|
510
|
+
res.end(readFileSync(target));
|
|
511
|
+
});
|
|
512
|
+
srv.on("error", (e) => {
|
|
513
|
+
if (e.code === "EADDRINUSE") fail(`volaro dev: port ${port} is already in use. Pass --port <n>.`);
|
|
514
|
+
fail(`volaro dev: ${e.message}`);
|
|
515
|
+
});
|
|
516
|
+
srv.listen(port, "127.0.0.1");
|
|
517
|
+
return srv;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Snapshot every .vl file under dir (bounded depth) as path -> "mtime:size".
|
|
521
|
+
// Polling beats fs.watch here: it is identical across platforms and editors
|
|
522
|
+
// (rename-replace, truncate-write and append all show up), which fs.watch is
|
|
523
|
+
// not, and the cost is trivial for a project-sized tree.
|
|
524
|
+
function scanVl(dir, depth = 6, acc = new Map(), excluded = null) {
|
|
525
|
+
let entries;
|
|
526
|
+
try {
|
|
527
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
528
|
+
} catch {
|
|
529
|
+
return acc;
|
|
530
|
+
}
|
|
531
|
+
for (const e of entries) {
|
|
532
|
+
if (e.name === "node_modules" || e.name === ".git" || e.name.startsWith(".")) continue;
|
|
533
|
+
const p = join(dir, e.name);
|
|
534
|
+
if (resolvePath(p) === excluded) continue;
|
|
535
|
+
if (e.isDirectory()) {
|
|
536
|
+
if (depth > 0) scanVl(p, depth - 1, acc, excluded);
|
|
537
|
+
} else if (extname(e.name) === ".vl") {
|
|
538
|
+
try {
|
|
539
|
+
const s = statSync(p);
|
|
540
|
+
acc.set(p, `${s.mtimeMs}:${s.size}`);
|
|
541
|
+
} catch {}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return acc;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function snapEqual(a, b) {
|
|
548
|
+
if (a.size !== b.size) return false;
|
|
549
|
+
for (const [k, v] of a) if (b.get(k) !== v) return false;
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Poll watchDir for .vl changes and call onChange (debounced by the interval).
|
|
554
|
+
// Returns a close() that stops polling.
|
|
555
|
+
function watchVl(watchDir, onChange, outDir) {
|
|
556
|
+
const snapshot = () => {
|
|
557
|
+
const acc = scanVl(watchDir, 6, new Map(), resolvePath(outDir));
|
|
558
|
+
const scanAssets = (dir) => {
|
|
559
|
+
try {
|
|
560
|
+
const s = lstatSync(dir);
|
|
561
|
+
acc.set(dir, `${s.mtimeMs}:${s.ctimeMs}:${s.size}`);
|
|
562
|
+
if (s.isDirectory() && !s.isSymbolicLink()) {
|
|
563
|
+
for (const name of readdirSync(dir)) scanAssets(join(dir, name));
|
|
564
|
+
}
|
|
565
|
+
} catch {}
|
|
566
|
+
};
|
|
567
|
+
scanAssets(join(watchDir, "assets"));
|
|
568
|
+
return acc;
|
|
569
|
+
};
|
|
570
|
+
let prev = snapshot();
|
|
571
|
+
const iv = setInterval(() => {
|
|
572
|
+
const now = snapshot();
|
|
573
|
+
if (!snapEqual(prev, now)) {
|
|
574
|
+
prev = now;
|
|
575
|
+
onChange();
|
|
576
|
+
}
|
|
577
|
+
}, 250);
|
|
578
|
+
iv.unref();
|
|
579
|
+
return () => clearInterval(iv);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function cmdDev(args) {
|
|
583
|
+
let source = null,
|
|
584
|
+
port = 5173,
|
|
585
|
+
outDir = null;
|
|
586
|
+
for (let i = 0; i < args.length; i++) {
|
|
587
|
+
const a = args[i];
|
|
588
|
+
if (a === "--port" || a === "-p") port = Number(args[++i]);
|
|
589
|
+
else if (a === "-o" || a === "--out") outDir = args[++i];
|
|
590
|
+
else if (a.startsWith("-")) fail(`volaro dev: unknown option ${a}`);
|
|
591
|
+
else if (!source) source = a;
|
|
592
|
+
}
|
|
593
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) fail("volaro dev: --port must be 1..65535");
|
|
594
|
+
const entry = resolveEntry(source);
|
|
595
|
+
const out = outDir ? resolvePath(outDir) : resolvePath("build");
|
|
596
|
+
// A project build's entry is the project directory itself (`volaro dev .`);
|
|
597
|
+
// watching its dirname would watch the PARENT directory instead.
|
|
598
|
+
const watchDir = statSync(entry).isDirectory() ? entry : dirname(entry);
|
|
599
|
+
|
|
600
|
+
process.stdout.write(`volaro dev: building ${relative(process.cwd(), entry) || entry}\n`);
|
|
601
|
+
const first = buildOnce(entry, out);
|
|
602
|
+
if (!first.ok) fail("volaro dev: initial build failed. Fix the errors above and re-run.");
|
|
603
|
+
|
|
604
|
+
let shuttingDown = false;
|
|
605
|
+
let stopWatch = () => {};
|
|
606
|
+
|
|
607
|
+
// ---- full-stack: run server.js, restart it on a successful rebuild ----
|
|
608
|
+
if (first.server) {
|
|
609
|
+
if (!nodeCanRunServer()) {
|
|
610
|
+
fail(
|
|
611
|
+
`volaro dev: this app builds a full-stack server (server.js), which needs Node ` +
|
|
612
|
+
`${MIN_NODE_FOR_SERVER.join(".")}+ for node:sqlite. You have ${process.versions.node}.`,
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
const serverPath = join(out, "server.js");
|
|
616
|
+
let child = null;
|
|
617
|
+
let restarting = false;
|
|
618
|
+
|
|
619
|
+
const startServer = () => {
|
|
620
|
+
child = spawn(process.execPath, [serverPath], {
|
|
621
|
+
stdio: "inherit",
|
|
622
|
+
env: { ...process.env, PORT: String(port) },
|
|
623
|
+
});
|
|
624
|
+
child.on("exit", (code, signal) => {
|
|
625
|
+
if (shuttingDown || restarting) return;
|
|
626
|
+
// the server died on its own (e.g. a port clash it reported to stderr)
|
|
627
|
+
stopWatch();
|
|
628
|
+
process.exit(signal ? 1 : code ?? 0);
|
|
629
|
+
});
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const restartServer = () =>
|
|
633
|
+
new Promise((done) => {
|
|
634
|
+
if (!child) return done();
|
|
635
|
+
restarting = true;
|
|
636
|
+
child.once("exit", () => {
|
|
637
|
+
restarting = false;
|
|
638
|
+
done();
|
|
639
|
+
});
|
|
640
|
+
child.kill("SIGTERM");
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
process.stdout.write(`volaro dev: full-stack app — starting ${relative(process.cwd(), serverPath) || serverPath}\n`);
|
|
644
|
+
startServer();
|
|
645
|
+
|
|
646
|
+
stopWatch = watchVl(watchDir, async () => {
|
|
647
|
+
process.stdout.write("volaro dev: change detected — rebuilding\n");
|
|
648
|
+
const r = buildOnce(entry, out);
|
|
649
|
+
if (!r.ok) {
|
|
650
|
+
process.stdout.write("volaro dev: build failed — server left running on the last good build\n");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
await restartServer();
|
|
654
|
+
startServer();
|
|
655
|
+
process.stdout.write("volaro dev: server restarted\n");
|
|
656
|
+
}, out);
|
|
657
|
+
|
|
658
|
+
const bye = () => {
|
|
659
|
+
if (shuttingDown) return;
|
|
660
|
+
shuttingDown = true;
|
|
661
|
+
stopWatch();
|
|
662
|
+
if (child) child.kill("SIGTERM");
|
|
663
|
+
setTimeout(() => {
|
|
664
|
+
if (child) child.kill("SIGKILL");
|
|
665
|
+
process.exit(0);
|
|
666
|
+
}, 2000).unref();
|
|
667
|
+
if (child) child.once("exit", () => process.exit(0));
|
|
668
|
+
else process.exit(0);
|
|
669
|
+
};
|
|
670
|
+
process.on("SIGINT", bye);
|
|
671
|
+
process.on("SIGTERM", bye);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ---- static single-page: serve the bundle, rebuild on change ----
|
|
676
|
+
const srv = staticServer(out, port);
|
|
677
|
+
process.stdout.write(`volaro dev: serving http://127.0.0.1:${port} (Ctrl+C to stop)\n`);
|
|
678
|
+
|
|
679
|
+
stopWatch = watchVl(watchDir, () => {
|
|
680
|
+
process.stdout.write("volaro dev: change detected — rebuilding\n");
|
|
681
|
+
const r = buildOnce(entry, out);
|
|
682
|
+
process.stdout.write(r.ok ? "volaro dev: rebuilt\n" : "volaro dev: build failed — keeping last good bundle\n");
|
|
683
|
+
}, out);
|
|
684
|
+
|
|
685
|
+
const bye = () => {
|
|
686
|
+
if (shuttingDown) return;
|
|
687
|
+
shuttingDown = true;
|
|
688
|
+
stopWatch();
|
|
689
|
+
srv.close(() => process.exit(0));
|
|
690
|
+
setTimeout(() => process.exit(0), 1000).unref();
|
|
691
|
+
};
|
|
692
|
+
process.on("SIGINT", bye);
|
|
693
|
+
process.on("SIGTERM", bye);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function cmdReference(which) {
|
|
697
|
+
if (which === "spec") {
|
|
698
|
+
// `volaro-language-spec.md` is the whole-language DESIGN — a superset of
|
|
699
|
+
// what this build accepts. `supported.md` is the version-specific
|
|
700
|
+
// shipped-feature guide; `crib.md` is the authoring reference.
|
|
701
|
+
process.stderr.write(
|
|
702
|
+
`note: \`${CLI} spec\` is the full language design — a SUPERSET of this build.\n` +
|
|
703
|
+
` \`${CLI} supported\` is what version ${pkg.version} actually accepts;\n` +
|
|
704
|
+
` \`${CLI} crib\` is the authoring reference.\n\n`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
const file = which === "supported" ? "supported" : which;
|
|
708
|
+
// `supported.md`'s own title carries this build's exact version as a
|
|
709
|
+
// `{{VERSION}}` placeholder rather than a hardcoded string -- found stale
|
|
710
|
+
// (still reading a prior release's version after a version bump, since
|
|
711
|
+
// nothing kept it in sync) in the Test_003 benchmark's packaged-docs
|
|
712
|
+
// finding. Substituted from `package.json` at print time so it can never
|
|
713
|
+
// drift again, the same way the "supported"-vs-"spec" note above already
|
|
714
|
+
// reads `pkg.version` live rather than a copy-pasted string.
|
|
715
|
+
process.stdout.write(read(`language/${file}.md`).replace("{{VERSION}}", pkg.version));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function cmdExample(name) {
|
|
719
|
+
if (name === "sensors" || name === "station") process.stdout.write(read(`examples/${name}.vl`));
|
|
720
|
+
else fail("usage: volaro example sensors|station");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function cmdRoutes(args) {
|
|
724
|
+
let json = false;
|
|
725
|
+
let project = null;
|
|
726
|
+
for (const a of args) {
|
|
727
|
+
if (a === "--json") json = true;
|
|
728
|
+
else if (a.startsWith("-")) fail(`usage: volaro routes [project] [--json]`, 2);
|
|
729
|
+
else if (!project) project = a;
|
|
730
|
+
else fail(`usage: volaro routes [project] [--json]`, 2);
|
|
731
|
+
}
|
|
732
|
+
const pyArgs = [resolvePath(project || ".")];
|
|
733
|
+
if (json) pyArgs.push("--json");
|
|
734
|
+
runPython(VALIDATOR_PP, "vlcheck.routes_cli", pyArgs);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const HELP = `Volaro ${pkg.version} — an application language for AI authoring and human review.
|
|
738
|
+
|
|
739
|
+
USAGE
|
|
740
|
+
volaro <command> [args] (installed as both \`volaro\` and \`vl\`;
|
|
741
|
+
\`volaro\` is the canonical name)
|
|
742
|
+
|
|
743
|
+
COMMANDS
|
|
744
|
+
check <path>... Validate .vl source. Runs the full resolver, not just
|
|
745
|
+
a structural pass. Exit non-zero on any error.
|
|
746
|
+
--benchmark-log <file> append raw timing records as JSONL
|
|
747
|
+
--benchmark-run <id> group attempts into one clean/recovery trial
|
|
748
|
+
build <file.vl> Transpile to a runnable bundle (default: ./build).
|
|
749
|
+
-o <dir> output directory
|
|
750
|
+
--data <file> data.json to bundle
|
|
751
|
+
--release fail on unresolved placeholders (e.g. img alt_todo:)
|
|
752
|
+
and ship the minified runtime
|
|
753
|
+
--benchmark-log <file> append raw timing records as JSONL
|
|
754
|
+
--benchmark-run <id> group attempts into one clean/recovery trial
|
|
755
|
+
dev [file.vl] Build, serve on http://127.0.0.1:5173, and rebuild
|
|
756
|
+
when a .vl file changes. Defaults to ./app.vl. A
|
|
757
|
+
full-stack build runs server.js instead (Node 22.5+)
|
|
758
|
+
and restarts it after each successful rebuild.
|
|
759
|
+
--port <n> dev server port (passed to server.js as $PORT)
|
|
760
|
+
routes [project] Print discovered browser pages (spec §8.15: URL,
|
|
761
|
+
source, layout chain, boundaries) and the
|
|
762
|
+
page-server route manifest (spec §10.2: final URL,
|
|
763
|
+
method, source file, parameter types, generated
|
|
764
|
+
client member, authorization). Project defaults to
|
|
765
|
+
".". Exits non-zero on any collision or project-
|
|
766
|
+
validity error (page/route or layout-composition).
|
|
767
|
+
--json emit the language-neutral manifest as JSON
|
|
768
|
+
crib The authoring reference (write from this).
|
|
769
|
+
supported What version ${pkg.version} actually accepts.
|
|
770
|
+
spec The full language design (a SUPERSET of this build).
|
|
771
|
+
example <name> Print a worked example: sensors | station.
|
|
772
|
+
version Print the version.
|
|
773
|
+
help This message.
|
|
774
|
+
|
|
775
|
+
The compiler is Python and ships inside this package. Set VOLARO_PYTHON to
|
|
776
|
+
choose the interpreter if \`python3\` is not the one you want.
|
|
777
|
+
|
|
778
|
+
SHIPPED SUBSET: single page (no router); \`input\` (full type matrix incl.
|
|
779
|
+
checkbox/radio), \`textarea\`, \`select\`/\`option\`/\`optgroup\`,
|
|
780
|
+
\`fieldset\`/\`legend\`, \`datalist\`, \`button\` (a toggle is \`button pressed:\`,
|
|
781
|
+
aria-pressed; no switch role), \`link\` (no standalone \`label\`); \`if\` as an expression is
|
|
782
|
+
binary; \`match\` is a statement; a module-level \`fn\` cannot be called from a
|
|
783
|
+
view. \`volaro crib\` + \`volaro supported\` are authoritative for this build;
|
|
784
|
+
\`volaro spec\` is the wider design, not what it accepts.
|
|
785
|
+
|
|
786
|
+
Run commands via your project's \`npm run …\` scripts or \`npx --no-install
|
|
787
|
+
volaro …\` inside the project. A bare \`npx volaro …\` from elsewhere may fetch
|
|
788
|
+
the wrong thing.
|
|
789
|
+
|
|
790
|
+
TO AN AI AGENT: run \`volaro crib\` and load its output — that is the complete
|
|
791
|
+
authoring reference. Then write source, run \`volaro check\`, and repair from the
|
|
792
|
+
diagnostics until it is clean.`;
|
|
793
|
+
|
|
794
|
+
// ---- dispatch ------------------------------------------------------------
|
|
795
|
+
|
|
796
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
797
|
+
switch (cmd) {
|
|
798
|
+
case "check":
|
|
799
|
+
cmdCheck(rest);
|
|
800
|
+
break;
|
|
801
|
+
case "build":
|
|
802
|
+
cmdBuild(rest);
|
|
803
|
+
break;
|
|
804
|
+
case "dev":
|
|
805
|
+
cmdDev(rest);
|
|
806
|
+
break;
|
|
807
|
+
case "routes":
|
|
808
|
+
cmdRoutes(rest);
|
|
809
|
+
break;
|
|
810
|
+
case "crib":
|
|
811
|
+
cmdReference("crib");
|
|
812
|
+
break;
|
|
813
|
+
case "spec":
|
|
814
|
+
cmdReference("spec");
|
|
815
|
+
break;
|
|
816
|
+
case "supported":
|
|
817
|
+
cmdReference("supported");
|
|
818
|
+
break;
|
|
819
|
+
case "example":
|
|
820
|
+
cmdExample(rest[0]);
|
|
821
|
+
break;
|
|
822
|
+
case "version":
|
|
823
|
+
case "--version":
|
|
824
|
+
case "-v":
|
|
825
|
+
process.stdout.write(pkg.version + "\n");
|
|
826
|
+
break;
|
|
827
|
+
case undefined:
|
|
828
|
+
case "help":
|
|
829
|
+
case "--help":
|
|
830
|
+
case "-h":
|
|
831
|
+
process.stdout.write(HELP + "\n");
|
|
832
|
+
break;
|
|
833
|
+
default:
|
|
834
|
+
fail(`${CLI}: unknown command "${cmd}". Run \`${CLI} help\`.`, 2);
|
|
34
835
|
}
|