@jay-framework/jay-stack-cli 0.15.6 → 0.16.1
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/page-components.md +31 -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 +177 -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 +149 -22
- 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
|
+
```
|
|
@@ -81,6 +81,37 @@ export const page = makeJayStackComponent<ProductPageContract>()
|
|
|
81
81
|
});
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
## Calling File Upload Actions
|
|
85
|
+
|
|
86
|
+
Actions created with `.withFiles()` accept browser `File` objects directly. Use `oninput` events on file inputs to drive signals, then pass them to the action:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { createSignal } from '@jay-framework/component';
|
|
90
|
+
import { uploadPhoto } from '../actions/upload.actions';
|
|
91
|
+
|
|
92
|
+
.withInteractive(function UploadPage(props, refs, fastViewState) {
|
|
93
|
+
const [result, setResult] = createSignal('');
|
|
94
|
+
const [selectedFile, setSelectedFile] = createSignal<File | undefined>(undefined);
|
|
95
|
+
|
|
96
|
+
refs.fileInput.oninput(({ event }) => {
|
|
97
|
+
setSelectedFile((event.target as HTMLInputElement).files?.[0]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
refs.uploadBtn.onclick(async () => {
|
|
101
|
+
const file = selectedFile();
|
|
102
|
+
if (!file) return;
|
|
103
|
+
const res = await uploadPhoto({ caption: 'My photo', photo: file });
|
|
104
|
+
setResult(res.message);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
render: () => ({ result: result() }),
|
|
109
|
+
};
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
No casting needed — browser `File` is assignable to `JayFile`.
|
|
114
|
+
|
|
84
115
|
## Combining with Headless Plugins
|
|
85
116
|
|
|
86
117
|
A page component handles page-level data. Plugin headless components handle their own data independently. Both render into the same page:
|
|
@@ -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
|
|
@@ -38,12 +38,99 @@ makeJayAction('name')
|
|
|
38
38
|
.withServices(SERVICE1, SERVICE2) // Inject services
|
|
39
39
|
.withMethod('PUT') // Override HTTP method (default: POST for actions)
|
|
40
40
|
.withCaching({ maxAge: 60 }) // Enable caching (queries only)
|
|
41
|
+
.withFiles({ maxFileSize: 5_000_000 }) // Accept file uploads (multipart/form-data)
|
|
41
42
|
.withHandler(async (input, svc1, svc2) => {
|
|
42
43
|
// Define handler
|
|
43
44
|
return result;
|
|
44
45
|
});
|
|
45
46
|
```
|
|
46
47
|
|
|
48
|
+
## .withFiles() — File Uploads
|
|
49
|
+
|
|
50
|
+
Actions can receive binary files (images, documents, etc.) via multipart/form-data.
|
|
51
|
+
Add `.withFiles()` to the builder chain to enable file uploads.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { makeJayAction, type JayFile } from '@jay-framework/fullstack-component';
|
|
55
|
+
import fs from 'fs';
|
|
56
|
+
|
|
57
|
+
export const uploadPhoto = makeJayAction('photos.upload')
|
|
58
|
+
.withFiles({ maxFileSize: 5 * 1024 * 1024 }) // 5MB limit, 10 files max (default)
|
|
59
|
+
.withHandler(async (input: { caption: string; photo: JayFile }) => {
|
|
60
|
+
// JayFile provides: name, type, size, path (temp file on disk)
|
|
61
|
+
const data = fs.readFileSync(input.photo.path);
|
|
62
|
+
// Process file...
|
|
63
|
+
return { fileName: input.photo.name, size: input.photo.size };
|
|
64
|
+
// Temp file is automatically cleaned up after handler returns
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### JayFile type
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
interface JayFile {
|
|
72
|
+
name: string; // Original filename
|
|
73
|
+
type: string; // MIME type (e.g., 'image/png')
|
|
74
|
+
size: number; // File size in bytes
|
|
75
|
+
path: string; // Absolute path to temp file on disk
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Multiple files
|
|
80
|
+
|
|
81
|
+
Use an array type for the field — files with the same field name are grouped:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
.withHandler(async (input: { images: JayFile[] }) => {
|
|
85
|
+
for (const img of input.images) {
|
|
86
|
+
// Process each file
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**File fields must be top-level properties** — not nested inside objects. Dynamic file fields via index signatures are supported:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
{ notes: string; screenshot: JayFile; extras: JayFile[] } // OK
|
|
95
|
+
{ notes: string; [key: string]: string | JayFile | undefined } // OK
|
|
96
|
+
{ meta: { photo: JayFile } } // NOT supported
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Streaming with files
|
|
100
|
+
|
|
101
|
+
`makeJayStream` also supports `.withFiles()`:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
export const processImages = makeJayStream('images.process')
|
|
105
|
+
.withFiles()
|
|
106
|
+
.withHandler(async function* (input: { images: JayFile[] }) {
|
|
107
|
+
for (const img of input.images) {
|
|
108
|
+
yield { step: 'processing', fileName: img.name };
|
|
109
|
+
}
|
|
110
|
+
yield { step: 'done' };
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Client-side usage
|
|
115
|
+
|
|
116
|
+
The client automatically sends `FormData` when `File` or `Blob` objects are present:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
refs.uploadBtn.onClick(async () => {
|
|
120
|
+
const fileInput = refs.fileInput.element as HTMLInputElement;
|
|
121
|
+
const file = fileInput.files?.[0];
|
|
122
|
+
const result = await uploadPhoto({ caption: 'My photo', photo: file });
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### FileUploadOptions
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
.withFiles() // Defaults: 10MB per file, 10 files max
|
|
130
|
+
.withFiles({ maxFileSize: 2 * 1024 * 1024 }) // 2MB limit
|
|
131
|
+
.withFiles({ maxFileSize: 20_000_000, maxFiles: 5 }) // 20MB, 5 files
|
|
132
|
+
```
|
|
133
|
+
|
|
47
134
|
## ActionError
|
|
48
135
|
|
|
49
136
|
Throw typed errors from action handlers:
|
|
@@ -109,17 +196,106 @@ outputSchema:
|
|
|
109
196
|
| `prop: string` | Required string |
|
|
110
197
|
| `prop?: number` | Optional number |
|
|
111
198
|
| `prop: boolean` | Required boolean |
|
|
199
|
+
| `prop: file` | File upload (`JayFile`) |
|
|
200
|
+
| `prop: file[]` | Multiple file uploads |
|
|
112
201
|
| `prop: enum(a \| b \| c)` | Required enum |
|
|
113
202
|
| `prop:` + nested block | Nested object |
|
|
114
203
|
| `prop:` + `- child: type` | Array of objects |
|
|
115
204
|
| `prop: record(T)` | Record with typed values |
|
|
116
205
|
| `prop: importedName` | Type from `import:` block |
|
|
117
206
|
|
|
207
|
+
## makeJayStream — Streaming (POST, NDJSON)
|
|
208
|
+
|
|
209
|
+
Streaming actions return an async generator that yields chunks:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { makeJayStream } from '@jay-framework/fullstack-component';
|
|
213
|
+
|
|
214
|
+
export const discoverParams = makeJayStream('routes.discoverParams')
|
|
215
|
+
.withServices(PRODUCTS_SERVICE)
|
|
216
|
+
.withHandler(async function* (input: { route: string }, productsService) {
|
|
217
|
+
let page = 1;
|
|
218
|
+
while (true) {
|
|
219
|
+
const products = await productsService.list({ page, pageSize: 100 });
|
|
220
|
+
yield products.map((p) => ({ slug: p.slug }));
|
|
221
|
+
if (!products.hasMore) break;
|
|
222
|
+
page++;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Consuming on the client
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
for await (const batch of discoverParams({ route: '/products/[slug]' })) {
|
|
231
|
+
console.log(batch); // [{ slug: 'item-a' }, { slug: 'item-b' }]
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Wire format
|
|
236
|
+
|
|
237
|
+
The server responds with NDJSON (newline-delimited JSON). Each line is a complete JSON object:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
{"chunk":[{"slug":"item-a"},{"slug":"item-b"}]}
|
|
241
|
+
{"chunk":[{"slug":"item-c"}]}
|
|
242
|
+
{"done":true}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### .jay-action for streaming
|
|
246
|
+
|
|
247
|
+
Add `streaming: true` to the metadata file:
|
|
248
|
+
|
|
249
|
+
```yaml
|
|
250
|
+
name: discoverParams
|
|
251
|
+
description: Discover URL params by querying the product catalog
|
|
252
|
+
streaming: true
|
|
253
|
+
inputSchema:
|
|
254
|
+
route: string
|
|
255
|
+
outputSchema:
|
|
256
|
+
- slug: string
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### .jay-action for file uploads
|
|
260
|
+
|
|
261
|
+
Use `file` type for upload fields. The generated TypeScript uses `JayFile`:
|
|
262
|
+
|
|
263
|
+
```yaml
|
|
264
|
+
name: uploadPhoto
|
|
265
|
+
description: Upload a product photo with caption
|
|
266
|
+
inputSchema:
|
|
267
|
+
caption: string
|
|
268
|
+
photo: file
|
|
269
|
+
attachments?: file[]
|
|
270
|
+
outputSchema:
|
|
271
|
+
fileId: string
|
|
272
|
+
message: string
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
For dynamic file fields, use `record(file)`:
|
|
276
|
+
|
|
277
|
+
```yaml
|
|
278
|
+
name: submitTask
|
|
279
|
+
description: Submit task with named file attachments
|
|
280
|
+
inputSchema:
|
|
281
|
+
notes: string
|
|
282
|
+
files: record(file)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
This generates `files: Record<string, JayFile>`.
|
|
286
|
+
|
|
118
287
|
## Type Helpers
|
|
119
288
|
|
|
120
289
|
```typescript
|
|
121
|
-
import {
|
|
290
|
+
import {
|
|
291
|
+
ActionInput,
|
|
292
|
+
ActionOutput,
|
|
293
|
+
isJayAction,
|
|
294
|
+
StreamChunk,
|
|
295
|
+
isJayStreamAction,
|
|
296
|
+
} from '@jay-framework/fullstack-component';
|
|
122
297
|
|
|
123
298
|
type SearchInput = ActionInput<typeof searchProducts>;
|
|
124
299
|
type SearchOutput = ActionOutput<typeof searchProducts>;
|
|
300
|
+
type ParamBatch = StreamChunk<typeof discoverParams>;
|
|
125
301
|
```
|
|
@@ -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) {
|
|
@@ -4239,15 +4352,29 @@ async function runAction(actionRef, options, projectRoot, initializeServices) {
|
|
|
4239
4352
|
async function runParams(contractRef, options, projectRoot, initializeServices) {
|
|
4240
4353
|
let viteServer;
|
|
4241
4354
|
try {
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
)
|
|
4247
|
-
|
|
4355
|
+
let pluginName;
|
|
4356
|
+
let contractName;
|
|
4357
|
+
if (contractRef.startsWith("@")) {
|
|
4358
|
+
const secondSlash = contractRef.indexOf("/", contractRef.indexOf("/") + 1);
|
|
4359
|
+
if (secondSlash === -1) {
|
|
4360
|
+
getLogger().error(
|
|
4361
|
+
chalk.red("❌ Invalid contract reference. Use format: @scope/plugin/contract")
|
|
4362
|
+
);
|
|
4363
|
+
process.exit(1);
|
|
4364
|
+
}
|
|
4365
|
+
pluginName = contractRef.substring(0, secondSlash);
|
|
4366
|
+
contractName = contractRef.substring(secondSlash + 1);
|
|
4367
|
+
} else {
|
|
4368
|
+
const slashIndex = contractRef.indexOf("/");
|
|
4369
|
+
if (slashIndex === -1) {
|
|
4370
|
+
getLogger().error(
|
|
4371
|
+
chalk.red("❌ Invalid contract reference. Use format: <plugin>/<contract>")
|
|
4372
|
+
);
|
|
4373
|
+
process.exit(1);
|
|
4374
|
+
}
|
|
4375
|
+
pluginName = contractRef.substring(0, slashIndex);
|
|
4376
|
+
contractName = contractRef.substring(slashIndex + 1);
|
|
4248
4377
|
}
|
|
4249
|
-
const pluginName = contractRef.substring(0, slashIndex);
|
|
4250
|
-
const contractName = contractRef.substring(slashIndex + 1);
|
|
4251
4378
|
if (options.verbose) {
|
|
4252
4379
|
getLogger().info("Starting Vite for TypeScript support...");
|
|
4253
4380
|
}
|
|
@@ -4282,21 +4409,21 @@ async function runParams(contractRef, options, projectRoot, initializeServices)
|
|
|
4282
4409
|
}
|
|
4283
4410
|
const { resolveServices } = await import("@jay-framework/stack-server-runtime");
|
|
4284
4411
|
const resolvedServices = resolveServices(component.services || []);
|
|
4285
|
-
const allParams = [];
|
|
4286
4412
|
const paramsGenerator = component.loadParams(resolvedServices);
|
|
4413
|
+
let total = 0;
|
|
4287
4414
|
for await (const batch of paramsGenerator) {
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4415
|
+
for (const params of batch) {
|
|
4416
|
+
if (options.yaml) {
|
|
4417
|
+
getLogger().important(YAML.stringify([params]).trimEnd());
|
|
4418
|
+
} else {
|
|
4419
|
+
getLogger().important(JSON.stringify(params));
|
|
4420
|
+
}
|
|
4421
|
+
total++;
|
|
4422
|
+
}
|
|
4294
4423
|
}
|
|
4295
4424
|
if (!options.yaml) {
|
|
4296
|
-
getLogger().important(
|
|
4297
|
-
|
|
4298
|
-
✅ Found ${allParams.length} param combination(s)`)
|
|
4299
|
-
);
|
|
4425
|
+
getLogger().important(chalk.green(`
|
|
4426
|
+
✅ Found ${total} param combination(s)`));
|
|
4300
4427
|
}
|
|
4301
4428
|
} catch (error) {
|
|
4302
4429
|
getLogger().error(chalk.red("❌ Failed to discover params:") + " " + error.message);
|
|
@@ -4671,8 +4798,8 @@ program.command("action <plugin/action>").description(
|
|
|
4671
4798
|
await runAction(actionRef, options, process.cwd(), initializeServicesForCli);
|
|
4672
4799
|
});
|
|
4673
4800
|
program.command("params <plugin/contract>").description(
|
|
4674
|
-
"Discover load param values for a contract (
|
|
4675
|
-
).option("--yaml", "Output
|
|
4801
|
+
"Discover load param values for a contract. Streams results as NDJSON (one JSON object per line) or YAML."
|
|
4802
|
+
).option("--yaml", "Output as YAML instead of NDJSON").option("-v, --verbose", "Show detailed output").action(async (contractRef, options) => {
|
|
4676
4803
|
await runParams(contractRef, options, process.cwd(), initializeServicesForCli);
|
|
4677
4804
|
});
|
|
4678
4805
|
program.parse(process.argv);
|
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.1",
|
|
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.1",
|
|
28
|
+
"@jay-framework/compiler-shared": "^0.16.1",
|
|
29
|
+
"@jay-framework/dev-server": "^0.16.1",
|
|
30
|
+
"@jay-framework/editor-server": "^0.16.1",
|
|
31
|
+
"@jay-framework/fullstack-component": "^0.16.1",
|
|
32
|
+
"@jay-framework/logger": "^0.16.1",
|
|
33
|
+
"@jay-framework/plugin-validator": "^0.16.1",
|
|
34
|
+
"@jay-framework/stack-server-runtime": "^0.16.1",
|
|
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.1",
|
|
46
46
|
"@types/express": "^5.0.2",
|
|
47
47
|
"@types/node": "^22.15.21",
|
|
48
48
|
"nodemon": "^3.0.3",
|