@jay-framework/jay-stack-cli 0.15.4 → 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/{contracts-and-plugins.md → designer/contracts-and-plugins.md} +1 -0
- 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 +805 -61
- package/package.json +10 -10
- /package/agent-kit-template/{cli-commands.md → designer/cli-commands.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,193 @@
|
|
|
1
|
+
# Contract Authoring Guide
|
|
2
|
+
|
|
3
|
+
Contracts (`.jay-contract` files) are the source of truth for a component's data shape. Define the contract before implementing the component.
|
|
4
|
+
|
|
5
|
+
## Basic Structure
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
name: ProductCard
|
|
9
|
+
description: Displays a single product with price and add-to-cart. Use for product grids and featured sections.
|
|
10
|
+
props:
|
|
11
|
+
- name: productId
|
|
12
|
+
type: string
|
|
13
|
+
required: true
|
|
14
|
+
description: The product to display
|
|
15
|
+
params:
|
|
16
|
+
slug: string
|
|
17
|
+
tags:
|
|
18
|
+
- tag: name
|
|
19
|
+
type: data
|
|
20
|
+
dataType: string
|
|
21
|
+
phase: slow
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Tag Types
|
|
25
|
+
|
|
26
|
+
### `data` — Read-only values
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
- tag: productName
|
|
30
|
+
type: data
|
|
31
|
+
dataType: string # string (default), number, boolean, date
|
|
32
|
+
required: true # optional, defaults to false
|
|
33
|
+
phase: slow # slow, fast, or fast+interactive
|
|
34
|
+
description: Display name
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `variant` — Enum/boolean for conditionals
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
- tag: status
|
|
41
|
+
type: variant
|
|
42
|
+
dataType: enum (AVAILABLE | OUT_OF_STOCK | PREORDER)
|
|
43
|
+
phase: fast+interactive
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### `interactive` — Element refs for user interaction
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
- tag: addToCart
|
|
50
|
+
type: interactive
|
|
51
|
+
elementType: HTMLButtonElement # HTMLAnchorElement, HTMLInputElement, HTMLSelectElement, etc.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Interactive tags are always `fast+interactive` — do not specify a phase.
|
|
55
|
+
|
|
56
|
+
A tag can be both data and interactive:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
- tag: quantityInput
|
|
60
|
+
type: [data, interactive]
|
|
61
|
+
dataType: number
|
|
62
|
+
elementType: HTMLInputElement
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `sub-contract` — Nested objects
|
|
66
|
+
|
|
67
|
+
Inline:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
- tag: pricing
|
|
71
|
+
type: sub-contract
|
|
72
|
+
tags:
|
|
73
|
+
- tag: amount
|
|
74
|
+
type: data
|
|
75
|
+
dataType: number
|
|
76
|
+
- tag: currency
|
|
77
|
+
type: data
|
|
78
|
+
dataType: string
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Linked (reference another contract file):
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
- tag: author
|
|
85
|
+
type: sub-contract
|
|
86
|
+
link: ./author.jay-contract
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `sub-contract` with `repeated: true` — Arrays
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
- tag: items
|
|
93
|
+
type: sub-contract
|
|
94
|
+
repeated: true
|
|
95
|
+
trackBy: id # Required: identifies each item
|
|
96
|
+
phase: fast
|
|
97
|
+
tags:
|
|
98
|
+
- tag: id
|
|
99
|
+
type: data
|
|
100
|
+
dataType: string
|
|
101
|
+
- tag: name
|
|
102
|
+
type: data
|
|
103
|
+
dataType: string
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`trackBy` must reference a `data` tag with `string` or `number` type within the same sub-contract.
|
|
107
|
+
|
|
108
|
+
## Async Data
|
|
109
|
+
|
|
110
|
+
Wrap any tag in `Promise<T>` with `async: true`:
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
- tag: reviews
|
|
114
|
+
type: data
|
|
115
|
+
async: true
|
|
116
|
+
dataType: string # Compiles to Promise<string>
|
|
117
|
+
|
|
118
|
+
- tag: relatedProducts
|
|
119
|
+
type: sub-contract
|
|
120
|
+
repeated: true
|
|
121
|
+
trackBy: id
|
|
122
|
+
async: true # Compiles to Promise<Array<...>>
|
|
123
|
+
tags:
|
|
124
|
+
- tag: id
|
|
125
|
+
type: data
|
|
126
|
+
dataType: string
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Rendering Phases
|
|
130
|
+
|
|
131
|
+
Each tag has a phase that determines when its data is available:
|
|
132
|
+
|
|
133
|
+
| Phase | When | Use For |
|
|
134
|
+
| ------------------ | ------------------ | --------------------------------------- |
|
|
135
|
+
| `slow` | Build time (SSG) | Static content, SEO data, product names |
|
|
136
|
+
| `fast` | Request time (SSR) | Per-request data, live pricing, stock |
|
|
137
|
+
| `fast+interactive` | Request + client | Data that also updates on the client |
|
|
138
|
+
|
|
139
|
+
**How to choose:**
|
|
140
|
+
|
|
141
|
+
- Can the data be known at build time? Use `slow`
|
|
142
|
+
- Does it change per request (user, time, session)? Use `fast`
|
|
143
|
+
- Does it also update on the client after interaction? Use `fast+interactive`
|
|
144
|
+
- Interactive tags (refs) are always `fast+interactive`
|
|
145
|
+
|
|
146
|
+
**Phase rules for arrays:** Child phases must be >= parent phase. If the array is `fast`, all children must be `fast` or later.
|
|
147
|
+
|
|
148
|
+
## Props vs Params
|
|
149
|
+
|
|
150
|
+
### Props — Component configuration
|
|
151
|
+
|
|
152
|
+
Props are passed by the parent component or jay-html template. Use for component inputs like IDs, configuration flags, display options.
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
props:
|
|
156
|
+
- name: productId
|
|
157
|
+
type: string
|
|
158
|
+
required: true
|
|
159
|
+
description: The product to display
|
|
160
|
+
- name: showPricing
|
|
161
|
+
type: boolean
|
|
162
|
+
default: 'true'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Params — URL route segments
|
|
166
|
+
|
|
167
|
+
Params come from dynamic route segments (`[slug]`, `[[lang]]`, `[...path]`). Use for page-level routing data.
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
params:
|
|
171
|
+
slug: string # required — from [slug]
|
|
172
|
+
lang: string? # optional — from [[lang]]
|
|
173
|
+
path: string[] # catch-all — from [...path]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Description Field
|
|
177
|
+
|
|
178
|
+
Always include a `description` at the contract level explaining when to use this contract:
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
name: product-search
|
|
182
|
+
description: Product listing with filters, sorting, and pagination. Use for search results and category pages.
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Validation Rules
|
|
186
|
+
|
|
187
|
+
- Tag names must be unique at each level
|
|
188
|
+
- `repeated: true` requires `trackBy`
|
|
189
|
+
- `trackBy` must reference a `data` tag with `string` or `number` type
|
|
190
|
+
- Interactive tags cannot have an explicit `phase`
|
|
191
|
+
- Sub-contracts must have either `tags` (inline) or `link` (external), not both
|
|
192
|
+
- Array children must have phase >= parent phase
|
|
193
|
+
- Prop names must be unique
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Plugin Structure
|
|
2
|
+
|
|
3
|
+
A plugin provides headless components, contracts, and actions. It can be a standalone npm package or inline within a project.
|
|
4
|
+
|
|
5
|
+
## plugin.yaml
|
|
6
|
+
|
|
7
|
+
The plugin manifest declares all contracts, actions, services, contexts, and configuration:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
name: my-plugin
|
|
11
|
+
contracts:
|
|
12
|
+
- name: product-page
|
|
13
|
+
contract: product-page.jay-contract
|
|
14
|
+
component: productPage
|
|
15
|
+
description: Complete product detail page with SSR
|
|
16
|
+
|
|
17
|
+
- name: product-search
|
|
18
|
+
contract: product-search.jay-contract
|
|
19
|
+
component: productSearch
|
|
20
|
+
description: Product listing with filters and pagination
|
|
21
|
+
|
|
22
|
+
actions:
|
|
23
|
+
- name: searchProducts
|
|
24
|
+
action: search-products.jay-action
|
|
25
|
+
- name: addToCart
|
|
26
|
+
action: add-to-cart.jay-action
|
|
27
|
+
|
|
28
|
+
services:
|
|
29
|
+
- name: my-store
|
|
30
|
+
marker: MY_STORE_SERVICE_MARKER
|
|
31
|
+
description: Provides product catalog API (query, filter, sort)
|
|
32
|
+
|
|
33
|
+
contexts:
|
|
34
|
+
- name: my-cart
|
|
35
|
+
marker: MY_CART_CONTEXT
|
|
36
|
+
description: Client-side cart state (add/remove items, totals)
|
|
37
|
+
|
|
38
|
+
setup:
|
|
39
|
+
handler: setup-handler
|
|
40
|
+
references: references-handler
|
|
41
|
+
configTemplate:
|
|
42
|
+
- source: templates/config.yaml
|
|
43
|
+
target: my-plugin.yaml
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Contract Entry Fields
|
|
47
|
+
|
|
48
|
+
- `name` — Contract name (used in `contract="..."` in jay-html)
|
|
49
|
+
- `contract` — Path to `.jay-contract` file (relative to plugin root)
|
|
50
|
+
- `component` — Export name of the component (e.g., `productPage`)
|
|
51
|
+
- `description` — What this component does and when to use it
|
|
52
|
+
|
|
53
|
+
### Action Entry Fields
|
|
54
|
+
|
|
55
|
+
- `name` — Action name (used with `jay-stack action <plugin>/<action>`)
|
|
56
|
+
- `action` — Path to `.jay-action` metadata file
|
|
57
|
+
|
|
58
|
+
### Service Entry Fields
|
|
59
|
+
|
|
60
|
+
- `name` — Service name (for identification in plugins-index)
|
|
61
|
+
- `marker` — Exported service marker constant (e.g., `MY_STORE_SERVICE_MARKER`)
|
|
62
|
+
- `description` — What APIs this service provides
|
|
63
|
+
- `doc` — (optional) Path to a markdown file documenting the service API
|
|
64
|
+
|
|
65
|
+
Services are server-side APIs created with `createJayService`. Other plugins and page components consume them via `.withServices(MARKER)`.
|
|
66
|
+
|
|
67
|
+
### Context Entry Fields
|
|
68
|
+
|
|
69
|
+
- `name` — Context name (for identification in plugins-index)
|
|
70
|
+
- `marker` — Exported context marker constant (e.g., `MY_CART_CONTEXT`)
|
|
71
|
+
- `description` — What reactive state this context provides
|
|
72
|
+
- `doc` — (optional) Path to a markdown file documenting the context API
|
|
73
|
+
|
|
74
|
+
Contexts are client-side reactive state. Other plugins and page components consume them via `.withContexts(MARKER)`.
|
|
75
|
+
|
|
76
|
+
### Documentation Files
|
|
77
|
+
|
|
78
|
+
When `doc` is specified, the markdown file must exist and (for NPM packages) be exported in `package.json`:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
services:
|
|
82
|
+
- name: my-store
|
|
83
|
+
marker: MY_STORE_SERVICE_MARKER
|
|
84
|
+
description: Product catalog API
|
|
85
|
+
doc: ./docs/my-store-service.md
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"exports": {
|
|
91
|
+
"./docs/my-store-service.md": "./docs/my-store-service.md"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Setup Fields
|
|
97
|
+
|
|
98
|
+
- `handler` — Setup handler for `jay-stack setup` (handles config, credentials)
|
|
99
|
+
- `references` — Reference generator for `jay-stack agent-kit` (generates discovery data)
|
|
100
|
+
- `configTemplate` — Config file templates to copy during setup
|
|
101
|
+
|
|
102
|
+
## Package Layout
|
|
103
|
+
|
|
104
|
+
### Standalone NPM Package
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
my-plugin/
|
|
108
|
+
├── plugin.yaml
|
|
109
|
+
├── package.json
|
|
110
|
+
├── lib/
|
|
111
|
+
│ ├── contracts/
|
|
112
|
+
│ │ ├── product-page.jay-contract
|
|
113
|
+
│ │ └── product-search.jay-contract
|
|
114
|
+
│ ├── actions/
|
|
115
|
+
│ │ ├── search-products.jay-action
|
|
116
|
+
│ │ └── add-to-cart.jay-action
|
|
117
|
+
│ ├── components/
|
|
118
|
+
│ │ ├── product-page.ts
|
|
119
|
+
│ │ └── product-search.ts
|
|
120
|
+
│ ├── services/
|
|
121
|
+
│ │ └── products-db.ts
|
|
122
|
+
│ └── init.ts
|
|
123
|
+
├── agent-kit/ # Optional: plugin-contributed guides
|
|
124
|
+
│ ├── designer/
|
|
125
|
+
│ │ └── my-plugin-usage.md
|
|
126
|
+
│ └── developer/
|
|
127
|
+
│ └── my-plugin-config.md
|
|
128
|
+
└── dist/
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Inline Plugin (within a project)
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
my-project/
|
|
135
|
+
├── src/
|
|
136
|
+
│ ├── pages/
|
|
137
|
+
│ │ └── ...
|
|
138
|
+
│ └── plugins/
|
|
139
|
+
│ └── my-plugin/
|
|
140
|
+
│ ├── plugin.yaml
|
|
141
|
+
│ ├── product-page.jay-contract
|
|
142
|
+
│ ├── product-page.ts
|
|
143
|
+
│ └── init.ts
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
See `examples/jay-stack/fake-shop` for a working example.
|
|
147
|
+
|
|
148
|
+
## package.json Exports
|
|
149
|
+
|
|
150
|
+
For NPM packages, declare exports so the framework can resolve the plugin:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"name": "@my-org/my-plugin",
|
|
155
|
+
"type": "module",
|
|
156
|
+
"exports": {
|
|
157
|
+
".": "./dist/index.js",
|
|
158
|
+
"./plugin.yaml": "./plugin.yaml"
|
|
159
|
+
},
|
|
160
|
+
"files": ["dist", "plugin.yaml", "lib/contracts", "lib/actions"]
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Plugin-Contributed Agent-Kit Guides
|
|
165
|
+
|
|
166
|
+
A plugin can include guides that are merged into the project's agent-kit during `jay-stack agent-kit`. Create an `agent-kit/` folder with subfolders for each role:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
my-plugin/
|
|
170
|
+
└── agent-kit/
|
|
171
|
+
├── designer/
|
|
172
|
+
│ └── my-plugin-usage.md # How to use contracts in jay-html
|
|
173
|
+
├── developer/
|
|
174
|
+
│ └── my-plugin-config.md # How to configure the plugin
|
|
175
|
+
└── plugin/
|
|
176
|
+
└── my-plugin-extending.md # How to extend the plugin
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Reference Declarations
|
|
180
|
+
|
|
181
|
+
Plugins can declare reference data generated by `jay-stack agent-kit`:
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
# In plugin.yaml
|
|
185
|
+
references:
|
|
186
|
+
- name: product-catalog
|
|
187
|
+
description: All products with IDs, slugs, names, and prices
|
|
188
|
+
file: product-catalog.json
|
|
189
|
+
- name: collection-schemas
|
|
190
|
+
description: Collection field schemas for filtering
|
|
191
|
+
file: collection-schemas.json
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The `agent-kit` command generates `references/<plugin>/INDEX.md` from these declarations.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Render Results
|
|
2
|
+
|
|
3
|
+
Each rendering phase (slow, fast) returns a render result indicating success, error, or redirect.
|
|
4
|
+
|
|
5
|
+
## phaseOutput — Success
|
|
6
|
+
|
|
7
|
+
Returns ViewState data and optional carry-forward data for the next phase:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { phaseOutput } from '@jay-framework/fullstack-component';
|
|
11
|
+
|
|
12
|
+
return phaseOutput(
|
|
13
|
+
{ title: 'My Product', price: 29.99 }, // ViewState — sent to template
|
|
14
|
+
{ productId: 'abc123' }, // CarryForward — passed to next phase only
|
|
15
|
+
);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
CarryForward is available in the next phase via `props.carryForward` but is not part of the ViewState.
|
|
19
|
+
|
|
20
|
+
## Error Results
|
|
21
|
+
|
|
22
|
+
Return errors to stop rendering and show an error page:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import {
|
|
26
|
+
notFound,
|
|
27
|
+
badRequest,
|
|
28
|
+
unauthorized,
|
|
29
|
+
forbidden,
|
|
30
|
+
serverError5xx,
|
|
31
|
+
clientError4xx,
|
|
32
|
+
} from '@jay-framework/fullstack-component';
|
|
33
|
+
|
|
34
|
+
// 404
|
|
35
|
+
if (!product) return notFound('Product not found');
|
|
36
|
+
|
|
37
|
+
// 400
|
|
38
|
+
if (!input.query) return badRequest('Query is required');
|
|
39
|
+
|
|
40
|
+
// 401
|
|
41
|
+
if (!session) return unauthorized('Please log in');
|
|
42
|
+
|
|
43
|
+
// 403
|
|
44
|
+
if (!canAccess) return forbidden('Access denied');
|
|
45
|
+
|
|
46
|
+
// Custom 4xx
|
|
47
|
+
return clientError4xx(429, 'Rate limit exceeded');
|
|
48
|
+
|
|
49
|
+
// 5xx
|
|
50
|
+
return serverError5xx(500, 'Database connection failed');
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Redirects
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { redirect3xx } from '@jay-framework/fullstack-component';
|
|
57
|
+
|
|
58
|
+
return redirect3xx(301, '/new-location');
|
|
59
|
+
return redirect3xx(302, `/products/${product.slug}`);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## RenderPipeline — Composable Rendering
|
|
63
|
+
|
|
64
|
+
For complex render logic, `RenderPipeline` chains operations with automatic error propagation:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { RenderPipeline } from '@jay-framework/fullstack-component';
|
|
68
|
+
|
|
69
|
+
const Pipeline = RenderPipeline.for<SlowViewState, CarryForward>();
|
|
70
|
+
|
|
71
|
+
return Pipeline.try(() => db.getProduct(props.slug))
|
|
72
|
+
.map((product) => product ?? Pipeline.notFound('Product not found'))
|
|
73
|
+
.map(async (product) => ({
|
|
74
|
+
...product,
|
|
75
|
+
reviews: await db.getReviews(product.id),
|
|
76
|
+
}))
|
|
77
|
+
.toPhaseOutput((product) => ({
|
|
78
|
+
viewState: { title: product.name, price: product.price },
|
|
79
|
+
carryForward: { productId: product.id },
|
|
80
|
+
}));
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Pipeline Factory Methods
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const P = RenderPipeline.for<VS, CF>();
|
|
87
|
+
|
|
88
|
+
P.ok(value); // Wrap a value
|
|
89
|
+
P.try(() => fetchData()); // Wrap a function (catches errors)
|
|
90
|
+
P.from(previousPhaseResult); // Continue from a prior phase result
|
|
91
|
+
P.notFound('message'); // Error pipeline
|
|
92
|
+
P.badRequest('message'); // Error pipeline
|
|
93
|
+
P.unauthorized('message'); // Error pipeline
|
|
94
|
+
P.forbidden('message'); // Error pipeline
|
|
95
|
+
P.serverError(500, 'message'); // Error pipeline
|
|
96
|
+
P.redirect(301, '/path'); // Redirect pipeline
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Pipeline Chain Methods
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
pipeline
|
|
103
|
+
.map(value => transform(value)) // Transform the value
|
|
104
|
+
.map(async value => await asyncOp(value)) // Async transform
|
|
105
|
+
.recover(error => P.ok(fallbackValue)) // Recover from errors
|
|
106
|
+
.toPhaseOutput(value => ({ // Convert to PhaseOutput
|
|
107
|
+
viewState: { ... },
|
|
108
|
+
carryForward: { ... },
|
|
109
|
+
}));
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Errors short-circuit — if any step returns an error pipeline, subsequent `.map()` calls are skipped.
|
|
@@ -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,116 @@
|
|
|
1
|
+
# Services and Initialization
|
|
2
|
+
|
|
3
|
+
Services provide dependency injection for server-side resources (databases, APIs, etc.).
|
|
4
|
+
|
|
5
|
+
## createJayService
|
|
6
|
+
|
|
7
|
+
Create a typed service marker:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { createJayService } from '@jay-framework/fullstack-component';
|
|
11
|
+
|
|
12
|
+
export interface ProductsDatabase {
|
|
13
|
+
getProduct(slug: string): Promise<Product | null>;
|
|
14
|
+
search(query: string): Promise<Product[]>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PRODUCTS_DB = createJayService<ProductsDatabase>('ProductsDatabase');
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The marker is a Symbol-based key — it provides type safety without coupling to a specific implementation.
|
|
21
|
+
|
|
22
|
+
## makeJayInit
|
|
23
|
+
|
|
24
|
+
Initialize services and client-side state during app startup:
|
|
25
|
+
|
|
26
|
+
### Server-only init
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { makeJayInit, registerService } from '@jay-framework/fullstack-component';
|
|
30
|
+
|
|
31
|
+
export const init = makeJayInit().withServer(async () => {
|
|
32
|
+
const db = await connectToDatabase();
|
|
33
|
+
registerService(PRODUCTS_DB, db);
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Server + Client init
|
|
38
|
+
|
|
39
|
+
The server can pass data to the client via the return value:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
export const init = makeJayInit()
|
|
43
|
+
.withServer(async () => {
|
|
44
|
+
registerService(PRODUCTS_DB, await connectDb());
|
|
45
|
+
return { currency: 'USD', storeId: 'store-123' };
|
|
46
|
+
})
|
|
47
|
+
.withClient((data) => {
|
|
48
|
+
// data is typed: { currency: string; storeId: string }
|
|
49
|
+
registerReactiveGlobalContext(STORE_CONFIG, () => ({
|
|
50
|
+
currency: data.currency,
|
|
51
|
+
storeId: data.storeId,
|
|
52
|
+
}));
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Client-only init
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
export const init = makeJayInit().withClient(() => {
|
|
60
|
+
initAnalytics();
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Using Services
|
|
65
|
+
|
|
66
|
+
### In Components
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
makeJayStackComponent<MyContract>()
|
|
70
|
+
.withServices(PRODUCTS_DB)
|
|
71
|
+
.withSlowlyRender(async (props, db) => {
|
|
72
|
+
const product = await db.getProduct(props.slug);
|
|
73
|
+
return phaseOutput({ title: product.name }, {});
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### In Actions
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
makeJayAction('products.search')
|
|
81
|
+
.withServices(PRODUCTS_DB)
|
|
82
|
+
.withHandler(async (input, db) => {
|
|
83
|
+
return db.search(input.query);
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### In loadParams
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
.withServices(PRODUCTS_DB)
|
|
91
|
+
.withLoadParams(async function* (db) {
|
|
92
|
+
const products = await db.getAll();
|
|
93
|
+
yield products.map(p => ({ slug: p.slug }));
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Listing in plugin.yaml
|
|
98
|
+
|
|
99
|
+
If your plugin provides services for other plugins to consume, list them in `plugin.yaml`:
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
services:
|
|
103
|
+
- name: products-db
|
|
104
|
+
marker: PRODUCTS_DB
|
|
105
|
+
description: Product catalog database API (query, search, get by slug)
|
|
106
|
+
doc: ./docs/products-db-service.md # optional — markdown documentation
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This makes the service discoverable in `plugins-index.yaml`. If `doc` is provided, the file must exist and be exported from the package.
|
|
110
|
+
|
|
111
|
+
## Service Lifecycle
|
|
112
|
+
|
|
113
|
+
- Services are registered during `makeJayInit().withServer()` callbacks
|
|
114
|
+
- Registration order follows plugin dependency order
|
|
115
|
+
- Services are available to all components and actions after init
|
|
116
|
+
- Services are server-side only — they are not sent to the client
|