@jay-framework/jay-stack-cli 0.19.8 → 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.
@@ -11,7 +11,7 @@ Jay Stack is a full-stack framework where:
11
11
  - **jay-html** templates provide the UI that binds to contract data
12
12
  - **Rendering phases** determine when data is available (build-time, request-time, client-side)
13
13
 
14
- Your job is to create `.jay-html` pages that bind to the data and interactions defined by contracts.
14
+ Your job is to create `.jay-html` pages that bind to the data and interactions defined by contracts. **All visible UI structure belongs in jay-html** — not in `page.ts` via `document.createElement`. Behavior and state live in `page.ts`; elements are reached through **refs** declared in the template.
15
15
 
16
16
  ## Rendering Phases
17
17
 
@@ -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.
@@ -119,3 +119,35 @@ A tag can be both data and interactive:
119
119
  ```
120
120
 
121
121
  This generates both a ViewState field and a ref.
122
+
123
+ ## DOM access rules (Jay Stack pages)
124
+
125
+ Refs are the **only supported path** from TypeScript to elements Jay renders. Direct `document` access bypasses the framework and can break rendering, updates, and performance.
126
+
127
+ ### Do
128
+
129
+ - Declare elements in **jay-html** with `ref="..."`.
130
+ - Drive overlays, lists, and visibility with **ViewState** (`if`, `forEach`, signal-backed bindings).
131
+ - Attach handlers with ref APIs: `refs.myRef.onclick`, `oninput`, `onkeydown`, etc.
132
+ - Use `refs.myRef.exec$((element, viewState) => …)` **inside handlers** for focus, scroll, measure, or native APIs.
133
+ - For drags: `setPointerCapture` on the ref element that received `pointerdown`, then listen on that element.
134
+
135
+ ### Avoid
136
+
137
+ - `document.querySelector` / `getElementById` to find template elements
138
+ - `document.createElement` + `appendChild` for UI that belongs in jay-html
139
+ - `document.addEventListener('mousemove'|'mouseup')` for drags (use pointer capture instead)
140
+
141
+ ### Rare `document` exceptions
142
+
143
+ Use only when no ref can exist, with an inline comment:
144
+
145
+ | Case | Example |
146
+ | ---------------------- | ----------------------------------------------------- |
147
+ | Offscreen processing | `document.createElement('canvas')` for image export |
148
+ | Coordinate hit-testing | `document.elementFromPoint` during cross-overlay drag |
149
+ | Tests | `document.dispatchEvent` in Vitest |
150
+
151
+ Global shortcuts or paste: prefer a root shell ref (`ref="appRoot"`) with capture listeners.
152
+
153
+ See also: `.cursor/skills/jay-dom-refs/SKILL.md` in the jay monorepo.
@@ -81,6 +81,12 @@ export const page = makeJayStackComponent<ProductPageContract>()
81
81
  });
82
82
  ```
83
83
 
84
+ ## DOM access in interactive phase
85
+
86
+ Reach rendered elements through **refs** only. Put overlays, panels, and lists in **jay-html**; drive them with ViewState from `.withInteractive`. Do not use `document.querySelector`, imperative `createElement` UI, or document-level drag listeners.
87
+
88
+ See [component-refs.md](component-refs.md#dom-access-rules-jay-stack-pages).
89
+
84
90
  ## Calling File Upload Actions
85
91
 
86
92
  Actions created with `.withFiles()` accept browser `File` objects directly. Use `oninput` events on file inputs to drive signals, then pass them to the action:
@@ -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. **Set up `plugin.yaml`**list contracts, actions, services, contexts, routes
16
- 6. **Configure build** — dual entry points (server + client), vite.config.ts, package.json exports
17
- 7. **Validate** with `jay-stack validate-plugin`
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
 
@@ -119,3 +119,35 @@ A tag can be both data and interactive:
119
119
  ```
120
120
 
121
121
  This generates both a ViewState field and a ref.
122
+
123
+ ## DOM access rules (Jay Stack pages)
124
+
125
+ Refs are the **only supported path** from TypeScript to elements Jay renders. Direct `document` access bypasses the framework and can break rendering, updates, and performance.
126
+
127
+ ### Do
128
+
129
+ - Declare elements in **jay-html** with `ref="..."`.
130
+ - Drive overlays, lists, and visibility with **ViewState** (`if`, `forEach`, signal-backed bindings).
131
+ - Attach handlers with ref APIs: `refs.myRef.onclick`, `oninput`, `onkeydown`, etc.
132
+ - Use `refs.myRef.exec$((element, viewState) => …)` **inside handlers** for focus, scroll, measure, or native APIs.
133
+ - For drags: `setPointerCapture` on the ref element that received `pointerdown`, then listen on that element.
134
+
135
+ ### Avoid
136
+
137
+ - `document.querySelector` / `getElementById` to find template elements
138
+ - `document.createElement` + `appendChild` for UI that belongs in jay-html
139
+ - `document.addEventListener('mousemove'|'mouseup')` for drags (use pointer capture instead)
140
+
141
+ ### Rare `document` exceptions
142
+
143
+ Use only when no ref can exist, with an inline comment:
144
+
145
+ | Case | Example |
146
+ | ---------------------- | ----------------------------------------------------- |
147
+ | Offscreen processing | `document.createElement('canvas')` for image export |
148
+ | Coordinate hit-testing | `document.elementFromPoint` during cross-overlay drag |
149
+ | Tests | `document.dispatchEvent` in Vitest |
150
+
151
+ Global shortcuts or paste: prefer a root shell ref (`ref="appRoot"`) with capture listeners.
152
+
153
+ See also: `.cursor/skills/jay-dom-refs/SKILL.md` in the jay monorepo.
@@ -63,7 +63,7 @@ commands:
63
63
 
64
64
  validators:
65
65
  - name: media-optimization
66
- handler: ./validators/media-validator
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` — Relative path to the validator module (must export a `validate` function)
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
- Validators run during `jay-stack validate` against every parsed jay-html file. The handler module exports a `validate` function that receives a `JayHtmlValidationContext` and returns an array of findings. See [validation.md](validation.md) for implementation details.
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` — Setup handler for `jay-stack setup` (handles config, credentials)
204
- - `references` — Reference generator for `jay-stack agent-kit` (generates discovery data)
205
- - `configTemplate` — Config file templates to copy during setup
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`. Declare validators in `plugin.yaml`:
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: ./validators/media-validator
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
- The handler module exports a `validate` function:
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 && !context.isNpmPackage) {
3386
- const handlerPath = path.join(context.pluginPath, validator.handler);
3387
- const extensions = ["", ".ts", ".js", "/index.ts", "/index.js"];
3388
- const found = extensions.some((ext) => fs.existsSync(handlerPath + ext));
3389
- if (!found) {
3390
- result.errors.push({
3391
- type: "file-missing",
3392
- message: `Validator "${validator.name}" handler not found: ${validator.handler}`,
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 contractPropNames = new Set((contract.props || []).map((p) => p.name));
4390
+ const contractPropNamesLower = new Set(
4391
+ (contract.props || []).map((p) => p.name.toLowerCase())
4392
+ );
4240
4393
  for (const prop of passedProps) {
4241
- if (!contractPropNames.has(prop)) {
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 && !passedProps.has(contractProp.name)) {
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
- printValidationResult(result, options.verbose ?? false);
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 printValidationResult(result, verbose) {
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
- const { initLogger: initLogger2, resolveProductionContext: resolveProductionContext2 } = await Promise.resolve().then(() => runProduction);
5466
- initLogger2();
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.19.8",
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.19.8",
28
- "@jay-framework/compiler-shared": "^0.19.8",
29
- "@jay-framework/dev-server": "^0.19.8",
30
- "@jay-framework/editor-server": "^0.19.8",
31
- "@jay-framework/fullstack-component": "^0.19.8",
32
- "@jay-framework/logger": "^0.19.8",
33
- "@jay-framework/plugin-validator": "^0.19.8",
34
- "@jay-framework/production-server": "^0.19.8",
35
- "@jay-framework/stack-server-runtime": "^0.19.8",
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.19.8",
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",