@jay-framework/jay-stack-cli 0.16.2 → 0.16.4

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.
@@ -125,6 +125,16 @@ src/pages/products/[slug]/
125
125
 
126
126
  The jay-html template uses unprefixed bindings for page data and key-prefixed bindings for plugin data.
127
127
 
128
+ ## Note on `.withClientDefaults()`
129
+
130
+ `withClientDefaults` is only needed when a headless component is used **inside a `forEach`** and new items can be added on the client (e.g., "Add Item" button). It provides initial ViewState for instances that don't exist during SSR.
131
+
132
+ You do NOT need it for:
133
+
134
+ - Components outside forEach — `withFastRender` provides SSR initial state
135
+ - Components inside a conditional (`if=`) — server data is computed for all discovered instances regardless of the condition's SSR value
136
+ - Static forEach where all items come from the server
137
+
128
138
  ## Builder API Reference
129
139
 
130
140
  See the plugin [component-structure.md](../plugin/component-structure.md) for the full builder API: `.withProps()`, `.withServices()`, `.withContexts()`, phase rendering, and render results.
@@ -13,7 +13,8 @@ A plugin provides headless components (data + interactions, no UI) that project
13
13
  3. **Define actions** with `.jay-action` metadata
14
14
  4. **Optionally add routes** — pages for admin tools and dashboards
15
15
  5. **Set up `plugin.yaml`** — list contracts, actions, services, contexts, routes
16
- 6. **Validate** with `jay-stack validate-plugin`
16
+ 6. **Configure build** dual entry points (server + client), vite.config.ts, package.json exports
17
+ 7. **Validate** with `jay-stack validate-plugin`
17
18
 
18
19
  ## Guides
19
20
 
@@ -96,17 +96,20 @@ Runs on each request. Receives props (including `query` for query parameters) an
96
96
  })
97
97
  ```
98
98
 
99
- ### `.withClientDefaults(fn)` — Default client ViewState
99
+ ### `.withClientDefaults(fn)` — Defaults for dynamically created forEach items
100
100
 
101
- Provides default values for client-side ViewState before hydration:
101
+ Required only when the component is used inside a `forEach` where new items can be added on the client. When a user adds a new item to a forEach array, the new instance has no server data — `withClientDefaults` provides the initial ViewState.
102
102
 
103
103
  ```typescript
104
- .withClientDefaults(() => ({
105
- quantity: 1,
106
- selectedVariant: 'default',
104
+ .withClientDefaults((props) => ({
105
+ viewState: { label: `Item ${props.itemId}`, value: 0 },
106
+ carryForward: {},
107
107
  }))
108
108
  ```
109
109
 
110
+ **When to use:** Component inside `forEach` + items can be added client-side.
111
+ **When NOT to use:** Components outside forEach, or forEach with server-only items. Use `withFastRender` instead for SSR initial state.
112
+
110
113
  ### `.withInteractive(ComponentConstructor)` — Client-side logic
111
114
 
112
115
  The interactive phase runs in the browser. Use hooks here (see component-state.md):
@@ -83,7 +83,15 @@ Linked (reference another contract file):
83
83
  ```yaml
84
84
  - tag: author
85
85
  type: sub-contract
86
- link: ./author.jay-contract
86
+ link: ./author.jay-contract # relative path (same package)
87
+ ```
88
+
89
+ For dynamic/materialized contracts linking to static contracts in a plugin package, use the package path:
90
+
91
+ ```yaml
92
+ - tag: gallery
93
+ type: sub-contract
94
+ link: '@my-org/my-plugin/media-gallery' # package path (cross-directory)
87
95
  ```
88
96
 
89
97
  ### `sub-contract` with `repeated: true` — Arrays
@@ -19,6 +19,16 @@ contracts:
19
19
  component: productSearch
20
20
  description: Product listing with filters and pagination
21
21
 
22
+ dynamic_contracts:
23
+ # Single contract: prefix used as the contract name directly
24
+ - prefix: product-page
25
+ component: productPage
26
+ generator: productPageContractGenerator
27
+ # Multiple contracts: prefix/name format (e.g., list/recipes, list/articles)
28
+ - prefix: list
29
+ component: dynamicList
30
+ generator: listContractGenerator
31
+
22
32
  actions:
23
33
  - name: searchProducts
24
34
  action: search-products.jay-action
@@ -56,6 +66,49 @@ setup:
56
66
  - `component` — Export name of the component (e.g., `productPage`)
57
67
  - `description` — What this component does and when to use it
58
68
 
69
+ ### Dynamic Contract Entry Fields
70
+
71
+ Dynamic contracts are generated at setup time from site-specific data (e.g., CMS collection schemas, extended product fields).
72
+
73
+ - `prefix` — Identifier for this dynamic contract group. Used as the contract name for single contracts, or as `prefix/name` for multiple.
74
+ - `component` — Export name of the headless component that serves these contracts
75
+ - `generator` — Export name of the generator function that produces contract YAML
76
+
77
+ **Single contract** — generator returns one `{ yaml }` without a name:
78
+
79
+ ```yaml
80
+ dynamic_contracts:
81
+ - prefix: product-page
82
+ component: productPage
83
+ generator: productPageContractGenerator
84
+ ```
85
+
86
+ Referenced as `contract="product-page"` in jay-html.
87
+
88
+ **Multiple contracts** — generator yields `{ name, yaml }` for each:
89
+
90
+ ```yaml
91
+ dynamic_contracts:
92
+ - prefix: list
93
+ component: dynamicList
94
+ generator: listContractGenerator
95
+ ```
96
+
97
+ Referenced as `contract="list/recipes"`, `contract="list/articles"` etc.
98
+
99
+ Contracts are materialized by `jay-stack agent-kit` or `jay-stack setup` and stored in `agent-kit/materialized-contracts/`.
100
+
101
+ **Linking to static contracts from generated YAML** — materialized contracts live in a different directory than the plugin source. Use the plugin's package path (not relative paths) for `link:` references to static contracts:
102
+
103
+ ```yaml
104
+ # In the generated contract YAML:
105
+ tags:
106
+ - tag: gallery
107
+ type: sub-contract
108
+ link: '@my-org/my-plugin/media-gallery' # package path — works from any directory
109
+ # NOT: link: ./media-gallery # relative path — breaks in materialized location
110
+ ```
111
+
59
112
  ### Action Entry Fields
60
113
 
61
114
  - `name` — Action name (used with `jay-stack action <plugin>/<action>`)
@@ -161,22 +214,101 @@ my-project/
161
214
 
162
215
  See `examples/jay-stack/fake-shop` for a working example.
163
216
 
217
+ ## Dual Entry Points
218
+
219
+ Jay plugins are fullstack — they run on both server and client. The build produces two bundles:
220
+
221
+ - **Server** (`dist/index.js`) — actions, services, SSR rendering, `init()`. Built with `vite build --ssr`.
222
+ - **Client** (`dist/index.client.js`) — components for hydration, context tokens, `init()`. Built with `vite build`.
223
+
224
+ Create two entry files:
225
+
226
+ | File | Exports |
227
+ | --------------------- | ---------------------------------------------------------- |
228
+ | `lib/index.ts` | Actions, services, components (SSR), init, service markers |
229
+ | `lib/index.client.ts` | Components (hydration), context markers, init |
230
+
231
+ Actions and service providers are server-only. Components appear in **both** entries.
232
+
233
+ ## Build Scripts
234
+
235
+ ```json
236
+ {
237
+ "scripts": {
238
+ "build": "npm run clean && npm run definitions && npm run build:client && npm run build:server && npm run build:copy-assets && npm run build:types && npm run validate",
239
+ "definitions": "jay-cli definitions lib",
240
+ "build:client": "vite build",
241
+ "build:server": "vite build --ssr",
242
+ "build:copy-assets": "cp lib/*.jay-contract* dist/",
243
+ "build:types": "tsup lib/index.ts lib/index.client.ts --dts-only --format esm",
244
+ "validate": "jay-stack-cli validate-plugin",
245
+ "clean": "rimraf dist"
246
+ }
247
+ }
248
+ ```
249
+
250
+ The `vite.config.ts` uses `isSsrBuild` to switch entry points:
251
+
252
+ ```typescript
253
+ import { resolve } from 'path';
254
+ import { defineConfig } from 'vite';
255
+ import { jayStackCompiler } from '@jay-framework/compiler-jay-stack';
256
+
257
+ const jayOptions = { tsConfigFilePath: resolve(__dirname, 'tsconfig.json'), outputDir: 'build' };
258
+
259
+ export default defineConfig(({ isSsrBuild }) => ({
260
+ plugins: [...jayStackCompiler(jayOptions)],
261
+ build: {
262
+ minify: false,
263
+ ssr: isSsrBuild,
264
+ emptyOutDir: false,
265
+ lib: {
266
+ entry: isSsrBuild
267
+ ? { index: resolve(__dirname, 'lib/index.ts') }
268
+ : { 'index.client': resolve(__dirname, 'lib/index.client.ts') },
269
+ formats: ['es'],
270
+ },
271
+ rollupOptions: {
272
+ external: [
273
+ '@jay-framework/component',
274
+ '@jay-framework/fullstack-component',
275
+ '@jay-framework/stack-client-runtime',
276
+ '@jay-framework/stack-server-runtime',
277
+ '@jay-framework/reactive',
278
+ '@jay-framework/runtime',
279
+ ],
280
+ },
281
+ },
282
+ }));
283
+ ```
284
+
164
285
  ## package.json Exports
165
286
 
166
- For NPM packages, declare exports so the framework can resolve the plugin:
287
+ For NPM packages, declare exports for both server and client entry points:
167
288
 
168
289
  ```json
169
290
  {
170
291
  "name": "@my-org/my-plugin",
171
292
  "type": "module",
293
+ "main": "dist/index.js",
172
294
  "exports": {
173
- ".": "./dist/index.js",
174
- "./plugin.yaml": "./plugin.yaml"
295
+ ".": {
296
+ "types": "./dist/index.d.ts",
297
+ "default": "./dist/index.js"
298
+ },
299
+ "./client": {
300
+ "types": "./dist/index.client.d.ts",
301
+ "default": "./dist/index.client.js"
302
+ },
303
+ "./plugin.yaml": "./plugin.yaml",
304
+ "./my-contract.jay-contract": "./dist/my-contract.jay-contract"
175
305
  },
176
- "files": ["dist", "plugin.yaml", "lib/contracts", "lib/actions"]
306
+ "files": ["dist", "plugin.yaml"]
177
307
  }
178
308
  ```
179
309
 
310
+ The `./client` export is required — the framework uses it for browser-side hydration code. The `.` export handles server-side rendering and action execution.
311
+
180
312
  ## Plugin-Contributed Agent-Kit Guides
181
313
 
182
314
  A plugin can include guides that are merged into the project's agent-kit during `jay-stack agent-kit`. Create an `agent-kit/` folder with subfolders for each role:
@@ -192,6 +324,34 @@ my-plugin/
192
324
  └── my-plugin-extending.md # How to extend the plugin
193
325
  ```
194
326
 
327
+ For NPM packages, include `agent-kit` in the `files` array:
328
+
329
+ ```json
330
+ {
331
+ "files": ["dist", "plugin.yaml", "agent-kit"]
332
+ }
333
+ ```
334
+
335
+ No `plugin.yaml` declaration needed — the CLI discovers guides by scanning the `agent-kit/` directory. Files are copied as-is into the project's `agent-kit/{role}/`.
336
+
337
+ **File format convention:** The first line after the `#` heading is used as the description in the INSTRUCTIONS.md index table. Write it as a short sentence explaining when to use this guide:
338
+
339
+ ```markdown
340
+ # Scroll Carousel
341
+
342
+ Horizontal slider with prev/next buttons and edge detection. Headless component — requires import.
343
+
344
+ ## Import
345
+
346
+ ...
347
+ ```
348
+
349
+ The INSTRUCTIONS.md table will show:
350
+
351
+ ```
352
+ | scroll-carousel.md | my-plugin | Horizontal slider with prev/next buttons and edge detection. Headless component — requires import. |
353
+ ```
354
+
195
355
  ## Reference Declarations
196
356
 
197
357
  Plugins can declare reference data generated by `jay-stack agent-kit`:
package/dist/index.js CHANGED
@@ -8,15 +8,19 @@ import path from "path";
8
8
  import fs, { promises } from "fs";
9
9
  import YAML from "yaml";
10
10
  import { getLogger, createDevLogger, setDevLogger } from "@jay-framework/logger";
11
- import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, ContractTagType, parseContract, generateElementFile, htmlElementTagNameMap } from "@jay-framework/compiler-jay-html";
11
+ import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, ContractTagType, parseContract, generateElementFile, generateServerElementFile, htmlElementTagNameMap } from "@jay-framework/compiler-jay-html";
12
12
  import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, resolvePluginManifest, LOCAL_PLUGIN_PATH, JayAtomicType, JayEnumType, loadPluginManifest, RuntimeMode, GenerateTarget } from "@jay-framework/compiler-shared";
13
- import { listContracts, materializeContracts } from "@jay-framework/stack-server-runtime";
13
+ import { scanPlugins as scanPlugins$1, listContracts, materializeContracts } from "@jay-framework/stack-server-runtime";
14
14
  import { listContracts as listContracts2, materializeContracts as materializeContracts2 } from "@jay-framework/stack-server-runtime";
15
15
  import { Command } from "commander";
16
16
  import chalk from "chalk";
17
17
  import { createRequire } from "module";
18
18
  import { glob } from "glob";
19
19
  import { parse } from "node-html-parser";
20
+ import path$1 from "node:path";
21
+ import fs$1 from "node:fs/promises";
22
+ import fsSync from "node:fs";
23
+ import { fileURLToPath } from "node:url";
20
24
  const DEFAULT_CONFIG = {
21
25
  devServer: {
22
26
  portRange: [3e3, 3100],
@@ -3452,6 +3456,14 @@ async function validatePackageJson(context, result) {
3452
3456
  suggestion: 'Add "." export for the main module entry'
3453
3457
  });
3454
3458
  }
3459
+ if (!packageJson.exports["./client"]) {
3460
+ result.warnings.push({
3461
+ type: "export-mismatch",
3462
+ message: 'package.json exports missing "./client" entry point',
3463
+ location: packageJsonPath,
3464
+ suggestion: 'Add "./client": "./dist/index.client.js" to exports. The client bundle provides components for hydration and client-side contexts. Build with: vite build (client) + vite build --ssr (server)'
3465
+ });
3466
+ }
3455
3467
  if (context.manifest.contracts) {
3456
3468
  for (const contract of context.manifest.contracts) {
3457
3469
  const contractExport = "./" + contract.contract;
@@ -4159,6 +4171,16 @@ async function validateJayFiles(options = {}) {
4159
4171
  } else if (options.verbose) {
4160
4172
  getLogger().info(chalk.green(`✓ ${relativePath}`));
4161
4173
  }
4174
+ const serverElementFile = generateServerElementFile(parsedFile.val);
4175
+ if (serverElementFile.validations.length > 0) {
4176
+ for (const validation of serverElementFile.validations) {
4177
+ errors.push({
4178
+ file: relativePath,
4179
+ message: `[SSR] ${validation}`,
4180
+ stage: "generate"
4181
+ });
4182
+ }
4183
+ }
4162
4184
  } catch (error) {
4163
4185
  errors.push({
4164
4186
  file: relativePath,
@@ -4622,32 +4644,101 @@ program.command("validate-plugin [path]").description("Validate a Jay Stack plug
4622
4644
  });
4623
4645
  const ALL_ROLES = ["designer", "developer", "plugin"];
4624
4646
  async function ensureAgentKitDocs(projectRoot, _force, mode) {
4625
- const path2 = await import("node:path");
4626
- const fs2 = await import("node:fs/promises");
4627
- const { fileURLToPath } = await import("node:url");
4628
- const agentKitDir = path2.join(projectRoot, "agent-kit");
4629
- const thisDir = path2.dirname(fileURLToPath(import.meta.url));
4630
- const templateDir = path2.resolve(thisDir, "..", "agent-kit-template");
4647
+ const agentKitDir = path$1.join(projectRoot, "agent-kit");
4648
+ const thisDir = path$1.dirname(fileURLToPath(import.meta.url));
4649
+ const templateDir = path$1.resolve(thisDir, "..", "agent-kit-template");
4631
4650
  const roles = mode && ALL_ROLES.includes(mode) ? [mode] : ALL_ROLES;
4632
4651
  for (const role of roles) {
4633
- const roleTemplateDir = path2.join(templateDir, role);
4634
- const roleOutputDir = path2.join(agentKitDir, role);
4652
+ const roleTemplateDir = path$1.join(templateDir, role);
4653
+ const roleOutputDir = path$1.join(agentKitDir, role);
4635
4654
  let files;
4636
4655
  try {
4637
- files = (await fs2.readdir(roleTemplateDir)).filter((f) => f.endsWith(".md"));
4656
+ files = (await fs$1.readdir(roleTemplateDir)).filter((f) => f.endsWith(".md"));
4638
4657
  } catch {
4639
4658
  continue;
4640
4659
  }
4641
- await fs2.mkdir(roleOutputDir, { recursive: true });
4660
+ await fs$1.mkdir(roleOutputDir, { recursive: true });
4642
4661
  for (const filename of files) {
4643
- await fs2.copyFile(
4644
- path2.join(roleTemplateDir, filename),
4645
- path2.join(roleOutputDir, filename)
4662
+ await fs$1.copyFile(
4663
+ path$1.join(roleTemplateDir, filename),
4664
+ path$1.join(roleOutputDir, filename)
4646
4665
  );
4647
4666
  getLogger().info(chalk.gray(` Created agent-kit/${role}/${filename}`));
4648
4667
  }
4649
4668
  }
4650
4669
  }
4670
+ async function mergePluginAgentKitGuides(projectRoot, mode) {
4671
+ const plugins = await scanPlugins$1({ projectRoot });
4672
+ const agentKitDir = path$1.join(projectRoot, "agent-kit");
4673
+ const roles = mode && ALL_ROLES.includes(mode) ? [mode] : ALL_ROLES;
4674
+ const copiedPerRole = /* @__PURE__ */ new Map();
4675
+ for (const [, plugin] of plugins) {
4676
+ const pluginAgentKitDir = path$1.join(plugin.pluginPath, "agent-kit");
4677
+ if (!fsSync.existsSync(pluginAgentKitDir))
4678
+ continue;
4679
+ for (const role of roles) {
4680
+ const roleSourceDir = path$1.join(pluginAgentKitDir, role);
4681
+ let files;
4682
+ try {
4683
+ files = (await fs$1.readdir(roleSourceDir)).filter(
4684
+ (f) => f.endsWith(".md") && f !== "INSTRUCTIONS.md"
4685
+ );
4686
+ } catch {
4687
+ continue;
4688
+ }
4689
+ if (files.length === 0)
4690
+ continue;
4691
+ const roleOutputDir = path$1.join(agentKitDir, role);
4692
+ await fs$1.mkdir(roleOutputDir, { recursive: true });
4693
+ for (const filename of files) {
4694
+ const sourcePath = path$1.join(roleSourceDir, filename);
4695
+ await fs$1.copyFile(sourcePath, path$1.join(roleOutputDir, filename));
4696
+ let description = "";
4697
+ try {
4698
+ const content = await fs$1.readFile(sourcePath, "utf-8");
4699
+ const lines = content.split("\n");
4700
+ let pastHeading = false;
4701
+ for (const line of lines) {
4702
+ if (line.startsWith("# ")) {
4703
+ pastHeading = true;
4704
+ continue;
4705
+ }
4706
+ if (pastHeading && line.trim()) {
4707
+ description = line.trim();
4708
+ break;
4709
+ }
4710
+ }
4711
+ } catch {
4712
+ }
4713
+ if (!copiedPerRole.has(role))
4714
+ copiedPerRole.set(role, []);
4715
+ copiedPerRole.get(role).push({ filename, pluginName: plugin.name, description });
4716
+ getLogger().info(
4717
+ chalk.gray(
4718
+ ` Copied agent-kit/${role}/${filename} from plugin "${plugin.name}"`
4719
+ )
4720
+ );
4721
+ }
4722
+ }
4723
+ }
4724
+ for (const [role, entries] of copiedPerRole) {
4725
+ const instructionsPath = path$1.join(agentKitDir, role, "INSTRUCTIONS.md");
4726
+ if (!fsSync.existsSync(instructionsPath))
4727
+ continue;
4728
+ const lines = [
4729
+ "",
4730
+ "## Plugin-Contributed Guides",
4731
+ "",
4732
+ "| File | Plugin | Description |",
4733
+ "| --- | --- | --- |"
4734
+ ];
4735
+ for (const { filename, pluginName, description } of entries) {
4736
+ lines.push(`| [${filename}](${filename}) | ${pluginName} | ${description} |`);
4737
+ }
4738
+ lines.push("");
4739
+ await fs$1.appendFile(instructionsPath, lines.join("\n"));
4740
+ }
4741
+ }
4651
4742
  async function generatePluginReferences(projectRoot, options, initErrors, viteServer) {
4652
4743
  const { discoverPluginsWithReferences, executePluginReferences } = await import("@jay-framework/stack-server-runtime");
4653
4744
  const plugins = await discoverPluginsWithReferences({
@@ -4782,6 +4873,7 @@ program.command("agent-kit").description(
4782
4873
  try {
4783
4874
  if (!options.list) {
4784
4875
  await ensureAgentKitDocs(projectRoot, options.force, options.mode);
4876
+ await mergePluginAgentKitGuides(projectRoot, options.mode);
4785
4877
  if (options.references !== false) {
4786
4878
  await generatePluginReferences(projectRoot, options, initErrors, viteServer);
4787
4879
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/jay-stack-cli",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
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.16.2",
28
- "@jay-framework/compiler-shared": "^0.16.2",
29
- "@jay-framework/dev-server": "^0.16.2",
30
- "@jay-framework/editor-server": "^0.16.2",
31
- "@jay-framework/fullstack-component": "^0.16.2",
32
- "@jay-framework/logger": "^0.16.2",
33
- "@jay-framework/plugin-validator": "^0.16.2",
34
- "@jay-framework/stack-server-runtime": "^0.16.2",
27
+ "@jay-framework/compiler-jay-html": "^0.16.4",
28
+ "@jay-framework/compiler-shared": "^0.16.4",
29
+ "@jay-framework/dev-server": "^0.16.4",
30
+ "@jay-framework/editor-server": "^0.16.4",
31
+ "@jay-framework/fullstack-component": "^0.16.4",
32
+ "@jay-framework/logger": "^0.16.4",
33
+ "@jay-framework/plugin-validator": "^0.16.4",
34
+ "@jay-framework/stack-server-runtime": "^0.16.4",
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.16.2",
45
+ "@jay-framework/dev-environment": "^0.16.4",
46
46
  "@types/express": "^5.0.2",
47
47
  "@types/node": "^22.15.21",
48
48
  "nodemon": "^3.0.3",