@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.
- 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/dev-server-service.md +126 -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 +175 -0
- package/agent-kit-template/developer/seo-guide.md +93 -0
- package/agent-kit-template/plugin/INSTRUCTIONS.md +43 -0
- package/agent-kit-template/plugin/actions-guide.md +184 -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/dev-server-service.md +137 -0
- package/agent-kit-template/plugin/plugin-routes.md +146 -0
- package/agent-kit-template/plugin/plugin-structure.md +210 -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 +791 -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,175 @@
|
|
|
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
|
|
162
|
+
|
|
163
|
+
## Plugin Routes
|
|
164
|
+
|
|
165
|
+
Plugins can provide their own pages via `routes` in `plugin.yaml`. These are backoffice tools, admin dashboards, or editors with boxed designs that don't need per-site customization.
|
|
166
|
+
|
|
167
|
+
Plugin routes appear alongside project routes. If your project defines the same route path in `src/pages/`, your page takes precedence — the plugin's page is skipped.
|
|
168
|
+
|
|
169
|
+
To override a plugin route, simply create a page at the same path:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
# Plugin provides /admin/products
|
|
173
|
+
# To customize, create:
|
|
174
|
+
src/pages/admin/products/page.jay-html
|
|
175
|
+
```
|
|
@@ -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,43 @@
|
|
|
1
|
+
# Jay Plugin Development — Agent Kit
|
|
2
|
+
|
|
3
|
+
This folder contains guides for creating jay-stack plugins: contracts, headless components, server actions, services, and plugin-provided routes.
|
|
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 also provide complete pages (backoffice tools, admin dashboards) via routes. 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. **Optionally add routes** — pages for admin tools and dashboards
|
|
15
|
+
5. **Set up `plugin.yaml`** — list contracts, actions, services, contexts, routes
|
|
16
|
+
6. **Validate** with `jay-stack validate-plugin`
|
|
17
|
+
|
|
18
|
+
## Guides
|
|
19
|
+
|
|
20
|
+
| File | Topic |
|
|
21
|
+
| ------------------------------------------------ | ----------------------------------------------------------------------- |
|
|
22
|
+
| [contracts-guide.md](contracts-guide.md) | Contract format: tags, types, phases, props, params, sub-contracts |
|
|
23
|
+
| [plugin-structure.md](plugin-structure.md) | plugin.yaml, package layout, exports |
|
|
24
|
+
| [component-structure.md](component-structure.md) | makeJayStackComponent, builder API, three-phase rendering |
|
|
25
|
+
| [component-state.md](component-state.md) | createSignal, createMemo, createEffect, createDerivedArray, createEvent |
|
|
26
|
+
| [component-refs.md](component-refs.md) | Refs, collection refs, element types |
|
|
27
|
+
| [component-data.md](component-data.md) | Immutable data, JSON Patch, createPatchableSignal |
|
|
28
|
+
| [component-context.md](component-context.md) | Context hooks: provide, reactive, global |
|
|
29
|
+
| [render-results.md](render-results.md) | phaseOutput, RenderPipeline, errors, redirects |
|
|
30
|
+
| [actions-guide.md](actions-guide.md) | makeJayAction, makeJayQuery, .jay-action files |
|
|
31
|
+
| [services-guide.md](services-guide.md) | createJayService, makeJayInit |
|
|
32
|
+
| [plugin-routes.md](plugin-routes.md) | Plugin-provided pages: routes, jay-html templates, page components |
|
|
33
|
+
| [seo-guide.md](seo-guide.md) | SEO head tags: title, meta, OG, canonical via phaseOutput |
|
|
34
|
+
| [validation.md](validation.md) | jay-stack validate-plugin usage |
|
|
35
|
+
| [dev-server-service.md](dev-server-service.md) | Dev server service API: routes, params, freeze management |
|
|
36
|
+
| `../references/<plugin>/` | Plugin reference data |
|
|
37
|
+
|
|
38
|
+
## Key Principles
|
|
39
|
+
|
|
40
|
+
- **Contract is the source of truth** — define it before implementing the component
|
|
41
|
+
- **Data is immutable** — never mutate ViewState directly, use JSON Patch
|
|
42
|
+
- **Phase-aware** — choose the right rendering phase for each piece of data
|
|
43
|
+
- **Props for configuration, params for URLs** — props are passed by parent components, params come from route segments
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
## makeJayStream — Streaming (POST, NDJSON)
|
|
119
|
+
|
|
120
|
+
Streaming actions return an async generator that yields chunks:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { makeJayStream } from '@jay-framework/fullstack-component';
|
|
124
|
+
|
|
125
|
+
export const discoverParams = makeJayStream('routes.discoverParams')
|
|
126
|
+
.withServices(PRODUCTS_SERVICE)
|
|
127
|
+
.withHandler(async function* (input: { route: string }, productsService) {
|
|
128
|
+
let page = 1;
|
|
129
|
+
while (true) {
|
|
130
|
+
const products = await productsService.list({ page, pageSize: 100 });
|
|
131
|
+
yield products.map((p) => ({ slug: p.slug }));
|
|
132
|
+
if (!products.hasMore) break;
|
|
133
|
+
page++;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Consuming on the client
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
for await (const batch of discoverParams({ route: '/products/[slug]' })) {
|
|
142
|
+
console.log(batch); // [{ slug: 'item-a' }, { slug: 'item-b' }]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Wire format
|
|
147
|
+
|
|
148
|
+
The server responds with NDJSON (newline-delimited JSON). Each line is a complete JSON object:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
{"chunk":[{"slug":"item-a"},{"slug":"item-b"}]}
|
|
152
|
+
{"chunk":[{"slug":"item-c"}]}
|
|
153
|
+
{"done":true}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### .jay-action for streaming
|
|
157
|
+
|
|
158
|
+
Add `streaming: true` to the metadata file:
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
name: discoverParams
|
|
162
|
+
description: Discover URL params by querying the product catalog
|
|
163
|
+
streaming: true
|
|
164
|
+
inputSchema:
|
|
165
|
+
route: string
|
|
166
|
+
outputSchema:
|
|
167
|
+
- slug: string
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Type Helpers
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import {
|
|
174
|
+
ActionInput,
|
|
175
|
+
ActionOutput,
|
|
176
|
+
isJayAction,
|
|
177
|
+
StreamChunk,
|
|
178
|
+
isJayStreamAction,
|
|
179
|
+
} from '@jay-framework/fullstack-component';
|
|
180
|
+
|
|
181
|
+
type SearchInput = ActionInput<typeof searchProducts>;
|
|
182
|
+
type SearchOutput = ActionOutput<typeof searchProducts>;
|
|
183
|
+
type ParamBatch = StreamChunk<typeof discoverParams>;
|
|
184
|
+
```
|
|
@@ -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.
|