@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.
Files changed (2) hide show
  1. package/README.md +421 -0
  2. 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.0",
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.8.0"
18
+ "@constela/core": "0.9.1"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.10.0",