@jay-framework/jay-stack-cli 0.15.6 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-kit-template/developer/dev-server-service.md +126 -0
- package/agent-kit-template/developer/routing.md +14 -0
- package/agent-kit-template/plugin/INSTRUCTIONS.md +7 -4
- package/agent-kit-template/plugin/actions-guide.md +60 -1
- package/agent-kit-template/plugin/dev-server-service.md +137 -0
- package/agent-kit-template/plugin/plugin-routes.md +146 -0
- package/agent-kit-template/plugin/plugin-structure.md +16 -0
- package/dist/index.js +114 -1
- package/package.json +10 -10
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Dev Server Service API
|
|
2
|
+
|
|
3
|
+
The dev server exposes a `DevServerService` for design board applications, CLI tools, and plugins. It provides route listing, param discovery, and freeze management.
|
|
4
|
+
|
|
5
|
+
## Service Marker
|
|
6
|
+
|
|
7
|
+
Registered as `DEV_SERVER_SERVICE` — injectable in actions and components:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { DEV_SERVER_SERVICE } from '@jay-framework/dev-server';
|
|
11
|
+
|
|
12
|
+
export const listAllRoutes = makeJayAction('admin.listRoutes')
|
|
13
|
+
.withServices(DEV_SERVER_SERVICE)
|
|
14
|
+
.withHandler(async (_input, devServer) => {
|
|
15
|
+
return devServer.listRoutes();
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Direct Access
|
|
20
|
+
|
|
21
|
+
Also returned from `mkDevServer()` for CLI usage:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const { service } = await mkDevServer(options);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Routes
|
|
28
|
+
|
|
29
|
+
### listRoutes()
|
|
30
|
+
|
|
31
|
+
Returns all page routes in the project:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const routes = service.listRoutes();
|
|
35
|
+
// [{ path: '/products/kitan/[[category]]', jayHtmlPath: '...', compPath: '...' }]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### loadRouteParams(route, onBatch)
|
|
39
|
+
|
|
40
|
+
Async generator that yields param batches from the route's `loadParams`:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
for await (const batch of service.loadRouteParams('/products/kitan/[[category]]')) {
|
|
44
|
+
console.log(batch); // [{ category: 'shirts' }, { category: 'pants' }]
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Returns empty if the route has no `page.ts` or no `loadParams`. Throws if the route doesn't exist.
|
|
49
|
+
|
|
50
|
+
## Freeze Management
|
|
51
|
+
|
|
52
|
+
### FreezeStore
|
|
53
|
+
|
|
54
|
+
Accessible via `service.freezeStore`:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const store = service.freezeStore;
|
|
58
|
+
|
|
59
|
+
// Save a ViewState snapshot
|
|
60
|
+
const entry = await store.save('/products/kitan', viewState);
|
|
61
|
+
|
|
62
|
+
// List freezes for a route
|
|
63
|
+
const freezes = await store.list('/products/kitan');
|
|
64
|
+
|
|
65
|
+
// Get a specific freeze
|
|
66
|
+
const freeze = await store.get('abc123');
|
|
67
|
+
|
|
68
|
+
// Rename
|
|
69
|
+
await store.rename('abc123', 'in-stock state');
|
|
70
|
+
|
|
71
|
+
// Delete
|
|
72
|
+
await store.delete('abc123');
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Editor Protocol
|
|
76
|
+
|
|
77
|
+
These APIs are also exposed via the editor protocol (Socket.IO) for design board applications:
|
|
78
|
+
|
|
79
|
+
| Protocol Message | Service Method |
|
|
80
|
+
| ----------------- | ----------------------------------------- |
|
|
81
|
+
| `listRoutes` | `service.listRoutes()` |
|
|
82
|
+
| `listFreezes` | `service.freezeStore.list(route)` |
|
|
83
|
+
| `renameFreeze` | `service.freezeStore.rename(id, name)` |
|
|
84
|
+
| `deleteFreeze` | `service.freezeStore.delete(id)` |
|
|
85
|
+
| `loadRouteParams` | `service.loadRouteParams(route, onBatch)` |
|
|
86
|
+
|
|
87
|
+
### Streaming Events
|
|
88
|
+
|
|
89
|
+
`loadRouteParams` streams batches via `routeParamsBatch` socket events:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// Client sends: { type: 'loadRouteParams', route: '/products/[slug]' }
|
|
93
|
+
// Server responds: { type: 'loadRouteParams', success: true }
|
|
94
|
+
// Server emits: { type: 'routeParamsBatch', route: '...', params: [...], hasMore: true }
|
|
95
|
+
// Server emits: { type: 'routeParamsBatch', route: '...', params: [...], hasMore: true }
|
|
96
|
+
// Server emits: { type: 'routeParamsBatch', route: '...', params: [], hasMore: false }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Freeze Changed Event
|
|
100
|
+
|
|
101
|
+
The `freezeChanged` socket event is emitted when jay-html or CSS files change. Design board applications should listen for this to refresh their frozen views:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
socket.on('freezeChanged', () => {
|
|
105
|
+
// Re-fetch frozen page fragments
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Iframe / Embed Mode
|
|
110
|
+
|
|
111
|
+
When a page is loaded inside an iframe with `?_jay_embed=true` (e.g., by the AIditor), the Alt+S shortcut is disabled (parent owns it). Freeze is triggered via `postMessage`:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Parent → iframe: request freeze
|
|
115
|
+
iframe.contentWindow.postMessage({ type: 'jay:requestFreeze' }, '*');
|
|
116
|
+
|
|
117
|
+
// Iframe → parent: freeze done
|
|
118
|
+
// { type: 'jay:freeze', id: string, route: string }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The parent constructs the frozen page URL:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
route + '?_jay_freeze=' + id // full page
|
|
125
|
+
route + '?_jay_freeze=' + id + '&format=fragment' // shadow DOM fragment
|
|
126
|
+
```
|
|
@@ -159,3 +159,17 @@ URL query parameters (`?page=2&sort=price`) are available in the **fast render p
|
|
|
159
159
|
- `props.query` is `Record<string, string>` — empty `{}` when no query string
|
|
160
160
|
- Not available in the slow phase (compile error) — slow results are cached by path params only
|
|
161
161
|
- In the interactive phase, use `new URLSearchParams(window.location.search)` directly
|
|
162
|
+
|
|
163
|
+
## Plugin Routes
|
|
164
|
+
|
|
165
|
+
Plugins can provide their own pages via `routes` in `plugin.yaml`. These are backoffice tools, admin dashboards, or editors with boxed designs that don't need per-site customization.
|
|
166
|
+
|
|
167
|
+
Plugin routes appear alongside project routes. If your project defines the same route path in `src/pages/`, your page takes precedence — the plugin's page is skipped.
|
|
168
|
+
|
|
169
|
+
To override a plugin route, simply create a page at the same path:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
# Plugin provides /admin/products
|
|
173
|
+
# To customize, create:
|
|
174
|
+
src/pages/admin/products/page.jay-html
|
|
175
|
+
```
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# Jay Plugin Development — Agent Kit
|
|
2
2
|
|
|
3
|
-
This folder contains guides for creating jay-stack plugins: contracts, headless components, server actions, and
|
|
3
|
+
This folder contains guides for creating jay-stack plugins: contracts, headless components, server actions, services, and plugin-provided routes.
|
|
4
4
|
|
|
5
5
|
## What is a Jay Plugin?
|
|
6
6
|
|
|
7
|
-
A plugin provides headless components (data + interactions, no UI) that project designers use via contracts. Plugins can be standalone npm packages or inline within a project (see `examples/jay-stack/fake-shop`).
|
|
7
|
+
A plugin provides headless components (data + interactions, no UI) that project designers use via contracts. Plugins can also provide complete pages (backoffice tools, admin dashboards) via routes. Plugins can be standalone npm packages or inline within a project (see `examples/jay-stack/fake-shop`).
|
|
8
8
|
|
|
9
9
|
## Workflow
|
|
10
10
|
|
|
11
11
|
1. **Define contracts first** — the contract is the source of truth
|
|
12
12
|
2. **Implement components** matching the contracts
|
|
13
13
|
3. **Define actions** with `.jay-action` metadata
|
|
14
|
-
4. **
|
|
15
|
-
5. **
|
|
14
|
+
4. **Optionally add routes** — pages for admin tools and dashboards
|
|
15
|
+
5. **Set up `plugin.yaml`** — list contracts, actions, services, contexts, routes
|
|
16
|
+
6. **Validate** with `jay-stack validate-plugin`
|
|
16
17
|
|
|
17
18
|
## Guides
|
|
18
19
|
|
|
@@ -28,8 +29,10 @@ A plugin provides headless components (data + interactions, no UI) that project
|
|
|
28
29
|
| [render-results.md](render-results.md) | phaseOutput, RenderPipeline, errors, redirects |
|
|
29
30
|
| [actions-guide.md](actions-guide.md) | makeJayAction, makeJayQuery, .jay-action files |
|
|
30
31
|
| [services-guide.md](services-guide.md) | createJayService, makeJayInit |
|
|
32
|
+
| [plugin-routes.md](plugin-routes.md) | Plugin-provided pages: routes, jay-html templates, page components |
|
|
31
33
|
| [seo-guide.md](seo-guide.md) | SEO head tags: title, meta, OG, canonical via phaseOutput |
|
|
32
34
|
| [validation.md](validation.md) | jay-stack validate-plugin usage |
|
|
35
|
+
| [dev-server-service.md](dev-server-service.md) | Dev server service API: routes, params, freeze management |
|
|
33
36
|
| `../references/<plugin>/` | Plugin reference data |
|
|
34
37
|
|
|
35
38
|
## Key Principles
|
|
@@ -115,11 +115,70 @@ outputSchema:
|
|
|
115
115
|
| `prop: record(T)` | Record with typed values |
|
|
116
116
|
| `prop: importedName` | Type from `import:` block |
|
|
117
117
|
|
|
118
|
+
## makeJayStream — Streaming (POST, NDJSON)
|
|
119
|
+
|
|
120
|
+
Streaming actions return an async generator that yields chunks:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { makeJayStream } from '@jay-framework/fullstack-component';
|
|
124
|
+
|
|
125
|
+
export const discoverParams = makeJayStream('routes.discoverParams')
|
|
126
|
+
.withServices(PRODUCTS_SERVICE)
|
|
127
|
+
.withHandler(async function* (input: { route: string }, productsService) {
|
|
128
|
+
let page = 1;
|
|
129
|
+
while (true) {
|
|
130
|
+
const products = await productsService.list({ page, pageSize: 100 });
|
|
131
|
+
yield products.map((p) => ({ slug: p.slug }));
|
|
132
|
+
if (!products.hasMore) break;
|
|
133
|
+
page++;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Consuming on the client
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
for await (const batch of discoverParams({ route: '/products/[slug]' })) {
|
|
142
|
+
console.log(batch); // [{ slug: 'item-a' }, { slug: 'item-b' }]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Wire format
|
|
147
|
+
|
|
148
|
+
The server responds with NDJSON (newline-delimited JSON). Each line is a complete JSON object:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
{"chunk":[{"slug":"item-a"},{"slug":"item-b"}]}
|
|
152
|
+
{"chunk":[{"slug":"item-c"}]}
|
|
153
|
+
{"done":true}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### .jay-action for streaming
|
|
157
|
+
|
|
158
|
+
Add `streaming: true` to the metadata file:
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
name: discoverParams
|
|
162
|
+
description: Discover URL params by querying the product catalog
|
|
163
|
+
streaming: true
|
|
164
|
+
inputSchema:
|
|
165
|
+
route: string
|
|
166
|
+
outputSchema:
|
|
167
|
+
- slug: string
|
|
168
|
+
```
|
|
169
|
+
|
|
118
170
|
## Type Helpers
|
|
119
171
|
|
|
120
172
|
```typescript
|
|
121
|
-
import {
|
|
173
|
+
import {
|
|
174
|
+
ActionInput,
|
|
175
|
+
ActionOutput,
|
|
176
|
+
isJayAction,
|
|
177
|
+
StreamChunk,
|
|
178
|
+
isJayStreamAction,
|
|
179
|
+
} from '@jay-framework/fullstack-component';
|
|
122
180
|
|
|
123
181
|
type SearchInput = ActionInput<typeof searchProducts>;
|
|
124
182
|
type SearchOutput = ActionOutput<typeof searchProducts>;
|
|
183
|
+
type ParamBatch = StreamChunk<typeof discoverParams>;
|
|
125
184
|
```
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Dev Server Service API
|
|
2
|
+
|
|
3
|
+
The dev server exposes a `DevServerService` for plugins, design board applications, and CLI tools. It provides route listing, param discovery, and freeze management.
|
|
4
|
+
|
|
5
|
+
## Service Marker
|
|
6
|
+
|
|
7
|
+
Registered as `DEV_SERVER_SERVICE` — inject in actions and components:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { DEV_SERVER_SERVICE } from '@jay-framework/dev-server';
|
|
11
|
+
|
|
12
|
+
export const listAllRoutes = makeJayAction('admin.listRoutes')
|
|
13
|
+
.withServices(DEV_SERVER_SERVICE)
|
|
14
|
+
.withHandler(async (_input, devServer) => {
|
|
15
|
+
return devServer.listRoutes();
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or in a component:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
makeJayStackComponent()
|
|
23
|
+
.withServices(DEV_SERVER_SERVICE)
|
|
24
|
+
.withFastRender(async (_props, devServer) => {
|
|
25
|
+
const routes = devServer.listRoutes();
|
|
26
|
+
return phaseOutput({ routes, routeCount: routes.length }, {});
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Direct Access
|
|
31
|
+
|
|
32
|
+
Also returned from `mkDevServer()` for CLI usage:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
const { service } = await mkDevServer(options);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Routes
|
|
39
|
+
|
|
40
|
+
### listRoutes()
|
|
41
|
+
|
|
42
|
+
Returns all page routes in the project (including plugin-provided routes):
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const routes = service.listRoutes();
|
|
46
|
+
// [{ path: '/products/kitan/[[category]]', jayHtmlPath: '...', compPath: '...' }]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### loadRouteParams(route)
|
|
50
|
+
|
|
51
|
+
Async generator that yields param batches from the route's `loadParams`:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
for await (const batch of service.loadRouteParams('/products/kitan/[[category]]')) {
|
|
55
|
+
console.log(batch); // [{ category: 'shirts' }, { category: 'pants' }]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Returns empty if the route has no `page.ts` or no `loadParams`. Throws if the route doesn't exist.
|
|
60
|
+
|
|
61
|
+
## Freeze Management
|
|
62
|
+
|
|
63
|
+
### FreezeStore
|
|
64
|
+
|
|
65
|
+
Accessible via `service.freezeStore`:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
const store = service.freezeStore;
|
|
69
|
+
|
|
70
|
+
// Save a ViewState snapshot
|
|
71
|
+
const entry = await store.save('/products/kitan', viewState);
|
|
72
|
+
|
|
73
|
+
// List freezes for a route
|
|
74
|
+
const freezes = await store.list('/products/kitan');
|
|
75
|
+
|
|
76
|
+
// Get a specific freeze
|
|
77
|
+
const freeze = await store.get('abc123');
|
|
78
|
+
|
|
79
|
+
// Rename
|
|
80
|
+
await store.rename('abc123', 'in-stock state');
|
|
81
|
+
|
|
82
|
+
// Delete
|
|
83
|
+
await store.delete('abc123');
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Editor Protocol
|
|
87
|
+
|
|
88
|
+
These APIs are also exposed via the editor protocol (Socket.IO) for design board applications:
|
|
89
|
+
|
|
90
|
+
| Protocol Message | Service Method |
|
|
91
|
+
| ----------------- | ----------------------------------------- |
|
|
92
|
+
| `listRoutes` | `service.listRoutes()` |
|
|
93
|
+
| `listFreezes` | `service.freezeStore.list(route)` |
|
|
94
|
+
| `renameFreeze` | `service.freezeStore.rename(id, name)` |
|
|
95
|
+
| `deleteFreeze` | `service.freezeStore.delete(id)` |
|
|
96
|
+
| `loadRouteParams` | `service.loadRouteParams(route, onBatch)` |
|
|
97
|
+
|
|
98
|
+
### Streaming Events
|
|
99
|
+
|
|
100
|
+
`loadRouteParams` streams batches via `routeParamsBatch` socket events:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// Client sends: { type: 'loadRouteParams', route: '/products/[slug]' }
|
|
104
|
+
// Server responds: { type: 'loadRouteParams', success: true }
|
|
105
|
+
// Server emits: { type: 'routeParamsBatch', route: '...', params: [...], hasMore: true }
|
|
106
|
+
// Server emits: { type: 'routeParamsBatch', route: '...', params: [...], hasMore: true }
|
|
107
|
+
// Server emits: { type: 'routeParamsBatch', route: '...', params: [], hasMore: false }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Freeze Changed Event
|
|
111
|
+
|
|
112
|
+
The `freezeChanged` socket event is emitted when jay-html or CSS files change. Design board applications should listen for this to refresh their frozen views:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
socket.on('freezeChanged', () => {
|
|
116
|
+
// Re-fetch frozen page fragments
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Iframe / Embed Mode
|
|
121
|
+
|
|
122
|
+
When a page is loaded inside an iframe with `?_jay_embed=true` (e.g., by the AIditor), the Alt+S shortcut is disabled (parent owns it). Freeze is triggered via `postMessage`:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Parent → iframe: request freeze
|
|
126
|
+
iframe.contentWindow.postMessage({ type: 'jay:requestFreeze' }, '*');
|
|
127
|
+
|
|
128
|
+
// Iframe → parent: freeze done
|
|
129
|
+
// { type: 'jay:freeze', id: string, route: string }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The parent constructs the frozen page URL:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
route + '?_jay_freeze=' + id // full page
|
|
136
|
+
route + '?_jay_freeze=' + id + '&format=fragment' // shadow DOM fragment
|
|
137
|
+
```
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Plugin Routes
|
|
2
|
+
|
|
3
|
+
Plugins can provide complete pages served by the dev server. This is designed for backoffice tools, admin dashboards, and editors — pages with a boxed design that doesn't need per-site visual customization.
|
|
4
|
+
|
|
5
|
+
## When to Use Plugin Routes
|
|
6
|
+
|
|
7
|
+
- **Admin dashboards** — product management, analytics, settings
|
|
8
|
+
- **Editor tools** — visual page editors, contract browsers
|
|
9
|
+
- **Developer tools** — debugging panels, state inspectors
|
|
10
|
+
|
|
11
|
+
Plugin routes are NOT for end-user pages that need visual customization per site. For those, provide headless components and let the project create its own pages.
|
|
12
|
+
|
|
13
|
+
## Creating a Plugin Route
|
|
14
|
+
|
|
15
|
+
A plugin route is a **headless component + jay-html template + route path**. It uses the same rendering pipeline as project pages.
|
|
16
|
+
|
|
17
|
+
### 1. Create the jay-html template
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<!-- pages/admin/page.jay-html -->
|
|
21
|
+
<html>
|
|
22
|
+
<head>
|
|
23
|
+
<script type="application/jay-data">
|
|
24
|
+
data:
|
|
25
|
+
title: string
|
|
26
|
+
items:
|
|
27
|
+
- name: string
|
|
28
|
+
count: number
|
|
29
|
+
</script>
|
|
30
|
+
<style>
|
|
31
|
+
.admin {
|
|
32
|
+
max-width: 800px;
|
|
33
|
+
margin: 0 auto;
|
|
34
|
+
padding: 20px;
|
|
35
|
+
font-family: system-ui;
|
|
36
|
+
}
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<div class="admin">
|
|
41
|
+
<h1>{title}</h1>
|
|
42
|
+
<div forEach="items" trackBy="name"><span>{name}</span>: <strong>{count}</strong></div>
|
|
43
|
+
</div>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Create the page component
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// pages/admin/page.ts
|
|
52
|
+
import { makeJayStackComponent, phaseOutput } from '@jay-framework/fullstack-component';
|
|
53
|
+
import { MY_SERVICE, MyService } from '../../services';
|
|
54
|
+
|
|
55
|
+
export const page = makeJayStackComponent()
|
|
56
|
+
.withProps<{}>()
|
|
57
|
+
.withServices(MY_SERVICE)
|
|
58
|
+
.withFastRender(async (_props, myService: MyService) => {
|
|
59
|
+
const data = await myService.getDashboardData();
|
|
60
|
+
return phaseOutput({ title: 'Admin Dashboard', items: data.items }, {});
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The page component follows the same pattern as any `makeJayStackComponent` — supports `withSlowlyRender`, `withFastRender`, `withInteractive`, `withLoadParams`, and `withServices`.
|
|
65
|
+
|
|
66
|
+
### 3. Declare the route in plugin.yaml
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
name: my-plugin
|
|
70
|
+
routes:
|
|
71
|
+
- path: /admin/dashboard
|
|
72
|
+
jayHtml: ./pages/admin/page.jay-html
|
|
73
|
+
component: ./pages/admin/page.ts
|
|
74
|
+
description: Admin dashboard showing key metrics
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For local plugins, `jayHtml` and `component` are relative paths. For NPM packages, use `package.json` export subpaths.
|
|
78
|
+
|
|
79
|
+
### 4. For NPM packages — export the page files
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"exports": {
|
|
84
|
+
"./admin-dashboard.jay-html": "./dist/pages/admin/page.jay-html",
|
|
85
|
+
"./admin-dashboard.css": "./dist/pages/admin/page.css"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Then reference the export subpath in plugin.yaml:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
routes:
|
|
94
|
+
- path: /admin/dashboard
|
|
95
|
+
jayHtml: admin-dashboard.jay-html
|
|
96
|
+
css: admin-dashboard.css
|
|
97
|
+
component: adminDashboard
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Route Parameters
|
|
101
|
+
|
|
102
|
+
Plugin routes support the same parameter patterns as project routes:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
routes:
|
|
106
|
+
- path: /admin/products/[id]
|
|
107
|
+
jayHtml: ./pages/product-detail/page.jay-html
|
|
108
|
+
component: ./pages/product-detail/page.ts
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The page component can use `withLoadParams` for SSG parameter discovery:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
export const page = makeJayStackComponent()
|
|
115
|
+
.withProps<{}>()
|
|
116
|
+
.withServices(PRODUCTS_SERVICE)
|
|
117
|
+
.withLoadParams<{ id: string }>(async function* (productsService) {
|
|
118
|
+
const products = await productsService.listAll();
|
|
119
|
+
yield products.map((p) => ({ id: p.id }));
|
|
120
|
+
})
|
|
121
|
+
.withSlowlyRender(async (props: { id: string }, productsService) => {
|
|
122
|
+
const product = await productsService.getById(props.id);
|
|
123
|
+
return phaseOutput({ name: product.name, price: product.price }, {});
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Route Priority
|
|
128
|
+
|
|
129
|
+
Project routes always take precedence. If the project creates a page at the same path, the plugin's route is skipped:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
src/pages/admin/dashboard/page.jay-html ← project wins
|
|
133
|
+
plugin provides /admin/dashboard ← skipped
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This lets projects override any plugin page without modifying the plugin.
|
|
137
|
+
|
|
138
|
+
## Prefix Convention
|
|
139
|
+
|
|
140
|
+
Each plugin should choose a recognizable route prefix to avoid collisions:
|
|
141
|
+
|
|
142
|
+
- `/admin/...` — admin tools
|
|
143
|
+
- `/aiditor/...` — AIditor editor
|
|
144
|
+
- `/cms/...` — content management
|
|
145
|
+
|
|
146
|
+
There is no enforced convention — just pick a prefix that's unique and descriptive.
|
|
@@ -35,6 +35,12 @@ contexts:
|
|
|
35
35
|
marker: MY_CART_CONTEXT
|
|
36
36
|
description: Client-side cart state (add/remove items, totals)
|
|
37
37
|
|
|
38
|
+
routes:
|
|
39
|
+
- path: /admin/dashboard
|
|
40
|
+
jayHtml: ./pages/admin/page.jay-html
|
|
41
|
+
component: ./pages/admin/page.ts
|
|
42
|
+
description: Admin dashboard with product stats
|
|
43
|
+
|
|
38
44
|
setup:
|
|
39
45
|
handler: setup-handler
|
|
40
46
|
references: references-handler
|
|
@@ -93,6 +99,16 @@ services:
|
|
|
93
99
|
}
|
|
94
100
|
```
|
|
95
101
|
|
|
102
|
+
### Route Entry Fields
|
|
103
|
+
|
|
104
|
+
- `path` — Route path (e.g., `/admin/products`, `/dashboard/[section]`)
|
|
105
|
+
- `jayHtml` — Path to the page's jay-html template (relative to plugin root, or export subpath for NPM)
|
|
106
|
+
- `css` — (optional) Path to the page's CSS file
|
|
107
|
+
- `component` — Path to the page component (relative to plugin root, or exported member name for NPM)
|
|
108
|
+
- `description` — What this page does
|
|
109
|
+
|
|
110
|
+
Plugin routes are served by the dev server alongside project routes. If a project defines the same route path, the project's page takes precedence.
|
|
111
|
+
|
|
96
112
|
### Setup Fields
|
|
97
113
|
|
|
98
114
|
- `handler` — Setup handler for `jay-stack setup` (handles config, credentials)
|
package/dist/index.js
CHANGED
|
@@ -2441,7 +2441,7 @@ async function startDevServer(options = {}) {
|
|
|
2441
2441
|
editorServer.onGetProjectInfo(handlers.onGetProjectInfo);
|
|
2442
2442
|
editorServer.onExport(handlers.onExport);
|
|
2443
2443
|
editorServer.onImport(handlers.onImport);
|
|
2444
|
-
const { server, viteServer, routes } = await mkDevServer({
|
|
2444
|
+
const { server, viteServer, routes, service } = await mkDevServer({
|
|
2445
2445
|
pagesRootFolder: path.resolve(resolvedConfig.devServer.pagesBase),
|
|
2446
2446
|
projectRootFolder: process.cwd(),
|
|
2447
2447
|
publicBaseUrlPath: "/",
|
|
@@ -2450,6 +2450,73 @@ async function startDevServer(options = {}) {
|
|
|
2450
2450
|
httpServer
|
|
2451
2451
|
});
|
|
2452
2452
|
app.use(server);
|
|
2453
|
+
const { freezeStore } = service;
|
|
2454
|
+
editorServer.onListRoutes(async () => ({
|
|
2455
|
+
type: "listRoutes",
|
|
2456
|
+
success: true,
|
|
2457
|
+
routes: service.listRoutes()
|
|
2458
|
+
}));
|
|
2459
|
+
if (freezeStore) {
|
|
2460
|
+
editorServer.onListFreezes(async (params) => ({
|
|
2461
|
+
type: "listFreezes",
|
|
2462
|
+
success: true,
|
|
2463
|
+
freezes: (await freezeStore.list(params.route)).map(
|
|
2464
|
+
({ id, name, route, routePattern, createdAt }) => ({
|
|
2465
|
+
id,
|
|
2466
|
+
name,
|
|
2467
|
+
route,
|
|
2468
|
+
routePattern,
|
|
2469
|
+
createdAt
|
|
2470
|
+
})
|
|
2471
|
+
)
|
|
2472
|
+
}));
|
|
2473
|
+
editorServer.onRenameFreeze(async (params) => ({
|
|
2474
|
+
type: "renameFreeze",
|
|
2475
|
+
success: await freezeStore.rename(params.id, params.name)
|
|
2476
|
+
}));
|
|
2477
|
+
editorServer.onDeleteFreeze(async (params) => ({
|
|
2478
|
+
type: "deleteFreeze",
|
|
2479
|
+
success: await freezeStore.delete(params.id)
|
|
2480
|
+
}));
|
|
2481
|
+
viteServer.watcher.on("change", (changedPath) => {
|
|
2482
|
+
if (changedPath.endsWith(".jay-html") || changedPath.endsWith(".css")) {
|
|
2483
|
+
editorServer.emitFreezeChanged();
|
|
2484
|
+
}
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
editorServer.onLoadRouteParams(async (params) => {
|
|
2488
|
+
const routePath = params.route;
|
|
2489
|
+
try {
|
|
2490
|
+
(async () => {
|
|
2491
|
+
try {
|
|
2492
|
+
for await (const batch of service.loadRouteParams(routePath)) {
|
|
2493
|
+
editorServer.emitRouteParamsBatch({
|
|
2494
|
+
type: "routeParamsBatch",
|
|
2495
|
+
route: routePath,
|
|
2496
|
+
params: batch,
|
|
2497
|
+
hasMore: true
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
editorServer.emitRouteParamsBatch({
|
|
2501
|
+
type: "routeParamsBatch",
|
|
2502
|
+
route: routePath,
|
|
2503
|
+
params: [],
|
|
2504
|
+
hasMore: false
|
|
2505
|
+
});
|
|
2506
|
+
} catch (err) {
|
|
2507
|
+
editorServer.emitRouteParamsBatch({
|
|
2508
|
+
type: "routeParamsBatch",
|
|
2509
|
+
route: routePath,
|
|
2510
|
+
params: [],
|
|
2511
|
+
hasMore: false
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
})();
|
|
2515
|
+
return { type: "loadRouteParams", success: true };
|
|
2516
|
+
} catch (err) {
|
|
2517
|
+
return { type: "loadRouteParams", success: false, error: err.message };
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2453
2520
|
const publicPath = path.resolve(resolvedConfig.devServer.publicFolder);
|
|
2454
2521
|
if (fs.existsSync(publicPath)) {
|
|
2455
2522
|
app.use(express.static(publicPath));
|
|
@@ -3067,6 +3134,52 @@ async function validateSchema(context, result) {
|
|
|
3067
3134
|
});
|
|
3068
3135
|
}
|
|
3069
3136
|
}
|
|
3137
|
+
if (manifest.routes) {
|
|
3138
|
+
if (!Array.isArray(manifest.routes)) {
|
|
3139
|
+
result.errors.push({
|
|
3140
|
+
type: "schema",
|
|
3141
|
+
message: 'Field "routes" must be an array',
|
|
3142
|
+
location: "plugin.yaml"
|
|
3143
|
+
});
|
|
3144
|
+
} else {
|
|
3145
|
+
manifest.routes.forEach((route, index) => {
|
|
3146
|
+
if (!route.path) {
|
|
3147
|
+
result.errors.push({
|
|
3148
|
+
type: "schema",
|
|
3149
|
+
message: `Route at index ${index} is missing "path" field`,
|
|
3150
|
+
location: "plugin.yaml"
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
if (!route.jayHtml) {
|
|
3154
|
+
result.errors.push({
|
|
3155
|
+
type: "schema",
|
|
3156
|
+
message: `Route "${route.path || index}" is missing "jayHtml" field`,
|
|
3157
|
+
location: "plugin.yaml",
|
|
3158
|
+
suggestion: "Specify the export subpath for the jay-html file"
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
if (!route.component) {
|
|
3162
|
+
result.errors.push({
|
|
3163
|
+
type: "schema",
|
|
3164
|
+
message: `Route "${route.path || index}" is missing "component" field`,
|
|
3165
|
+
location: "plugin.yaml",
|
|
3166
|
+
suggestion: "Specify the exported member name for the page component"
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
if (route.jayHtml) {
|
|
3170
|
+
validateDocFile(
|
|
3171
|
+
route.jayHtml,
|
|
3172
|
+
`route "${route.path}" jayHtml`,
|
|
3173
|
+
context,
|
|
3174
|
+
result
|
|
3175
|
+
);
|
|
3176
|
+
}
|
|
3177
|
+
if (route.css) {
|
|
3178
|
+
validateDocFile(route.css, `route "${route.path}" css`, context, result);
|
|
3179
|
+
}
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3070
3183
|
}
|
|
3071
3184
|
function resolveContractFile(contractSpec, context) {
|
|
3072
3185
|
if (context.isNpmPackage) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/jay-stack-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"test:watch": "vitest"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@jay-framework/compiler-jay-html": "^0.
|
|
28
|
-
"@jay-framework/compiler-shared": "^0.
|
|
29
|
-
"@jay-framework/dev-server": "^0.
|
|
30
|
-
"@jay-framework/editor-server": "^0.
|
|
31
|
-
"@jay-framework/fullstack-component": "^0.
|
|
32
|
-
"@jay-framework/logger": "^0.
|
|
33
|
-
"@jay-framework/plugin-validator": "^0.
|
|
34
|
-
"@jay-framework/stack-server-runtime": "^0.
|
|
27
|
+
"@jay-framework/compiler-jay-html": "^0.16.0",
|
|
28
|
+
"@jay-framework/compiler-shared": "^0.16.0",
|
|
29
|
+
"@jay-framework/dev-server": "^0.16.0",
|
|
30
|
+
"@jay-framework/editor-server": "^0.16.0",
|
|
31
|
+
"@jay-framework/fullstack-component": "^0.16.0",
|
|
32
|
+
"@jay-framework/logger": "^0.16.0",
|
|
33
|
+
"@jay-framework/plugin-validator": "^0.16.0",
|
|
34
|
+
"@jay-framework/stack-server-runtime": "^0.16.0",
|
|
35
35
|
"chalk": "^4.1.2",
|
|
36
36
|
"commander": "^14.0.0",
|
|
37
37
|
"express": "^5.0.1",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"yaml": "^2.3.4"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@jay-framework/dev-environment": "^0.
|
|
45
|
+
"@jay-framework/dev-environment": "^0.16.0",
|
|
46
46
|
"@types/express": "^5.0.2",
|
|
47
47
|
"@types/node": "^22.15.21",
|
|
48
48
|
"nodemon": "^3.0.3",
|