@jay-framework/jay-stack-cli 0.15.5 → 0.15.6
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/agent-kit-template/{INSTRUCTIONS.md → designer/INSTRUCTIONS.md} +11 -8
- package/agent-kit-template/{jay-html-syntax.md → designer/jay-html-components.md} +89 -158
- package/agent-kit-template/designer/jay-html-styling.md +97 -0
- package/agent-kit-template/designer/jay-html-syntax.md +44 -0
- package/agent-kit-template/designer/jay-html-template-syntax.md +203 -0
- package/agent-kit-template/developer/INSTRUCTIONS.md +34 -0
- package/agent-kit-template/developer/cli-commands.md +228 -0
- package/agent-kit-template/developer/component-data.md +109 -0
- package/agent-kit-template/developer/component-refs.md +117 -0
- package/agent-kit-template/developer/component-state.md +140 -0
- package/agent-kit-template/developer/configuration.md +76 -0
- package/agent-kit-template/developer/page-components.md +103 -0
- package/agent-kit-template/developer/page-contracts.md +114 -0
- package/agent-kit-template/developer/project-structure.md +242 -0
- package/agent-kit-template/developer/render-results.md +112 -0
- package/agent-kit-template/developer/routing.md +161 -0
- package/agent-kit-template/developer/seo-guide.md +93 -0
- package/agent-kit-template/plugin/INSTRUCTIONS.md +40 -0
- package/agent-kit-template/plugin/actions-guide.md +125 -0
- package/agent-kit-template/plugin/component-context.md +103 -0
- package/agent-kit-template/plugin/component-data.md +109 -0
- package/agent-kit-template/plugin/component-refs.md +117 -0
- package/agent-kit-template/plugin/component-state.md +140 -0
- package/agent-kit-template/plugin/component-structure.md +174 -0
- package/agent-kit-template/plugin/contracts-guide.md +193 -0
- package/agent-kit-template/plugin/plugin-structure.md +194 -0
- package/agent-kit-template/plugin/render-results.md +112 -0
- package/agent-kit-template/plugin/seo-guide.md +93 -0
- package/agent-kit-template/plugin/services-guide.md +116 -0
- package/agent-kit-template/plugin/validation.md +101 -0
- package/dist/index.js +678 -60
- package/package.json +10 -10
- /package/agent-kit-template/{cli-commands.md → designer/cli-commands.md} +0 -0
- /package/agent-kit-template/{contracts-and-plugins.md → designer/contracts-and-plugins.md} +0 -0
- /package/agent-kit-template/{project-structure.md → designer/project-structure.md} +0 -0
- /package/agent-kit-template/{routing.md → designer/routing.md} +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Directory-Based Routing
|
|
2
|
+
|
|
3
|
+
## Route Structure
|
|
4
|
+
|
|
5
|
+
Pages live under `src/pages/`. Directory names become URL segments.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/pages/
|
|
9
|
+
├── page.jay-html → /
|
|
10
|
+
├── about/
|
|
11
|
+
│ └── page.jay-html → /about
|
|
12
|
+
├── products/
|
|
13
|
+
│ ├── page.jay-html → /products
|
|
14
|
+
│ └── [slug]/
|
|
15
|
+
│ └── page.jay-html → /products/:slug
|
|
16
|
+
├── blog/
|
|
17
|
+
│ ├── page.jay-html → /blog
|
|
18
|
+
│ └── [[slug]]/
|
|
19
|
+
│ └── page.jay-html → /blog/:slug (optional)
|
|
20
|
+
└── files/
|
|
21
|
+
└── [...path]/
|
|
22
|
+
└── page.jay-html → /files/* (catch-all)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Dynamic Routes
|
|
26
|
+
|
|
27
|
+
| Syntax | Meaning | Example |
|
|
28
|
+
| ------------ | ------------------ | --------------------------------------- |
|
|
29
|
+
| `[param]` | Required parameter | `[slug]` → `/products/:slug` |
|
|
30
|
+
| `[[param]]` | Optional parameter | `[[slug]]` → `/blog` or `/blog/my-post` |
|
|
31
|
+
| `[...param]` | Catch-all | `[...path]` → matches any sub-path |
|
|
32
|
+
|
|
33
|
+
## Route Priority
|
|
34
|
+
|
|
35
|
+
Static routes match before dynamic routes (most specific first):
|
|
36
|
+
|
|
37
|
+
1. **Static segments** (exact match) — highest priority
|
|
38
|
+
2. **`[param]`** — required dynamic param
|
|
39
|
+
3. **`[[param]]`** — optional param
|
|
40
|
+
4. **`[...param]`** — catch-all — lowest priority
|
|
41
|
+
|
|
42
|
+
## Static Route Overrides
|
|
43
|
+
|
|
44
|
+
A static route can override a dynamic route for a specific URL — giving one particular page a custom layout while the dynamic route handles everything else:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
src/pages/products/
|
|
48
|
+
├── [slug]/page.jay-html # dynamic: /products/:slug
|
|
49
|
+
└── ceramic-flower-vase/page.jay-html # static override for this specific product
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The static `ceramic-flower-vase/` route takes priority over `[slug]/` for that URL, but all other product URLs still use the dynamic route.
|
|
53
|
+
|
|
54
|
+
### Static Override Params (`jay-params`)
|
|
55
|
+
|
|
56
|
+
Static override routes often use the same contract as the dynamic route they override. Since the static route has no dynamic directory segment, the params must be declared explicitly using `<script type="application/jay-params">`:
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<!-- src/pages/products/ceramic-flower-vase/page.jay-html -->
|
|
60
|
+
<html>
|
|
61
|
+
<head>
|
|
62
|
+
<script type="application/jay-params">
|
|
63
|
+
slug: ceramic-flower-vase
|
|
64
|
+
</script>
|
|
65
|
+
<script
|
|
66
|
+
type="application/jay-headless"
|
|
67
|
+
plugin="wix-stores"
|
|
68
|
+
contract="product-page"
|
|
69
|
+
key="product"
|
|
70
|
+
></script>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<h1>{product.productName}</h1>
|
|
74
|
+
</body>
|
|
75
|
+
</html>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The script body is YAML. The declared params are passed to the component as if extracted from a dynamic URL segment. Without this, the component would receive no param values.
|
|
79
|
+
|
|
80
|
+
## Page Files
|
|
81
|
+
|
|
82
|
+
Each page directory can contain:
|
|
83
|
+
|
|
84
|
+
| File | Purpose |
|
|
85
|
+
| ------------------- | ----------------------------------- |
|
|
86
|
+
| `page.jay-html` | Template (required for rendering) |
|
|
87
|
+
| `page.jay-contract` | Page-level data contract (optional) |
|
|
88
|
+
|
|
89
|
+
### page.jay-contract
|
|
90
|
+
|
|
91
|
+
Defines the page's own ViewState — data that the page's server-side code provides:
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
name: Page
|
|
95
|
+
tags:
|
|
96
|
+
- tag: title
|
|
97
|
+
type: data
|
|
98
|
+
dataType: string
|
|
99
|
+
phase: slow
|
|
100
|
+
- tag: items
|
|
101
|
+
type: sub-contract
|
|
102
|
+
repeated: true
|
|
103
|
+
trackBy: id
|
|
104
|
+
tags:
|
|
105
|
+
- tag: id
|
|
106
|
+
type: data
|
|
107
|
+
dataType: string
|
|
108
|
+
- tag: name
|
|
109
|
+
type: data
|
|
110
|
+
dataType: string
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Dynamic Routes and Contract Params
|
|
114
|
+
|
|
115
|
+
When a component on the page — whether the page contract, a headless component, or a headfull full-stack component — declares `params`, the page should be placed in a dynamic route directory that provides those params.
|
|
116
|
+
|
|
117
|
+
For example, if a headless component's contract declares:
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
name: product-page
|
|
121
|
+
params:
|
|
122
|
+
slug: string
|
|
123
|
+
tags:
|
|
124
|
+
- ...
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Then the page using this component should live at a route that provides a `slug` param:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
src/pages/products/[slug]/page.jay-html
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Multiple components on the same page can each declare params. The route directory must provide all required params across all components. For example, if the page contract requires `lang` and a headless component requires `slug`, the page should live at `src/pages/[lang]/products/[slug]/page.jay-html`.
|
|
134
|
+
|
|
135
|
+
### Discovering Param Values
|
|
136
|
+
|
|
137
|
+
For SSG with dynamic routes, the plugin component provides a `loadParams` generator that yields all valid param combinations. Use it to discover what routes will be generated:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
jay-stack params wix-stores/product-page
|
|
141
|
+
# Output: [{"slug": "blue-shirt"}, {"slug": "red-hat"}, ...]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Params are always strings (URL params).
|
|
145
|
+
|
|
146
|
+
## Query Parameters
|
|
147
|
+
|
|
148
|
+
URL query parameters (`?page=2&sort=price`) are available in the **fast render phase only** via `props.query`:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
.withFastRender(async (props, carryForward, dbService) => {
|
|
152
|
+
const page = parseInt(props.query.page || '1');
|
|
153
|
+
const sort = props.query.sort || 'name';
|
|
154
|
+
const products = await dbService.getProducts({ page, sort });
|
|
155
|
+
return phaseOutput({ products, currentPage: page }, {});
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- `props.query` is `Record<string, string>` — empty `{}` when no query string
|
|
160
|
+
- Not available in the slow phase (compile error) — slow results are cached by path params only
|
|
161
|
+
- In the interactive phase, use `new URLSearchParams(window.location.search)` directly
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# SEO Head Tags
|
|
2
|
+
|
|
3
|
+
Components inject `<title>`, `<meta>`, `<link>` tags into the HTML `<head>` during SSR by returning `headTags` from `phaseOutput()`.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
return phaseOutput(
|
|
9
|
+
{ title: product.name, price: product.price },
|
|
10
|
+
{ productId: product.id },
|
|
11
|
+
{
|
|
12
|
+
headTags: [
|
|
13
|
+
{ tag: 'title', children: `${product.name} | My Store` },
|
|
14
|
+
{ tag: 'meta', attrs: { name: 'description', content: product.description } },
|
|
15
|
+
{ tag: 'meta', attrs: { property: 'og:title', content: product.name } },
|
|
16
|
+
{ tag: 'meta', attrs: { property: 'og:description', content: product.description } },
|
|
17
|
+
{ tag: 'meta', attrs: { property: 'og:image', content: product.imageUrl } },
|
|
18
|
+
{ tag: 'link', attrs: { rel: 'canonical', href: canonicalUrl } },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## HeadTag Type
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
interface HeadTag {
|
|
28
|
+
tag: string; // 'title', 'meta', 'link', etc.
|
|
29
|
+
attrs?: Record<string, string>; // HTML attributes
|
|
30
|
+
children?: string; // Text content (for <title>, <script>, etc.)
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Common SEO Tags
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Page title
|
|
38
|
+
{ tag: 'title', children: 'Product Name | Store' }
|
|
39
|
+
|
|
40
|
+
// Meta description
|
|
41
|
+
{ tag: 'meta', attrs: { name: 'description', content: 'Product description here' } }
|
|
42
|
+
|
|
43
|
+
// Open Graph
|
|
44
|
+
{ tag: 'meta', attrs: { property: 'og:title', content: 'Product Name' } }
|
|
45
|
+
{ tag: 'meta', attrs: { property: 'og:description', content: 'Description' } }
|
|
46
|
+
{ tag: 'meta', attrs: { property: 'og:image', content: 'https://...' } }
|
|
47
|
+
{ tag: 'meta', attrs: { property: 'og:type', content: 'product' } }
|
|
48
|
+
|
|
49
|
+
// Twitter Card
|
|
50
|
+
{ tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' } }
|
|
51
|
+
{ tag: 'meta', attrs: { name: 'twitter:title', content: 'Product Name' } }
|
|
52
|
+
|
|
53
|
+
// Canonical URL
|
|
54
|
+
{ tag: 'link', attrs: { rel: 'canonical', href: 'https://example.com/products/slug' } }
|
|
55
|
+
|
|
56
|
+
// JSON-LD structured data
|
|
57
|
+
{ tag: 'script', attrs: { type: 'application/ld+json' }, children: JSON.stringify(structuredData) }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Mapping Generic SEO Data
|
|
61
|
+
|
|
62
|
+
If the data source provides a generic structure (array of tags with type/props/children), map it:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const headTags = seoData.tags.map((tag) => ({
|
|
66
|
+
tag: tag.type,
|
|
67
|
+
attrs: Object.fromEntries(tag.props.map((p) => [p.key, p.value])),
|
|
68
|
+
children: tag.children,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
return phaseOutput(viewState, carryForward, { headTags });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Phase Rules
|
|
75
|
+
|
|
76
|
+
- Return headTags from **slow** phase for build-time SEO data (product name, description)
|
|
77
|
+
- Return headTags from **fast** phase for per-request data (pricing, availability)
|
|
78
|
+
- Fast phase headTags **replace** slow phase entirely (no merge)
|
|
79
|
+
- No interactive phase — head tags are SSR-only
|
|
80
|
+
|
|
81
|
+
## Collision Rules
|
|
82
|
+
|
|
83
|
+
- `<title>` — singleton, last-write-wins
|
|
84
|
+
- `<meta name="X">` — keyed by `name`, last-write-wins
|
|
85
|
+
- `<meta property="X">` — keyed by `property`, last-write-wins
|
|
86
|
+
- `<link rel="canonical">` — singleton, last-write-wins
|
|
87
|
+
- Other tags — always included (no dedup)
|
|
88
|
+
- A warning is logged on collision between different components
|
|
89
|
+
|
|
90
|
+
## Restrictions
|
|
91
|
+
|
|
92
|
+
- Head tags from components inside `forEach` are ignored
|
|
93
|
+
- The framework handles HTML escaping automatically
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Jay Plugin Development — Agent Kit
|
|
2
|
+
|
|
3
|
+
This folder contains guides for creating jay-stack plugins: contracts, headless components, server actions, and services.
|
|
4
|
+
|
|
5
|
+
## What is a Jay Plugin?
|
|
6
|
+
|
|
7
|
+
A plugin provides headless components (data + interactions, no UI) that project designers use via contracts. Plugins can be standalone npm packages or inline within a project (see `examples/jay-stack/fake-shop`).
|
|
8
|
+
|
|
9
|
+
## Workflow
|
|
10
|
+
|
|
11
|
+
1. **Define contracts first** — the contract is the source of truth
|
|
12
|
+
2. **Implement components** matching the contracts
|
|
13
|
+
3. **Define actions** with `.jay-action` metadata
|
|
14
|
+
4. **Set up `plugin.yaml`**
|
|
15
|
+
5. **Validate** with `jay-stack validate-plugin`
|
|
16
|
+
|
|
17
|
+
## Guides
|
|
18
|
+
|
|
19
|
+
| File | Topic |
|
|
20
|
+
| ------------------------------------------------ | ----------------------------------------------------------------------- |
|
|
21
|
+
| [contracts-guide.md](contracts-guide.md) | Contract format: tags, types, phases, props, params, sub-contracts |
|
|
22
|
+
| [plugin-structure.md](plugin-structure.md) | plugin.yaml, package layout, exports |
|
|
23
|
+
| [component-structure.md](component-structure.md) | makeJayStackComponent, builder API, three-phase rendering |
|
|
24
|
+
| [component-state.md](component-state.md) | createSignal, createMemo, createEffect, createDerivedArray, createEvent |
|
|
25
|
+
| [component-refs.md](component-refs.md) | Refs, collection refs, element types |
|
|
26
|
+
| [component-data.md](component-data.md) | Immutable data, JSON Patch, createPatchableSignal |
|
|
27
|
+
| [component-context.md](component-context.md) | Context hooks: provide, reactive, global |
|
|
28
|
+
| [render-results.md](render-results.md) | phaseOutput, RenderPipeline, errors, redirects |
|
|
29
|
+
| [actions-guide.md](actions-guide.md) | makeJayAction, makeJayQuery, .jay-action files |
|
|
30
|
+
| [services-guide.md](services-guide.md) | createJayService, makeJayInit |
|
|
31
|
+
| [seo-guide.md](seo-guide.md) | SEO head tags: title, meta, OG, canonical via phaseOutput |
|
|
32
|
+
| [validation.md](validation.md) | jay-stack validate-plugin usage |
|
|
33
|
+
| `../references/<plugin>/` | Plugin reference data |
|
|
34
|
+
|
|
35
|
+
## Key Principles
|
|
36
|
+
|
|
37
|
+
- **Contract is the source of truth** — define it before implementing the component
|
|
38
|
+
- **Data is immutable** — never mutate ViewState directly, use JSON Patch
|
|
39
|
+
- **Phase-aware** — choose the right rendering phase for each piece of data
|
|
40
|
+
- **Props for configuration, params for URLs** — props are passed by parent components, params come from route segments
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Server Actions
|
|
2
|
+
|
|
3
|
+
Actions provide RPC-style server endpoints for client-to-server communication.
|
|
4
|
+
|
|
5
|
+
## makeJayAction — Mutations (POST)
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { makeJayAction } from '@jay-framework/fullstack-component';
|
|
9
|
+
|
|
10
|
+
export const addToCart = makeJayAction('cart.addToCart')
|
|
11
|
+
.withServices(CART_SERVICE)
|
|
12
|
+
.withHandler(async (input: { productId: string; quantity: number }, cartService) => {
|
|
13
|
+
const cart = await cartService.addItem(input.productId, input.quantity);
|
|
14
|
+
return { cartItemCount: cart.items.length };
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## makeJayQuery — Reads (GET)
|
|
19
|
+
|
|
20
|
+
Queries use GET and support caching:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { makeJayQuery } from '@jay-framework/fullstack-component';
|
|
24
|
+
|
|
25
|
+
export const searchProducts = makeJayQuery('products.search')
|
|
26
|
+
.withServices(PRODUCTS_SERVICE)
|
|
27
|
+
.withCaching({ maxAge: 60 })
|
|
28
|
+
.withHandler(async (input: { query: string; page?: number }, productsDb) => {
|
|
29
|
+
const results = await productsDb.search(input.query, input.page);
|
|
30
|
+
return { products: results.items, totalCount: results.total };
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Builder API
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
makeJayAction('name')
|
|
38
|
+
.withServices(SERVICE1, SERVICE2) // Inject services
|
|
39
|
+
.withMethod('PUT') // Override HTTP method (default: POST for actions)
|
|
40
|
+
.withCaching({ maxAge: 60 }) // Enable caching (queries only)
|
|
41
|
+
.withHandler(async (input, svc1, svc2) => {
|
|
42
|
+
// Define handler
|
|
43
|
+
return result;
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## ActionError
|
|
48
|
+
|
|
49
|
+
Throw typed errors from action handlers:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { ActionError } from '@jay-framework/fullstack-component';
|
|
53
|
+
|
|
54
|
+
throw new ActionError('OUT_OF_STOCK', 'Only 2 units available');
|
|
55
|
+
throw new ActionError('INVALID_INPUT', 'Product ID is required');
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Calling Actions from Client
|
|
59
|
+
|
|
60
|
+
Actions are callable functions on the client:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
.withInteractive(function MyComp(props, refs) {
|
|
64
|
+
refs.addToCart.onClick(async () => {
|
|
65
|
+
const result = await addToCart({
|
|
66
|
+
productId: props.productId,
|
|
67
|
+
quantity: 1,
|
|
68
|
+
});
|
|
69
|
+
// result.cartItemCount
|
|
70
|
+
});
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Calling Actions from Server
|
|
75
|
+
|
|
76
|
+
When called from server-side code (e.g., within a render phase), services are automatically injected — no network call is made.
|
|
77
|
+
|
|
78
|
+
## .jay-action Metadata Files
|
|
79
|
+
|
|
80
|
+
Each action should have a `.jay-action` file describing its input/output schemas for agent discovery:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
name: searchProducts
|
|
84
|
+
description: Search products with text, filters, sorting, and pagination
|
|
85
|
+
|
|
86
|
+
import:
|
|
87
|
+
productCard: product-card.jay-contract
|
|
88
|
+
|
|
89
|
+
inputSchema:
|
|
90
|
+
query: string
|
|
91
|
+
filters?:
|
|
92
|
+
inStockOnly?: boolean
|
|
93
|
+
minPrice?: number
|
|
94
|
+
maxPrice?: number
|
|
95
|
+
sortBy?: enum(relevance | price_asc | price_desc)
|
|
96
|
+
pageSize?: number
|
|
97
|
+
|
|
98
|
+
outputSchema:
|
|
99
|
+
products:
|
|
100
|
+
- productCard
|
|
101
|
+
totalCount: number
|
|
102
|
+
hasMore: boolean
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Jay-Type Notation for Schemas
|
|
106
|
+
|
|
107
|
+
| Notation | Meaning |
|
|
108
|
+
| ------------------------- | ------------------------- |
|
|
109
|
+
| `prop: string` | Required string |
|
|
110
|
+
| `prop?: number` | Optional number |
|
|
111
|
+
| `prop: boolean` | Required boolean |
|
|
112
|
+
| `prop: enum(a \| b \| c)` | Required enum |
|
|
113
|
+
| `prop:` + nested block | Nested object |
|
|
114
|
+
| `prop:` + `- child: type` | Array of objects |
|
|
115
|
+
| `prop: record(T)` | Record with typed values |
|
|
116
|
+
| `prop: importedName` | Type from `import:` block |
|
|
117
|
+
|
|
118
|
+
## Type Helpers
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { ActionInput, ActionOutput, isJayAction } from '@jay-framework/fullstack-component';
|
|
122
|
+
|
|
123
|
+
type SearchInput = ActionInput<typeof searchProducts>;
|
|
124
|
+
type SearchOutput = ActionOutput<typeof searchProducts>;
|
|
125
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Component Context
|
|
2
|
+
|
|
3
|
+
Context provides a way to share state between components without passing props through every level.
|
|
4
|
+
|
|
5
|
+
## Context Markers
|
|
6
|
+
|
|
7
|
+
Create a typed marker to identify a context:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { createContextMarker } from '@jay-framework/component';
|
|
11
|
+
|
|
12
|
+
interface CartContext {
|
|
13
|
+
itemCount: () => number;
|
|
14
|
+
addItem: (productId: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CART_CONTEXT = createContextMarker<CartContext>('CartContext');
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## provideContext
|
|
21
|
+
|
|
22
|
+
Provide a non-reactive context value to child components:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { provideContext } from '@jay-framework/component';
|
|
26
|
+
|
|
27
|
+
provideContext(CART_CONTEXT, {
|
|
28
|
+
itemCount: () => items().length,
|
|
29
|
+
addItem: (id) => addToCartAction({ productId: id }),
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## provideReactiveContext
|
|
34
|
+
|
|
35
|
+
Provide a reactive context — the factory function has access to hooks:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { provideReactiveContext, createSignal } from '@jay-framework/component';
|
|
39
|
+
|
|
40
|
+
const cartCtx = provideReactiveContext(CART_CONTEXT, () => {
|
|
41
|
+
const [items, setItems] = createSignal<CartItem[]>([]);
|
|
42
|
+
return {
|
|
43
|
+
itemCount: () => items().length,
|
|
44
|
+
addItem: (id) => setItems((prev) => [...prev, { productId: id }]),
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The returned value is the context instance, usable in the same component.
|
|
50
|
+
|
|
51
|
+
## registerReactiveGlobalContext
|
|
52
|
+
|
|
53
|
+
Register a context globally during client initialization (in `makeJayInit`):
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { registerReactiveGlobalContext, createSignal } from '@jay-framework/component';
|
|
57
|
+
|
|
58
|
+
export const init = makeJayInit().withClient(() => {
|
|
59
|
+
registerReactiveGlobalContext(CART_CONTEXT, () => {
|
|
60
|
+
const [items, setItems] = createSignal<CartItem[]>([]);
|
|
61
|
+
return {
|
|
62
|
+
itemCount: () => items().length,
|
|
63
|
+
addItem: (id) => setItems((prev) => [...prev, { productId: id }]),
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Global contexts are available to all components without explicit providing.
|
|
70
|
+
|
|
71
|
+
## Consuming Context
|
|
72
|
+
|
|
73
|
+
Components consume contexts via `.withContexts()` on the builder:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
makeJayStackComponent<MyContract>()
|
|
77
|
+
.withContexts(CART_CONTEXT)
|
|
78
|
+
.withInteractive(function MyComp(props, refs, cartCtx) {
|
|
79
|
+
refs.addToCart.onClick(() => {
|
|
80
|
+
cartCtx.addItem(props.productId);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
render: () => ({
|
|
85
|
+
cartCount: cartCtx.itemCount(),
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Listing in plugin.yaml
|
|
92
|
+
|
|
93
|
+
If your plugin provides contexts for other plugins to consume, list them in `plugin.yaml`:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
contexts:
|
|
97
|
+
- name: cart
|
|
98
|
+
marker: CART_CONTEXT
|
|
99
|
+
description: Client-side cart state (item count, add/remove items, totals)
|
|
100
|
+
doc: ./docs/cart-context.md # optional — markdown documentation
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This makes the context discoverable in `plugins-index.yaml`. If `doc` is provided, the file must exist and be exported from the package.
|
|
@@ -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.
|