@constela/builder 0.2.0 → 0.2.2
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 +421 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# @constela/builder
|
|
2
|
+
|
|
3
|
+
Type-safe builders for constructing Constela AST programmatically.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @constela/builder
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### JSON (Primary)
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"version": "1.0",
|
|
18
|
+
"state": {
|
|
19
|
+
"count": { "type": "number", "initial": 0 }
|
|
20
|
+
},
|
|
21
|
+
"actions": [
|
|
22
|
+
{
|
|
23
|
+
"name": "increment",
|
|
24
|
+
"steps": [{ "do": "update", "target": "count", "operation": "increment" }]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"view": {
|
|
28
|
+
"kind": "element",
|
|
29
|
+
"tag": "button",
|
|
30
|
+
"props": { "onClick": { "event": "click", "action": "increment" } },
|
|
31
|
+
"children": [{ "kind": "text", "value": { "expr": "state", "name": "count" } }]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### TypeScript Builder (Equivalent)
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import {
|
|
40
|
+
createProgram,
|
|
41
|
+
numberField,
|
|
42
|
+
action, increment,
|
|
43
|
+
button, text,
|
|
44
|
+
state, onClick,
|
|
45
|
+
} from '@constela/builder';
|
|
46
|
+
|
|
47
|
+
const program = createProgram({
|
|
48
|
+
state: {
|
|
49
|
+
count: numberField(0),
|
|
50
|
+
},
|
|
51
|
+
actions: [
|
|
52
|
+
action('increment', [increment('count')]),
|
|
53
|
+
],
|
|
54
|
+
view: button(
|
|
55
|
+
{ onClick: onClick('increment') },
|
|
56
|
+
[text(state('count'))]
|
|
57
|
+
),
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API Reference
|
|
62
|
+
|
|
63
|
+
### Expression Builders
|
|
64
|
+
|
|
65
|
+
Expressions compute values from state, variables, and literals.
|
|
66
|
+
|
|
67
|
+
| Builder | JSON Equivalent | Description |
|
|
68
|
+
|---------|----------------|-------------|
|
|
69
|
+
| `lit(value)` | `{ "expr": "lit", "value": ... }` | Literal value |
|
|
70
|
+
| `state(name, path?)` | `{ "expr": "state", "name": "...", "path": "..." }` | State reference |
|
|
71
|
+
| `variable(name, path?)` | `{ "expr": "var", "name": "...", "path": "..." }` | Loop/event variable |
|
|
72
|
+
| `bin(op, left, right)` | `{ "expr": "bin", "op": "...", ... }` | Binary operation |
|
|
73
|
+
| `not(operand)` | `{ "expr": "not", "operand": ... }` | Logical negation |
|
|
74
|
+
| `cond(if, then, else)` | `{ "expr": "cond", ... }` | Conditional |
|
|
75
|
+
| `get(base, path)` | `{ "expr": "get", "base": ..., "path": "..." }` | Property access |
|
|
76
|
+
|
|
77
|
+
**Binary Operator Shorthands:**
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Arithmetic
|
|
81
|
+
add(left, right) // +
|
|
82
|
+
sub(left, right) // -
|
|
83
|
+
mul(left, right) // *
|
|
84
|
+
divide(left, right) // /
|
|
85
|
+
|
|
86
|
+
// Comparison
|
|
87
|
+
eq(left, right) // ==
|
|
88
|
+
neq(left, right) // !=
|
|
89
|
+
lt(left, right) // <
|
|
90
|
+
lte(left, right) // <=
|
|
91
|
+
gt(left, right) // >
|
|
92
|
+
gte(left, right) // >=
|
|
93
|
+
|
|
94
|
+
// Logical
|
|
95
|
+
and(left, right) // &&
|
|
96
|
+
or(left, right) // ||
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Example:**
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// JSON: { "expr": "bin", "op": ">", "left": { "expr": "state", "name": "count" }, "right": { "expr": "lit", "value": 0 } }
|
|
103
|
+
gt(state('count'), lit(0))
|
|
104
|
+
|
|
105
|
+
// JSON: { "expr": "cond", "if": ..., "then": { "expr": "lit", "value": "Yes" }, "else": { "expr": "lit", "value": "No" } }
|
|
106
|
+
cond(gt(state('count'), lit(0)), lit('Yes'), lit('No'))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### State Builders
|
|
110
|
+
|
|
111
|
+
Define reactive state fields.
|
|
112
|
+
|
|
113
|
+
| Builder | JSON Equivalent |
|
|
114
|
+
|---------|----------------|
|
|
115
|
+
| `numberField(initial)` | `{ "type": "number", "initial": 0 }` |
|
|
116
|
+
| `stringField(initial)` | `{ "type": "string", "initial": "" }` |
|
|
117
|
+
| `booleanField(initial)` | `{ "type": "boolean", "initial": false }` |
|
|
118
|
+
| `listField(initial?)` | `{ "type": "list", "initial": [] }` |
|
|
119
|
+
| `objectField(initial)` | `{ "type": "object", "initial": { ... } }` |
|
|
120
|
+
|
|
121
|
+
**Example:**
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const stateDefinition = {
|
|
125
|
+
count: numberField(0),
|
|
126
|
+
query: stringField(''),
|
|
127
|
+
todos: listField([]),
|
|
128
|
+
isVisible: booleanField(true),
|
|
129
|
+
form: objectField({ name: '', email: '' }),
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Action Builders
|
|
134
|
+
|
|
135
|
+
Define actions with steps.
|
|
136
|
+
|
|
137
|
+
| Builder | JSON Equivalent |
|
|
138
|
+
|---------|----------------|
|
|
139
|
+
| `action(name, steps)` | `{ "name": "...", "steps": [...] }` |
|
|
140
|
+
| `set(target, value)` | `{ "do": "set", "target": "...", "value": ... }` |
|
|
141
|
+
| `update(target, op, value?)` | `{ "do": "update", "target": "...", ... }` |
|
|
142
|
+
| `fetch(url, options?)` | `{ "do": "fetch", "url": ..., ... }` |
|
|
143
|
+
| `navigate(url, options?)` | `{ "do": "navigate", "url": ... }` |
|
|
144
|
+
|
|
145
|
+
**Update Operation Shorthands:**
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
increment(target, value?) // Add to number
|
|
149
|
+
decrement(target, value?) // Subtract from number
|
|
150
|
+
push(target, value) // Add item to list
|
|
151
|
+
pop(target) // Remove last item
|
|
152
|
+
toggle(target) // Flip boolean
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Example:**
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// JSON: { "name": "addTodo", "steps": [{ "do": "update", "target": "todos", "operation": "push", "value": { "expr": "var", "name": "payload" } }] }
|
|
159
|
+
action('addTodo', [
|
|
160
|
+
push('todos', variable('payload')),
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
// Fetch with callbacks
|
|
164
|
+
action('loadData', [
|
|
165
|
+
fetch(lit('/api/data'), {
|
|
166
|
+
method: 'GET',
|
|
167
|
+
result: 'response',
|
|
168
|
+
onSuccess: [set('data', variable('response'))],
|
|
169
|
+
onError: [set('error', lit('Failed to load'))],
|
|
170
|
+
}),
|
|
171
|
+
])
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### View Builders
|
|
175
|
+
|
|
176
|
+
Build UI declaratively.
|
|
177
|
+
|
|
178
|
+
| Builder | JSON Equivalent |
|
|
179
|
+
|---------|----------------|
|
|
180
|
+
| `element(tag, props?, children?)` | `{ "kind": "element", "tag": "...", ... }` |
|
|
181
|
+
| `text(value)` | `{ "kind": "text", "value": ... }` |
|
|
182
|
+
| `ifNode(condition, then, else?)` | `{ "kind": "if", ... }` |
|
|
183
|
+
| `each(items, as, body, options?)` | `{ "kind": "each", ... }` |
|
|
184
|
+
| `component(name, props?, children?)` | `{ "kind": "component", "name": "...", ... }` |
|
|
185
|
+
| `slot(name?)` | `{ "kind": "slot" }` |
|
|
186
|
+
|
|
187
|
+
**Element Shorthands:**
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
div(props?, children?)
|
|
191
|
+
span(props?, children?)
|
|
192
|
+
button(props?, children?)
|
|
193
|
+
input(props?, children?)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Example:**
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Conditional rendering
|
|
200
|
+
ifNode(
|
|
201
|
+
gt(state('count'), lit(0)),
|
|
202
|
+
text(lit('Positive')),
|
|
203
|
+
text(lit('Zero or negative'))
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// List rendering
|
|
207
|
+
each(
|
|
208
|
+
state('todos'),
|
|
209
|
+
'todo',
|
|
210
|
+
div({}, [text(variable('todo', 'text'))]),
|
|
211
|
+
{ index: 'i', key: variable('todo', 'id') }
|
|
212
|
+
)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Event Builders
|
|
216
|
+
|
|
217
|
+
Bind events to actions.
|
|
218
|
+
|
|
219
|
+
| Builder | JSON Equivalent |
|
|
220
|
+
|---------|----------------|
|
|
221
|
+
| `onClick(action, payload?)` | `{ "event": "click", "action": "..." }` |
|
|
222
|
+
| `onInput(action, payload?)` | `{ "event": "input", "action": "...", "payload": ... }` |
|
|
223
|
+
| `onChange(action, payload?)` | `{ "event": "change", "action": "...", "payload": ... }` |
|
|
224
|
+
| `onSubmit(action, payload?)` | `{ "event": "submit", "action": "..." }` |
|
|
225
|
+
|
|
226
|
+
**Note:** `onInput` and `onChange` automatically include `{ "expr": "var", "name": "event", "path": "target.value" }` as payload.
|
|
227
|
+
|
|
228
|
+
**Example:**
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
input({
|
|
232
|
+
value: state('query'),
|
|
233
|
+
onInput: onInput('updateQuery'),
|
|
234
|
+
})
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Program Builder
|
|
238
|
+
|
|
239
|
+
Compose the complete program.
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
createProgram({
|
|
243
|
+
route?: { path: string, title?: Expression, layout?: string },
|
|
244
|
+
state: Record<string, StateField>,
|
|
245
|
+
actions: ActionDefinition[],
|
|
246
|
+
view: ViewNode,
|
|
247
|
+
components?: Record<string, ComponentDef>,
|
|
248
|
+
})
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Complete Example: Todo List
|
|
252
|
+
|
|
253
|
+
### JSON
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"version": "1.0",
|
|
258
|
+
"state": {
|
|
259
|
+
"todos": { "type": "list", "initial": [] },
|
|
260
|
+
"newTodo": { "type": "string", "initial": "" }
|
|
261
|
+
},
|
|
262
|
+
"actions": [
|
|
263
|
+
{
|
|
264
|
+
"name": "updateInput",
|
|
265
|
+
"steps": [{ "do": "set", "target": "newTodo", "value": { "expr": "var", "name": "payload" } }]
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"name": "addTodo",
|
|
269
|
+
"steps": [
|
|
270
|
+
{ "do": "update", "target": "todos", "operation": "push", "value": { "expr": "state", "name": "newTodo" } },
|
|
271
|
+
{ "do": "set", "target": "newTodo", "value": { "expr": "lit", "value": "" } }
|
|
272
|
+
]
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "removeTodo",
|
|
276
|
+
"steps": [{ "do": "update", "target": "todos", "operation": "remove", "value": { "expr": "var", "name": "payload" } }]
|
|
277
|
+
}
|
|
278
|
+
],
|
|
279
|
+
"view": {
|
|
280
|
+
"kind": "element",
|
|
281
|
+
"tag": "div",
|
|
282
|
+
"children": [
|
|
283
|
+
{
|
|
284
|
+
"kind": "element",
|
|
285
|
+
"tag": "div",
|
|
286
|
+
"children": [
|
|
287
|
+
{
|
|
288
|
+
"kind": "element",
|
|
289
|
+
"tag": "input",
|
|
290
|
+
"props": {
|
|
291
|
+
"value": { "expr": "state", "name": "newTodo" },
|
|
292
|
+
"onInput": { "event": "input", "action": "updateInput", "payload": { "expr": "var", "name": "event", "path": "target.value" } }
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"kind": "element",
|
|
297
|
+
"tag": "button",
|
|
298
|
+
"props": { "onClick": { "event": "click", "action": "addTodo" } },
|
|
299
|
+
"children": [{ "kind": "text", "value": { "expr": "lit", "value": "Add" } }]
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"kind": "each",
|
|
305
|
+
"items": { "expr": "state", "name": "todos" },
|
|
306
|
+
"as": "todo",
|
|
307
|
+
"index": "i",
|
|
308
|
+
"body": {
|
|
309
|
+
"kind": "element",
|
|
310
|
+
"tag": "div",
|
|
311
|
+
"children": [
|
|
312
|
+
{ "kind": "text", "value": { "expr": "var", "name": "todo" } },
|
|
313
|
+
{
|
|
314
|
+
"kind": "element",
|
|
315
|
+
"tag": "button",
|
|
316
|
+
"props": { "onClick": { "event": "click", "action": "removeTodo", "payload": { "expr": "var", "name": "i" } } },
|
|
317
|
+
"children": [{ "kind": "text", "value": { "expr": "lit", "value": "Delete" } }]
|
|
318
|
+
}
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
]
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### TypeScript Builder (Equivalent)
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import {
|
|
331
|
+
createProgram,
|
|
332
|
+
listField, stringField,
|
|
333
|
+
action, set, push, update,
|
|
334
|
+
div, input, button, text, each,
|
|
335
|
+
state, variable, lit,
|
|
336
|
+
onClick, onInput,
|
|
337
|
+
} from '@constela/builder';
|
|
338
|
+
|
|
339
|
+
const program = createProgram({
|
|
340
|
+
state: {
|
|
341
|
+
todos: listField<string>([]),
|
|
342
|
+
newTodo: stringField(''),
|
|
343
|
+
},
|
|
344
|
+
actions: [
|
|
345
|
+
action('updateInput', [
|
|
346
|
+
set('newTodo', variable('payload')),
|
|
347
|
+
]),
|
|
348
|
+
action('addTodo', [
|
|
349
|
+
push('todos', state('newTodo')),
|
|
350
|
+
set('newTodo', lit('')),
|
|
351
|
+
]),
|
|
352
|
+
action('removeTodo', [
|
|
353
|
+
update('todos', 'remove', variable('payload')),
|
|
354
|
+
]),
|
|
355
|
+
],
|
|
356
|
+
view: div({}, [
|
|
357
|
+
div({}, [
|
|
358
|
+
input({
|
|
359
|
+
value: state('newTodo'),
|
|
360
|
+
onInput: onInput('updateInput'),
|
|
361
|
+
}),
|
|
362
|
+
button(
|
|
363
|
+
{ onClick: onClick('addTodo') },
|
|
364
|
+
[text(lit('Add'))]
|
|
365
|
+
),
|
|
366
|
+
]),
|
|
367
|
+
each(
|
|
368
|
+
state('todos'),
|
|
369
|
+
'todo',
|
|
370
|
+
div({}, [
|
|
371
|
+
text(variable('todo')),
|
|
372
|
+
button(
|
|
373
|
+
{ onClick: onClick('removeTodo', variable('i')) },
|
|
374
|
+
[text(lit('Delete'))]
|
|
375
|
+
),
|
|
376
|
+
]),
|
|
377
|
+
{ index: 'i' }
|
|
378
|
+
),
|
|
379
|
+
]),
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Integration with @constela/compiler
|
|
384
|
+
|
|
385
|
+
The builder produces AST that can be passed directly to the compiler.
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { createProgram, /* builders */ } from '@constela/builder';
|
|
389
|
+
import { compile } from '@constela/compiler';
|
|
390
|
+
|
|
391
|
+
// Build program
|
|
392
|
+
const program = createProgram({
|
|
393
|
+
state: { count: numberField(0) },
|
|
394
|
+
actions: [action('increment', [increment('count')])],
|
|
395
|
+
view: button({ onClick: onClick('increment') }, [text(state('count'))]),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Compile
|
|
399
|
+
const result = compile(program);
|
|
400
|
+
|
|
401
|
+
if (result.ok) {
|
|
402
|
+
// Use compiled program with runtime
|
|
403
|
+
console.log(result.program);
|
|
404
|
+
} else {
|
|
405
|
+
console.error(result.errors);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## When to Use Builders
|
|
410
|
+
|
|
411
|
+
| Use Case | Recommendation |
|
|
412
|
+
|----------|----------------|
|
|
413
|
+
| Static apps | Write JSON directly |
|
|
414
|
+
| Dynamic generation | Use builders |
|
|
415
|
+
| Code generation tools | Use builders |
|
|
416
|
+
| Testing | Use builders for test fixtures |
|
|
417
|
+
| IDE with JSON support | Write JSON directly |
|
|
418
|
+
|
|
419
|
+
## License
|
|
420
|
+
|
|
421
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constela/builder",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Type-safe builders for constructing Constela AST programmatically",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@constela/core": "0.
|
|
18
|
+
"@constela/core": "0.9.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^20.10.0",
|