@velarscript/cli 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +117 -0
- package/dist/application-package-host.d.ts +5 -0
- package/dist/application-package-host.d.ts.map +1 -0
- package/dist/application-package-host.js +67 -0
- package/dist/application-package-host.js.map +1 -0
- package/dist/bounded-text.d.ts +2 -0
- package/dist/bounded-text.d.ts.map +1 -0
- package/dist/bounded-text.js +13 -0
- package/dist/bounded-text.js.map +1 -0
- package/dist/browser-process-owner.d.ts +15 -0
- package/dist/browser-process-owner.d.ts.map +1 -0
- package/dist/browser-process-owner.js +151 -0
- package/dist/browser-process-owner.js.map +1 -0
- package/dist/browser-test-runner.d.ts +11 -0
- package/dist/browser-test-runner.d.ts.map +1 -0
- package/dist/browser-test-runner.js +1022 -0
- package/dist/browser-test-runner.js.map +1 -0
- package/dist/build-engine-tool.d.ts +3 -0
- package/dist/build-engine-tool.d.ts.map +1 -0
- package/dist/build-engine-tool.js +34 -0
- package/dist/build-engine-tool.js.map +1 -0
- package/dist/build-staging.d.ts +3 -0
- package/dist/build-staging.d.ts.map +1 -0
- package/dist/build-staging.js +3 -0
- package/dist/build-staging.js.map +1 -0
- package/dist/bundled-application-package-host-registry.d.ts +10 -0
- package/dist/bundled-application-package-host-registry.d.ts.map +1 -0
- package/dist/bundled-application-package-host-registry.js +16 -0
- package/dist/bundled-application-package-host-registry.js.map +1 -0
- package/dist/bundled-extension-registry.d.ts +22 -0
- package/dist/bundled-extension-registry.d.ts.map +1 -0
- package/dist/bundled-extension-registry.js +10 -0
- package/dist/bundled-extension-registry.js.map +1 -0
- package/dist/canonical-path.d.ts +3 -0
- package/dist/canonical-path.d.ts.map +1 -0
- package/dist/canonical-path.js +30 -0
- package/dist/canonical-path.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1274 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +403 -0
- package/dist/config.js.map +1 -0
- package/dist/deployment-verifier.d.ts +34 -0
- package/dist/deployment-verifier.d.ts.map +1 -0
- package/dist/deployment-verifier.js +260 -0
- package/dist/deployment-verifier.js.map +1 -0
- package/dist/dev-server.d.ts +3 -0
- package/dist/dev-server.d.ts.map +1 -0
- package/dist/dev-server.js +464 -0
- package/dist/dev-server.js.map +1 -0
- package/dist/embedded-modules.d.ts +17 -0
- package/dist/embedded-modules.d.ts.map +1 -0
- package/dist/embedded-modules.js +33 -0
- package/dist/embedded-modules.js.map +1 -0
- package/dist/extension-metadata.d.ts +23 -0
- package/dist/extension-metadata.d.ts.map +1 -0
- package/dist/extension-metadata.js +254 -0
- package/dist/extension-metadata.js.map +1 -0
- package/dist/file-integrity.d.ts +7 -0
- package/dist/file-integrity.d.ts.map +1 -0
- package/dist/file-integrity.js +14 -0
- package/dist/file-integrity.js.map +1 -0
- package/dist/framework-host.d.ts +12 -0
- package/dist/framework-host.d.ts.map +1 -0
- package/dist/framework-host.js +46 -0
- package/dist/framework-host.js.map +1 -0
- package/dist/host-error.d.ts +6 -0
- package/dist/host-error.d.ts.map +1 -0
- package/dist/host-error.js +54 -0
- package/dist/host-error.js.map +1 -0
- package/dist/installed-package.d.ts +9 -0
- package/dist/installed-package.d.ts.map +1 -0
- package/dist/installed-package.js +23 -0
- package/dist/installed-package.js.map +1 -0
- package/dist/language-server-bundle-entry.d.ts +2 -0
- package/dist/language-server-bundle-entry.d.ts.map +1 -0
- package/dist/language-server-bundle-entry.js +7 -0
- package/dist/language-server-bundle-entry.js.map +1 -0
- package/dist/language-server-tool.d.ts +3 -0
- package/dist/language-server-tool.d.ts.map +1 -0
- package/dist/language-server-tool.js +30 -0
- package/dist/language-server-tool.js.map +1 -0
- package/dist/language-server.d.ts +9 -0
- package/dist/language-server.d.ts.map +1 -0
- package/dist/language-server.js +1733 -0
- package/dist/language-server.js.map +1 -0
- package/dist/mechanical-fixer.d.ts +30 -0
- package/dist/mechanical-fixer.d.ts.map +1 -0
- package/dist/mechanical-fixer.js +103 -0
- package/dist/mechanical-fixer.js.map +1 -0
- package/dist/module-assets.d.ts +11 -0
- package/dist/module-assets.d.ts.map +1 -0
- package/dist/module-assets.js +90 -0
- package/dist/module-assets.js.map +1 -0
- package/dist/npm.d.ts +23 -0
- package/dist/npm.d.ts.map +1 -0
- package/dist/npm.js +456 -0
- package/dist/npm.js.map +1 -0
- package/dist/official-language-server-extensions.d.ts +2 -0
- package/dist/official-language-server-extensions.d.ts.map +1 -0
- package/dist/official-language-server-extensions.js +38 -0
- package/dist/official-language-server-extensions.js.map +1 -0
- package/dist/official-script-language-service.d.ts +2 -0
- package/dist/official-script-language-service.d.ts.map +1 -0
- package/dist/official-script-language-service.js +10 -0
- package/dist/official-script-language-service.js.map +1 -0
- package/dist/official-tool-assets.d.ts +3 -0
- package/dist/official-tool-assets.d.ts.map +1 -0
- package/dist/official-tool-assets.js +41 -0
- package/dist/official-tool-assets.js.map +1 -0
- package/dist/ownership-graph.d.ts +46 -0
- package/dist/ownership-graph.d.ts.map +1 -0
- package/dist/ownership-graph.js +255 -0
- package/dist/ownership-graph.js.map +1 -0
- package/dist/package-manager.d.ts +20 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/package-manager.js +345 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/packaged-official-tool.d.ts +8 -0
- package/dist/packaged-official-tool.d.ts.map +1 -0
- package/dist/packaged-official-tool.js +32 -0
- package/dist/packaged-official-tool.js.map +1 -0
- package/dist/preview-server.d.ts +11 -0
- package/dist/preview-server.d.ts.map +1 -0
- package/dist/preview-server.js +163 -0
- package/dist/preview-server.js.map +1 -0
- package/dist/production-build.d.ts +57 -0
- package/dist/production-build.d.ts.map +1 -0
- package/dist/production-build.js +332 -0
- package/dist/production-build.js.map +1 -0
- package/dist/production-verifier.d.ts +9 -0
- package/dist/production-verifier.d.ts.map +1 -0
- package/dist/production-verifier.js +268 -0
- package/dist/production-verifier.js.map +1 -0
- package/dist/program-runner.d.ts +7 -0
- package/dist/program-runner.d.ts.map +1 -0
- package/dist/program-runner.js +98 -0
- package/dist/program-runner.js.map +1 -0
- package/dist/project-check.d.ts +31 -0
- package/dist/project-check.d.ts.map +1 -0
- package/dist/project-check.js +95 -0
- package/dist/project-check.js.map +1 -0
- package/dist/project-format.d.ts +6 -0
- package/dist/project-format.d.ts.map +1 -0
- package/dist/project-format.js +21 -0
- package/dist/project-format.js.map +1 -0
- package/dist/project-semantic.d.ts +62 -0
- package/dist/project-semantic.d.ts.map +1 -0
- package/dist/project-semantic.js +1079 -0
- package/dist/project-semantic.js.map +1 -0
- package/dist/project-session.d.ts +36 -0
- package/dist/project-session.d.ts.map +1 -0
- package/dist/project-session.js +339 -0
- package/dist/project-session.js.map +1 -0
- package/dist/project-task-bundle-entry.d.ts +2 -0
- package/dist/project-task-bundle-entry.d.ts.map +1 -0
- package/dist/project-task-bundle-entry.js +18 -0
- package/dist/project-task-bundle-entry.js.map +1 -0
- package/dist/project-task-invocation.d.ts +16 -0
- package/dist/project-task-invocation.d.ts.map +1 -0
- package/dist/project-task-invocation.js +72 -0
- package/dist/project-task-invocation.js.map +1 -0
- package/dist/project-task-tool.d.ts +3 -0
- package/dist/project-task-tool.d.ts.map +1 -0
- package/dist/project-task-tool.js +61 -0
- package/dist/project-task-tool.js.map +1 -0
- package/dist/project.d.ts +71 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +1750 -0
- package/dist/project.js.map +1 -0
- package/dist/reproduction.d.ts +50 -0
- package/dist/reproduction.d.ts.map +1 -0
- package/dist/reproduction.js +321 -0
- package/dist/reproduction.js.map +1 -0
- package/dist/script-language-service.d.ts +82 -0
- package/dist/script-language-service.d.ts.map +1 -0
- package/dist/script-language-service.js +18 -0
- package/dist/script-language-service.js.map +1 -0
- package/dist/source-limits.d.ts +5 -0
- package/dist/source-limits.d.ts.map +1 -0
- package/dist/source-limits.js +19 -0
- package/dist/source-limits.js.map +1 -0
- package/dist/standalone-build.d.ts +15 -0
- package/dist/standalone-build.d.ts.map +1 -0
- package/dist/standalone-build.js +76 -0
- package/dist/standalone-build.js.map +1 -0
- package/dist/standard-modules.d.ts +22 -0
- package/dist/standard-modules.d.ts.map +1 -0
- package/dist/standard-modules.js +3066 -0
- package/dist/standard-modules.js.map +1 -0
- package/dist/static-deployment.d.ts +48 -0
- package/dist/static-deployment.d.ts.map +1 -0
- package/dist/static-deployment.js +142 -0
- package/dist/static-deployment.js.map +1 -0
- package/dist/test-output.d.ts +20 -0
- package/dist/test-output.d.ts.map +1 -0
- package/dist/test-output.js +119 -0
- package/dist/test-output.js.map +1 -0
- package/dist/test-runner.d.ts +13 -0
- package/dist/test-runner.d.ts.map +1 -0
- package/dist/test-runner.js +226 -0
- package/dist/test-runner.js.map +1 -0
- package/dist/typescript-declarations.d.ts +20 -0
- package/dist/typescript-declarations.d.ts.map +1 -0
- package/dist/typescript-declarations.js +1609 -0
- package/dist/typescript-declarations.js.map +1 -0
- package/dist/uncaught-program-error.d.ts +24 -0
- package/dist/uncaught-program-error.d.ts.map +1 -0
- package/dist/uncaught-program-error.js +105 -0
- package/dist/uncaught-program-error.js.map +1 -0
- package/dist/unowned-errors.d.ts +56 -0
- package/dist/unowned-errors.d.ts.map +1 -0
- package/dist/unowned-errors.js +82 -0
- package/dist/unowned-errors.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace-index.d.ts +78 -0
- package/dist/workspace-index.d.ts.map +1 -0
- package/dist/workspace-index.js +493 -0
- package/dist/workspace-index.js.map +1 -0
- package/package.json +45 -0
- package/skill/ai-skill.md +749 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
# The VelarScript AI skill brief
|
|
2
|
+
|
|
3
|
+
This is the language brief for AI agents working in a VelarScript project. It
|
|
4
|
+
is agent-agnostic markdown, it ships inside the toolchain, and `velar skill`
|
|
5
|
+
prints it verbatim — no network, no external documentation. Every code fence
|
|
6
|
+
in this file compiles against the current compiler; the brief cannot drift
|
|
7
|
+
from the language.
|
|
8
|
+
|
|
9
|
+
## What VelarScript is
|
|
10
|
+
|
|
11
|
+
VelarScript (Vel) is an extensible programming language for the AI era where the
|
|
12
|
+
framework is the language: `component`, `state`, and `look` are keywords rather
|
|
13
|
+
than imports, so one checked language covers markup, styling, state, and tests.
|
|
14
|
+
You write the Vel and every later change; the owner reads the result; the
|
|
15
|
+
compiler guards each change. Vel's parents are JavaScript and Python: ask the
|
|
16
|
+
mother about behavior — the program runs inside her — and the father about spelling.
|
|
17
|
+
**Write on your priors, and the compiler will teach you the rest**: where JS
|
|
18
|
+
offers five spellings Vel keeps one, and every removed or mistaken spelling
|
|
19
|
+
gets a diagnostic naming the one current spelling. Expect to self-correct in
|
|
20
|
+
one round.
|
|
21
|
+
|
|
22
|
+
The working loop:
|
|
23
|
+
|
|
24
|
+
1. Write ordinary code the way a fluent JS/Python author would.
|
|
25
|
+
2. Run `velar check`. Do exactly what each diagnostic says.
|
|
26
|
+
3. Run `velar fix` to apply every rewrite the diagnostics already named
|
|
27
|
+
(retired spellings with one successor, line-ending semicolons, and the rest
|
|
28
|
+
of that family); it never rewrites anything that needs a decision, so what
|
|
29
|
+
is left after it is the real work.
|
|
30
|
+
4. Run `velar test`. Run `velar format` to settle layout.
|
|
31
|
+
|
|
32
|
+
Do not invent workarounds for a diagnostic; it is the language telling you
|
|
33
|
+
the canonical spelling.
|
|
34
|
+
|
|
35
|
+
What a program can compute needs no import; what reaches outside the program
|
|
36
|
+
must be imported. A prefix is permanent only when it mirrors a namespace-shaped
|
|
37
|
+
JavaScript global, so there are exactly four and the list is closed:
|
|
38
|
+
`Json.` (`parse`, `tryParse`, `stringify`, `stableStringify`, `clone`,
|
|
39
|
+
`isSerializable`), `Promise.` (`all`, `race`, `sleep`, `timeout`, `retry`,
|
|
40
|
+
`map`, `series`), `Math.` (`pi`, `e`, `tau`, `infinity`, `min`, `max`, `clamp`,
|
|
41
|
+
`sign`, `trunc`, `sqrt`, `cbrt`, `pow`, `exp`, `log`, `log2`, `log10`, `sin`,
|
|
42
|
+
`cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `degrees`, `radians`, `hypot`,
|
|
43
|
+
`random`, `randomInt`, `gcd`, `lcm`), and `Text.` (`trimStart`, `trimEnd`,
|
|
44
|
+
`capitalize`, `title`, `lines`, `lineStarts`, `chunks`, `words`, `slug`,
|
|
45
|
+
`normalize`, `truncate`, `indent`, `dedent`, `normalizeWhitespace`, `utf8Size`,
|
|
46
|
+
`escapeHtml`, `codePoint`, `fromCodePoint`, `matches`, `findMatch`,
|
|
47
|
+
`findMatches`, `replaceMatches`, `splitPattern`). A string method is a core
|
|
48
|
+
operation; `Text.*` is the extension toolbox, and nothing moves between them.
|
|
49
|
+
Web visual builders are named imports from `velar/look` — there is no `Look`
|
|
50
|
+
global in JavaScript, so there is no `Look.` prefix here either. These names
|
|
51
|
+
need no import and cannot be shadowed: `const Text = 1` is rejected. `print`,
|
|
52
|
+
`str`, `number`, `equals`, and `range` are likewise in the Core prelude —
|
|
53
|
+
`equals(a, b)` is the one content-comparison spelling. One roster grants and
|
|
54
|
+
protects these names. Capabilities stay explicit imports. Durations use `ms` or
|
|
55
|
+
`s`, so write `await Promise.sleep(250ms)`, not a bare number.
|
|
56
|
+
|
|
57
|
+
Use checked binary/random/task, manifest Workers, pull WebSockets, Node SQLite,
|
|
58
|
+
Web binary IndexedDB, and official adapters. A direct `for index in range(...):` is a native counter; range as a value is a List. Use `UInt16Buffer` for 16-bit numeric state,
|
|
59
|
+
`UInt8Buffer` for compact data, and bounded `UInt32Builder`/`Float32Builder` values for variable-size numeric output.
|
|
60
|
+
|
|
61
|
+
## Project setup
|
|
62
|
+
|
|
63
|
+
A VelarScript project is a directory containing a `velar.json` manifest. Let
|
|
64
|
+
the toolchain write it — `velar create my-app` scaffolds the Web template, and
|
|
65
|
+
`--template node|desktop|docs|library|component` picks another. Each writes
|
|
66
|
+
`velar.json`, a `package.json` whose scripts are the gates, a `src/` tree, a
|
|
67
|
+
passing test, and an `AGENTS.md`.
|
|
68
|
+
|
|
69
|
+
Writing the manifest yourself: `formatVersion` is required, `extensions` may be
|
|
70
|
+
omitted by a project that loads none, `entry` defaults to `src/main.vel`,
|
|
71
|
+
`outDir` to `dist`, and `publicDir` to `public`.
|
|
72
|
+
|
|
73
|
+
A Core project (CLI, library, Node) loads no extensions:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"formatVersion": 2,
|
|
78
|
+
"entry": "src/main.vel",
|
|
79
|
+
"extensions": []
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A Web project activates the Web extension **by package name**. That one line is
|
|
84
|
+
what turns on `component`, JSX, `state`, `computed`, `resource`, `action`,
|
|
85
|
+
`watch`, `look`, and `mount`; without it `component` is an unknown declaration
|
|
86
|
+
keyword and every JSX token is a parse error:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"formatVersion": 2,
|
|
91
|
+
"entry": "src/main.vel",
|
|
92
|
+
"extensions": ["@velarscript/web"]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
An extension owns its own manifest key — `"web": {"title": "My App"}` sets the
|
|
97
|
+
document title. Anything else in `velar.json` is rejected by name.
|
|
98
|
+
|
|
99
|
+
**Files the toolchain owns.** `dist/` is build output, and in a Web build the
|
|
100
|
+
toolchain writes `dist/index.html` itself — the title comes from `web.title`, the
|
|
101
|
+
favicon from `web.icon` (a `publicDir`-relative `.svg`/`.png`/`.ico` path; the
|
|
102
|
+
build fails if no such file exists), the
|
|
103
|
+
mount host is `<div id="app"></div>`, and assets are content-hashed. Never author
|
|
104
|
+
that file; a `public/index.html` is overwritten by the generated one, while
|
|
105
|
+
everything else in `public/` is copied through. `.velar/` is scratch; both belong in `.gitignore`.
|
|
106
|
+
|
|
107
|
+
**Tests.** `velar test` finds every `*.test.vel` file under the project (skipping
|
|
108
|
+
`outDir` and `publicDir`) and runs its `test "name":` blocks. The name is a
|
|
109
|
+
sentence about the code, quoted verbatim by the reporter and unique in its
|
|
110
|
+
module; the body may `await` directly and needs no `export`. A file that declares no tests is a failure rather than a skip.
|
|
111
|
+
`velar test --browser` runs `*.browser.test.vel` in a real browser — and bare
|
|
112
|
+
`--browser` is **Chromium only**. Three engines is `--browser=all` (or
|
|
113
|
+
`--browser all`), which is what any cross-browser claim costs;
|
|
114
|
+
`--browser=firefox` and `--browser=webkit` pick one.
|
|
115
|
+
|
|
116
|
+
**Separate the mounted entrypoint from testable code.** A test runs in Node with
|
|
117
|
+
no DOM, so a headless test that imports the module calling `mount` fails on
|
|
118
|
+
`document`. Keep the entry trivial and put everything worth testing in modules it
|
|
119
|
+
imports:
|
|
120
|
+
|
|
121
|
+
```velar fragment
|
|
122
|
+
// src/main.vel — the mounted entrypoint; no test imports this file
|
|
123
|
+
import {App} from "./app.vel"
|
|
124
|
+
|
|
125
|
+
mount(<App />, "#app")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Components, functions, and types live in `src/app.vel` and its neighbours;
|
|
129
|
+
`src/app.test.vel` tests those exports headlessly and
|
|
130
|
+
`src/app.browser.test.vel` drives the mounted application.
|
|
131
|
+
|
|
132
|
+
**A browser test drives the page; it does not build one.** Its body runs in the
|
|
133
|
+
test process against a page already running the production build, so `mount`,
|
|
134
|
+
JSX, and `document` are unavailable there — the driving surface is
|
|
135
|
+
`velar/web-test`, and selectors are ordinary CSS:
|
|
136
|
+
|
|
137
|
+
```velar fragment
|
|
138
|
+
import {expect} from "velar/test"
|
|
139
|
+
import {browser, localStorage} from "velar/web-test"
|
|
140
|
+
|
|
141
|
+
test "adding a link shows it in the list":
|
|
142
|
+
await browser.open("/")
|
|
143
|
+
await browser.fill("#title", "Vel")
|
|
144
|
+
await browser.click("#add")
|
|
145
|
+
await browser.waitForText("[data-item]", "Vel")
|
|
146
|
+
expect(await browser.text("[data-count]")).toBe("1")
|
|
147
|
+
expect(await localStorage.get("reading")).toBe(`[{"title":"Vel"}]`)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`browser` also carries `reload`, `press`, `select`, `attribute`, `count`,
|
|
151
|
+
`visible`, `waitFor`, `currentPath`, and `viewport`; `localStorage` and
|
|
152
|
+
`sessionStorage` read and write the page's raw storage. Any unhandled error —
|
|
153
|
+
in the page or in the test process — fails the test.
|
|
154
|
+
|
|
155
|
+
`velar/web-test` may be imported **only** from a `*.browser.test.vel` module.
|
|
156
|
+
Reaching for it from application code, from a plain `*.test.vel`, through
|
|
157
|
+
`import js`, or through a re-export is rejected on the import line — rename the
|
|
158
|
+
module or move the browser test into one of its own. Application code that
|
|
159
|
+
needs the page reaches it through `velar/browser` instead.
|
|
160
|
+
|
|
161
|
+
## The traps your reflexes will hit
|
|
162
|
+
|
|
163
|
+
Everything in this table was hit by real models writing Vel blind. All but
|
|
164
|
+
the first two rows produce a teaching diagnostic, so `velar check` will catch
|
|
165
|
+
them; the first two are the **silent traps** in the list — read them twice.
|
|
166
|
+
|
|
167
|
+
| Your reflex | Write instead |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| `"${value}"` or `` `${value}` `` template interpolation | `f"{value}"` or `` f`{value}` ``. Only the `f` prefix interpolates, in either delimiter; `${...}` is legal literal text in every string — including a backtick one — so nothing warns you. |
|
|
170
|
+
| `a // b` floor division | `//` starts a comment, so the rest of the line disappears and `const c = a // b` silently binds `a`. Write `(a / b).floor()`. |
|
|
171
|
+
| `# comment` | `// comment` (`///` documents the following declaration). |
|
|
172
|
+
| `function f(...)`, `fn f(...)` | `def f(...) -> Result:` |
|
|
173
|
+
| `interface X:`, `record X:`, `struct X:` | `type X:` — one keyword for record shapes and aliases. |
|
|
174
|
+
| `items.length` | `items.size` (also on strings, Sets, Maps). |
|
|
175
|
+
| `items.push(x)` | `items.append(x)`. There is no `splice`/`shift`/`unshift`/mutating `sort`; use `insert`, `pop`, `remove`, `extend`, and the copying `sorted()`/`reversed()`. |
|
|
176
|
+
| `if value:` truthiness | Conditions accept only `bool`/`bool?`. Test presence explicitly: `if value != null:`. |
|
|
177
|
+
| `value is null` | `value == null` / `value != null` — `is` tests runtime types, `null` is a value. |
|
|
178
|
+
| `switch`, or an `if`/`else if` ladder over an enum | `match` with `case _:` as the only fallback. |
|
|
179
|
+
| Renaming a binding away from `type`, `state`, `from`, `match`, `as`, `action`, `resource`, `watch`, `look`, `component` | Don't. Declaration words are contextual: each declares only in its own shape, so `const {type, from} = event` and `const state = "ready"` are ordinary code in Core and Web alike. `enum` and `case` are the exceptions — `enum` is a real VelarScript keyword, `case` is reserved by JavaScript — so neither can be a binding name; both stay fine as record fields, member names, and `match` branches. |
|
|
180
|
+
| A component's `mounted:` / `cleanup:` block | `@mounted:` / `@cleanup:`. Lifecycle hooks live in the language's `@` namespace, which is why a component can also declare its own `def mounted()`. |
|
|
181
|
+
| Two statements on one line | One statement per line; there are no semicolons. A line starting with `.` or `?.` continues the previous line, so method chains format normally. |
|
|
182
|
+
| `count++` | `count += 1` |
|
|
183
|
+
| `call(name: value)` named argument | `call(name=value)` |
|
|
184
|
+
| Importing `range` | `range(...)` is a Core prelude function and needs no import. |
|
|
185
|
+
| `import {sqrt} from "velar/math"` | `Math.sqrt(x)`; `Math.` is permanent. `velar fix` performs the rewrite. |
|
|
186
|
+
| `Look.spacing(16px)` | `spacing(16px)` with `import {spacing} from "velar/look"`; the `Look.` prefix is retired. `velar fix` performs the rewrite. |
|
|
187
|
+
| `"""triple-quoted"""` for a block of text | A layout string: a double quote followed immediately by a newline opens it; a quote back at the opening line's indentation closes it. Backtick strings are real, but always single-line. |
|
|
188
|
+
| Escaping `\"` through a JSON, HTML, or selector string | Use backticks: `` `{"name":"Nova"}` `` is the same `string` value, with `"` as ordinary text. Prefixes are orthogonal (`` f` ``, `` r` ``, `` rf` ``), and `velar format` picks the delimiter for you (`"` by default, backticks when the text contains `"`), so write whichever is convenient. |
|
|
189
|
+
| `007`, `.5` | Write `7`, `0.5`. Explicit `0xFF`, `0b1010`, and `0o17` integers are supported; legacy leading-zero octal is not. Group long digits with `_` — `1_000_000`. `Infinity` and `NaN` are not literals: write `1 / 0` and `0 / 0`. |
|
|
190
|
+
| `a == b == c` | Equality never chains: `a == b and b == c`. Ordered chains work but must point one way — `0 < index <= size` is fine, `a < b > c` is not. An `in` or `is` test inside a comparison needs parentheses. |
|
|
191
|
+
| A line that is only a value — `x == 5`, `items[0]`, `"a note"` | A statement must do something: call, assign, `await`, or `async`. A computed-and-discarded value is a compile error, and a bare string is not a docstring — use `//`. |
|
|
192
|
+
| A block comment that starts or ends beside code on a multi-line span | `/* */` exists and nests — commenting out a region that already holds a comment works — but a multi-line one takes whole lines: only `/*` on its opening line, only `*/` on its closing line. Within a single line it can sit anywhere: `call(/* why */ value)`. |
|
|
193
|
+
| `x if cond else y` | `cond ? x : y` |
|
|
194
|
+
| `&&`, `\|\|`, `!`, `===`, `var`, `elif`, `None`, `undefined` | `and`, `or`, `not`, `==`, `let`/`const`, `else if`, `null`, `null`. |
|
|
195
|
+
| `f"{user}"` or `str(user)` on a record | Text conversion accepts strings, numbers, bools, enums, and `null` only. `print(user)` inspects a value; permanent `Json.stringify(user)` builds data text without an import. |
|
|
196
|
+
| Calling an async function and moving on | A dropped Promise is a compile error. `await task()` to wait; `async task()` to run it detached. |
|
|
197
|
+
| `flag or name ?? fallback` | Parenthesize — `??` never shares an unparenthesized chain with `and`/`or`. |
|
|
198
|
+
| `onClick={handler}` | `on:click={handler}`; form binding is `bind:value={state}`. |
|
|
199
|
+
| Implicit `{props.children}` | Declare it: a `children: WebNode` prop receives the JSX tag body. |
|
|
200
|
+
| `map[key]` reads | `map.get(key)` returns `T?`. On Lists, `[index]` throws on a bug; `.get(index)` returns `null` when absence is an expected answer. |
|
|
201
|
+
| `[...text]` or `list(text)` for characters | `text.split("")` — the empty separator splits per Unicode code point. |
|
|
202
|
+
| `x !== x` or `Number.isNaN(x)` | Number predicates are members: `x.isNaN()`, `x.isFinite()`, `x.isInteger()`. `NaN == NaN` is `true` — equality is SameValueZero. |
|
|
203
|
+
| `text.trim().size == 0` blank test | `text.isBlank()` — `true` for empty or whitespace-only text. |
|
|
204
|
+
| `while true:` plus a `pop()` null check to drain a List | `pop(index=-1)` returns `T` and throws `IndexError` when empty or out of range, so drain with `while items.size > 0:`. |
|
|
205
|
+
| `1 == "1"`, `user == "a"`, `A.member == B.member`, `raw == Kind.member` | `==`/`!=` require the operand types to intersect. Compare enums with `Kind.parse(raw) == Kind.member` when the text must name a member — `parse` throws otherwise — or `str(Kind.member) == raw` when unknown values must be ignored, as on an open wire protocol. `value == null` on an optional is always fine. |
|
|
206
|
+
| `[1, 2] == [1, 2]` content comparison | Collection `==` is identity; `equals(a, b)` compares data deeply (Lists ordered, Sets/Maps by members, SameValueZero leaves) with no import. |
|
|
207
|
+
| Iterating or spreading an enum object | `Status.values()` returns the members in declaration order as a fresh `List<Status>`. |
|
|
208
|
+
| `sorted()`, `min()`, or `sorted(by=)` over enums | Only `number`, `string`, and single-category unions are ordered. Give the order explicitly with `sorted(by=row => row.rank)` or a string-backed enum (`low = "1-low"`). |
|
|
209
|
+
|
|
210
|
+
The long tail is deliberately not in this table: the diagnostic will name
|
|
211
|
+
the current spelling when you hit it.
|
|
212
|
+
|
|
213
|
+
## Declarations at a glance
|
|
214
|
+
|
|
215
|
+
Functions are `def`, with typed parameters, defaults, and `name=value` calls:
|
|
216
|
+
|
|
217
|
+
```velar
|
|
218
|
+
def formatName(name: string, prefix: string = "@") -> string:
|
|
219
|
+
return f"{prefix}{name}"
|
|
220
|
+
|
|
221
|
+
print(formatName("ada"))
|
|
222
|
+
print(formatName("ada", prefix="#"))
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
A generic body that must order, interpolate, or serialize its type parameter
|
|
226
|
+
names a bound — `def label<T: Text>(value: T)`; the diagnostic names the one
|
|
227
|
+
you need.
|
|
228
|
+
|
|
229
|
+
**There are two arrows and they are not interchangeable.** `=>` is the
|
|
230
|
+
value-level arrow: it introduces a lambda body. `->` is the type-level arrow:
|
|
231
|
+
it names a result. They stand next to each other most often on a callback prop
|
|
232
|
+
— the type is written with `->`, the value handed to it with `=>`:
|
|
233
|
+
|
|
234
|
+
```velar
|
|
235
|
+
type Transform = (value: number) -> number
|
|
236
|
+
|
|
237
|
+
const double: Transform = value => value * 2
|
|
238
|
+
|
|
239
|
+
component Stepper(step: number, onChange: (next: number) -> null):
|
|
240
|
+
return <button type="button" on:click={() => onChange(step + 1)}>+1</button>
|
|
241
|
+
|
|
242
|
+
mount(<Stepper step={double(1)} onChange={next => print(next)} />, "#app")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Parameter names in a function type are optional but worth writing:
|
|
246
|
+
`(title: string, author: string) -> Promise<null>` says at the call site what
|
|
247
|
+
`(string, string) -> Promise<null>` makes you guess. An async callback's
|
|
248
|
+
**type** names the Promise, while an `async def` **declaration** annotates the
|
|
249
|
+
resolved value — `async def loadUser(id: string) -> User` satisfies
|
|
250
|
+
`(id: string) -> Promise<User>`.
|
|
251
|
+
|
|
252
|
+
`type` declares record shapes and aliases, `T?` is optional, and every record
|
|
253
|
+
type carries a runtime validator for untrusted data:
|
|
254
|
+
|
|
255
|
+
```velar
|
|
256
|
+
type User:
|
|
257
|
+
id: string
|
|
258
|
+
name: string
|
|
259
|
+
avatar: string?
|
|
260
|
+
|
|
261
|
+
type UserId = string
|
|
262
|
+
|
|
263
|
+
def load(untrusted: unknown) -> User:
|
|
264
|
+
return User.parse(untrusted)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`enum` declares finite string-backed states; a member may map an external
|
|
268
|
+
wire spelling without losing its nominal identity:
|
|
269
|
+
|
|
270
|
+
```velar
|
|
271
|
+
enum Status:
|
|
272
|
+
pending
|
|
273
|
+
active
|
|
274
|
+
done
|
|
275
|
+
|
|
276
|
+
enum ProviderEventKind:
|
|
277
|
+
textDelta = "response.output_text.delta"
|
|
278
|
+
completed = "response.completed"
|
|
279
|
+
|
|
280
|
+
const status: Status = Status.active
|
|
281
|
+
print(ProviderEventKind.textDelta)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Classes use typed body fields, one explicit constructor, and explicit `self`;
|
|
285
|
+
instances are called directly, without `new`:
|
|
286
|
+
|
|
287
|
+
```velar
|
|
288
|
+
class Session:
|
|
289
|
+
let active: bool = true
|
|
290
|
+
|
|
291
|
+
constructor(const id: string):
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
def close():
|
|
295
|
+
self.active = false
|
|
296
|
+
|
|
297
|
+
@dispose:
|
|
298
|
+
self.close()
|
|
299
|
+
|
|
300
|
+
const session = Session("session-1")
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`@name` members belong to the language and can never collide with yours.
|
|
304
|
+
`@dispose:` is the release contract — never called directly — that
|
|
305
|
+
`using name = expression` runs on every exit from the owning scope (block end,
|
|
306
|
+
`return`, `break`, `continue`, throw), in reverse declaration order. A derived
|
|
307
|
+
`@dispose:` adds to its base's; the compiler runs derived first, then base.
|
|
308
|
+
Standard handles already have it, so `using watcher = await watchFiles(path)`
|
|
309
|
+
above an `async for` needs no `try`/`finally`. An owned value may not leave its
|
|
310
|
+
scope: `return handle`, storing it outside, or capturing it in a closure that
|
|
311
|
+
escapes are rejected — return the data you read from it, or move the `using` up
|
|
312
|
+
to the scope that really owns it. A JavaScript handle is owned by composition:
|
|
313
|
+
hold it in a field of a class whose `@dispose:` releases it.
|
|
314
|
+
|
|
315
|
+
Components (Web extension) return JSX directly — there is no `render` block.
|
|
316
|
+
`state` holds a fact, `computed name = ...` derives and is read bare, `action`
|
|
317
|
+
performs a user operation with reactive `pending`/`error`:
|
|
318
|
+
|
|
319
|
+
```velar
|
|
320
|
+
component Counter(label: string):
|
|
321
|
+
state count = 0
|
|
322
|
+
computed caption = f"{label}: {count}"
|
|
323
|
+
|
|
324
|
+
action reset():
|
|
325
|
+
count = 0
|
|
326
|
+
|
|
327
|
+
def bump():
|
|
328
|
+
count += 1
|
|
329
|
+
|
|
330
|
+
return <section>
|
|
331
|
+
<button type="button" on:click={bump}>{caption}</button>
|
|
332
|
+
<button type="button" disabled={reset.pending} on:click={reset}>Reset</button>
|
|
333
|
+
</section>
|
|
334
|
+
|
|
335
|
+
mount(<Counter label="Clicks" />, "#app")
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
`look` is the checked visual language — a value, composed per element with
|
|
339
|
+
`look={...}`. CSS keywords are quoted strings; property names are real DOM
|
|
340
|
+
camelCase; units are literal:
|
|
341
|
+
|
|
342
|
+
```velar
|
|
343
|
+
import {border, rgb, spacing} from "velar/look"
|
|
344
|
+
|
|
345
|
+
const buttonLook = look:
|
|
346
|
+
border = border(0px, rgb(220, 224, 235))
|
|
347
|
+
borderRadius = 10px
|
|
348
|
+
padding = spacing(10px, 14px)
|
|
349
|
+
cursor = "pointer"
|
|
350
|
+
|
|
351
|
+
if @hover:
|
|
352
|
+
background = rgb(235, 240, 255)
|
|
353
|
+
|
|
354
|
+
component SaveButton(children: WebNode):
|
|
355
|
+
return <button look={buttonLook} type="button">{children}</button>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
A `look:` literal is built once, so its conditions and values cannot read state; put a reactive visual on the element with `look={active ? a : b}` or `look:color={...}`. Declare checked motion as a module-level `keyframes:` value and pass it to `animate` from `velar/look`; disable nonessential motion at the CSS layer:
|
|
359
|
+
|
|
360
|
+
```velar
|
|
361
|
+
import {animate} from "velar/look"
|
|
362
|
+
|
|
363
|
+
const spin = keyframes:
|
|
364
|
+
from:
|
|
365
|
+
rotate = 0deg
|
|
366
|
+
to:
|
|
367
|
+
rotate = 1turn
|
|
368
|
+
|
|
369
|
+
const rotatingLook = look:
|
|
370
|
+
if not motion.reduced:
|
|
371
|
+
animation = animate(spin, 1s, easing="linear", loop=true)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
The `animation` property accepts only `Animation`, `List<Animation>`, or `null`; a CSS animation string is rejected. Bind a changing animation on the element with `look:animation={active ? animate(spin, 1s) : null}`. Native animation longhands remain outside Look because `animate` owns the checked contract.
|
|
375
|
+
|
|
376
|
+
Form state binds with `bind:value={name}` (also a writable path such as `bind:value={form.email}`), `bind:checked={flag}`, and `bind:group={choice}` — radio state holds the selected input's `value`, checkbox `List<string>` state holds the checked values; the event object has no `target`.
|
|
377
|
+
|
|
378
|
+
## The idioms
|
|
379
|
+
|
|
380
|
+
These are the canonical shapes, distilled from the project's best-practices
|
|
381
|
+
canon. When two spellings both work, use the one shown here.
|
|
382
|
+
|
|
383
|
+
### Model the data first
|
|
384
|
+
|
|
385
|
+
Give every finite state an enum, every shape a `type`, and return multiple
|
|
386
|
+
values as a named record with shorthand fields — never an out-parameter:
|
|
387
|
+
|
|
388
|
+
```velar
|
|
389
|
+
type TextMeasure:
|
|
390
|
+
lines: number
|
|
391
|
+
words: number
|
|
392
|
+
|
|
393
|
+
def measure(text: string) -> TextMeasure:
|
|
394
|
+
const lines = text.split("\n").size
|
|
395
|
+
const words = text.split(" ").size
|
|
396
|
+
return {lines, words}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Null discipline
|
|
400
|
+
|
|
401
|
+
One spelling per job: test presence with `!= null`, default with `??`,
|
|
402
|
+
reach through maybes with `?.`, and narrow once — then use the value
|
|
403
|
+
directly:
|
|
404
|
+
|
|
405
|
+
```velar
|
|
406
|
+
type Ticket:
|
|
407
|
+
title: string
|
|
408
|
+
assignee: string?
|
|
409
|
+
|
|
410
|
+
def notifyLine(ticket: Ticket) -> string:
|
|
411
|
+
if ticket.assignee != null:
|
|
412
|
+
return f"notify {ticket.assignee}"
|
|
413
|
+
return f"unassigned: {ticket.title}"
|
|
414
|
+
|
|
415
|
+
def owner(ticket: Ticket) -> string:
|
|
416
|
+
return ticket.assignee ?? "unassigned"
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Chains over cursors
|
|
420
|
+
|
|
421
|
+
Collection work is method chains; a hand-advanced index loop is the
|
|
422
|
+
exception for genuine cursor algorithms. Need the position? Use the
|
|
423
|
+
two-slot loop, never a shadow counter. Three or more `or value == ...`
|
|
424
|
+
comparisons want a `Set`:
|
|
425
|
+
|
|
426
|
+
```velar
|
|
427
|
+
type Task:
|
|
428
|
+
title: string
|
|
429
|
+
priority: number
|
|
430
|
+
done: bool
|
|
431
|
+
|
|
432
|
+
const tasks: List<Task> = []
|
|
433
|
+
const titles = tasks
|
|
434
|
+
.filter(task => not task.done)
|
|
435
|
+
.sorted(by=task => task.priority)
|
|
436
|
+
.map(task => task.title)
|
|
437
|
+
|
|
438
|
+
for title, index in titles:
|
|
439
|
+
print(f"{index}: {title}")
|
|
440
|
+
|
|
441
|
+
const closingWords = Set(["done", "closed", "resolved"])
|
|
442
|
+
|
|
443
|
+
def isClosing(word: string) -> bool:
|
|
444
|
+
return word in closingWords
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
One-liners worth knowing: `values.flatMap(x => x.parts)` maps then flattens
|
|
448
|
+
one level; `values.filter(x => x != null)` — exactly that predicate — drops
|
|
449
|
+
absences and narrows `List<T?>` to `List<T>`; Sets combine with the copying
|
|
450
|
+
algebra `a.union(b)`, `a.intersection(b)`, `a.difference(b)`.
|
|
451
|
+
|
|
452
|
+
Mutate state directly — deep reactivity is the default, so rebuild-the-list
|
|
453
|
+
spellings are noise: `tasks.append(task)`, `tasks[0].done = true`,
|
|
454
|
+
`panel.open = not panel.open`.
|
|
455
|
+
|
|
456
|
+
### Dispatch with match
|
|
457
|
+
|
|
458
|
+
A finite state fans out through one `match` with `case _:` as the only
|
|
459
|
+
fallback:
|
|
460
|
+
|
|
461
|
+
```velar
|
|
462
|
+
enum Phase:
|
|
463
|
+
todo
|
|
464
|
+
doing
|
|
465
|
+
done
|
|
466
|
+
|
|
467
|
+
def advance(phase: Phase) -> Phase:
|
|
468
|
+
match phase:
|
|
469
|
+
case Phase.todo:
|
|
470
|
+
return Phase.doing
|
|
471
|
+
case Phase.doing:
|
|
472
|
+
return Phase.done
|
|
473
|
+
case _:
|
|
474
|
+
return Phase.todo
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Small functions, guard first
|
|
478
|
+
|
|
479
|
+
Handle empty and missing cases with early returns, then write the happy
|
|
480
|
+
path unindented. Contracts are one `assert condition else "message"` per
|
|
481
|
+
rule at the top:
|
|
482
|
+
|
|
483
|
+
```velar
|
|
484
|
+
def firstLine(text: string) -> string:
|
|
485
|
+
assert text.size <= 1000000 else "Text is beyond the supported size"
|
|
486
|
+
if text == "":
|
|
487
|
+
return ""
|
|
488
|
+
return text.split("\n")[0]
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Callbacks stay arrows while they are one expression; promote two-statement
|
|
492
|
+
logic to a named `def`. Name arguments where a bare value would read as a
|
|
493
|
+
mystery: `buttonLook(dangerous=true)`, never `buttonLook(true)`.
|
|
494
|
+
|
|
495
|
+
### Strings
|
|
496
|
+
|
|
497
|
+
Build text with f-strings — numbers, bools, enums, and Web unit values with a declared text form interpolate directly.
|
|
498
|
+
Data becomes text through permanent `Json.stringify`. Multi-line text is
|
|
499
|
+
a layout string, not a stack of `\n` escapes. Text that contains `"` — a JSON
|
|
500
|
+
fixture, a quoted selector — goes in backticks instead of being escaped:
|
|
501
|
+
|
|
502
|
+
```velar
|
|
503
|
+
const count = 3
|
|
504
|
+
const gap = 16px
|
|
505
|
+
const summary = f"{count} open tickets"
|
|
506
|
+
const fixture = `{"open":3,"state":"ready"}`
|
|
507
|
+
const gapLabel = f"gap: {gap}"
|
|
508
|
+
const usage = "
|
|
509
|
+
velar check
|
|
510
|
+
velar test
|
|
511
|
+
"
|
|
512
|
+
print(summary)
|
|
513
|
+
print(fixture)
|
|
514
|
+
print(gapLabel)
|
|
515
|
+
print(usage)
|
|
516
|
+
print(Json.stringify({open: count}))
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Components: four cells, one job each
|
|
520
|
+
|
|
521
|
+
`state` holds a fact. `computed` derives from facts. `resource` loads async
|
|
522
|
+
data. `action` performs a user operation. Read a resource as
|
|
523
|
+
`value != null`; render nothing with `null`; key dynamic children:
|
|
524
|
+
|
|
525
|
+
```velar fragment
|
|
526
|
+
component TicketPanel(id: string):
|
|
527
|
+
state draft = ""
|
|
528
|
+
resource ticket: Ticket = loadTicket(id)
|
|
529
|
+
computed heading = ticket.value?.title ?? "Loading"
|
|
530
|
+
|
|
531
|
+
action save():
|
|
532
|
+
await saveDraft(id, draft)
|
|
533
|
+
|
|
534
|
+
watch id:
|
|
535
|
+
async ticket.reload()
|
|
536
|
+
|
|
537
|
+
return <section>
|
|
538
|
+
<h2>{heading}</h2>
|
|
539
|
+
<textarea bind:value={draft}></textarea>
|
|
540
|
+
<button disabled={save.pending} on:click={save}>Save</button>
|
|
541
|
+
</section>
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Lifecycle is two sibling blocks in the language's own `@` namespace —
|
|
545
|
+
`@mounted:` runs once after the DOM is inserted and may `await`; `@cleanup:`
|
|
546
|
+
runs once before the component is destroyed and is synchronous:
|
|
547
|
+
|
|
548
|
+
```velar fragment
|
|
549
|
+
component Chart(points: List<number>):
|
|
550
|
+
let canvas: CanvasElement? = null
|
|
551
|
+
|
|
552
|
+
@mounted:
|
|
553
|
+
if canvas != null:
|
|
554
|
+
drawChart(canvas, points)
|
|
555
|
+
|
|
556
|
+
@cleanup:
|
|
557
|
+
releaseChart()
|
|
558
|
+
|
|
559
|
+
return <canvas ref={canvas}></canvas>
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**A resource loads once, at mount, and does not refetch when its inputs
|
|
563
|
+
change** — a new `id` prop leaves the old data on screen. "Refetch when the
|
|
564
|
+
input changes" is the `watch` above: watch the input, and start `reload()` with
|
|
565
|
+
the detached `async` statement, because a watch body is synchronous. `reload()`
|
|
566
|
+
re-evaluates the initializer against the current inputs, keeps the last value if
|
|
567
|
+
it fails, and puts the failure in `error`. Actions do not queue either: two
|
|
568
|
+
clicks run two calls, `pending` means any call is active, so guard with
|
|
569
|
+
`disabled={save.pending}`.
|
|
570
|
+
|
|
571
|
+
Conditional rendering is an ordinary expression — there are no magic JSX
|
|
572
|
+
control-flow attributes:
|
|
573
|
+
|
|
574
|
+
```velar fragment
|
|
575
|
+
component Panel:
|
|
576
|
+
return <section>
|
|
577
|
+
{loading ? <p aria-busy="true">Loading…</p> : <Results items={items} />}
|
|
578
|
+
<ul>
|
|
579
|
+
{items.map(item => <li key={item.id}>{item.title}</li>)}
|
|
580
|
+
</ul>
|
|
581
|
+
</section>
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Component props are live reactive inputs and their data is mutable by default.
|
|
585
|
+
Writing `item.title = next` through a prop publishes through the same deep
|
|
586
|
+
reactive path as writing the source state. Write `items: readonly List<Item>`
|
|
587
|
+
when the component author deliberately wants a read-only contract; that
|
|
588
|
+
explicit view travels into helpers and nested data without copying or freezing.
|
|
589
|
+
|
|
590
|
+
`velar/storage` persists JSON and validates on the way back in, so a read needs
|
|
591
|
+
a **named** runtime type — a primitive or generic spelling is a type, not a
|
|
592
|
+
value:
|
|
593
|
+
|
|
594
|
+
```velar fragment
|
|
595
|
+
import {storage} from "velar/storage"
|
|
596
|
+
|
|
597
|
+
type SavedItem:
|
|
598
|
+
title: string
|
|
599
|
+
|
|
600
|
+
type SavedItems = List<SavedItem>
|
|
601
|
+
|
|
602
|
+
const items = storage.get("reading", SavedItems, [])
|
|
603
|
+
storage.set("reading", items)
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
The third argument is the fallback for missing or invalid data; `session` and
|
|
607
|
+
`database(name)` from the same module carry the same shape.
|
|
608
|
+
|
|
609
|
+
### Errors and async
|
|
610
|
+
|
|
611
|
+
Throw `Error` (or a subclass) with a message that names the broken rule.
|
|
612
|
+
An error's class is its only classification — discriminate with
|
|
613
|
+
`if error is FileNotFoundError:` and read `error.code` (the declared class
|
|
614
|
+
name) only when the identity must survive a log or JSON boundary. The nameable
|
|
615
|
+
capability classes need no import: `FileNotFoundError`, `PermissionError`,
|
|
616
|
+
`NotADirectoryError`, `FileExistsError`, `AddressInUseError`, plus
|
|
617
|
+
`ValidationError`, `NarrowingError`, and `IndexError`.
|
|
618
|
+
Validate untrusted data at the boundary with `Type.parse`, then trust the
|
|
619
|
+
types inward. `await` every call whose result or completion you depend on:
|
|
620
|
+
|
|
621
|
+
```velar
|
|
622
|
+
type Config:
|
|
623
|
+
baseUrl: string
|
|
624
|
+
|
|
625
|
+
def parseConfig(raw: unknown) -> Config:
|
|
626
|
+
const config = Config.parse(raw)
|
|
627
|
+
if config.baseUrl == "":
|
|
628
|
+
throw Error("Config requires a non-empty baseUrl")
|
|
629
|
+
return config
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
An expected failure is an optional, not a block: `try expression` produces
|
|
633
|
+
`null` when anything in the chain throws, and its result must be consumed —
|
|
634
|
+
`const settings = try Settings.parse(raw) ?? defaults`. Use `try`/`catch` when the details matter.
|
|
635
|
+
|
|
636
|
+
### Modules
|
|
637
|
+
|
|
638
|
+
Export and import by name; a package's public face is a barrel of explicit
|
|
639
|
+
re-exports — `export {measure, firstLine} from "./text.vel"`. There is no
|
|
640
|
+
`import type`: Vel does not erase types, so a type carries its runtime
|
|
641
|
+
validator and a type import is an ordinary import.
|
|
642
|
+
|
|
643
|
+
## When Vel is in your way
|
|
644
|
+
|
|
645
|
+
Vel maintains checked exits so a missing capability never strands the
|
|
646
|
+
project. In order:
|
|
647
|
+
|
|
648
|
+
1. **Missing stdlib capability or a third-party npm package** — declare a
|
|
649
|
+
checked boundary with `extern module`; it is the first choice:
|
|
650
|
+
|
|
651
|
+
```velar
|
|
652
|
+
type Payload:
|
|
653
|
+
id: string
|
|
654
|
+
|
|
655
|
+
extern module "some-sdk":
|
|
656
|
+
export def load() -> unknown
|
|
657
|
+
|
|
658
|
+
import js {load} from "some-sdk"
|
|
659
|
+
|
|
660
|
+
print(Payload.parse(load()).id)
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
An `extern module` block governs only the file that contains it, so **declare it
|
|
664
|
+
once in an adapter module** and re-export a checked surface. Do not paste extern
|
|
665
|
+
blocks across consumers:
|
|
666
|
+
|
|
667
|
+
```velar
|
|
668
|
+
extern module "text-tools":
|
|
669
|
+
export def format(value: string) -> string
|
|
670
|
+
|
|
671
|
+
export class Formatter:
|
|
672
|
+
constructor(prefix: string)
|
|
673
|
+
def format(value: number) -> string
|
|
674
|
+
|
|
675
|
+
import js {Formatter, format} from "text-tools"
|
|
676
|
+
|
|
677
|
+
export const formatText = format
|
|
678
|
+
export type TextFormatter = Formatter
|
|
679
|
+
|
|
680
|
+
export def formatter(prefix: string) -> Formatter:
|
|
681
|
+
return Formatter(prefix)
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Consumers then write ordinary Vel — `import {TextFormatter, formatText,
|
|
685
|
+
formatter} from "./text-tools.vel"` — and every call, construction, and
|
|
686
|
+
annotation is checked. A function export re-exports directly as a value; a class
|
|
687
|
+
needs a factory `def` (a class name is not a value) plus an exported `type`
|
|
688
|
+
alias so consumers can annotate. Validate anything declared `-> unknown` inside
|
|
689
|
+
the adapter, so only checked types leave it.
|
|
690
|
+
|
|
691
|
+
**Extern arguments are read-only.** What crosses is the raw identity, so a
|
|
692
|
+
package that writes into what you passed changes the real data while nothing on
|
|
693
|
+
the Vel side observes it — no recompute, no re-render, no refreshed flow fact —
|
|
694
|
+
until some unrelated Vel assignment invalidates. Have the package **return** the
|
|
695
|
+
data and assign the result on the Vel side.
|
|
696
|
+
|
|
697
|
+
2. **Quick raw access** — `import js unsafe` admits the value as `any`.
|
|
698
|
+
Operations on an `any` are raw JavaScript with no runtime check anywhere: it
|
|
699
|
+
is assignable to every type without validation, and an `any` holding
|
|
700
|
+
`undefined` even answers `false` to `== null`. The import statement is the
|
|
701
|
+
only correctness boundary — validate with `Type.parse` there, before the
|
|
702
|
+
value touches typed code.
|
|
703
|
+
3. **Styling beyond Look** — `import css unsafe "./file.css" before look`
|
|
704
|
+
(or `after look`); trusted markup renders through `unsafe:html`.
|
|
705
|
+
4. **A suspected compiler defect blocking you** — run `velar repro` (below),
|
|
706
|
+
then take the final exit: `velar build` output is readable, source-mapped
|
|
707
|
+
JavaScript that runs without the toolchain.
|
|
708
|
+
|
|
709
|
+
The full decision tree, including the honest limits of each hatch, is
|
|
710
|
+
[docs/escape-hatches.md](https://github.com/VelarOS-AI/VelarScript/blob/main/docs/escape-hatches.md).
|
|
711
|
+
|
|
712
|
+
## When you hit a compiler wall
|
|
713
|
+
|
|
714
|
+
A diagnostic that cannot be satisfied, an emit that misbehaves, a check that
|
|
715
|
+
contradicts this brief: you are this language's reporter as much as its author,
|
|
716
|
+
and the channel is the one a human uses. Do not quietly work around a defect and
|
|
717
|
+
leave the next reader to rediscover it.
|
|
718
|
+
|
|
719
|
+
1. **Run `velar repro`.** It writes a self-contained minimal reproduction to
|
|
720
|
+
disk and prints the path — the source the diagnostic touches, `velar.json`,
|
|
721
|
+
the verbatim output, and the versions. It uploads nothing and collects
|
|
722
|
+
nothing about the machine; whether to send it is the human's decision.
|
|
723
|
+
2. **Fill in the produced `README.md`.** Two of its three sections are blanks:
|
|
724
|
+
*What I wrote (or wanted to write)* and *How I resolved it* — the workaround,
|
|
725
|
+
or the single word `blocked`. *What the compiler said* is already filled in
|
|
726
|
+
verbatim; do not trim it.
|
|
727
|
+
3. **File it** with the repository's defect template
|
|
728
|
+
(`.github/ISSUE_TEMPLATE/`), which asks for exactly those three sections.
|
|
729
|
+
Paste the README.
|
|
730
|
+
|
|
731
|
+
A word that reads wrong is worth the same trip. The spelling-objection template
|
|
732
|
+
exists for it, no alternative word is required, and while there is no
|
|
733
|
+
compatibility promise, changing a word costs nothing yet.
|
|
734
|
+
|
|
735
|
+
## Where to look up what this brief leaves out
|
|
736
|
+
|
|
737
|
+
The repository carries a **tour** that shows every spelling exactly once, as
|
|
738
|
+
compiling projects you can run: `examples/tour/core/` (17 numbered chapters,
|
|
739
|
+
values through testing), `examples/tour/web/` (13 — components, Look, routing,
|
|
740
|
+
both kinds of test), `examples/tour/desktop/` (4). When you are about to guess
|
|
741
|
+
at a spelling, open the chapter instead. `examples/app/` is the companion — one
|
|
742
|
+
real application, showing how the pieces are put together.
|
|
743
|
+
|
|
744
|
+
## The meta-rule
|
|
745
|
+
|
|
746
|
+
Above everything in this brief: **run `velar check` and do what the
|
|
747
|
+
diagnostic says.** Diagnostics name the one current spelling; they are the
|
|
748
|
+
canon's enforcement arm, and they outrank any memory of this page. If the
|
|
749
|
+
compiler and this brief ever appear to disagree, the compiler is right.
|