@jay-framework/jay-stack-cli 0.15.5 → 0.16.0

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 (39) hide show
  1. package/agent-kit-template/{INSTRUCTIONS.md → designer/INSTRUCTIONS.md} +11 -8
  2. package/agent-kit-template/{jay-html-syntax.md → designer/jay-html-components.md} +89 -158
  3. package/agent-kit-template/designer/jay-html-styling.md +97 -0
  4. package/agent-kit-template/designer/jay-html-syntax.md +44 -0
  5. package/agent-kit-template/designer/jay-html-template-syntax.md +203 -0
  6. package/agent-kit-template/developer/INSTRUCTIONS.md +34 -0
  7. package/agent-kit-template/developer/cli-commands.md +228 -0
  8. package/agent-kit-template/developer/component-data.md +109 -0
  9. package/agent-kit-template/developer/component-refs.md +117 -0
  10. package/agent-kit-template/developer/component-state.md +140 -0
  11. package/agent-kit-template/developer/configuration.md +76 -0
  12. package/agent-kit-template/developer/dev-server-service.md +126 -0
  13. package/agent-kit-template/developer/page-components.md +103 -0
  14. package/agent-kit-template/developer/page-contracts.md +114 -0
  15. package/agent-kit-template/developer/project-structure.md +242 -0
  16. package/agent-kit-template/developer/render-results.md +112 -0
  17. package/agent-kit-template/developer/routing.md +175 -0
  18. package/agent-kit-template/developer/seo-guide.md +93 -0
  19. package/agent-kit-template/plugin/INSTRUCTIONS.md +43 -0
  20. package/agent-kit-template/plugin/actions-guide.md +184 -0
  21. package/agent-kit-template/plugin/component-context.md +103 -0
  22. package/agent-kit-template/plugin/component-data.md +109 -0
  23. package/agent-kit-template/plugin/component-refs.md +117 -0
  24. package/agent-kit-template/plugin/component-state.md +140 -0
  25. package/agent-kit-template/plugin/component-structure.md +174 -0
  26. package/agent-kit-template/plugin/contracts-guide.md +193 -0
  27. package/agent-kit-template/plugin/dev-server-service.md +137 -0
  28. package/agent-kit-template/plugin/plugin-routes.md +146 -0
  29. package/agent-kit-template/plugin/plugin-structure.md +210 -0
  30. package/agent-kit-template/plugin/render-results.md +112 -0
  31. package/agent-kit-template/plugin/seo-guide.md +93 -0
  32. package/agent-kit-template/plugin/services-guide.md +116 -0
  33. package/agent-kit-template/plugin/validation.md +101 -0
  34. package/dist/index.js +791 -60
  35. package/package.json +10 -10
  36. /package/agent-kit-template/{cli-commands.md → designer/cli-commands.md} +0 -0
  37. /package/agent-kit-template/{contracts-and-plugins.md → designer/contracts-and-plugins.md} +0 -0
  38. /package/agent-kit-template/{project-structure.md → designer/project-structure.md} +0 -0
  39. /package/agent-kit-template/{routing.md → designer/routing.md} +0 -0
@@ -0,0 +1,109 @@
1
+ # Immutable Data and Patching
2
+
3
+ In Jay, ViewState data is immutable. Never mutate objects directly — use signals and JSON Patch for updates.
4
+
5
+ ## Immutable Data Model
6
+
7
+ ViewState objects passed to the render function are immutable snapshots. The framework compares old and new snapshots to determine what changed in the DOM.
8
+
9
+ ```typescript
10
+ // WRONG — never mutate directly
11
+ viewState.items.push(newItem);
12
+ viewState.count = 5;
13
+
14
+ // RIGHT — return new values from signals
15
+ const [count, setCount] = createSignal(0);
16
+ setCount(5);
17
+
18
+ return { render: () => ({ count: count() }) };
19
+ ```
20
+
21
+ ## JSON Patch for Complex Updates
22
+
23
+ For objects with many fields, use `createPatchableSignal` with JSON Patch operations instead of replacing the entire object:
24
+
25
+ ```typescript
26
+ import { createPatchableSignal } from '@jay-framework/component';
27
+ import { REPLACE, ADD, REMOVE } from '@jay-framework/json-patch';
28
+
29
+ const [state, setState, patchState] = createPatchableSignal({
30
+ title: 'Product',
31
+ price: 29.99,
32
+ tags: ['sale', 'featured'],
33
+ details: { color: 'red', size: 'M' },
34
+ });
35
+ ```
36
+
37
+ ### Patch Operations
38
+
39
+ **REPLACE** — Update an existing value:
40
+
41
+ ```typescript
42
+ patchState({ op: REPLACE, path: ['price'], value: 19.99 });
43
+ patchState({ op: REPLACE, path: ['details', 'color'], value: 'blue' });
44
+ ```
45
+
46
+ **ADD** — Add a new field or array item:
47
+
48
+ ```typescript
49
+ patchState({ op: ADD, path: ['tags', 1], value: 'new-tag' }); // Insert at index 1
50
+ patchState({ op: ADD, path: ['details', 'weight'], value: '500g' });
51
+ ```
52
+
53
+ **REMOVE** — Remove a field or array item:
54
+
55
+ ```typescript
56
+ patchState({ op: REMOVE, path: ['tags', 0] }); // Remove first tag
57
+ patchState({ op: REMOVE, path: ['details', 'size'] });
58
+ ```
59
+
60
+ **MOVE** — Move a value from one path to another:
61
+
62
+ ```typescript
63
+ import { MOVE } from '@jay-framework/json-patch';
64
+
65
+ patchState({ op: MOVE, from: ['tags', 0], path: ['tags', 2] }); // Reorder array item
66
+ patchState({ op: MOVE, from: ['details', 'color'], path: ['primaryColor'] }); // Relocate field
67
+ ```
68
+
69
+ ### Multiple Patches
70
+
71
+ Apply multiple patches at once — the framework batches them into a single update:
72
+
73
+ ```typescript
74
+ patchState(
75
+ { op: REPLACE, path: ['price'], value: 19.99 },
76
+ { op: REPLACE, path: ['details', 'color'], value: 'blue' },
77
+ );
78
+ ```
79
+
80
+ ### When to Use Patch vs Set
81
+
82
+ - **Simple values** (number, string, boolean): use `setSignal(newValue)`
83
+ - **Objects with few fields**: use `setSignal({ ...old, field: newValue })`
84
+ - **Complex nested objects**: use `patchState` for surgical updates
85
+ - **Arrays with identity tracking**: use `patchState` with ADD/REMOVE
86
+
87
+ ## createDerivedArray (Map Hook)
88
+
89
+ Transform an array reactively with smart caching. Only remaps items that actually changed:
90
+
91
+ ```typescript
92
+ import { createDerivedArray } from '@jay-framework/component';
93
+
94
+ const displayProducts = createDerivedArray(
95
+ () => products(),
96
+ (item, index, length) => ({
97
+ label: `${item().name} - ${formatPrice(item().price)}`,
98
+ position: `${index() + 1} of ${length()}`,
99
+ }),
100
+ );
101
+ ```
102
+
103
+ Key behavior:
104
+
105
+ - If an item's object identity hasn't changed, the cached mapped result is reused
106
+ - `index()` and `length()` are tracked — if you don't call them, changes to index/length won't trigger a remap
107
+ - Returns a `Getter<U[]>` — read with `displayProducts()`
108
+
109
+ See [component-state.md](component-state.md) for the full hook reference.
@@ -0,0 +1,117 @@
1
+ # Component Refs
2
+
3
+ Refs provide access to DOM elements declared as `interactive` in the contract. They are the second parameter of the interactive constructor.
4
+
5
+ ## Single Refs
6
+
7
+ A ref maps to one DOM element:
8
+
9
+ ```yaml
10
+ # Contract
11
+ - tag: addToCart
12
+ type: interactive
13
+ elementType: HTMLButtonElement
14
+ ```
15
+
16
+ ```typescript
17
+ // Component
18
+ .withInteractive(function MyComp(props, refs) {
19
+ refs.addToCart.onClick(() => {
20
+ // handle click
21
+ });
22
+ })
23
+ ```
24
+
25
+ ### Ref Methods
26
+
27
+ Refs provide type-safe access to the DOM element:
28
+
29
+ ```typescript
30
+ refs.submitButton.onClick(() => {
31
+ /* ... */
32
+ });
33
+
34
+ // exec$ gives direct access to the element and current ViewState
35
+ refs.submitButton.exec$((element, viewState) => {
36
+ element.disabled = viewState.isSubmitting;
37
+ });
38
+ ```
39
+
40
+ ## Collection Refs
41
+
42
+ When an interactive tag is inside a `repeated` sub-contract, the ref becomes a collection. In jay-html, collection refs use the `$` suffix:
43
+
44
+ ```html
45
+ <div forEach="items" trackBy="id">
46
+ <button ref="itemButton$">Click</button>
47
+ </div>
48
+ ```
49
+
50
+ The `$` is stripped from the name in the contract and component code:
51
+
52
+ ```yaml
53
+ # Contract
54
+ - tag: items
55
+ type: sub-contract
56
+ repeated: true
57
+ trackBy: id
58
+ tags:
59
+ - tag: id
60
+ type: data
61
+ dataType: string
62
+ - tag: itemButton
63
+ type: interactive
64
+ elementType: HTMLButtonElement
65
+ ```
66
+
67
+ ### Collection Ref Methods
68
+
69
+ ```typescript
70
+ // Map over all items in the collection
71
+ const labels = refs.itemButton.map((proxy, viewState, coordinate) => {
72
+ return viewState.name;
73
+ });
74
+
75
+ // Find a specific item
76
+ const target = refs.itemButton.find((viewState) => viewState.id === 'target-id');
77
+
78
+ // Find by coordinate
79
+ const target = refs.itemButton.find((viewState, coordinate) =>
80
+ sameCoordinate(coordinate, ['item-2', 'itemButton']),
81
+ );
82
+ ```
83
+
84
+ ## Element Types
85
+
86
+ Common element types for interactive tags:
87
+
88
+ | Element Type | Use For |
89
+ | --------------------- | --------------------- |
90
+ | `HTMLButtonElement` | Buttons, clickable |
91
+ | `HTMLAnchorElement` | Links |
92
+ | `HTMLInputElement` | Text inputs, checkbox |
93
+ | `HTMLSelectElement` | Dropdowns |
94
+ | `HTMLTextAreaElement` | Multi-line text |
95
+ | `HTMLFormElement` | Forms |
96
+ | `HTMLDivElement` | Generic containers |
97
+
98
+ Multiple element types (when the same ref may bind to different elements):
99
+
100
+ ```yaml
101
+ - tag: trigger
102
+ type: interactive
103
+ elementType: HTMLButtonElement | HTMLAnchorElement
104
+ ```
105
+
106
+ ## Data + Interactive
107
+
108
+ A tag can be both data and interactive:
109
+
110
+ ```yaml
111
+ - tag: quantityInput
112
+ type: [data, interactive]
113
+ dataType: number
114
+ elementType: HTMLInputElement
115
+ ```
116
+
117
+ This generates both a ViewState field and a ref.
@@ -0,0 +1,140 @@
1
+ # Component State Hooks
2
+
3
+ All hooks are used inside the interactive phase (the `withInteractive` constructor function). They provide reactive state management for client-side behavior.
4
+
5
+ ## createSignal
6
+
7
+ Creates a reactive getter/setter pair:
8
+
9
+ ```typescript
10
+ import { createSignal } from '@jay-framework/component';
11
+
12
+ const [count, setCount] = createSignal(0);
13
+
14
+ // Read
15
+ count(); // 0
16
+
17
+ // Write
18
+ setCount(5); // set to 5
19
+ setCount((n) => n + 1); // increment
20
+ ```
21
+
22
+ Can initialize from a getter (reactive dependency):
23
+
24
+ ```typescript
25
+ const [label, setLabel] = createSignal(() => 'Hello ' + props.name());
26
+ ```
27
+
28
+ ## createPatchableSignal
29
+
30
+ Creates a signal with JSON Patch support for fine-grained updates to complex objects:
31
+
32
+ ```typescript
33
+ import { createPatchableSignal } from '@jay-framework/component';
34
+ import { REPLACE } from '@jay-framework/json-patch';
35
+
36
+ const [data, setData, patchData] = createPatchableSignal({
37
+ label: 'Hello',
38
+ count: 0,
39
+ nested: { value: 42 },
40
+ });
41
+
42
+ // Patch a specific field
43
+ patchData({ op: REPLACE, path: ['label'], value: 'Updated' });
44
+
45
+ // Patch nested field
46
+ patchData({ op: REPLACE, path: ['nested', 'value'], value: 99 });
47
+ ```
48
+
49
+ See [component-data.md](component-data.md) for more on immutable data and patching.
50
+
51
+ ## createMemo
52
+
53
+ Creates a memoized computed value that recalculates only when dependencies change:
54
+
55
+ ```typescript
56
+ import { createMemo } from '@jay-framework/component';
57
+
58
+ const fullName = createMemo(() => `${firstName()} ${lastName()}`);
59
+
60
+ // Read
61
+ fullName(); // recomputes only when firstName() or lastName() change
62
+ ```
63
+
64
+ With initial value:
65
+
66
+ ```typescript
67
+ const total = createMemo((prev) => prev + latestValue(), 0);
68
+ ```
69
+
70
+ ## createEffect
71
+
72
+ Registers a side effect that runs on mount and when dependencies change. Optional cleanup function:
73
+
74
+ ```typescript
75
+ import { createEffect } from '@jay-framework/component';
76
+
77
+ createEffect(() => {
78
+ const handler = () => setWindowWidth(window.innerWidth);
79
+ window.addEventListener('resize', handler);
80
+ return () => window.removeEventListener('resize', handler); // cleanup
81
+ });
82
+ ```
83
+
84
+ Effects track reactive dependencies automatically:
85
+
86
+ ```typescript
87
+ createEffect(() => {
88
+ document.title = `${count()} items`; // reruns when count() changes
89
+ });
90
+ ```
91
+
92
+ ## createDerivedArray
93
+
94
+ Efficiently maps an array with smart caching. Only remaps items that actually changed:
95
+
96
+ ```typescript
97
+ import { createDerivedArray } from '@jay-framework/component';
98
+
99
+ const displayItems = createDerivedArray(
100
+ () => products(),
101
+ (item, index, length) => ({
102
+ name: item().name,
103
+ displayPrice: formatPrice(item().price),
104
+ isLast: index() === length() - 1,
105
+ }),
106
+ );
107
+
108
+ // Read the mapped array
109
+ displayItems();
110
+ ```
111
+
112
+ Key optimizations:
113
+
114
+ - Reuses mapped items when the source item hasn't changed
115
+ - Only tracks `index()` and `length()` if you actually call them
116
+ - Uses object identity (not deep equality) for cache hits
117
+
118
+ ## createEvent
119
+
120
+ Creates an event emitter for component-to-parent communication:
121
+
122
+ ```typescript
123
+ import { createEvent } from '@jay-framework/component';
124
+
125
+ const onChange = createEvent<{ value: number }>((emitter) => {
126
+ emitter.emit({ value: count() });
127
+ });
128
+ ```
129
+
130
+ ## useReactive
131
+
132
+ Gets the current reactive context for advanced use cases:
133
+
134
+ ```typescript
135
+ import { useReactive } from '@jay-framework/component';
136
+
137
+ const reactive = useReactive();
138
+ ```
139
+
140
+ Most components won't need this — prefer the higher-level hooks above.
@@ -0,0 +1,174 @@
1
+ # Component Structure
2
+
3
+ Full-stack components use `makeJayStackComponent` with a fluent builder API and three rendering phases.
4
+
5
+ ## Basic Structure
6
+
7
+ ```typescript
8
+ import { makeJayStackComponent, phaseOutput } from '@jay-framework/fullstack-component';
9
+ import type { MyContract } from './my-contract.generated';
10
+
11
+ export const myComponent = makeJayStackComponent<MyContract>()
12
+ .withSlowlyRender(async (props) => {
13
+ const data = await fetchStaticData();
14
+ return phaseOutput(
15
+ { title: data.title, description: data.description }, // ViewState
16
+ { productId: data.id }, // CarryForward
17
+ );
18
+ })
19
+ .withFastRender(async (props) => {
20
+ return phaseOutput({ price: await getPrice(), inStock: true }, {});
21
+ })
22
+ .withInteractive(function MyComponent(props, refs) {
23
+ // Client-side hooks here
24
+ return { render: () => ({}) };
25
+ });
26
+ ```
27
+
28
+ ## Builder API
29
+
30
+ ### `.withProps<T>()`
31
+
32
+ Declare the props type (must match contract `props`):
33
+
34
+ ```typescript
35
+ makeJayStackComponent<MyContract>().withProps<{ productId: string; currency?: string }>();
36
+ ```
37
+
38
+ ### `.withServices(...markers)`
39
+
40
+ Inject server-side services:
41
+
42
+ ```typescript
43
+ .withServices(DATABASE_SERVICE, CACHE_SERVICE)
44
+ .withSlowlyRender(async (props, db, cache) => {
45
+ // db and cache are injected
46
+ })
47
+ ```
48
+
49
+ ### `.withContexts(...markers)`
50
+
51
+ Consume client-side contexts in the interactive phase:
52
+
53
+ ```typescript
54
+ .withContexts(CART_CONTEXT)
55
+ .withInteractive(function MyComp(props, refs, cartCtx) {
56
+ // cartCtx available in interactive phase
57
+ })
58
+ ```
59
+
60
+ ### `.withLoadParams(fn)`
61
+
62
+ Generate URL params for SSG (static site generation). Returns an async iterable of param arrays:
63
+
64
+ ```typescript
65
+ .withLoadParams(async function* (db) {
66
+ const products = await db.getAllProducts();
67
+ yield products.map(p => ({ slug: p.slug }));
68
+ })
69
+ ```
70
+
71
+ ### `.withSlowlyRender(fn)` — Build-time rendering
72
+
73
+ Runs during SSG. Has access to props and services. Returns `phaseOutput(viewState, carryForward)`.
74
+
75
+ CarryForward data is passed to the fast phase but not included in the ViewState.
76
+
77
+ ```typescript
78
+ .withSlowlyRender(async (props, db) => {
79
+ const product = await db.getProduct(props.slug);
80
+ if (!product) return notFound('Product not found');
81
+ return phaseOutput(
82
+ { title: product.name, description: product.desc },
83
+ { productId: product.id }, // Available in fast phase
84
+ );
85
+ })
86
+ ```
87
+
88
+ ### `.withFastRender(fn)` — Request-time rendering
89
+
90
+ Runs on each request. Receives props (including `query` for query parameters) and carry-forward from slow phase.
91
+
92
+ ```typescript
93
+ .withFastRender(async (props, db) => {
94
+ const price = await db.getPrice(props.carryForward.productId);
95
+ return phaseOutput({ price, inStock: price > 0 }, {});
96
+ })
97
+ ```
98
+
99
+ ### `.withClientDefaults(fn)` — Default client ViewState
100
+
101
+ Provides default values for client-side ViewState before hydration:
102
+
103
+ ```typescript
104
+ .withClientDefaults(() => ({
105
+ quantity: 1,
106
+ selectedVariant: 'default',
107
+ }))
108
+ ```
109
+
110
+ ### `.withInteractive(ComponentConstructor)` — Client-side logic
111
+
112
+ The interactive phase runs in the browser. Use hooks here (see component-state.md):
113
+
114
+ ```typescript
115
+ .withInteractive(function ProductPage(props, refs) {
116
+ const [quantity, setQuantity] = createSignal(1);
117
+
118
+ refs.addToCart.onClick(() => {
119
+ addToCartAction({ productId: props.productId, quantity: quantity() });
120
+ });
121
+
122
+ return {
123
+ render: () => ({
124
+ quantity: quantity(),
125
+ }),
126
+ };
127
+ })
128
+ ```
129
+
130
+ ## Render Return Types
131
+
132
+ Each phase can return:
133
+
134
+ - `phaseOutput(viewState, carryForward)` — success
135
+ - `notFound()`, `badRequest()`, `unauthorized()`, `forbidden()` — client errors
136
+ - `serverError5xx(status, message)` — server errors
137
+ - `redirect3xx(status, location)` — redirects
138
+
139
+ See [render-results.md](render-results.md) for details.
140
+
141
+ ## Props and Contract Alignment
142
+
143
+ The component's props type must match the contract's `props` section:
144
+
145
+ ```yaml
146
+ # Contract
147
+ props:
148
+ - name: productId
149
+ type: string
150
+ required: true
151
+ ```
152
+
153
+ ```typescript
154
+ // Component
155
+ makeJayStackComponent<MyContract>().withProps<{ productId: string }>();
156
+ ```
157
+
158
+ ## Params and Contract Alignment
159
+
160
+ If the contract has `params`, the component should use `withLoadParams` and the route must have matching dynamic segments:
161
+
162
+ ```yaml
163
+ # Contract
164
+ params:
165
+ slug: string
166
+ ```
167
+
168
+ ```typescript
169
+ // Component
170
+ .withLoadParams(async function* (db) {
171
+ const items = await db.getAll();
172
+ yield items.map(i => ({ slug: i.slug }));
173
+ })
174
+ ```