@jay-framework/jay-stack-cli 0.20.0 → 0.21.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/jay-html-components.md +42 -0
- package/agent-kit-template/plugin/INSTRUCTIONS.md +20 -3
- package/agent-kit-template/plugin/plugin-structure.md +16 -6
- package/agent-kit-template/plugin/setup-guide.md +177 -0
- package/agent-kit-template/plugin/validation.md +18 -4
- package/dist/index.js +215 -33
- package/package.json +11 -11
|
@@ -71,6 +71,20 @@ Use `<jay:contract-name>` tags with props:
|
|
|
71
71
|
</jay:product-widget>
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
**With bindings from page data** (props from keyed components or page ViewState):
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<!-- p is a keyed headless component providing product data -->
|
|
78
|
+
<jay:category-products categorySlug="{p.categorySlug}" limit="4">
|
|
79
|
+
<div class="product-card">
|
|
80
|
+
<h3>{name}</h3>
|
|
81
|
+
<span>{price}</span>
|
|
82
|
+
</div>
|
|
83
|
+
</jay:category-products>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use `{path}` syntax to bind props to values from the page's ViewState. The binding is resolved at render time — works with both slow and fast phase data.
|
|
87
|
+
|
|
74
88
|
**With forEach** (dynamic props from parent data):
|
|
75
89
|
|
|
76
90
|
```html
|
|
@@ -85,6 +99,34 @@ Use `<jay:contract-name>` tags with props:
|
|
|
85
99
|
|
|
86
100
|
Inside `<jay:...>`, bindings resolve to **that instance's** contract tags (not the parent).
|
|
87
101
|
|
|
102
|
+
### Prop binding summary
|
|
103
|
+
|
|
104
|
+
| Syntax | Resolves to | Example |
|
|
105
|
+
| --------------------------------- | -------------------- | ------------------------- |
|
|
106
|
+
| `prop="literal"` | Literal string value | `productId="prod-1"` |
|
|
107
|
+
| `prop="{field}"` | Page ViewState field | `slug="{p.categorySlug}"` |
|
|
108
|
+
| `prop="{field}"` (inside forEach) | ForEach item field | `productId="{_id}"` |
|
|
109
|
+
|
|
110
|
+
### Prop phase constraints
|
|
111
|
+
|
|
112
|
+
Contract props can declare a `phase` (defaults to `slow`). The binding source must be available at that phase:
|
|
113
|
+
|
|
114
|
+
- A **slow** prop (default) must bind to a literal, a route param, or a slow-phase tag
|
|
115
|
+
- A **fast** prop can also bind to fast-phase tags
|
|
116
|
+
|
|
117
|
+
If a slow prop binds to a fast-phase field, `jay-stack validate` flags an error — the component's slow render would receive an empty value.
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
# In the component's contract:
|
|
121
|
+
props:
|
|
122
|
+
- name: categorySlug
|
|
123
|
+
type: string
|
|
124
|
+
phase: slow # Must be available at build time
|
|
125
|
+
- name: filter
|
|
126
|
+
type: string
|
|
127
|
+
phase: fast # Only needs to be available at request time
|
|
128
|
+
```
|
|
129
|
+
|
|
88
130
|
## Headfull Components
|
|
89
131
|
|
|
90
132
|
In Jay Stack, headfull components are full-stack. They must have a `.jay-contract` file and are created using `makeJayStackComponent` in their `.ts` file. They support server rendering (slow/fast/interactive phases) and must include a `contract` attribute in the import.
|
|
@@ -12,9 +12,25 @@ A plugin provides headless components (data + interactions, no UI) that project
|
|
|
12
12
|
2. **Implement components** matching the contracts
|
|
13
13
|
3. **Define actions** with `.jay-action` metadata
|
|
14
14
|
4. **Optionally add routes** — pages for admin tools and dashboards
|
|
15
|
-
5. **
|
|
16
|
-
6. **
|
|
17
|
-
7. **
|
|
15
|
+
5. **Optionally add validators** — custom jay-html validation rules
|
|
16
|
+
6. **Optionally add setup/references handlers** — config templating, add-menu generation
|
|
17
|
+
7. **Set up `plugin.yaml`** — list contracts, actions, services, contexts, routes, validators, setup
|
|
18
|
+
8. **Configure build** — dual entry points (server + client), vite.config.ts, package.json exports
|
|
19
|
+
9. **Validate** with `jay-stack validate-plugin`
|
|
20
|
+
|
|
21
|
+
## Plugin Lifecycle — CLI Commands
|
|
22
|
+
|
|
23
|
+
The plugin participates in four CLI commands, each running different hooks:
|
|
24
|
+
|
|
25
|
+
| Command | When | What runs from your plugin |
|
|
26
|
+
| --------------------------- | ------------------ | ---------------------------------------------------------------------------------- |
|
|
27
|
+
| `jay-stack validate-plugin` | Plugin development | Checks plugin.yaml structure, contracts, exports, handler references |
|
|
28
|
+
| `jay-stack setup <plugin>` | Project setup | `setup.handler` — creates config files, validates credentials |
|
|
29
|
+
| `jay-stack agent-kit` | Before development | `setup.references` — generates add-menu items, reference data |
|
|
30
|
+
| `jay-stack validate` | During development | `validators[].handler` — runs your validation rules against project jay-html files |
|
|
31
|
+
|
|
32
|
+
**`validate-plugin`** validates YOUR plugin's structure. Run it during plugin development.
|
|
33
|
+
**`validate`** runs your plugin's validators against a PROJECT that uses your plugin. Run it from the project.
|
|
18
34
|
|
|
19
35
|
## Guides
|
|
20
36
|
|
|
@@ -36,6 +52,7 @@ A plugin provides headless components (data + interactions, no UI) that project
|
|
|
36
52
|
| [seo-guide.md](seo-guide.md) | SEO head tags: title, meta, OG, canonical via phaseOutput |
|
|
37
53
|
| [commands-guide.md](commands-guide.md) | makeCliCommand, .jay-command files, CONSOLE_CONTEXT, jay-stack run |
|
|
38
54
|
| [validation.md](validation.md) | jay-stack validate-plugin, writing custom jay-html validators |
|
|
55
|
+
| [setup-guide.md](setup-guide.md) | Setup handlers, references handlers, add-menu generation |
|
|
39
56
|
| [dev-server-service.md](dev-server-service.md) | Dev server service API: routes, params, freeze management |
|
|
40
57
|
| `../references/<plugin>/` | Plugin reference data |
|
|
41
58
|
|
|
@@ -63,7 +63,7 @@ commands:
|
|
|
63
63
|
|
|
64
64
|
validators:
|
|
65
65
|
- name: media-optimization
|
|
66
|
-
handler:
|
|
66
|
+
handler: validateMediaOptimization
|
|
67
67
|
description: Ensures media URLs use resize parameters
|
|
68
68
|
|
|
69
69
|
setup:
|
|
@@ -193,16 +193,26 @@ Commands are CLI operations run via `jay-stack run`. Use `makeCliCommand()` to c
|
|
|
193
193
|
### Validator Entry Fields
|
|
194
194
|
|
|
195
195
|
- `name` — Validator name (shown in validation output as `plugin-name/validator-name`)
|
|
196
|
-
- `handler` —
|
|
196
|
+
- `handler` — Export name (NPM plugins) or relative path (local plugins) to the validator function
|
|
197
197
|
- `description` — (optional) What this validator checks
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
**NPM plugins:** `handler` is the export name from the package entry point (e.g., `validateMediaOptimization`). The function must be exported from `lib/index.ts`.
|
|
200
|
+
**Local plugins:** `handler` is a relative path to the module (e.g., `./validators/media-validator`). The module must export a `validate` function.
|
|
201
|
+
|
|
202
|
+
Validators run during `jay-stack validate` against every parsed jay-html file in the project. See [validation.md](validation.md) for implementation details.
|
|
200
203
|
|
|
201
204
|
### Setup Fields
|
|
202
205
|
|
|
203
|
-
- `handler` —
|
|
204
|
-
- `references` —
|
|
205
|
-
- `
|
|
206
|
+
- `handler` — Export name (NPM) or relative path (local) for `jay-stack setup <plugin>`. Creates config files, validates credentials and services.
|
|
207
|
+
- `references` — Export name (NPM) or relative path (local) for `jay-stack agent-kit`. Generates discovery data: add-menu items, reference files.
|
|
208
|
+
- `description` — (optional) What this setup does
|
|
209
|
+
|
|
210
|
+
**NPM plugins:** `handler` and `references` are export names from the package entry point.
|
|
211
|
+
**Local plugins:** relative paths to the handler modules.
|
|
212
|
+
|
|
213
|
+
`jay-stack validate-plugin` checks that these handlers exist and are correctly exported.
|
|
214
|
+
|
|
215
|
+
See [setup-guide.md](setup-guide.md) for implementation details.
|
|
206
216
|
|
|
207
217
|
## Package Layout
|
|
208
218
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Plugin Setup & References
|
|
2
|
+
|
|
3
|
+
Plugins can provide two hooks for project configuration and AI agent discovery:
|
|
4
|
+
|
|
5
|
+
- **Setup handler** — runs during `jay-stack setup <plugin>`. Creates config files, validates credentials, copies AIditor assets.
|
|
6
|
+
- **References handler** — runs during `jay-stack agent-kit`. Generates discovery data (add-menu items, reference files) using live services.
|
|
7
|
+
|
|
8
|
+
## When Each Runs
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
jay-stack setup <plugin> → setup.handler()
|
|
12
|
+
jay-stack agent-kit → setup.references() (after contract materialization)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Setup runs once when a project first installs the plugin. Agent-kit runs whenever the developer regenerates the agent kit — it can use live services to produce fresh data.
|
|
16
|
+
|
|
17
|
+
## Declaring in plugin.yaml
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
setup:
|
|
21
|
+
handler: setupMyPlugin # export name (NPM) or ./path (local)
|
|
22
|
+
references: generateMyReferences # export name (NPM) or ./path (local)
|
|
23
|
+
description: Install My Plugin config and AIditor catalog
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**NPM plugins:** both values are export names from the package entry point (`lib/index.ts`).
|
|
27
|
+
**Local plugins:** relative paths to the handler modules.
|
|
28
|
+
|
|
29
|
+
`jay-stack validate-plugin` checks that these exist and are correctly exported.
|
|
30
|
+
|
|
31
|
+
## Writing a Setup Handler
|
|
32
|
+
|
|
33
|
+
The setup handler creates config files and AIditor assets. It receives a `PluginSetupContext` and returns a `PluginSetupResult`.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import type { PluginSetupContext, PluginSetupResult } from '@jay-framework/stack-server-runtime';
|
|
37
|
+
import fs from 'node:fs';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
|
|
40
|
+
export async function setupMyPlugin(ctx: PluginSetupContext): Promise<PluginSetupResult> {
|
|
41
|
+
if (ctx.initError) {
|
|
42
|
+
return { status: 'error', message: `Init failed: ${ctx.initError.message}` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const configCreated: string[] = [];
|
|
46
|
+
|
|
47
|
+
// Write AIditor add-menu catalog
|
|
48
|
+
const addMenuPath = path.join(ctx.projectRoot, 'agent-kit/aiditor/add-menu/my-plugin.yaml');
|
|
49
|
+
if (!fs.existsSync(addMenuPath) || ctx.force) {
|
|
50
|
+
fs.mkdirSync(path.dirname(addMenuPath), { recursive: true });
|
|
51
|
+
fs.writeFileSync(addMenuPath, templateContent, 'utf-8');
|
|
52
|
+
configCreated.push('agent-kit/aiditor/add-menu/my-plugin.yaml');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
status: 'configured',
|
|
57
|
+
configCreated,
|
|
58
|
+
message:
|
|
59
|
+
configCreated.length > 0
|
|
60
|
+
? 'My Plugin catalog installed.'
|
|
61
|
+
: 'My Plugin catalog already present (use --force to rewrite).',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### PluginSetupContext
|
|
67
|
+
|
|
68
|
+
| Field | Type | Description |
|
|
69
|
+
| ------------- | --------- | ----------------------------------------------------------------- |
|
|
70
|
+
| `pluginName` | `string` | Plugin name from plugin.yaml |
|
|
71
|
+
| `projectRoot` | `string` | Absolute project root path |
|
|
72
|
+
| `configDir` | `string` | Config directory (from `.jay` configBase, defaults to `./config`) |
|
|
73
|
+
| `services` | `Map` | Registered services (may be empty if init failed) |
|
|
74
|
+
| `initError` | `Error?` | Present if plugin init failed — check this before using services |
|
|
75
|
+
| `force` | `boolean` | Whether `--force` flag was passed |
|
|
76
|
+
|
|
77
|
+
### PluginSetupResult
|
|
78
|
+
|
|
79
|
+
| Field | Type | Description |
|
|
80
|
+
| --------------- | ------------------------------------------- | ----------------------------------------------- |
|
|
81
|
+
| `status` | `'configured' \| 'needs-config' \| 'error'` | Overall result |
|
|
82
|
+
| `configCreated` | `string[]?` | Config files created (relative to project root) |
|
|
83
|
+
| `message` | `string?` | Human-readable status message |
|
|
84
|
+
|
|
85
|
+
## Writing a References Handler
|
|
86
|
+
|
|
87
|
+
The references handler generates discovery data at agent-kit time. It can use live services (database queries, API calls) to produce dynamic content.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import type {
|
|
91
|
+
PluginReferencesContext,
|
|
92
|
+
PluginReferencesResult,
|
|
93
|
+
} from '@jay-framework/stack-server-runtime';
|
|
94
|
+
import fs from 'node:fs';
|
|
95
|
+
import path from 'node:path';
|
|
96
|
+
|
|
97
|
+
export async function generateMyReferences(
|
|
98
|
+
ctx: PluginReferencesContext,
|
|
99
|
+
): Promise<PluginReferencesResult> {
|
|
100
|
+
if (ctx.initError) {
|
|
101
|
+
return { referencesCreated: [], message: `Skipped: ${ctx.initError.message}` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Example: generate add-menu items from live data
|
|
105
|
+
const outputPath = path.join(ctx.projectRoot, 'agent-kit/aiditor/add-menu/my-plugin.yaml');
|
|
106
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
107
|
+
|
|
108
|
+
const items = [
|
|
109
|
+
{ id: 'my-plugin:feature-1', title: 'Feature 1', category: 'My Plugin', prompt: '...' },
|
|
110
|
+
];
|
|
111
|
+
fs.writeFileSync(outputPath, yaml.dump({ items }), 'utf-8');
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
referencesCreated: ['agent-kit/aiditor/add-menu/my-plugin.yaml'],
|
|
115
|
+
message: `Generated ${items.length} add-menu items`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### PluginReferencesContext
|
|
121
|
+
|
|
122
|
+
| Field | Type | Description |
|
|
123
|
+
| --------------- | --------- | --------------------------------------------------------------- |
|
|
124
|
+
| `pluginName` | `string` | Plugin name from plugin.yaml |
|
|
125
|
+
| `projectRoot` | `string` | Absolute project root path |
|
|
126
|
+
| `referencesDir` | `string` | Directory for reference data (`agent-kit/references/<plugin>/`) |
|
|
127
|
+
| `services` | `Map` | Registered services |
|
|
128
|
+
| `initError` | `Error?` | Present if plugin init failed |
|
|
129
|
+
| `force` | `boolean` | Whether `--force` flag was passed |
|
|
130
|
+
|
|
131
|
+
### PluginReferencesResult
|
|
132
|
+
|
|
133
|
+
| Field | Type | Description |
|
|
134
|
+
| ------------------- | ---------- | ---------------------------------------- |
|
|
135
|
+
| `referencesCreated` | `string[]` | Files created (relative to project root) |
|
|
136
|
+
| `message` | `string?` | Human-readable status message |
|
|
137
|
+
|
|
138
|
+
## Setup vs References — When to Use Which
|
|
139
|
+
|
|
140
|
+
| Use case | Handler | Why |
|
|
141
|
+
| -------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------- |
|
|
142
|
+
| Copy static template files (add-menu catalog, skill guides) | `setup.handler` | Templates don't change — copy once |
|
|
143
|
+
| Generate data from live services (product catalogs, CMS schemas) | `setup.references` | Needs services initialized; regenerated on each `agent-kit` run |
|
|
144
|
+
| Validate credentials / API keys | `setup.handler` | Part of initial project configuration |
|
|
145
|
+
| Write AIditor add-menu from project-specific data (DESIGN.md tokens) | `setup.references` | Data comes from project files, not static templates |
|
|
146
|
+
|
|
147
|
+
## AIditor Add-Menu Items
|
|
148
|
+
|
|
149
|
+
Both handlers can write to `agent-kit/aiditor/add-menu/<plugin-name>.yaml`. The AIditor discovers and loads all YAML files in this directory.
|
|
150
|
+
|
|
151
|
+
Each item:
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
items:
|
|
155
|
+
- id: my-plugin:feature-name # unique ID
|
|
156
|
+
title: Feature Name # shown in the add menu
|
|
157
|
+
category: My Plugin # grouping
|
|
158
|
+
subCategory: Components # sub-grouping
|
|
159
|
+
pluginName: my-plugin # optional: plugin attribution
|
|
160
|
+
packageName: '@my-org/my-plugin' # optional: npm package name
|
|
161
|
+
prompt: | # instructions for the AI agent
|
|
162
|
+
Use headless component @my-org/my-plugin / contract feature-name.
|
|
163
|
+
Read agent-kit/designer/feature-name.md for usage guide.
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Exporting Handlers
|
|
167
|
+
|
|
168
|
+
For NPM plugins, export the handlers from the package entry point:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// lib/index.ts
|
|
172
|
+
export { setupMyPlugin } from './setup.js';
|
|
173
|
+
export { generateMyReferences } from './references.js';
|
|
174
|
+
// ... other exports (components, actions, services)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
For local plugins, use relative paths in plugin.yaml instead of export names.
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Plugin Validation
|
|
2
2
|
|
|
3
|
+
Two separate validation commands:
|
|
4
|
+
|
|
5
|
+
- **`jay-stack validate-plugin`** — validates your plugin's own structure (run during plugin development)
|
|
6
|
+
- **`jay-stack validate`** — validates a project's jay-html files, including running your plugin's custom validators (run from a project that uses your plugin)
|
|
7
|
+
|
|
8
|
+
## validate-plugin
|
|
9
|
+
|
|
3
10
|
Run `jay-stack validate-plugin` to check your plugin for errors.
|
|
4
11
|
|
|
5
12
|
## Usage
|
|
@@ -119,20 +126,27 @@ Values: `title`, `meta:<name>` (e.g., `meta:description`), `link:<rel>` (e.g., `
|
|
|
119
126
|
|
|
120
127
|
Validators access this via `ctx.headlessImports[].providedHeadTags`.
|
|
121
128
|
|
|
122
|
-
## Plugin Validators
|
|
129
|
+
## Plugin Validators (jay-stack validate)
|
|
123
130
|
|
|
124
|
-
Plugins can provide custom jay-html validation rules that run during `jay-stack validate
|
|
131
|
+
Plugins can provide custom jay-html validation rules that run during `jay-stack validate` in projects that use your plugin. Declare validators in `plugin.yaml`:
|
|
125
132
|
|
|
126
133
|
```yaml
|
|
127
134
|
validators:
|
|
128
135
|
- name: media-optimization
|
|
129
|
-
handler:
|
|
136
|
+
handler: validateMediaOptimization # export name from package entry point
|
|
130
137
|
description: Ensures media URLs use resize parameters
|
|
131
138
|
```
|
|
132
139
|
|
|
140
|
+
**Handler format:**
|
|
141
|
+
|
|
142
|
+
- **NPM plugins** — `handler` is an export name from the package entry point (e.g., `validateMediaOptimization`). The function must be exported from `lib/index.ts`.
|
|
143
|
+
- **Local plugins** (`src/plugins/`) — `handler` is a relative path to the module (e.g., `./validators/media-validator`). The module must export a `validate` function.
|
|
144
|
+
|
|
145
|
+
`jay-stack validate-plugin` checks that the handler exists and is correctly exported.
|
|
146
|
+
|
|
133
147
|
### Writing a Validator
|
|
134
148
|
|
|
135
|
-
|
|
149
|
+
Export the validator function from the package entry point (for NPM) or from the handler module (for local):
|
|
136
150
|
|
|
137
151
|
```typescript
|
|
138
152
|
import type { JayHtmlValidatorFn, JayHtmlValidationFinding } from '@jay-framework/compiler-shared';
|
package/dist/index.js
CHANGED
|
@@ -2748,15 +2748,6 @@ async function runRebuild(projectPath, options) {
|
|
|
2748
2748
|
process.exit(1);
|
|
2749
2749
|
}
|
|
2750
2750
|
}
|
|
2751
|
-
const runProduction = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
2752
|
-
__proto__: null,
|
|
2753
|
-
initLogger,
|
|
2754
|
-
resolveProductionContext,
|
|
2755
|
-
resolveVersionFromPackageJson,
|
|
2756
|
-
runBuild,
|
|
2757
|
-
runRebuild,
|
|
2758
|
-
runServe
|
|
2759
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
2760
2751
|
const s = createRequire(import.meta.url), e = s("typescript"), u = e;
|
|
2761
2752
|
new Proxy(e, {
|
|
2762
2753
|
get(t, r) {
|
|
@@ -3210,6 +3201,14 @@ async function validateSchema(context, result) {
|
|
|
3210
3201
|
location: "plugin.yaml",
|
|
3211
3202
|
suggestion: 'Specify the exported member name from the module (e.g., "moodTracker")'
|
|
3212
3203
|
});
|
|
3204
|
+
} else {
|
|
3205
|
+
validateHandlerRef(
|
|
3206
|
+
contract.component,
|
|
3207
|
+
`Contract "${contract.name}" component`,
|
|
3208
|
+
`plugin.yaml contracts[${index}]`,
|
|
3209
|
+
context,
|
|
3210
|
+
result
|
|
3211
|
+
);
|
|
3213
3212
|
}
|
|
3214
3213
|
});
|
|
3215
3214
|
}
|
|
@@ -3242,6 +3241,24 @@ async function validateSchema(context, result) {
|
|
|
3242
3241
|
suggestion: 'Specify prefix for dynamic contract names (e.g., "cms")'
|
|
3243
3242
|
});
|
|
3244
3243
|
}
|
|
3244
|
+
if (config.component) {
|
|
3245
|
+
validateHandlerRef(
|
|
3246
|
+
config.component,
|
|
3247
|
+
`dynamic_contracts[${prefix}] component`,
|
|
3248
|
+
`plugin.yaml dynamic_contracts`,
|
|
3249
|
+
context,
|
|
3250
|
+
result
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3253
|
+
if (config.generator) {
|
|
3254
|
+
validateHandlerRef(
|
|
3255
|
+
config.generator,
|
|
3256
|
+
`dynamic_contracts[${prefix}] generator`,
|
|
3257
|
+
`plugin.yaml dynamic_contracts`,
|
|
3258
|
+
context,
|
|
3259
|
+
result
|
|
3260
|
+
);
|
|
3261
|
+
}
|
|
3245
3262
|
}
|
|
3246
3263
|
}
|
|
3247
3264
|
if (!manifest.contracts && !manifest.dynamic_contracts) {
|
|
@@ -3312,6 +3329,23 @@ async function validateSchema(context, result) {
|
|
|
3312
3329
|
});
|
|
3313
3330
|
}
|
|
3314
3331
|
}
|
|
3332
|
+
if (manifest.actions) {
|
|
3333
|
+
for (const entry of manifest.actions) {
|
|
3334
|
+
const exportName = typeof entry === "string" ? entry : entry.name;
|
|
3335
|
+
if (exportName) {
|
|
3336
|
+
validateHandlerRef(
|
|
3337
|
+
exportName,
|
|
3338
|
+
`Action "${exportName}"`,
|
|
3339
|
+
"plugin.yaml actions",
|
|
3340
|
+
context,
|
|
3341
|
+
result
|
|
3342
|
+
);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
if (manifest.init) {
|
|
3347
|
+
validateHandlerRef(manifest.init, "Init handler", "plugin.yaml init", context, result);
|
|
3348
|
+
}
|
|
3315
3349
|
if (manifest.routes) {
|
|
3316
3350
|
if (!Array.isArray(manifest.routes)) {
|
|
3317
3351
|
result.errors.push({
|
|
@@ -3343,6 +3377,14 @@ async function validateSchema(context, result) {
|
|
|
3343
3377
|
location: "plugin.yaml",
|
|
3344
3378
|
suggestion: "Specify the exported member name for the page component"
|
|
3345
3379
|
});
|
|
3380
|
+
} else {
|
|
3381
|
+
validateHandlerRef(
|
|
3382
|
+
route.component,
|
|
3383
|
+
`Route "${route.path}" component`,
|
|
3384
|
+
`plugin.yaml routes`,
|
|
3385
|
+
context,
|
|
3386
|
+
result
|
|
3387
|
+
);
|
|
3346
3388
|
}
|
|
3347
3389
|
if (route.jayHtml) {
|
|
3348
3390
|
validateDocFile(
|
|
@@ -3382,22 +3424,105 @@ async function validateSchema(context, result) {
|
|
|
3382
3424
|
suggestion: "Specify the relative path to the validator handler module"
|
|
3383
3425
|
});
|
|
3384
3426
|
}
|
|
3385
|
-
if (validator.handler
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
location: "plugin.yaml validators",
|
|
3394
|
-
suggestion: `Create the validator handler at ${handlerPath}.ts`
|
|
3395
|
-
});
|
|
3396
|
-
}
|
|
3427
|
+
if (validator.handler) {
|
|
3428
|
+
validateHandlerRef(
|
|
3429
|
+
validator.handler,
|
|
3430
|
+
`Validator "${validator.name}" handler`,
|
|
3431
|
+
"plugin.yaml validators",
|
|
3432
|
+
context,
|
|
3433
|
+
result
|
|
3434
|
+
);
|
|
3397
3435
|
}
|
|
3398
3436
|
});
|
|
3399
3437
|
}
|
|
3400
3438
|
}
|
|
3439
|
+
if (manifest.setup) {
|
|
3440
|
+
if (manifest.setup.handler) {
|
|
3441
|
+
validateHandlerRef(
|
|
3442
|
+
manifest.setup.handler,
|
|
3443
|
+
"Setup handler",
|
|
3444
|
+
"plugin.yaml setup.handler",
|
|
3445
|
+
context,
|
|
3446
|
+
result
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
if (manifest.setup.references) {
|
|
3450
|
+
validateHandlerRef(
|
|
3451
|
+
manifest.setup.references,
|
|
3452
|
+
"References handler",
|
|
3453
|
+
"plugin.yaml setup.references",
|
|
3454
|
+
context,
|
|
3455
|
+
result
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
function checkExportExists(exportName, context) {
|
|
3461
|
+
const packageJsonPath = path.join(context.pluginPath, "package.json");
|
|
3462
|
+
if (!fs.existsSync(packageJsonPath))
|
|
3463
|
+
return true;
|
|
3464
|
+
let mainPath;
|
|
3465
|
+
try {
|
|
3466
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
3467
|
+
if (packageJson.exports?.["."]) {
|
|
3468
|
+
const entry = packageJson.exports["."];
|
|
3469
|
+
const entryPath = typeof entry === "string" ? entry : entry.default || entry.import;
|
|
3470
|
+
if (entryPath)
|
|
3471
|
+
mainPath = path.join(context.pluginPath, entryPath);
|
|
3472
|
+
}
|
|
3473
|
+
if (!mainPath && packageJson.main) {
|
|
3474
|
+
mainPath = path.join(context.pluginPath, packageJson.main);
|
|
3475
|
+
}
|
|
3476
|
+
} catch {
|
|
3477
|
+
return true;
|
|
3478
|
+
}
|
|
3479
|
+
if (!mainPath || !fs.existsSync(mainPath))
|
|
3480
|
+
return true;
|
|
3481
|
+
try {
|
|
3482
|
+
const content = fs.readFileSync(mainPath, "utf-8");
|
|
3483
|
+
const patterns = [
|
|
3484
|
+
new RegExp(`export\\s*\\{[^}]*\\b${exportName}\\b[^}]*\\}`, "m"),
|
|
3485
|
+
new RegExp(`export\\s+(?:async\\s+)?function\\s+${exportName}\\b`),
|
|
3486
|
+
new RegExp(`export\\s+(?:const|let|var)\\s+${exportName}\\b`)
|
|
3487
|
+
];
|
|
3488
|
+
return patterns.some((p) => p.test(content));
|
|
3489
|
+
} catch {
|
|
3490
|
+
return true;
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
function isRelativePath(value) {
|
|
3494
|
+
return value.startsWith("./") || value.startsWith("../");
|
|
3495
|
+
}
|
|
3496
|
+
function validateHandlerRef(value, label, location, context, result) {
|
|
3497
|
+
if (context.isNpmPackage) {
|
|
3498
|
+
if (isRelativePath(value)) {
|
|
3499
|
+
result.errors.push({
|
|
3500
|
+
type: "export-mismatch",
|
|
3501
|
+
message: `${label} "${value}" is a relative path, but NPM plugins must use an export name`,
|
|
3502
|
+
location,
|
|
3503
|
+
suggestion: `Export the function from the package entry point and use the export name instead of a path`
|
|
3504
|
+
});
|
|
3505
|
+
} else if (!checkExportExists(value, context)) {
|
|
3506
|
+
result.errors.push({
|
|
3507
|
+
type: "export-mismatch",
|
|
3508
|
+
message: `${label} "${value}" is not exported from the package`,
|
|
3509
|
+
location,
|
|
3510
|
+
suggestion: `Add "export { ${value} } from '...'" to the package entry point`
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
} else if (isRelativePath(value)) {
|
|
3514
|
+
const handlerPath = path.join(context.pluginPath, value);
|
|
3515
|
+
const extensions = ["", ".ts", ".js", "/index.ts", "/index.js"];
|
|
3516
|
+
const found = extensions.some((ext) => fs.existsSync(handlerPath + ext));
|
|
3517
|
+
if (!found) {
|
|
3518
|
+
result.errors.push({
|
|
3519
|
+
type: "file-missing",
|
|
3520
|
+
message: `${label} not found: ${value}`,
|
|
3521
|
+
location,
|
|
3522
|
+
suggestion: `Create the handler at ${handlerPath}.ts`
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3401
3526
|
}
|
|
3402
3527
|
function resolveContractFile(contractSpec, context) {
|
|
3403
3528
|
if (context.isNpmPackage) {
|
|
@@ -4218,6 +4343,32 @@ const HEADLESS_SKIP_ATTRS = /* @__PURE__ */ new Set([
|
|
|
4218
4343
|
"jay-coordinate-base",
|
|
4219
4344
|
"jay-scope"
|
|
4220
4345
|
]);
|
|
4346
|
+
const PHASE_ORDER = {
|
|
4347
|
+
slow: 0,
|
|
4348
|
+
fast: 1,
|
|
4349
|
+
"fast+interactive": 2
|
|
4350
|
+
};
|
|
4351
|
+
function resolveBindingPhase(bindingPath, jayHtml) {
|
|
4352
|
+
const segments = bindingPath.split(".");
|
|
4353
|
+
const root = segments[0];
|
|
4354
|
+
const keyedImport = jayHtml.headlessImports.find((i) => i.key === root && i.contract);
|
|
4355
|
+
if (keyedImport?.contract) {
|
|
4356
|
+
const tagPath = segments.slice(1).join(".");
|
|
4357
|
+
if (!tagPath)
|
|
4358
|
+
return void 0;
|
|
4359
|
+
const tag = resolveContractTag(keyedImport.contract, tagPath);
|
|
4360
|
+
if (!tag)
|
|
4361
|
+
return void 0;
|
|
4362
|
+
return tag.phase || "slow";
|
|
4363
|
+
}
|
|
4364
|
+
if (jayHtml.contract) {
|
|
4365
|
+
const tag = resolveContractTag(jayHtml.contract, bindingPath);
|
|
4366
|
+
if (!tag)
|
|
4367
|
+
return void 0;
|
|
4368
|
+
return tag.phase || "slow";
|
|
4369
|
+
}
|
|
4370
|
+
return void 0;
|
|
4371
|
+
}
|
|
4221
4372
|
function checkHeadlessInstanceProps(jayHtml, file) {
|
|
4222
4373
|
const imports = jayHtml.headlessImports;
|
|
4223
4374
|
const warnings = [];
|
|
@@ -4236,25 +4387,53 @@ function checkHeadlessInstanceProps(jayHtml, file) {
|
|
|
4236
4387
|
}
|
|
4237
4388
|
}
|
|
4238
4389
|
if (passedProps.size > 0) {
|
|
4239
|
-
const
|
|
4390
|
+
const contractPropNamesLower = new Set(
|
|
4391
|
+
(contract.props || []).map((p) => p.name.toLowerCase())
|
|
4392
|
+
);
|
|
4240
4393
|
for (const prop of passedProps) {
|
|
4241
|
-
if (!
|
|
4242
|
-
imp.key ? `${imp.key} (${contractName})` : contractName;
|
|
4394
|
+
if (!contractPropNamesLower.has(prop.toLowerCase())) {
|
|
4243
4395
|
warnings.push(
|
|
4244
4396
|
`<jay:${contractName}> passes attribute "${prop}" but the "${contract.name}" contract does not declare it as a prop. Add to ${contractName}.jay-contract: props: [{ name: ${prop}, type: string }]`
|
|
4245
4397
|
);
|
|
4246
4398
|
}
|
|
4247
4399
|
}
|
|
4248
4400
|
}
|
|
4401
|
+
const passedPropsLower = new Set([...passedProps].map((p) => p.toLowerCase()));
|
|
4249
4402
|
if (contract.props) {
|
|
4250
4403
|
for (const contractProp of contract.props) {
|
|
4251
|
-
if (contractProp.required && !
|
|
4404
|
+
if (contractProp.required && !passedPropsLower.has(contractProp.name.toLowerCase())) {
|
|
4252
4405
|
warnings.push(
|
|
4253
4406
|
`<jay:${contractName}> is missing required prop "${contractProp.name}" declared in the "${contract.name}" contract.`
|
|
4254
4407
|
);
|
|
4255
4408
|
}
|
|
4256
4409
|
}
|
|
4257
4410
|
}
|
|
4411
|
+
if (contract.props) {
|
|
4412
|
+
const lowerAttrs = {};
|
|
4413
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
4414
|
+
lowerAttrs[k.toLowerCase()] = v;
|
|
4415
|
+
}
|
|
4416
|
+
for (const contractProp of contract.props) {
|
|
4417
|
+
const attrValue = lowerAttrs[contractProp.name.toLowerCase()];
|
|
4418
|
+
if (!attrValue)
|
|
4419
|
+
continue;
|
|
4420
|
+
const bindingMatch = attrValue.match(/^\{(.+)\}$/);
|
|
4421
|
+
if (!bindingMatch)
|
|
4422
|
+
continue;
|
|
4423
|
+
const bindingPath = bindingMatch[1];
|
|
4424
|
+
const sourcePhase = resolveBindingPhase(bindingPath, jayHtml);
|
|
4425
|
+
if (!sourcePhase)
|
|
4426
|
+
continue;
|
|
4427
|
+
const propPhase = contractProp.phase ?? "slow";
|
|
4428
|
+
const sourceOrder = PHASE_ORDER[sourcePhase] ?? 0;
|
|
4429
|
+
const propOrder = PHASE_ORDER[propPhase] ?? 0;
|
|
4430
|
+
if (sourceOrder > propOrder) {
|
|
4431
|
+
warnings.push(
|
|
4432
|
+
`<jay:${contractName}> prop "${contractProp.name}" (phase: ${propPhase}) is bound to {${bindingPath}} which is phase: ${sourcePhase}. The binding source phase must be ≤ the prop phase. Use a ${propPhase}-phase binding, a route param, or a literal value.`
|
|
4433
|
+
);
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4258
4437
|
}
|
|
4259
4438
|
}
|
|
4260
4439
|
for (const child of element.childNodes ?? []) {
|
|
@@ -4294,6 +4473,7 @@ async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
|
|
|
4294
4473
|
if (!plugin.manifest.validators)
|
|
4295
4474
|
continue;
|
|
4296
4475
|
for (const validatorDef of plugin.manifest.validators) {
|
|
4476
|
+
const source = `${plugin.name}/${validatorDef.name}`;
|
|
4297
4477
|
let validatorFn;
|
|
4298
4478
|
try {
|
|
4299
4479
|
let handlerModule;
|
|
@@ -4308,19 +4488,22 @@ async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
|
|
|
4308
4488
|
errors.push({
|
|
4309
4489
|
file: `plugin:${plugin.name}`,
|
|
4310
4490
|
message: `Validator "${validatorDef.name}" handler does not export a "validate" function`,
|
|
4311
|
-
stage: "plugin"
|
|
4491
|
+
stage: "plugin",
|
|
4492
|
+
source
|
|
4312
4493
|
});
|
|
4494
|
+
loadedValidators.push(source);
|
|
4313
4495
|
continue;
|
|
4314
4496
|
}
|
|
4315
4497
|
} catch (loadErr) {
|
|
4316
4498
|
errors.push({
|
|
4317
4499
|
file: `plugin:${plugin.name}`,
|
|
4318
4500
|
message: `Failed to load validator "${validatorDef.name}": ${loadErr.message}`,
|
|
4319
|
-
stage: "plugin"
|
|
4501
|
+
stage: "plugin",
|
|
4502
|
+
source
|
|
4320
4503
|
});
|
|
4504
|
+
loadedValidators.push(source);
|
|
4321
4505
|
continue;
|
|
4322
4506
|
}
|
|
4323
|
-
const source = `${plugin.name}/${validatorDef.name}`;
|
|
4324
4507
|
loadedValidators.push(source);
|
|
4325
4508
|
for (const { relativePath, parsed } of parsedFiles) {
|
|
4326
4509
|
const pageContractPath = parsed.contractRef ? path.resolve(
|
|
@@ -4660,12 +4843,12 @@ async function runValidatePlugin(pluginPath, options) {
|
|
|
4660
4843
|
strict: options.strict,
|
|
4661
4844
|
generateTypes: options.generateTypes
|
|
4662
4845
|
});
|
|
4663
|
-
|
|
4846
|
+
printPluginValidationResult(result, options.verbose ?? false);
|
|
4664
4847
|
if (!result.valid || options.strict && result.warnings.length > 0) {
|
|
4665
4848
|
process.exit(1);
|
|
4666
4849
|
}
|
|
4667
4850
|
}
|
|
4668
|
-
function
|
|
4851
|
+
function printPluginValidationResult(result, verbose) {
|
|
4669
4852
|
const logger = getLogger();
|
|
4670
4853
|
if (result.valid && result.warnings.length === 0) {
|
|
4671
4854
|
logger.important(chalk.green("Plugin validation successful!\n"));
|
|
@@ -5462,9 +5645,8 @@ program.command("rebuild").description("Rebuild instances by contract, route, or
|
|
|
5462
5645
|
});
|
|
5463
5646
|
program.command("cleanup").description("Delete orphaned files from previous rebuilds").option("--version <n>", "Build version (default: from package.json)").option("-p, --path <path>", "Project root (default: cwd)").action(async (options) => {
|
|
5464
5647
|
try {
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
const ctx = await resolveProductionContext2(options.path, options.version);
|
|
5648
|
+
initLogger();
|
|
5649
|
+
const ctx = await resolveProductionContext(options.path, options.version);
|
|
5468
5650
|
const { cleanupOrphanedFiles } = await import("@jay-framework/production-server");
|
|
5469
5651
|
await cleanupOrphanedFiles(ctx.buildRoot, ctx.version);
|
|
5470
5652
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/jay-stack-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,15 +24,15 @@
|
|
|
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/production-server": "^0.
|
|
35
|
-
"@jay-framework/stack-server-runtime": "^0.
|
|
27
|
+
"@jay-framework/compiler-jay-html": "^0.21.0",
|
|
28
|
+
"@jay-framework/compiler-shared": "^0.21.0",
|
|
29
|
+
"@jay-framework/dev-server": "^0.21.0",
|
|
30
|
+
"@jay-framework/editor-server": "^0.21.0",
|
|
31
|
+
"@jay-framework/fullstack-component": "^0.21.0",
|
|
32
|
+
"@jay-framework/logger": "^0.21.0",
|
|
33
|
+
"@jay-framework/plugin-validator": "^0.21.0",
|
|
34
|
+
"@jay-framework/production-server": "^0.21.0",
|
|
35
|
+
"@jay-framework/stack-server-runtime": "^0.21.0",
|
|
36
36
|
"chalk": "^4.1.2",
|
|
37
37
|
"commander": "^14.0.0",
|
|
38
38
|
"express": "^5.0.1",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"yaml": "^2.3.4"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@jay-framework/dev-environment": "^0.
|
|
46
|
+
"@jay-framework/dev-environment": "^0.21.0",
|
|
47
47
|
"@types/express": "^5.0.2",
|
|
48
48
|
"@types/node": "^22.15.21",
|
|
49
49
|
"nodemon": "^3.0.3",
|