@jay-framework/jay-stack-cli 0.25.4 → 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 +88 -11
- 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.d.ts +14 -0
- package/dist/index.js +340 -249
- package/package.json +16 -16
- package/dist/index-B5oavmfA.js +0 -1803
|
@@ -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:
|
|
@@ -35,24 +35,28 @@ When running `jay-stack-cli setup` without `--interactive`, plugins that need us
|
|
|
35
35
|
|
|
36
36
|
```yaml
|
|
37
37
|
setup-needs-answer:
|
|
38
|
-
plugin:
|
|
39
|
-
key:
|
|
38
|
+
plugin: '@jay-framework/some-plugin'
|
|
39
|
+
key: apiKey
|
|
40
40
|
type: input
|
|
41
41
|
message: 'Enter your API key'
|
|
42
|
+
choices: # present only for select-type questions
|
|
43
|
+
- us: United States
|
|
42
44
|
```
|
|
43
45
|
|
|
44
|
-
To provide the answer, create a YAML file and re-run:
|
|
46
|
+
To provide the answer, create a YAML file keyed by the `key` field and re-run:
|
|
45
47
|
|
|
46
48
|
```yaml
|
|
47
49
|
# answers.yaml
|
|
48
|
-
|
|
50
|
+
apiKey: 'your-answer'
|
|
49
51
|
```
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
54
|
jay-stack-cli setup --answers answers.yaml
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
Repeat until all plugins report `configured`. The flow is iterative — each run may reveal the next question.
|
|
57
|
+
Repeat until all plugins report `configured`. The flow is iterative — each run may reveal the next question. The `key` is the stable join field between a question and its answer.
|
|
58
|
+
|
|
59
|
+
> **Exception — Wix login is not answerable via a file.** The Wix plugins' credential setup shells out to the Wix CLI for a browser-based OAuth login (`npx @wix/cli login`). In non-interactive mode it reports `needs-config` ("requires interactive mode") rather than a `setup-needs-answer:` — there is no key you can put in `answers.yaml`. A human must run `jay-stack-cli setup --interactive` in a real terminal once; after that the app secret, client ID, and site ID are fetched/derived automatically. `wix-deploy` also needs a live Wix CLI session at deploy time (`jay-stack run wix-deploy/deploy`) — re-run `npx @wix/cli login` if it reports the session is missing.
|
|
56
60
|
|
|
57
61
|
### Setup order
|
|
58
62
|
|
|
@@ -64,11 +68,84 @@ jay-stack-cli agent-kit # 2. Generate contracts and discovery data
|
|
|
64
68
|
jay-stack-cli build # 3. Production build
|
|
65
69
|
```
|
|
66
70
|
|
|
71
|
+
## Running in cloud / remote environments
|
|
72
|
+
|
|
73
|
+
Running the dev server behind a cloud preview proxy (sandboxes, Codespaces, Gitpod, etc.) has three
|
|
74
|
+
non-obvious requirements.
|
|
75
|
+
|
|
76
|
+
### 1. Allow the proxy's host (Vite host check)
|
|
77
|
+
|
|
78
|
+
The dev server runs Vite in middleware mode, and Vite's host check 403s any request whose `Host` header
|
|
79
|
+
isn't `localhost` ("Blocked request. This host is not allowed."). Allow the external host via `.jay`:
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
# .jay
|
|
83
|
+
devServer:
|
|
84
|
+
allowedHosts:
|
|
85
|
+
- .mysandbox.dev # Vite host-suffix syntax — matches any subdomain
|
|
86
|
+
# or, to disable the check entirely (dev only):
|
|
87
|
+
# allowedHosts: true
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Platforms that already set Vite's `__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS` env var (comma-separated
|
|
91
|
+
hosts, or `true`) need **no** `.jay` change — the dev server reads and merges it. Do **not** add a
|
|
92
|
+
`src/pages/vite.config.ts` to work around this; use the config above (DL#220).
|
|
93
|
+
|
|
94
|
+
> HMR over a proxy may additionally need Vite's `server.hmr` client settings — not required for serving
|
|
95
|
+
> pages, only for live reload through the proxy.
|
|
96
|
+
|
|
97
|
+
### 2. Binding and port
|
|
98
|
+
|
|
99
|
+
`jay-stack-cli dev` takes **no** `--port` / `--host` flags. The Express server binds all interfaces, so
|
|
100
|
+
the container is reachable as-is; set the port in `.jay` (`devServer.portRange`, e.g. `[3000, 3000]` to
|
|
101
|
+
pin it).
|
|
102
|
+
|
|
103
|
+
### 3. Credentials are read from files, not env vars
|
|
104
|
+
|
|
105
|
+
Plugins read credentials from files written by `setup`, not from `process.env`. Wix, for example, uses
|
|
106
|
+
`config/.wix.yaml` (backend services) and `wix.config.json` (deploy target) — both gitignored. A
|
|
107
|
+
container that only has secrets as env vars must **materialize them into those files before** starting
|
|
108
|
+
the dev server (e.g. a startup script that writes `config/.wix.yaml` from `WIX_*` env vars). Putting
|
|
109
|
+
`WIX_*` under a compose `environment:` block does **not** configure the plugins.
|
|
110
|
+
|
|
111
|
+
### 4. AIditor (visual editor) in a container
|
|
112
|
+
|
|
113
|
+
The `/aiditor` route runs an agent **server-side** via the Claude Agent SDK, so the environment running
|
|
114
|
+
the dev server — not the user's laptop — must have Claude Code available:
|
|
115
|
+
|
|
116
|
+
- **Install the `claude` CLI in the image.** It is not part of the Node base image.
|
|
117
|
+
- **Provide `ANTHROPIC_API_KEY` as a secret/env var.** `claude login` is interactive and cannot complete
|
|
118
|
+
headless, so in a container the API key is the only viable auth. (Locally, `claude login` also works.)
|
|
119
|
+
- **It's dev-only.** The `/aiditor` route is served by the dev server; it is excluded from production
|
|
120
|
+
deploy (`--exclude-plugins aiditor`). If you don't need visual editing in a given environment, you can
|
|
121
|
+
ignore it — the rest of the site scaffolds, builds, runs, and deploys without it; only the `/aiditor`
|
|
122
|
+
route's agent stays inert when the CLI/key are absent.
|
|
123
|
+
|
|
124
|
+
A cloud platform that boots the dev server but neither installs the CLI nor injects the key will serve a
|
|
125
|
+
working site with a non-functional `/aiditor` — the single most common surprise. Install both, or
|
|
126
|
+
document that visual editing is unavailable there.
|
|
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
|
+
|
|
138
|
+
### Health / shutdown (containers)
|
|
139
|
+
|
|
140
|
+
Run with `--test-mode` to expose `GET /_jay/health` (readiness probe) and `POST /_jay/shutdown` (graceful
|
|
141
|
+
stop) — the sanctioned hooks for container healthchecks and lifecycle, instead of signal-guessing.
|
|
142
|
+
|
|
67
143
|
## Guides
|
|
68
144
|
|
|
69
|
-
| File | Topic
|
|
70
|
-
| ------------------------------------------ |
|
|
71
|
-
| [production-build.md](production-build.md) | Build pipeline, output structure, frontend/backend split
|
|
72
|
-
| [serving-modes.md](serving-modes.md) | Self-hosted, CDN, BaaS (fetch handler), CLI flags
|
|
73
|
-
| [fetch-handler.md](fetch-handler.md) | Fetch handler, ArtifactStore interface, BaaS custom stores
|
|
74
|
-
| [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:
|