@jay-framework/jay-stack-cli 0.25.5 → 0.26.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/designer/contracts-and-plugins.md +14 -0
- package/agent-kit-template/designer/validation-guide.md +26 -10
- package/agent-kit-template/developer/INSTRUCTIONS.md +19 -18
- package/agent-kit-template/developer/configuration.md +4 -0
- package/agent-kit-template/developer/page-components.md +9 -6
- package/agent-kit-template/developer/query-param-pages.md +426 -0
- package/agent-kit-template/developer/render-results.md +28 -1
- package/agent-kit-template/devops/INSTRUCTIONS.md +17 -6
- package/agent-kit-template/devops/fetch-handler.md +21 -0
- package/agent-kit-template/devops/invalidation.md +2 -7
- package/agent-kit-template/devops/production-build.md +8 -3
- package/agent-kit-template/devops/serving-modes.md +25 -0
- package/agent-kit-template/devops/source-editing.md +89 -0
- package/agent-kit-template/plugin/INSTRUCTIONS.md +25 -24
- package/agent-kit-template/plugin/actions-guide.md +57 -0
- package/agent-kit-template/plugin/contracts-guide.md +42 -0
- package/agent-kit-template/plugin/render-results.md +19 -0
- package/agent-kit-template/plugin/source-editing.md +70 -0
- package/agent-kit-template/plugin/validation.md +36 -0
- package/dist/index.js +332 -247
- package/package.json +16 -16
- package/dist/index-_Gyf630q.js +0 -1812
|
@@ -15,7 +15,15 @@ return phaseOutput(
|
|
|
15
15
|
);
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
CarryForward is
|
|
18
|
+
CarryForward is passed to the next phase as a **positional argument**, not a field on `props`,
|
|
19
|
+
and is not part of the ViewState. The fast render receives it as its **second parameter**
|
|
20
|
+
(before services): `withFastRender(async (props, carryForward, ...services) => …)`. The
|
|
21
|
+
interactive phase receives it as its **fourth** parameter, after `props`, `refs` and the fast
|
|
22
|
+
ViewState signals.
|
|
23
|
+
|
|
24
|
+
For query-param pages, use this to precompute the no-params default in the slow phase and
|
|
25
|
+
forward it to the fast phase instead of querying on every request — see
|
|
26
|
+
[query-param-pages.md](query-param-pages.md).
|
|
19
27
|
|
|
20
28
|
### Response Headers (fast phase only)
|
|
21
29
|
|
|
@@ -31,6 +39,25 @@ return phaseOutput(
|
|
|
31
39
|
|
|
32
40
|
Use this when the page renders per-user data that must not be cached by CDN or browser. Can be combined with `headTags` in the same options object.
|
|
33
41
|
|
|
42
|
+
### Freshness — `ttl` and `grace` (slow phase only)
|
|
43
|
+
|
|
44
|
+
A slow render runs at build time, so its data ages. Return `ttl` (and optionally `grace`) — both in **seconds** — to tell the production server when the data goes stale:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
return phaseOutput(
|
|
48
|
+
{ name: product.name, price: product.price },
|
|
49
|
+
{ productId: product.id },
|
|
50
|
+
{ ttl: 300, grace: 6 * 60 * 60 }, // fresh for 5 minutes, usable for 6 hours
|
|
51
|
+
);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **`ttl`** — the data is `fresh` for `ttl` seconds after it was rendered. After that it is `stale`: the server serves it immediately (`Cache-Control: no-store`) and re-runs the slow render in the background. No `ttl` → the data never goes stale (today's behavior). Minimum effective `ttl` is 10 seconds.
|
|
55
|
+
- **`grace`** — absolute age after which the data is `expired`: the server waits (up to `JAY_STALE_WAIT_MS`, default 2000) for the refresh before answering. Default `max(12h, ttl)`.
|
|
56
|
+
- Responses carry `jay-freshness: fresh | stale | expired, <age>ms`.
|
|
57
|
+
- A page with several slow parts (page, keyed headless components, headless instances) gets the **minimum positive** `ttl` and the minimum positive `grace` across all of them; `0` / `null` are ignored.
|
|
58
|
+
- With `RenderPipeline`, return `ttl` / `grace` from the `toPhaseOutput` callback next to `viewState` and `carryForward`.
|
|
59
|
+
- Pick a `ttl` that matches how quickly the underlying data changes, not how fast you want pages to be: every `ttl` expiry costs one slow render per instance.
|
|
60
|
+
|
|
34
61
|
### Cookies (fast phase only)
|
|
35
62
|
|
|
36
63
|
The fast phase receives `props.cookies` — a `Record<string, string>` parsed from the HTTP `Cookie` header:
|
|
@@ -125,6 +125,16 @@ A cloud platform that boots the dev server but neither installs the CLI nor inje
|
|
|
125
125
|
working site with a non-functional `/aiditor` — the single most common surprise. Install both, or
|
|
126
126
|
document that visual editing is unavailable there.
|
|
127
127
|
|
|
128
|
+
### Verbose diagnostics (`-v`)
|
|
129
|
+
|
|
130
|
+
`jay-stack dev -v` (verbose) additionally **writes each rendered page's generated client HTML** to
|
|
131
|
+
`<buildFolder>/debug/client-entry/<route>.html` (e.g. `build/dev/debug/client-entry/products-[slug].html`).
|
|
132
|
+
This is a write-only debugging aid — inspect it when a page renders wrong, hydration mismatches, or a
|
|
133
|
+
client bundle looks off; it mirrors exactly what the browser receives before Vite transforms it. It is
|
|
134
|
+
**off by default** (no per-request disk writes in normal dev) and **best-effort** (a failed debug write
|
|
135
|
+
never fails the page request). The filename is the route path only — the query string is dropped — so a
|
|
136
|
+
proxy that carries a long auth token as a query param can't blow past the OS filename limit (DL#229).
|
|
137
|
+
|
|
128
138
|
### Health / shutdown (containers)
|
|
129
139
|
|
|
130
140
|
Run with `--test-mode` to expose `GET /_jay/health` (readiness probe) and `POST /_jay/shutdown` (graceful
|
|
@@ -132,9 +142,10 @@ stop) — the sanctioned hooks for container healthchecks and lifecycle, instead
|
|
|
132
142
|
|
|
133
143
|
## Guides
|
|
134
144
|
|
|
135
|
-
| File | Topic
|
|
136
|
-
| ------------------------------------------ |
|
|
137
|
-
| [production-build.md](production-build.md) | Build pipeline, output structure, frontend/backend split
|
|
138
|
-
| [serving-modes.md](serving-modes.md) | Self-hosted, CDN, BaaS (fetch handler), CLI flags
|
|
139
|
-
| [fetch-handler.md](fetch-handler.md) | Fetch handler, ArtifactStore interface, BaaS custom stores
|
|
140
|
-
| [invalidation.md](invalidation.md) | Rebuild, renderer server, cleanup
|
|
145
|
+
| File | Topic |
|
|
146
|
+
| ------------------------------------------ | ----------------------------------------------------------- |
|
|
147
|
+
| [production-build.md](production-build.md) | Build pipeline, output structure, frontend/backend split |
|
|
148
|
+
| [serving-modes.md](serving-modes.md) | Self-hosted, CDN, BaaS (fetch handler), CLI flags |
|
|
149
|
+
| [fetch-handler.md](fetch-handler.md) | Fetch handler, ArtifactStore interface, BaaS custom stores |
|
|
150
|
+
| [invalidation.md](invalidation.md) | Rebuild, renderer server, cleanup |
|
|
151
|
+
| [source-editing.md](source-editing.md) | Enable + use the source-editing inspector (AIditor, DL#219) |
|
|
@@ -106,9 +106,30 @@ interface ArtifactStore {
|
|
|
106
106
|
loadModule(modulePath: string, local?: boolean): Promise<any>;
|
|
107
107
|
getAssetPath(relativePath: string): string;
|
|
108
108
|
getBuildDir(): string;
|
|
109
|
+
|
|
110
|
+
// Instances — the manifest holds routes only; the store owns the instance index
|
|
111
|
+
getInstance(
|
|
112
|
+
route: RouteEntry,
|
|
113
|
+
params: Record<string, string>,
|
|
114
|
+
): Promise<InstanceEntry | undefined>;
|
|
115
|
+
listInstances(route: RouteEntry, options?: { fresh?: boolean }): AsyncIterable<InstanceRef>;
|
|
116
|
+
|
|
117
|
+
// Optional — implement to enable stale refresh and render-on-miss (omit for a read-only store)
|
|
118
|
+
writeCacheData?(
|
|
119
|
+
route: RouteEntry,
|
|
120
|
+
params: Record<string, string>,
|
|
121
|
+
entry: CacheEntry,
|
|
122
|
+
): Promise<void>;
|
|
123
|
+
removeInstance?(route: RouteEntry, params: Record<string, string>): Promise<void>;
|
|
124
|
+
readCacheDataFresh?(relativePath: string): Promise<CacheEntry>;
|
|
109
125
|
}
|
|
110
126
|
```
|
|
111
127
|
|
|
128
|
+
- `readCacheData` must throw `CacheNotFoundError` (exported from `/serve`) when the instance has no data.
|
|
129
|
+
- `getInstance` returns `undefined` only for a real "does not exist" — other errors must throw (they are not a 404).
|
|
130
|
+
- `writeCacheData` upserts the data at the path derived by `deriveCachePath(route.pattern, params)` and keeps the index consistent (the item itself is the index entry). Race handling should be idempotent (adopt a concurrent winner), not a lock.
|
|
131
|
+
- `listInstances` may serve a TTL-cached listing (`SwrCache`, default 10 minutes); `fresh: true` re-reads. Page through the backend's own page-size limit.
|
|
132
|
+
|
|
112
133
|
`loadModule` handles all module loading — server elements, page components, headless components. The `local` flag indicates whether the path is relative to the build directory (`true`) or an npm package (`false`). For filesystem deployments, local modules resolve from `basePath` and npm modules use bare `import()`. BaaS implementations resolve all modules from their pre-bundled registry, ignoring the `local` flag.
|
|
113
134
|
|
|
114
135
|
For serve-only imports (no build-time dependencies), use `@jay-framework/production-server/serve`.
|
|
@@ -29,7 +29,7 @@ jay-stack rebuild --route /products/[slug] --params '{"slug":"blue-widget"}'
|
|
|
29
29
|
|
|
30
30
|
### How it works
|
|
31
31
|
|
|
32
|
-
1. Reads the route manifest to find affected routes
|
|
32
|
+
1. Reads the route manifest to find affected routes, and the artifact store's instance listing for their instances
|
|
33
33
|
2. Re-runs slow render with fresh data
|
|
34
34
|
3. Re-compiles server element and client bundle
|
|
35
35
|
4. Updates the instance entry in the manifest
|
|
@@ -69,9 +69,4 @@ The manifest tracks which contracts each route uses. When a webhook says "produc
|
|
|
69
69
|
|
|
70
70
|
## Build Output After Rebuild
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
- `backend/pre-rendered/` — updated jay-html, cache, server element
|
|
75
|
-
- `frontend/pages/` — updated client bundle, CSS
|
|
76
|
-
|
|
77
|
-
Old files are not deleted immediately — they're tracked in `cleanup-manifest.json`. This avoids breaking in-flight requests that still reference old bundles.
|
|
72
|
+
A rebuilt instance overwrites its `backend/pre-rendered/{route}/page[_hash].cache.json` in place (atomically, at the same derived path), so no manifest update and no orphaned files are involved. Route-level bundles in `frontend/` are not rebuilt by an instance rebuild.
|
|
@@ -34,7 +34,7 @@ build/v{n}/
|
|
|
34
34
|
│ └── logo.png
|
|
35
35
|
│
|
|
36
36
|
├── backend/ # Server-only artifacts (→ container)
|
|
37
|
-
│ ├── route-manifest.json #
|
|
37
|
+
│ ├── route-manifest.json # Routes (no instances), action registry
|
|
38
38
|
│ ├── build-metadata.json # Version, timestamp, instance count
|
|
39
39
|
│ ├── server/ # Compiled server code
|
|
40
40
|
│ │ ├── init.js
|
|
@@ -63,11 +63,16 @@ The build is **environment-agnostic**. The same output serves any deployment mod
|
|
|
63
63
|
|
|
64
64
|
`backend/route-manifest.json` contains:
|
|
65
65
|
|
|
66
|
-
- **routes** — pattern, segments, server module path,
|
|
67
|
-
- **instances** — `preRenderedPath` and `serverElementPath` (relative to `backend/`), `clientBundlePath` and `clientCssPath` (relative to `frontend/`)
|
|
66
|
+
- **routes** — pattern, segments, server module path, route-level `serverElementPath`, `routeClientBundlePath` and `routeCssPath`. The manifest holds **routes only** — it does not list instances.
|
|
68
67
|
- **actions** — server module paths, action names
|
|
69
68
|
- **sharedManifest** — maps package names to hashed filenames in `frontend/shared/`
|
|
70
69
|
|
|
70
|
+
## Instances
|
|
71
|
+
|
|
72
|
+
An instance (one set of route params) is identified by its data file, `backend/pre-rendered/{route}/page[_{hash}].cache.json`. The path is derived from the route pattern and the params (`_` + first 8 hex of the md5 of the sorted-params JSON), and the file carries `route`, `params` and the freshness fields (`renderedAt`, `ttl`, `grace`) itself — it is its own index entry.
|
|
73
|
+
|
|
74
|
+
The `ArtifactStore` lists a route's instances (sitemap, llms.txt, rebuild) from those files, so new instances created after the build — by [render-on-miss](serving-modes.md#fresh-data-and-new-items) or a rebuild — appear without touching the manifest.
|
|
75
|
+
|
|
71
76
|
## Project Structure Requirements
|
|
72
77
|
|
|
73
78
|
For production builds to work correctly:
|
|
@@ -116,3 +116,28 @@ jay-stack serve --role renderer --port 4001
|
|
|
116
116
|
```
|
|
117
117
|
|
|
118
118
|
The renderer server listens for data change webhooks and rebuilds affected page instances. The main server picks up the updated artifacts automatically (it re-reads the manifest when `build-metadata.json` changes).
|
|
119
|
+
|
|
120
|
+
## Fresh Data and New Items
|
|
121
|
+
|
|
122
|
+
The main server keeps slow-render data fresh and serves items that did not exist at build time. Each server instance does this independently against its own artifact store (a shared store, such as Wix's data collection, is refreshed once for everyone).
|
|
123
|
+
|
|
124
|
+
**Stale-while-revalidate.** When slow render returns `ttl` / `grace` (seconds, see the developer `render-results.md` guide), the server classifies each instance as `fresh`, `stale` or `expired`:
|
|
125
|
+
|
|
126
|
+
| State | Served | Headers |
|
|
127
|
+
| --------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
128
|
+
| `fresh` | Stored data; `max-age` / `s-maxage` clamped to the remaining TTL | `jay-freshness: fresh, <age>ms` |
|
|
129
|
+
| `stale` | Stored data immediately, refresh runs in the background | `Cache-Control: no-store`, `jay-freshness: stale, <age>ms` |
|
|
130
|
+
| `expired` | Waits up to `JAY_STALE_WAIT_MS` (2000) for the refresh, else the stored data | as `stale`, or `fresh` if the refresh finished |
|
|
131
|
+
|
|
132
|
+
Old data is never cached by CDNs (`no-store`) so a refresh becomes visible at once; validators (`ETag` gets `-<renderedAt>` appended, `Last-Modified`) are kept so crawlers and browsers can still revalidate cheaply.
|
|
133
|
+
|
|
134
|
+
**Render on miss.** A URL that matches a route but has no instance (e.g. a product published after the build) is slow-rendered on the first request, stored, and served. If the slow render answers `notFound()` the request is a real 404 (remembered in memory for 10 minutes); if it cannot render right now the server answers `503` with `Retry-After: 2`.
|
|
135
|
+
|
|
136
|
+
**Limits.** Concurrent requests for the same instance share one slow render. At most `JAY_SLOW_RENDER_CONCURRENCY` (default 5) slow renders run at once across refreshes and misses; there is no queue — a refresh that finds all slots busy serves the old data, a miss answers 503. A failed refresh backs off before retrying.
|
|
137
|
+
|
|
138
|
+
| Variable | Default | Meaning |
|
|
139
|
+
| ----------------------------- | ------- | ------------------------------------------------------ |
|
|
140
|
+
| `JAY_SLOW_RENDER_CONCURRENCY` | `5` | Max concurrent slow renders (refresh + render-on-miss) |
|
|
141
|
+
| `JAY_STALE_WAIT_MS` | `2000` | Max wait for the refresh of an `expired` instance |
|
|
142
|
+
| `JAY_MISS_TIMEOUT_MS` | `8000` | Max wait of a render-on-miss before 503 |
|
|
143
|
+
| `JAY_FRESHNESS_HEADER` | on | Set to `off` to omit the `jay-freshness` header |
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Source Editing — DevOps / AIditor Integration Guide
|
|
2
|
+
|
|
3
|
+
**Audience:** AIditor (or a similar visual editor) and the devops integrating it. How to turn on Jay's
|
|
4
|
+
source-editing inspector and use it to map a rendered pixel back to its `.jay-html` source (DL#219).
|
|
5
|
+
|
|
6
|
+
**This is dev-only.** The inspector is served only by the dev server, and only when enabled. It never
|
|
7
|
+
affects a production build.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Enable it
|
|
12
|
+
|
|
13
|
+
In the project's `.jay` file:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
devServer:
|
|
17
|
+
sourceEditing: true
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
When on, the dev server injects `jay-dev-*` markers into rendered pages (`jay-dev-src` line ranges on every
|
|
21
|
+
element, `jay-dev-item` on `forEach` roots, `jay-dev-region*` on region roots). Off by default — the markers
|
|
22
|
+
change rendered output, so enable only for source-editing sessions, never in production.
|
|
23
|
+
|
|
24
|
+
## 2. Open a page in embed mode
|
|
25
|
+
|
|
26
|
+
Load the page in an `<iframe>` with the `_jay_embed` query param:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
http://localhost:<port>/<route>?_jay_embed
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Embed mode registers the inspector's `message` listener (shared with the freeze protocol). The page posts
|
|
33
|
+
`jay:route` to the parent on load.
|
|
34
|
+
|
|
35
|
+
## 3. Ask "what is this?" — the postMessage protocol
|
|
36
|
+
|
|
37
|
+
Send a point or a rectangle (iframe-relative CSS pixels); receive the resolved source location.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
// parent → iframe
|
|
41
|
+
{ type: 'jay:inspectPoint', requestId, x, y }
|
|
42
|
+
{ type: 'jay:inspectRect', requestId, x, y, width, height }
|
|
43
|
+
|
|
44
|
+
// iframe → parent
|
|
45
|
+
{ type: 'jay:inspectResult', requestId,
|
|
46
|
+
file, // project-relative page jay-html path (where the lines live)
|
|
47
|
+
route, routePattern, // derive route params from these
|
|
48
|
+
node: { // the element actually hit / chosen; null if nothing marked
|
|
49
|
+
lines: { from, to }, // 1-based line range in `file`
|
|
50
|
+
tag, // e.g. 'li', 'span'
|
|
51
|
+
html, // one-level HTML of the element (open tag + authored attrs + inline text;
|
|
52
|
+
// grandchildren elided) — the single content field
|
|
53
|
+
rect: { x, y, width, height }, // on-screen box, for overlaying a highlight
|
|
54
|
+
contentFromBinding?: true, // set when the element is generated from a bound value (html-string)
|
|
55
|
+
} | null,
|
|
56
|
+
stack: [ // enclosing data scopes, innermost → outermost (plain wrappers omitted)
|
|
57
|
+
{ kind: 'forEach', lines, itemId }, // itemId = runtime trackBy record id
|
|
58
|
+
{ kind: 'region', lines, name, props? }, // name = contract; props = static primitive props
|
|
59
|
+
] }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**`inspectRect`** (drag selection) uses the rect rule: among marked elements the box encloses (overlap over
|
|
63
|
+
the element's own area ≥ 0.8), the **largest** is chosen; if none, it falls back to the box center.
|
|
64
|
+
|
|
65
|
+
## 4. Decide how to edit (the decision rule)
|
|
66
|
+
|
|
67
|
+
- **Static content** (no `contentFromBinding`, not inside a region): the text/attribute is authored in the
|
|
68
|
+
jay-html — edit `file` at `node.lines`.
|
|
69
|
+
- **Bound content** (`contentFromBinding`, or inside a `region`/`forEach` frame): the value comes from data,
|
|
70
|
+
not the markup. Read the binding at `node.lines` (e.g. `{post.content}`), then resolve its source:
|
|
71
|
+
- `stack` region frame → the `name` is the contract; map it via `agent-kit/plugins-index.yaml` to the
|
|
72
|
+
plugin's **editing guide** (see the plugin **source-editing** guide).
|
|
73
|
+
- `forEach` frame `itemId` identifies the record; route params (`route`/`routePattern`) identify the page
|
|
74
|
+
instance.
|
|
75
|
+
- Editing a bound value means editing its source (e.g. a markdown `.md` file, or a plugin's data via its
|
|
76
|
+
`devOnly` action), not the rendered markup.
|
|
77
|
+
|
|
78
|
+
The inspector returns **locators, not resolved bindings** — it stays client-only, and the agent reads the
|
|
79
|
+
jay-html + agent-kit to resolve. (The dev server does not resolve contracts; see DL#219 §2.)
|
|
80
|
+
|
|
81
|
+
## 5. Try it
|
|
82
|
+
|
|
83
|
+
The smoke-test example ships a live harness: run `yarn dev` in `examples/jay-stack/smoke-test` and open
|
|
84
|
+
`http://localhost:3300/what-is-this.html` — hover/drag over any page and watch the `jay:inspectResult`.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
See also: the developer **configuration** guide (the `.jay` `sourceEditing` field) and the plugin
|
|
89
|
+
**source-editing** guide (making a plugin's contracts editable).
|
|
@@ -34,30 +34,31 @@ The plugin participates in four CLI commands, each running different hooks:
|
|
|
34
34
|
|
|
35
35
|
## Guides
|
|
36
36
|
|
|
37
|
-
| File | Topic
|
|
38
|
-
| ------------------------------------------------------ |
|
|
39
|
-
| [Contract Authoring Guide](../contracts/GUIDE.md) | Writing contracts: syntax, page/component/linked contracts, examples
|
|
40
|
-
| [contracts-guide.md](contracts-guide.md) | Plugin-specific contract concerns
|
|
41
|
-
| [plugin-structure.md](plugin-structure.md) | plugin.yaml, package layout, exports
|
|
42
|
-
| [component-structure.md](component-structure.md) | makeJayStackComponent, builder API, three-phase rendering
|
|
43
|
-
| [design-system-guide.md](design-system-guide.md) | Shipping a template designers flatten; upgrades via `jay-stack sync`
|
|
44
|
-
| [component-state.md](component-state.md) | createSignal, createMemo, createEffect, createDerivedArray, createEvent
|
|
45
|
-
| [component-refs.md](component-refs.md) | Refs, collection refs, element types
|
|
46
|
-
| [component-data.md](component-data.md) | Immutable data, JSON Patch, createPatchableSignal
|
|
47
|
-
| [component-context.md](component-context.md) | Context hooks: provide, reactive, global
|
|
48
|
-
| [render-results.md](render-results.md) | phaseOutput, RenderPipeline, errors, redirects
|
|
49
|
-
| [actions-guide.md](actions-guide.md) | makeJayAction, makeJayQuery, .jay-action files
|
|
50
|
-
| [webhooks-guide.md](webhooks-guide.md) | makeWebhook, data change invalidation, renderer server
|
|
51
|
-
| [services-guide.md](services-guide.md) | createJayService, makeJayInit
|
|
52
|
-
| [plugin-routes.md](plugin-routes.md) | Plugin-provided pages: routes, jay-html templates, page components
|
|
53
|
-
| [seo-guide.md](seo-guide.md) | SEO head tags: title, meta, OG, canonical via phaseOutput
|
|
54
|
-
| [commands-guide.md](commands-guide.md) | makeCliCommand, .jay-command files, CONSOLE_CONTEXT, jay-stack run
|
|
55
|
-
| [validation.md](validation.md) | jay-stack validate-plugin, writing custom jay-html validators
|
|
56
|
-
| [setup-guide.md](setup-guide.md) | Setup handlers, agent-kit handlers, references generation
|
|
57
|
-
| [add-menu-guide.md](add-menu-guide.md) | AIditor add-menu items: schema, interaction, browse, presentation
|
|
58
|
-
| [aiditor-settings-guide.md](aiditor-settings-guide.md) | AIditor Project settings tabs: template, materialization, devOnly route
|
|
59
|
-
| [
|
|
60
|
-
|
|
|
37
|
+
| File | Topic |
|
|
38
|
+
| ------------------------------------------------------ | ---------------------------------------------------------------------------- |
|
|
39
|
+
| [Contract Authoring Guide](../contracts/GUIDE.md) | Writing contracts: syntax, page/component/linked contracts, examples |
|
|
40
|
+
| [contracts-guide.md](contracts-guide.md) | Plugin-specific contract concerns; outcomes as variants, text as defaults |
|
|
41
|
+
| [plugin-structure.md](plugin-structure.md) | plugin.yaml, package layout, exports |
|
|
42
|
+
| [component-structure.md](component-structure.md) | makeJayStackComponent, builder API, three-phase rendering |
|
|
43
|
+
| [design-system-guide.md](design-system-guide.md) | Shipping a template designers flatten; upgrades via `jay-stack sync` |
|
|
44
|
+
| [component-state.md](component-state.md) | createSignal, createMemo, createEffect, createDerivedArray, createEvent |
|
|
45
|
+
| [component-refs.md](component-refs.md) | Refs, collection refs, element types |
|
|
46
|
+
| [component-data.md](component-data.md) | Immutable data, JSON Patch, createPatchableSignal |
|
|
47
|
+
| [component-context.md](component-context.md) | Context hooks: provide, reactive, global |
|
|
48
|
+
| [render-results.md](render-results.md) | phaseOutput, RenderPipeline, errors, redirects |
|
|
49
|
+
| [actions-guide.md](actions-guide.md) | makeJayAction, makeJayQuery, .jay-action files |
|
|
50
|
+
| [webhooks-guide.md](webhooks-guide.md) | makeWebhook, data change invalidation, renderer server |
|
|
51
|
+
| [services-guide.md](services-guide.md) | createJayService, makeJayInit |
|
|
52
|
+
| [plugin-routes.md](plugin-routes.md) | Plugin-provided pages: routes, jay-html templates, page components |
|
|
53
|
+
| [seo-guide.md](seo-guide.md) | SEO head tags: title, meta, OG, canonical via phaseOutput |
|
|
54
|
+
| [commands-guide.md](commands-guide.md) | makeCliCommand, .jay-command files, CONSOLE_CONTEXT, jay-stack run |
|
|
55
|
+
| [validation.md](validation.md) | jay-stack validate-plugin, writing custom jay-html validators |
|
|
56
|
+
| [setup-guide.md](setup-guide.md) | Setup handlers, agent-kit handlers, references generation |
|
|
57
|
+
| [add-menu-guide.md](add-menu-guide.md) | AIditor add-menu items: schema, interaction, browse, presentation |
|
|
58
|
+
| [aiditor-settings-guide.md](aiditor-settings-guide.md) | AIditor Project settings tabs: template, materialization, devOnly route |
|
|
59
|
+
| [source-editing.md](source-editing.md) | Make a plugin's contracts editable: editingGuide/readonly, coverage (DL#219) |
|
|
60
|
+
| [dev-server-service.md](dev-server-service.md) | Dev server service API: routes, params, freeze management |
|
|
61
|
+
| `../references/<plugin>/` | Plugin reference data |
|
|
61
62
|
|
|
62
63
|
## Key Principles
|
|
63
64
|
|
|
@@ -49,6 +49,7 @@ makeJayAction('name')
|
|
|
49
49
|
.withMethod('PUT') // Override HTTP method (default: POST for actions)
|
|
50
50
|
.withCaching({ maxAge: 60 }) // Enable caching (queries only)
|
|
51
51
|
.withFiles() // Accept file uploads (multipart/form-data)
|
|
52
|
+
.withRequest() // Receive the caller's cookies as argument 2 (see below)
|
|
52
53
|
.withHandler(async (input, svc1, svc2) => {
|
|
53
54
|
// Define handler
|
|
54
55
|
return result;
|
|
@@ -133,6 +134,62 @@ refs.uploadBtn.onClick(async () => {
|
|
|
133
134
|
});
|
|
134
135
|
```
|
|
135
136
|
|
|
137
|
+
## .withRequest() — Reading the Caller's Cookies
|
|
138
|
+
|
|
139
|
+
Opt in with `.withRequest()` and the handler receives an `ActionRequest` as its **second** argument, before the services:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { makeJayAction, ActionError } from '@jay-framework/fullstack-component';
|
|
143
|
+
|
|
144
|
+
export const updateAccount = makeJayAction('account.update')
|
|
145
|
+
.withServices(ACCOUNT_SERVICE)
|
|
146
|
+
.withRequest()
|
|
147
|
+
.withHandler(async (input: { name: string }, request, account) => {
|
|
148
|
+
const session = request.cookies.session; // server-trusted, includes HttpOnly cookies
|
|
149
|
+
if (!session) throw new ActionError('UNAUTHORIZED', 'Not signed in');
|
|
150
|
+
return account.update(session, input.name);
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Without `.withRequest()`, the handler is `(input, ...services)` as before. The client call is always `action(input)`.
|
|
155
|
+
|
|
156
|
+
Rules:
|
|
157
|
+
|
|
158
|
+
- **Identity comes from `request.cookies`, never from `input`.** Input is client-controlled and spoofable.
|
|
159
|
+
- **No `.withCaching()`.** A cookie-reading handler is per-user. Combining both throws when the action is defined. Every response of a `.withRequest()` action is sent with `Cache-Control: private, no-store`.
|
|
160
|
+
- **A GET query (`makeJayQuery`) must not mutate.** Only non-GET actions are protected by the same-origin check.
|
|
161
|
+
- **Non-GET calls must come from the same origin.** A cross-site browser request (by `Sec-Fetch-Site`, or `Origin` when that header is absent) is rejected with 403 `CROSS_ORIGIN_REQUEST` before the handler runs. Jay's client always calls same-origin. Requests with neither header (server-to-server, curl) are allowed.
|
|
162
|
+
- **Direct server-side calls get no cookies.** Calling a `.withRequest()` action directly (from another handler, a render phase, or the CLI) passes `{ cookies: {} }`. To act for the same caller, put the logic in a shared function and pass `request` to it explicitly.
|
|
163
|
+
|
|
164
|
+
### Setting cookies (login, refresh, logout)
|
|
165
|
+
|
|
166
|
+
Page JS cannot write an HttpOnly credential, so credential cookies are set by an action:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
export const login = makeJayAction('account.login')
|
|
170
|
+
.withServices(AUTH_SERVICE)
|
|
171
|
+
.withRequest()
|
|
172
|
+
.withHandler(async (input: { email: string; password: string }, request, auth) => {
|
|
173
|
+
const token = await auth.signIn(input.email, input.password);
|
|
174
|
+
request.setCookie('session', token, { maxAge: 60 * 60 * 24 * 30 });
|
|
175
|
+
return { ok: true };
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
export const logout = makeJayAction('account.logout')
|
|
179
|
+
.withRequest()
|
|
180
|
+
.withHandler(async (input: {}, request) => {
|
|
181
|
+
request.clearCookie('session'); // same path/domain as when it was set
|
|
182
|
+
return { ok: true };
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- **Defaults:** `httpOnly: true`, `sameSite: 'Lax'`, `path: '/'`, and `secure` when the request is https (including `x-forwarded-proto: https`). Options: `httpOnly`, `secure`, `sameSite` (`'Strict' | 'Lax' | 'None'`; `'None'` forces `secure`), `path`, `domain`, `maxAge` (seconds), `expires`.
|
|
187
|
+
- **Cookies are sent only on success.** If the handler throws (including `ActionError`), cookies it set are discarded. To clear a cookie on failure, return a success value that says so.
|
|
188
|
+
- **`clearCookie` must use the same `path` and `domain`** the cookie was set with, or the browser keeps the original.
|
|
189
|
+
- Setting the same name twice keeps the last write. Cookie names must be RFC 6265 tokens.
|
|
190
|
+
- **Streams cannot set cookies.** `makeJayStream(...).withRequest()` gives a read-only `StreamActionRequest` (`request.cookies` only), because headers are sent before the generator runs.
|
|
191
|
+
- **Direct calls cannot set cookies.** There is no HTTP response; the cookie is discarded with a warning.
|
|
192
|
+
|
|
136
193
|
## ActionError
|
|
137
194
|
|
|
138
195
|
Throw typed errors from action handlers:
|
|
@@ -122,6 +122,48 @@ For dynamic/materialized contracts linking to static contracts in a plugin packa
|
|
|
122
122
|
|
|
123
123
|
`trackBy` must reference a `data` tag with `string` or `number` type within the same sub-contract.
|
|
124
124
|
|
|
125
|
+
## Outcomes and User-Visible Text
|
|
126
|
+
|
|
127
|
+
The page owns every word the visitor reads. A plugin reports **what happened**; the page decides **how it reads** (wording, language, layout). So any outcome a page may want to render differently — an error, a failure kind, a success, an empty or unavailable state — must be a **variant**, never only a string.
|
|
128
|
+
|
|
129
|
+
- **One kind of outcome** → a `boolean` variant (`hasError`, `isSubmitted`).
|
|
130
|
+
- **Several kinds** → an `enum` variant with one member per kind the page may word differently, plus a neutral member (`NONE`, `IDLE`) for "nothing to report".
|
|
131
|
+
- **Text is optional default copy.** A `data` string next to the variant (`errorMessage`, `statusMessage`) is fine as an English default. Never make it the only signal: a page must not have to test for a non-empty string, or match its text, to know what happened.
|
|
132
|
+
- **Keep codes stable and locale-neutral.** Name members by meaning (`NETWORK`, `PERMISSION`), not by message. **Appending** a member is safe; inserting, renaming, removing, or reordering one is a breaking change.
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
# ✅ Outcome as variants; text is a default the page may ignore
|
|
136
|
+
- {
|
|
137
|
+
tag: submitState,
|
|
138
|
+
type: variant,
|
|
139
|
+
dataType: 'enum (IDLE | SUBMITTING | SUCCEEDED | FAILED)',
|
|
140
|
+
phase: fast+interactive,
|
|
141
|
+
}
|
|
142
|
+
- {
|
|
143
|
+
tag: submitFailure,
|
|
144
|
+
type: variant,
|
|
145
|
+
dataType: 'enum (NONE | VALIDATION | REJECTED | NETWORK)',
|
|
146
|
+
phase: fast+interactive,
|
|
147
|
+
}
|
|
148
|
+
- {
|
|
149
|
+
tag: statusMessage,
|
|
150
|
+
type: data,
|
|
151
|
+
dataType: string,
|
|
152
|
+
phase: fast+interactive,
|
|
153
|
+
description: English default text,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# ❌ Outcome only as text — the page can't localize or redesign it without parsing English
|
|
157
|
+
- { tag: statusMessage, type: data, dataType: string, phase: fast+interactive }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```html
|
|
161
|
+
<!-- The page writes its own copy, in its own language -->
|
|
162
|
+
<p if="form.submitFailure === NETWORK">Problème de connexion, réessayez.</p>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Emit enum values from the enum the contract compiles to (or a mirror of it in the same order); the runtime value is the member's index. Document each member's meaning in the plugin's agent-kit.
|
|
166
|
+
|
|
125
167
|
## Async Data
|
|
126
168
|
|
|
127
169
|
Wrap any tag in `Promise<T>` with `async: true`:
|
|
@@ -31,6 +31,25 @@ return phaseOutput(
|
|
|
31
31
|
|
|
32
32
|
Use this when the component renders per-user data that must not be cached by CDN or browser. Can be combined with `headTags` in the same options object.
|
|
33
33
|
|
|
34
|
+
### Freshness — `ttl` and `grace` (slow phase only)
|
|
35
|
+
|
|
36
|
+
A slow render runs at build time, so its data ages. Return `ttl` (and optionally `grace`) — both in **seconds** — to tell the production server when the data goes stale:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
return phaseOutput(
|
|
40
|
+
{ name: product.name, price: product.price },
|
|
41
|
+
{ productId: product.id },
|
|
42
|
+
{ ttl: 300, grace: 6 * 60 * 60 }, // fresh for 5 minutes, usable for 6 hours
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **`ttl`** — the data is `fresh` for `ttl` seconds after it was rendered. After that it is `stale`: the server serves it immediately (`Cache-Control: no-store`) and re-runs the slow render in the background. No `ttl` → the data never goes stale (today's behavior). Minimum effective `ttl` is 10 seconds.
|
|
47
|
+
- **`grace`** — absolute age after which the data is `expired`: the server waits (up to `JAY_STALE_WAIT_MS`, default 2000) for the refresh before answering. Default `max(12h, ttl)`.
|
|
48
|
+
- Responses carry `jay-freshness: fresh | stale | expired, <age>ms`.
|
|
49
|
+
- A page with several slow parts (page, keyed headless components, headless instances) gets the **minimum positive** `ttl` and the minimum positive `grace` across all of them; `0` / `null` are ignored.
|
|
50
|
+
- With `RenderPipeline`, return `ttl` / `grace` from the `toPhaseOutput` callback next to `viewState` and `carryForward`.
|
|
51
|
+
- Pick a `ttl` that matches how quickly the underlying data changes, not how fast you want pages to be: every `ttl` expiry costs one slow render per instance.
|
|
52
|
+
|
|
34
53
|
### Cookies (fast phase only)
|
|
35
54
|
|
|
36
55
|
The fast phase receives `props.cookies` — a `Record<string, string>` parsed from the HTTP `Cookie` header:
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Source Editing — Plugin Contributor Guide
|
|
2
|
+
|
|
3
|
+
**Audience:** Plugin authors (and AI agents) making a plugin's contracts **editable at the source** (DL#219).
|
|
4
|
+
|
|
5
|
+
**Related:** [actions-guide.md](actions-guide.md) (`devOnly` actions), [plugin-routes.md](plugin-routes.md)
|
|
6
|
+
(`devOnly` routes), and the devops **source-editing** guide (the inspector protocol the AIditor uses).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
When a page binds content to your contract — e.g. `<h1>{productPage.productName}</h1>` — the source-editing
|
|
13
|
+
inspector resolves that pixel to the contract, but it can't know **how** your data is edited: which record,
|
|
14
|
+
what the write path is, or where an editor UI lives. Only your plugin knows. So each contract declares its
|
|
15
|
+
**editing story**, and `jay-stack validate` warns when one is missing.
|
|
16
|
+
|
|
17
|
+
## Declare the editing story (required for coverage)
|
|
18
|
+
|
|
19
|
+
In `plugin.yaml`, each contract declares **one** of:
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
contracts:
|
|
23
|
+
- name: product-page
|
|
24
|
+
contract: ./product-page.jay-contract
|
|
25
|
+
component: productPage
|
|
26
|
+
editingGuide: ./editing/product-page.md # how an agent edits this contract's bound data
|
|
27
|
+
- name: stock-badge
|
|
28
|
+
contract: ./stock-badge.jay-contract
|
|
29
|
+
component: stockBadge
|
|
30
|
+
readonly: true # this contract's data isn't editable in place
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **`editingGuide`** — relative path to a markdown guide shipped by the plugin.
|
|
34
|
+
- **`readonly: true`** — the contract's data is derived/not editable in place (e.g. a computed badge).
|
|
35
|
+
|
|
36
|
+
`jay-stack validate` emits a `source-editing-coverage` warning for any contract that declares neither (and
|
|
37
|
+
checks the `editingGuide` file exists).
|
|
38
|
+
|
|
39
|
+
## What the editing guide contains
|
|
40
|
+
|
|
41
|
+
The guide is the **single entry point** an agent follows to edit content bound to your contract. It should tell
|
|
42
|
+
the agent what to do with the identity the inspector provides:
|
|
43
|
+
|
|
44
|
+
- The inspector's `stack` gives the **record identity** — a `forEach` frame's `itemId` (trackBy), a `region`
|
|
45
|
+
frame's static `props`, plus the page's route params (`route`/`routePattern`).
|
|
46
|
+
- From that, instruct the agent to either:
|
|
47
|
+
- **Write the source** — call a `devOnly` action that persists the change (see actions-guide.md), or
|
|
48
|
+
- **Open an editor** — call a `devOnly` action that returns a URL and open it, or
|
|
49
|
+
- offer **several options** and let the agent choose.
|
|
50
|
+
|
|
51
|
+
Action names are ordinary (nothing is reserved). Example mapping for a stores plugin:
|
|
52
|
+
|
|
53
|
+
| Contract | Record key (from inspect result) | Guide instructs |
|
|
54
|
+
| ---------------- | -------------------------------- | ----------------------------------------- |
|
|
55
|
+
| `product-page` | `slug` (route param) | `getEditUrl(slug)` → fetch slug→productId |
|
|
56
|
+
| `product-search` | `productId` (`forEach` `itemId`) | `getEditUrl(productId)` |
|
|
57
|
+
|
|
58
|
+
## Discovery
|
|
59
|
+
|
|
60
|
+
`plugin.yaml` is the source of truth: an agent that resolves a region to your contract reads `editingGuide`
|
|
61
|
+
from the plugin's `plugin.yaml` and then reads the guide file. Keep the guide concise and imperative — it is
|
|
62
|
+
read by an agent at edit time. (Surfacing `editingGuide` into `agent-kit/plugins-index.yaml` alongside the
|
|
63
|
+
contract entry, so it's one hop from the index, is a planned enhancement.)
|
|
64
|
+
|
|
65
|
+
## Checklist
|
|
66
|
+
|
|
67
|
+
1. Add `editingGuide:` (or `readonly: true`) to each contract in `plugin.yaml`.
|
|
68
|
+
2. Write the guide at that path — the decision rule above: identity → write action / URL action.
|
|
69
|
+
3. Expose the write/URL path as a `devOnly` action (actions-guide.md).
|
|
70
|
+
4. Run `jay-stack validate` — resolve any `source-editing-coverage` warnings.
|
|
@@ -251,3 +251,39 @@ export const validate: JayHtmlValidatorFn = (ctx) => {
|
|
|
251
251
|
return findings;
|
|
252
252
|
};
|
|
253
253
|
```
|
|
254
|
+
|
|
255
|
+
## Project-Level Validators (DL#232)
|
|
256
|
+
|
|
257
|
+
A per-file validator sees one jay-html file at a time. A **project-level validator** runs **once per
|
|
258
|
+
project** — for concerns that are not a single file: a config file, a `.well-known` file, a policy
|
|
259
|
+
file, cross-file consistency. Declare them under `projectValidators` (same `./tools` handler rules as
|
|
260
|
+
`validators`):
|
|
261
|
+
|
|
262
|
+
```yaml
|
|
263
|
+
projectValidators:
|
|
264
|
+
- name: seo-site
|
|
265
|
+
handler: validateProject # export name from the ./tools entry
|
|
266
|
+
description: robots.txt, site identity, and ARD checks
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The handler receives only `{ projectRoot }` (and the plugin's own `settings`) — **not** parsed config
|
|
270
|
+
or parsed jay-html. A validator that needs `.jay` config reads it itself via `projectRoot`:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import type { ProjectValidatorFn, ProjectValidationFinding } from '@jay-framework/compiler-shared';
|
|
274
|
+
|
|
275
|
+
export const validateProject: ProjectValidatorFn = async (ctx) => {
|
|
276
|
+
const findings: ProjectValidationFinding[] = [];
|
|
277
|
+
// ctx.projectRoot — absolute project root path
|
|
278
|
+
// ctx.settings — the plugin's own settings (optional)
|
|
279
|
+
return findings;
|
|
280
|
+
};
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Each `ProjectValidationFinding` has `severity` (`'error'` | `'warning'`), `file` (project-relative,
|
|
284
|
+
or `.jay`), `message`, and optional `suggestion`. Project validators run **after** the per-file pass
|
|
285
|
+
and are **skipped under `--compile-only`**. Findings surface with `source: plugin:<plugin>/<name>`.
|
|
286
|
+
|
|
287
|
+
Built-in example: `@jay-framework/seo-validator` ships a `seo-site` project validator (robots.txt,
|
|
288
|
+
`site.baseUrl`, `site.name`, and ARD `/.well-known/ard.json`) — these moved out of core `validate`
|
|
289
|
+
into the plugin via this hook (DL#232), so a project only gets them when it installs seo-validator.
|