@jay-framework/jay-stack-cli 0.16.3 → 0.16.5

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.
@@ -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>`)
@@ -271,6 +324,34 @@ my-plugin/
271
324
  └── my-plugin-extending.md # How to extend the plugin
272
325
  ```
273
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
+
274
355
  ## Reference Declarations
275
356
 
276
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],
@@ -4167,6 +4171,16 @@ async function validateJayFiles(options = {}) {
4167
4171
  } else if (options.verbose) {
4168
4172
  getLogger().info(chalk.green(`✓ ${relativePath}`));
4169
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
+ }
4170
4184
  } catch (error) {
4171
4185
  errors.push({
4172
4186
  file: relativePath,
@@ -4630,32 +4644,101 @@ program.command("validate-plugin [path]").description("Validate a Jay Stack plug
4630
4644
  });
4631
4645
  const ALL_ROLES = ["designer", "developer", "plugin"];
4632
4646
  async function ensureAgentKitDocs(projectRoot, _force, mode) {
4633
- const path2 = await import("node:path");
4634
- const fs2 = await import("node:fs/promises");
4635
- const { fileURLToPath } = await import("node:url");
4636
- const agentKitDir = path2.join(projectRoot, "agent-kit");
4637
- const thisDir = path2.dirname(fileURLToPath(import.meta.url));
4638
- 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");
4639
4650
  const roles = mode && ALL_ROLES.includes(mode) ? [mode] : ALL_ROLES;
4640
4651
  for (const role of roles) {
4641
- const roleTemplateDir = path2.join(templateDir, role);
4642
- const roleOutputDir = path2.join(agentKitDir, role);
4652
+ const roleTemplateDir = path$1.join(templateDir, role);
4653
+ const roleOutputDir = path$1.join(agentKitDir, role);
4643
4654
  let files;
4644
4655
  try {
4645
- files = (await fs2.readdir(roleTemplateDir)).filter((f) => f.endsWith(".md"));
4656
+ files = (await fs$1.readdir(roleTemplateDir)).filter((f) => f.endsWith(".md"));
4646
4657
  } catch {
4647
4658
  continue;
4648
4659
  }
4649
- await fs2.mkdir(roleOutputDir, { recursive: true });
4660
+ await fs$1.mkdir(roleOutputDir, { recursive: true });
4650
4661
  for (const filename of files) {
4651
- await fs2.copyFile(
4652
- path2.join(roleTemplateDir, filename),
4653
- path2.join(roleOutputDir, filename)
4662
+ await fs$1.copyFile(
4663
+ path$1.join(roleTemplateDir, filename),
4664
+ path$1.join(roleOutputDir, filename)
4654
4665
  );
4655
4666
  getLogger().info(chalk.gray(` Created agent-kit/${role}/${filename}`));
4656
4667
  }
4657
4668
  }
4658
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
+ }
4659
4742
  async function generatePluginReferences(projectRoot, options, initErrors, viteServer) {
4660
4743
  const { discoverPluginsWithReferences, executePluginReferences } = await import("@jay-framework/stack-server-runtime");
4661
4744
  const plugins = await discoverPluginsWithReferences({
@@ -4790,6 +4873,7 @@ program.command("agent-kit").description(
4790
4873
  try {
4791
4874
  if (!options.list) {
4792
4875
  await ensureAgentKitDocs(projectRoot, options.force, options.mode);
4876
+ await mergePluginAgentKitGuides(projectRoot, options.mode);
4793
4877
  if (options.references !== false) {
4794
4878
  await generatePluginReferences(projectRoot, options, initErrors, viteServer);
4795
4879
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/jay-stack-cli",
3
- "version": "0.16.3",
3
+ "version": "0.16.5",
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.3",
28
- "@jay-framework/compiler-shared": "^0.16.3",
29
- "@jay-framework/dev-server": "^0.16.3",
30
- "@jay-framework/editor-server": "^0.16.3",
31
- "@jay-framework/fullstack-component": "^0.16.3",
32
- "@jay-framework/logger": "^0.16.3",
33
- "@jay-framework/plugin-validator": "^0.16.3",
34
- "@jay-framework/stack-server-runtime": "^0.16.3",
27
+ "@jay-framework/compiler-jay-html": "^0.16.5",
28
+ "@jay-framework/compiler-shared": "^0.16.5",
29
+ "@jay-framework/dev-server": "^0.16.5",
30
+ "@jay-framework/editor-server": "^0.16.5",
31
+ "@jay-framework/fullstack-component": "^0.16.5",
32
+ "@jay-framework/logger": "^0.16.5",
33
+ "@jay-framework/plugin-validator": "^0.16.5",
34
+ "@jay-framework/stack-server-runtime": "^0.16.5",
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.3",
45
+ "@jay-framework/dev-environment": "^0.16.5",
46
46
  "@types/express": "^5.0.2",
47
47
  "@types/node": "^22.15.21",
48
48
  "nodemon": "^3.0.3",