@velarscript/cli 0.11.1 → 0.12.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/README.md +15 -10
- package/dist/cli.js +138 -40
- package/dist/cli.js.map +1 -1
- package/dist/deployment-verifier.d.ts.map +1 -1
- package/dist/deployment-verifier.js +1 -3
- package/dist/deployment-verifier.js.map +1 -1
- package/dist/extension-metadata.d.ts.map +1 -1
- package/dist/extension-metadata.js +1 -0
- package/dist/extension-metadata.js.map +1 -1
- package/dist/language-server-tool.d.ts +0 -1
- package/dist/language-server-tool.d.ts.map +1 -1
- package/dist/language-server-tool.js +0 -4
- package/dist/language-server-tool.js.map +1 -1
- package/dist/node-application.d.ts +13 -0
- package/dist/node-application.d.ts.map +1 -0
- package/dist/node-application.js +276 -0
- package/dist/node-application.js.map +1 -0
- package/dist/official-language-server-extensions.d.ts.map +1 -1
- package/dist/official-language-server-extensions.js +13 -1
- package/dist/official-language-server-extensions.js.map +1 -1
- package/dist/production-build.d.ts +1 -1
- package/dist/production-build.d.ts.map +1 -1
- package/dist/production-build.js +0 -2
- package/dist/production-build.js.map +1 -1
- package/dist/production-verifier.js +1 -17
- package/dist/production-verifier.js.map +1 -1
- package/dist/project-format.d.ts +1 -1
- package/dist/project-format.d.ts.map +1 -1
- package/dist/project-format.js +1 -1
- package/dist/project-format.js.map +1 -1
- package/dist/project.d.ts +3 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +64 -0
- package/dist/project.js.map +1 -1
- package/dist/standard-modules.d.ts +3 -12
- package/dist/standard-modules.d.ts.map +1 -1
- package/dist/standard-modules.js +13 -3045
- package/dist/standard-modules.js.map +1 -1
- package/dist/static-deployment.d.ts +1 -8
- package/dist/static-deployment.d.ts.map +1 -1
- package/dist/static-deployment.js +3 -28
- package/dist/static-deployment.js.map +1 -1
- package/dist/test-output.d.ts +1 -1
- package/dist/test-output.d.ts.map +1 -1
- package/dist/test-output.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/package.json +9 -11
- package/skill/ai-skill-desktop.md +84 -0
- package/skill/ai-skill-node.md +301 -0
- package/skill/ai-skill-web.md +150 -0
- package/skill/ai-skill.md +36 -263
- package/dist/build-engine-tool.d.ts +0 -3
- package/dist/build-engine-tool.d.ts.map +0 -1
- package/dist/build-engine-tool.js +0 -34
- package/dist/build-engine-tool.js.map +0 -1
- package/dist/packaged-official-tool.d.ts +0 -8
- package/dist/packaged-official-tool.d.ts.map +0 -1
- package/dist/packaged-official-tool.js +0 -32
- package/dist/packaged-official-tool.js.map +0 -1
- package/dist/project-task-bundle-entry.d.ts +0 -2
- package/dist/project-task-bundle-entry.d.ts.map +0 -1
- package/dist/project-task-bundle-entry.js +0 -18
- package/dist/project-task-bundle-entry.js.map +0 -1
- package/dist/project-task-invocation.d.ts +0 -16
- package/dist/project-task-invocation.d.ts.map +0 -1
- package/dist/project-task-invocation.js +0 -72
- package/dist/project-task-invocation.js.map +0 -1
- package/dist/project-task-tool.d.ts +0 -3
- package/dist/project-task-tool.d.ts.map +0 -1
- package/dist/project-task-tool.js +0 -61
- package/dist/project-task-tool.js.map +0 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# The VelarScript Node AI skill brief
|
|
2
|
+
|
|
3
|
+
Load this after `velar skill core`. This brief contains only the Node owner’s
|
|
4
|
+
contract. Core does not learn server declarations, path-pattern strings, Node
|
|
5
|
+
capabilities, or application configuration.
|
|
6
|
+
|
|
7
|
+
## Ownership and application entry
|
|
8
|
+
|
|
9
|
+
A Node service activates the extension and names one exported `ServeApp`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"formatVersion": 2,
|
|
14
|
+
"entry": "src/main.vel",
|
|
15
|
+
"outDir": "dist",
|
|
16
|
+
"publicDir": "public",
|
|
17
|
+
"extensions": ["@velarscript/node"],
|
|
18
|
+
"node": {
|
|
19
|
+
"app": "app",
|
|
20
|
+
"host": "127.0.0.1",
|
|
21
|
+
"port": 3000,
|
|
22
|
+
"maxBodyBytes": 16777216,
|
|
23
|
+
"build": {"sourceMaps": false}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The entry exports application data; it does not need a manually named function
|
|
29
|
+
or top-level `serve(...)` call:
|
|
30
|
+
|
|
31
|
+
```velar fragment
|
|
32
|
+
import {app as routes} from "./app.vel"
|
|
33
|
+
|
|
34
|
+
export const app = routes
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Use `velar dev` while editing, `velar serve` for checked production runtime
|
|
38
|
+
behavior, and `velar build` for a standalone Node output directory. A direct
|
|
39
|
+
`serve(app, port=0)` call is still correct in integration tests or an embedded
|
|
40
|
+
server.
|
|
41
|
+
|
|
42
|
+
`@name` keeps its one language-wide role: it qualifies a compiler-owned name
|
|
43
|
+
in the current context. In a `server` block, the available names are `@get`,
|
|
44
|
+
`@post`, `@put`, `@patch`, `@delete`, and `@notFound`. They are not decorators,
|
|
45
|
+
functions, imports, annotations, first-class values, or user extension points.
|
|
46
|
+
|
|
47
|
+
## Routes and checked inputs
|
|
48
|
+
|
|
49
|
+
A server is an immutable anonymous route table:
|
|
50
|
+
|
|
51
|
+
```velar
|
|
52
|
+
import {HttpError, created} from "velar/serve"
|
|
53
|
+
|
|
54
|
+
type CreateArticle:
|
|
55
|
+
title: string
|
|
56
|
+
|
|
57
|
+
export server articles:
|
|
58
|
+
/// Reports whether this service is ready.
|
|
59
|
+
@get(p"/health") => {ok: true}
|
|
60
|
+
|
|
61
|
+
@get(p"/articles/{id:number}", details: bool = false):
|
|
62
|
+
if id < 1:
|
|
63
|
+
throw HttpError(404, {error: "article_not_found"})
|
|
64
|
+
return {id, details}
|
|
65
|
+
|
|
66
|
+
@post(p"/articles", input: CreateArticle):
|
|
67
|
+
return created({id: 1, title: input.title})
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The path is written once. A capture such as `{id:number}` declares `id`
|
|
72
|
+
directly in the route body, so never repeat it in the argument list. Captures
|
|
73
|
+
require an ASCII half-width `:` and accept `string`, `number`, `bool`, or
|
|
74
|
+
a named enum. The Node-only `p"..."` prefix marks a compile-time reverse
|
|
75
|
+
matcher; an ordinary string is not accepted, and Core does not recognize this
|
|
76
|
+
prefix.
|
|
77
|
+
|
|
78
|
+
Unadorned scalar and `List<scalar>` parameters are query inputs. A default makes
|
|
79
|
+
the input optional. Repeated query values populate a List; a repeated scalar is
|
|
80
|
+
a 422 error. On `POST`, `PUT`, or `PATCH`, one concrete Data parameter is the
|
|
81
|
+
checked JSON body. A `Request` parameter explicitly requests the complete
|
|
82
|
+
request, including `queryAll` and cooperative `cancellation`. Ambiguous bodies,
|
|
83
|
+
duplicate declarations, conflicting path shapes, and unsupported path types are
|
|
84
|
+
compile errors.
|
|
85
|
+
|
|
86
|
+
`@notFound()` is the one application fallback for a path that matches no
|
|
87
|
+
route. It may omit its parameter or accept one explicitly typed `Request`.
|
|
88
|
+
Returning Data keeps status 404; return an explicit response such as
|
|
89
|
+
`json(value, status=410)` to choose another final status. A matched route's
|
|
90
|
+
`HttpError` and framework 405 responses do not enter this fallback. Declare at
|
|
91
|
+
most one on the final application; an app that owns `@notFound` cannot be moved
|
|
92
|
+
under a non-root `prefix`, because a global fallback has no unambiguous prefix
|
|
93
|
+
scope. Compose prefixed route tables first, then declare the fallback on the
|
|
94
|
+
outer `server`. A catch-all route such as `staticFiles("/", ...)` is a matched
|
|
95
|
+
route and therefore owns its own file fallback instead of entering
|
|
96
|
+
`@notFound`.
|
|
97
|
+
|
|
98
|
+
Use ordinary values for the cases that need more than inference:
|
|
99
|
+
|
|
100
|
+
```velar
|
|
101
|
+
import {input, provide, security} from "velar/serve"
|
|
102
|
+
|
|
103
|
+
type User:
|
|
104
|
+
id: string
|
|
105
|
+
|
|
106
|
+
type UploadMetadata:
|
|
107
|
+
title: string
|
|
108
|
+
|
|
109
|
+
const currentUser = provide(
|
|
110
|
+
inputs={token: security.bearer()},
|
|
111
|
+
resolve=async values => {id: values.token},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
server account:
|
|
115
|
+
@get(p"/me",
|
|
116
|
+
user=input.dependency(currentUser),
|
|
117
|
+
tenant=input.header("x-tenant"),
|
|
118
|
+
session=input.cookie("session", default=null),
|
|
119
|
+
) => {id: user.id, tenant, session}
|
|
120
|
+
|
|
121
|
+
@post(p"/images",
|
|
122
|
+
metadata=input.form(UploadMetadata),
|
|
123
|
+
image=input.upload("image", maxBytes=8_388_608),
|
|
124
|
+
) => {title: metadata.title, filename: image.filename}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`input.query`, `input.header`, and `input.cookie` select named scalar
|
|
128
|
+
values. `input.form(Type)` checks URL-encoded or multipart fields, including
|
|
129
|
+
repeated fields for `List<scalar>` properties; duplicate scalar fields fail.
|
|
130
|
+
`input.upload` returns an `Upload` whose bytes are valid only for the request
|
|
131
|
+
lifetime; copy or persist them before retaining data. `security.apiKey`,
|
|
132
|
+
`basic`, `bearer`, `oauth2`, and `openId` parse credentials and also
|
|
133
|
+
feed OpenAPI security schemes.
|
|
134
|
+
|
|
135
|
+
`provide(inputs, resolve, scope="request", release=null, eager=false)` declares
|
|
136
|
+
a dependency as data. A request-scoped provider resolves once per request even
|
|
137
|
+
when injected repeatedly and releases after response/background completion.
|
|
138
|
+
An app-scoped provider may depend only on other app-scoped providers, may be
|
|
139
|
+
eager, and releases during shutdown. Cycles and provider-budget exhaustion fail
|
|
140
|
+
closed. Do not build a controller or container layer around it.
|
|
141
|
+
|
|
142
|
+
## Composition, lifecycle, and middleware
|
|
143
|
+
|
|
144
|
+
Compose route tables as values:
|
|
145
|
+
|
|
146
|
+
```velar fragment
|
|
147
|
+
import {Request, RouteDocumentation, docs, lifecycle, middleware, prefix, staticFiles, use} from "velar/serve"
|
|
148
|
+
|
|
149
|
+
const service = lifecycle(
|
|
150
|
+
prefix("/api", articles),
|
|
151
|
+
startup=async () => null,
|
|
152
|
+
shutdown=async () => null,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const hardened = use(service, [
|
|
156
|
+
middleware.trustedHosts(["api.example.com", "127.0.0.1"]),
|
|
157
|
+
middleware.cors(origins=["https://app.example.com"], credentials=true),
|
|
158
|
+
middleware.requestId(),
|
|
159
|
+
middleware.securityHeaders(),
|
|
160
|
+
middleware.compression(minimumBytes=1024),
|
|
161
|
+
middleware.timeout(10_000),
|
|
162
|
+
middleware.concurrency(256),
|
|
163
|
+
])
|
|
164
|
+
|
|
165
|
+
const articleDocs: RouteDocumentation = {summary: "Read article", tags: ["articles"], errors: Map([[404, "Article not found"]])}
|
|
166
|
+
const routeDocs: Map<string, RouteDocumentation> = Map([["GET /api/articles/{id:number}", articleDocs]])
|
|
167
|
+
|
|
168
|
+
export server app:
|
|
169
|
+
...docs(hardened, title="Article API", version="1.0.0", routes=routeDocs)
|
|
170
|
+
...staticFiles("/assets", root="public")
|
|
171
|
+
@notFound(request: Request) => {error: "route_not_found", path: request.path}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`prefix` changes a literal route prefix; `bodyLimit` narrows one route
|
|
175
|
+
table’s body budget; `use` attaches middleware only to the routes it receives.
|
|
176
|
+
A middleware continuation is single-use. Built-ins also include access logging
|
|
177
|
+
and explicit error recovery. A timeout response does not pretend downstream
|
|
178
|
+
work was cancelled: its request resources remain alive until that work really
|
|
179
|
+
ends, and the runtime caps unfinished timed-out tasks.
|
|
180
|
+
|
|
181
|
+
`lifecycle` owns paired startup and shutdown hooks. Successful startups unwind
|
|
182
|
+
in reverse order; a failed startup does not run its paired shutdown.
|
|
183
|
+
`background(response, task)` keeps request-scoped resources valid until the
|
|
184
|
+
task finishes. `setCookie` and
|
|
185
|
+
`clearCookie` add checked cookie headers, preserving separate `Set-Cookie`
|
|
186
|
+
fields. Never start long-lived work by
|
|
187
|
+
dropping a Promise; use an owned lifecycle, background task, Worker, or process.
|
|
188
|
+
|
|
189
|
+
`docs` adds a bundled offline UI and OpenAPI 3.1 JSON. Compiler route types
|
|
190
|
+
and response helpers supply parameter, body, response, content-type, and static
|
|
191
|
+
success-status schemas. Applicable framework-generated 400, 401, 413, 415, and
|
|
192
|
+
422 responses are included automatically; a preceding `///` comment supplies
|
|
193
|
+
the route description. The optional ordinary typed
|
|
194
|
+
`Map<string, RouteDocumentation>` can add
|
|
195
|
+
summary, description, tags, success status, documented error statuses, or hide
|
|
196
|
+
a route, keyed by strings such as `"GET /articles/{id:number}"`. This metadata
|
|
197
|
+
is data, not another `@` category.
|
|
198
|
+
|
|
199
|
+
## Responses and realtime
|
|
200
|
+
|
|
201
|
+
Ordinary Data returns JSON. Use the explicit helpers only when transport
|
|
202
|
+
semantics matter: `json`, `created`, `noContent`, `redirect`, `text`,
|
|
203
|
+
`file`, `stream`, `sse`, `background`, `setCookie`, and
|
|
204
|
+
`clearCookie`. `HttpError(status, body, headers=null)` is the expected HTTP
|
|
205
|
+
failure. Unexpected failures are reported on stderr and become an opaque 500.
|
|
206
|
+
|
|
207
|
+
`HEAD` reuses `GET` without a body. `OPTIONS` and 405 responses publish a
|
|
208
|
+
complete `Allow` header. Final response statuses are 200 through 599; 204 and
|
|
209
|
+
304 are bodyless. Streaming follows write backpressure; each chunk and the
|
|
210
|
+
total stream are bounded. SSE accepts text or checked
|
|
211
|
+
`{data, event?, id?, retry?}` events. Static and returned files are canonical
|
|
212
|
+
root-contained, streamed reads with validators and one byte range.
|
|
213
|
+
|
|
214
|
+
`velar/websocket.listen({http: app, ...})` serves a `ServeApp` and WebSocket
|
|
215
|
+
upgrades on one native server and owns the application lifecycle. Connections
|
|
216
|
+
are pull-based. Always consume `next()`, handle backpressure, and stop the
|
|
217
|
+
server.
|
|
218
|
+
|
|
219
|
+
## Tests
|
|
220
|
+
|
|
221
|
+
Use `velar/server-test` only from `*.test.vel`. It runs the real ServeApp
|
|
222
|
+
router, providers, lifecycle, cookies, forms, uploads, streams, and files in
|
|
223
|
+
process without opening a port:
|
|
224
|
+
|
|
225
|
+
```velar fragment
|
|
226
|
+
import {client} from "velar/server-test"
|
|
227
|
+
import {expect} from "velar/test"
|
|
228
|
+
import {app} from "./app.vel"
|
|
229
|
+
|
|
230
|
+
test "health endpoint":
|
|
231
|
+
const api = await client(app)
|
|
232
|
+
try:
|
|
233
|
+
const response = await api.get("/api/health")
|
|
234
|
+
expect(response.status).toBe(200)
|
|
235
|
+
expect(await response.text()).toContain("ok")
|
|
236
|
+
finally:
|
|
237
|
+
await api.close()
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Pass a `Map<Provider, value>` as the second `client` argument to override
|
|
241
|
+
dependencies. Request options support headers, JSON, text, form Maps, and file
|
|
242
|
+
Maps. Test WebSocket behavior through a real local `velar/websocket` server,
|
|
243
|
+
because WebSocket upgrades are a transport boundary rather than an in-process
|
|
244
|
+
HTTP request.
|
|
245
|
+
|
|
246
|
+
## Memory and ownership rules
|
|
247
|
+
|
|
248
|
+
The runtime has hard ceilings; application code must preserve them:
|
|
249
|
+
|
|
250
|
+
- Request bodies are at most 16 MiB, with narrower per-route and project limits.
|
|
251
|
+
HTTP request bodies, buffered responses, static files, and stream chunks also
|
|
252
|
+
share one 128 MiB isolated-host budget.
|
|
253
|
+
- Live inbound HTTP requests and middleware concurrency are capped at 4,096.
|
|
254
|
+
ServeApp routes, request providers, app providers, and timed-out unfinished
|
|
255
|
+
work have separate count limits.
|
|
256
|
+
- WebSocket queued bytes and pending-send bytes share a 128 MiB budget. Global
|
|
257
|
+
limits also cover active connections, pending connection objects, queued
|
|
258
|
+
message objects, and pending send Promises, including zero-byte messages.
|
|
259
|
+
- Multipart parsing reuses bounded body storage; an `Upload` is invalidated
|
|
260
|
+
when its request scope ends. Provider caches and release lists are explicitly
|
|
261
|
+
cleared at request or application shutdown.
|
|
262
|
+
- Development rebuilds serialize changes, retain one last-good child, stop the
|
|
263
|
+
old child before replacing it, and delete temporary build sandboxes.
|
|
264
|
+
|
|
265
|
+
Set smaller limits for the domain instead of treating framework maxima as
|
|
266
|
+
defaults. Stream large data, consume pull queues promptly, close every owned
|
|
267
|
+
capability, and keep caches explicitly bounded.
|
|
268
|
+
|
|
269
|
+
## Concurrency and shutdown rules
|
|
270
|
+
|
|
271
|
+
Concurrency limits are admission control, not a lock around application data.
|
|
272
|
+
The runtime guarantees that an application-scoped provider initializes once,
|
|
273
|
+
concurrent `stop` or `close` calls join the same completion, shutdown refuses
|
|
274
|
+
new work, requests receive cooperative cancellation, and provider release begins
|
|
275
|
+
only after every admitted request has finished its response, timed-out
|
|
276
|
+
continuation, background work, and request cleanup. `stop(grace=30000)` bounds
|
|
277
|
+
the drain wait without releasing live request resources early. WebSocket stop
|
|
278
|
+
closes connections in parallel with a bounded handshake and releases unread
|
|
279
|
+
queues and pending-send reservations.
|
|
280
|
+
|
|
281
|
+
Application code still owns business-level consistency. Do not use an
|
|
282
|
+
unprotected module-level mutable record as a cross-request database. Put
|
|
283
|
+
multi-write invariants in a database transaction or another capability that
|
|
284
|
+
provides atomic operations. Always await stream writes, allow only one active
|
|
285
|
+
pull from an async iterator, and make retryable handlers idempotent. A
|
|
286
|
+
`middleware.concurrency` limit protects capacity; it does not make a shared
|
|
287
|
+
read-modify-write sequence atomic.
|
|
288
|
+
|
|
289
|
+
## Node capabilities and finish
|
|
290
|
+
|
|
291
|
+
Use `velar/fs`, `velar/path`, `velar/process`, `velar/env`,
|
|
292
|
+
`velar/terminal`, `velar/http`, `velar/worker`, and `velar/websocket` instead of
|
|
293
|
+
ambient Node globals. Databases are ordinary external source packages: use the
|
|
294
|
+
engine-neutral `@velarscript/database` model contract and install an adapter
|
|
295
|
+
such as `@velarscript/sqlite`. Third-party packages cross a checked `extern
|
|
296
|
+
module` adapter first; keep `import js unsafe` at one narrow validation
|
|
297
|
+
boundary.
|
|
298
|
+
|
|
299
|
+
Run `velar format --check`, `velar check`, `velar test`, and
|
|
300
|
+
`velar build`. The complete runnable spelling lives in
|
|
301
|
+
`examples/tour/node/`; current diagnostics outrank this brief.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# The VelarScript Web AI skill brief
|
|
2
|
+
|
|
3
|
+
Load this after `velar skill core`. This file contains only the Web extension's
|
|
4
|
+
contract; Core syntax stays owned by the Core brief. It ships with the
|
|
5
|
+
toolchain and `velar skill web` prints it verbatim.
|
|
6
|
+
|
|
7
|
+
## Ownership
|
|
8
|
+
|
|
9
|
+
The manifest activates Web explicitly:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"formatVersion": 2,
|
|
14
|
+
"entry": "src/main.vel",
|
|
15
|
+
"extensions": ["@velarscript/web"],
|
|
16
|
+
"web": {"title": "My App"}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`@velarscript/web` owns `component`, JSX, `state`, `computed`, `resource`,
|
|
21
|
+
`action`, `watch`, `look`, `keyframes`, `mount`, Web units, its standard
|
|
22
|
+
modules, and the `web` manifest key. Without that extension these are not Core
|
|
23
|
+
syntax. Web visual builders are named imports from `velar/look`; there is no
|
|
24
|
+
`Look.` namespace.
|
|
25
|
+
|
|
26
|
+
`@name` keeps the same language-wide role: it qualifies a compiler-owned name
|
|
27
|
+
in the current context. Components own `@mounted:` and `@cleanup:`; Look owns
|
|
28
|
+
conditions such as `@hover` and `@before`. They are not decorators, calls, or
|
|
29
|
+
values, and user code cannot declare new `@` names.
|
|
30
|
+
|
|
31
|
+
The toolchain writes `dist/index.html`; do not author it. `web.title` and
|
|
32
|
+
`web.icon` supply document metadata, `public/` is copied through, and other
|
|
33
|
+
assets are content-hashed.
|
|
34
|
+
|
|
35
|
+
## Components and reactivity
|
|
36
|
+
|
|
37
|
+
A component returns JSX directly. The four reactive cells have distinct jobs:
|
|
38
|
+
|
|
39
|
+
- `state` owns a fact.
|
|
40
|
+
- `computed name = ...` derives synchronously and is read bare.
|
|
41
|
+
- `resource name: T = load()` owns async loading state.
|
|
42
|
+
- `action name():` owns a user operation and exposes `pending` and `error`.
|
|
43
|
+
|
|
44
|
+
```velar fragment
|
|
45
|
+
component TicketPanel(id: string):
|
|
46
|
+
state draft = ""
|
|
47
|
+
resource ticket: Ticket = loadTicket(id)
|
|
48
|
+
computed heading = ticket.value?.title ?? "Loading"
|
|
49
|
+
|
|
50
|
+
action save():
|
|
51
|
+
await saveDraft(id, draft)
|
|
52
|
+
|
|
53
|
+
watch id:
|
|
54
|
+
async ticket.reload()
|
|
55
|
+
|
|
56
|
+
return <section>
|
|
57
|
+
<h2>{heading}</h2>
|
|
58
|
+
<textarea bind:value={draft}></textarea>
|
|
59
|
+
<button disabled={save.pending} on:click={save}>Save</button>
|
|
60
|
+
</section>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A resource loads once at mount. Refetching after an input changes is an
|
|
64
|
+
explicit `watch` plus detached `async resource.reload()`. Actions do not queue;
|
|
65
|
+
disable or otherwise guard a trigger when concurrent calls are unwanted.
|
|
66
|
+
|
|
67
|
+
`@mounted:` runs after insertion and may await. `@cleanup:` runs before removal
|
|
68
|
+
and is synchronous. A component may still declare ordinary methods named
|
|
69
|
+
`mounted` or `cleanup` because compiler-owned names occupy a separate namespace.
|
|
70
|
+
|
|
71
|
+
Events use `on:click={handler}`. Writable form paths use `bind:value`,
|
|
72
|
+
`bind:checked`, or `bind:group`; do not read `event.target`. A JSX tag body is
|
|
73
|
+
received only through an explicitly declared `children: WebNode` prop.
|
|
74
|
+
Conditional rendering is an expression, and repeated children need stable
|
|
75
|
+
`key` values.
|
|
76
|
+
|
|
77
|
+
Props are live reactive inputs. Their data is mutable unless the author writes
|
|
78
|
+
an explicit `readonly` view.
|
|
79
|
+
|
|
80
|
+
## Look
|
|
81
|
+
|
|
82
|
+
`look:` is checked visual data. CSS keywords are strings, property names use
|
|
83
|
+
DOM camelCase, and units are literals:
|
|
84
|
+
|
|
85
|
+
```velar fragment
|
|
86
|
+
import {border, rgb, spacing} from "velar/look"
|
|
87
|
+
|
|
88
|
+
const buttonLook = look:
|
|
89
|
+
border = border(0px, rgb(220, 224, 235))
|
|
90
|
+
borderRadius = 10px
|
|
91
|
+
padding = spacing(10px, 14px)
|
|
92
|
+
cursor = "pointer"
|
|
93
|
+
|
|
94
|
+
if @hover:
|
|
95
|
+
background = rgb(235, 240, 255)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A module-level Look literal is built once and cannot read reactive state. Put a
|
|
99
|
+
changing visual on the element with `look={active ? enabledLook : disabledLook}`
|
|
100
|
+
or a typed `look:color={...}` binding. Checked motion is a module-level
|
|
101
|
+
`keyframes:` value passed to `animate` from `velar/look`; do not use a raw CSS
|
|
102
|
+
animation string. Unsupported styling uses explicit `import css unsafe
|
|
103
|
+
"./file.css" before look` or `after look`.
|
|
104
|
+
|
|
105
|
+
## Storage and tests
|
|
106
|
+
|
|
107
|
+
`velar/storage` stores JSON and validates on read. A generic type spelling is
|
|
108
|
+
not a runtime value, so name the complete type:
|
|
109
|
+
|
|
110
|
+
```velar fragment
|
|
111
|
+
import {storage} from "velar/storage"
|
|
112
|
+
|
|
113
|
+
type SavedItem:
|
|
114
|
+
title: string
|
|
115
|
+
|
|
116
|
+
type SavedItems = List<SavedItem>
|
|
117
|
+
|
|
118
|
+
const items = storage.get("reading", SavedItems, [])
|
|
119
|
+
storage.set("reading", items)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Keep `src/main.vel` as the mounted entry and put testable components and logic
|
|
123
|
+
in imported modules. Plain `*.test.vel` files run headlessly. A
|
|
124
|
+
`*.browser.test.vel` file drives the already-built page through
|
|
125
|
+
`velar/web-test`; it does not mount a second app and cannot use JSX or
|
|
126
|
+
`document` in its test body:
|
|
127
|
+
|
|
128
|
+
```velar fragment
|
|
129
|
+
import {expect} from "velar/test"
|
|
130
|
+
import {browser, localStorage} from "velar/web-test"
|
|
131
|
+
|
|
132
|
+
test "adding a link shows it in the list":
|
|
133
|
+
await browser.open("/")
|
|
134
|
+
await browser.fill("#title", "Vel")
|
|
135
|
+
await browser.click("#add")
|
|
136
|
+
await browser.waitForText("[data-item]", "Vel")
|
|
137
|
+
expect(await browser.text("[data-count]")).toBe("1")
|
|
138
|
+
expect(await localStorage.get("reading")).toBe(`[{"title":"Vel"}]`)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Bare `velar test --browser` means Chromium; use `--browser=all` for a
|
|
142
|
+
cross-browser claim. `velar/web-test` is legal only in browser-test modules.
|
|
143
|
+
Application code that needs the page uses checked `velar/browser` APIs.
|
|
144
|
+
|
|
145
|
+
## Finish
|
|
146
|
+
|
|
147
|
+
Run `velar format`, `velar check`, `velar test`, the project's browser tests,
|
|
148
|
+
and `velar build`. The complete runnable vocabulary lives in
|
|
149
|
+
`examples/tour/web/`; diagnostics outrank this brief when the two appear to
|
|
150
|
+
disagree.
|