@jay-framework/jay-stack-cli 0.24.5 → 0.24.7

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.
Files changed (26) hide show
  1. package/agent-kit-template/INSTRUCTIONS.md +24 -1
  2. package/agent-kit-template/contracts/GUIDE.md +1 -1
  3. package/agent-kit-template/contracts/component-contracts.md +42 -12
  4. package/agent-kit-template/designer/INSTRUCTIONS.md +23 -20
  5. package/agent-kit-template/designer/cli-commands.md +46 -0
  6. package/agent-kit-template/designer/contracts-and-plugins.md +30 -1
  7. package/agent-kit-template/designer/design-system-guide.md +535 -0
  8. package/agent-kit-template/designer/jay-html-components.md +101 -31
  9. package/agent-kit-template/designer/jay-html-styling.md +69 -0
  10. package/agent-kit-template/designer/jay-html-syntax.md +22 -13
  11. package/agent-kit-template/designer/jay-html-template-syntax.md +3 -3
  12. package/agent-kit-template/designer/project-structure.md +1 -1
  13. package/agent-kit-template/designer/routing.md +2 -2
  14. package/agent-kit-template/designer/validation-guide.md +31 -5
  15. package/agent-kit-template/developer/INSTRUCTIONS.md +24 -19
  16. package/agent-kit-template/developer/page-components.md +23 -2
  17. package/agent-kit-template/developer/project-structure.md +1 -1
  18. package/agent-kit-template/developer/routing.md +1 -1
  19. package/agent-kit-template/devops/production-build.md +1 -1
  20. package/agent-kit-template/plugin/INSTRUCTIONS.md +3 -1
  21. package/agent-kit-template/plugin/component-structure.md +1 -1
  22. package/agent-kit-template/plugin/contracts-guide.md +18 -0
  23. package/agent-kit-template/plugin/design-system-guide.md +173 -0
  24. package/dist/{index-B9vCxAZ1.js → index-l6QVpvnY.js} +19 -14
  25. package/dist/index.js +1903 -199
  26. package/package.json +13 -12
package/dist/index.js CHANGED
@@ -7,22 +7,24 @@ import path$1 from "path";
7
7
  import fs$1, { promises } from "fs";
8
8
  import YAML from "yaml";
9
9
  import { getLogger, setDevLogger, createDevLogger } from "@jay-framework/logger";
10
- import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, parseAction, parseContract, generateElementFile, generateServerElementFile, htmlElementTagNameMap, loadLinkedContract, getLinkedContractDir } from "@jay-framework/compiler-jay-html";
10
+ import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, parseContract, compileContract, parseAction, generateElementFile, generateServerElementFile, htmlElementTagNameMap, loadLinkedContract, getLinkedContractDir } from "@jay-framework/compiler-jay-html";
11
+ import { checkValidationErrors, JAY_CONTRACT_EXTENSION as JAY_CONTRACT_EXTENSION$1, loadPluginManifest as loadPluginManifest$1, normalizeActionEntry as normalizeActionEntry$1, JAY_EXTENSION as JAY_EXTENSION$1, RuntimeMode as RuntimeMode$1, GenerateTarget, findDynamicContract, isEnumType as isEnumType$1, equalJayTypes, prettifyHtml } from "@jay-framework/compiler-shared";
11
12
  import fs$2 from "node:fs/promises";
12
13
  import * as path from "node:path";
13
14
  import path__default from "node:path";
15
+ import { scanPlugins, sortPluginsByDependencies, getServiceRegistry, actionRegistry, resolveServices, ActionRegistry, discoverPluginsWithInit, executePluginServerInits, runInitCallbacks, registerService } from "@jay-framework/stack-server-runtime";
14
16
  import { createRequire } from "module";
15
17
  import jsBeautify from "js-beautify";
16
- import { scanPlugins, sortPluginsByDependencies, getServiceRegistry, actionRegistry, resolveServices, ActionRegistry, discoverPluginsWithInit, executePluginServerInits, runInitCallbacks, registerService } from "@jay-framework/stack-server-runtime";
17
18
  import "node:crypto";
18
19
  import * as fs from "node:fs";
19
20
  import fs__default from "node:fs";
20
21
  import { createRequire as createRequire$1 } from "node:module";
21
22
  import { isJayAction, isJayStreamAction, isJayCliCommand, CONSOLE_CONTEXT } from "@jay-framework/fullstack-component";
23
+ import { parse, NodeType, HTMLElement } from "node-html-parser";
22
24
  import { Command } from "commander";
23
25
  import chalk from "chalk";
24
- import { loadPluginManifest as loadPluginManifest$1, normalizeActionEntry as normalizeActionEntry$1, JAY_EXTENSION, RuntimeMode as RuntimeMode$1, GenerateTarget, JAY_CONTRACT_EXTENSION, findDynamicContract } from "@jay-framework/compiler-shared";
25
26
  import { glob } from "glob";
27
+ import postcss from "postcss";
26
28
  import { fileURLToPath } from "node:url";
27
29
  import { select, confirm, input } from "@inquirer/prompts";
28
30
  const DEFAULT_CONFIG = {
@@ -88,12 +90,13 @@ function updateConfig(updates) {
88
90
  getLogger().warn(`Failed to update .jay config file: ${error}`);
89
91
  }
90
92
  }
91
- async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
92
- for (const route of routes) {
93
- const jayHtmlPath = route.fsRoute.jayHtmlPath;
93
+ async function generatePageDefinitionFiles(jayHtmlPaths, tsConfigPath, projectRoot) {
94
+ for (const jayHtmlPath of jayHtmlPaths) {
94
95
  if (!fs$1.existsSync(jayHtmlPath)) {
95
96
  continue;
96
97
  }
98
+ const dirname = path$1.dirname(jayHtmlPath);
99
+ const filename = path$1.basename(jayHtmlPath);
97
100
  const definitionFilePath = jayHtmlPath + ".d.ts";
98
101
  try {
99
102
  const [sourceStats, defStats] = await Promise.all([
@@ -107,8 +110,6 @@ async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
107
110
  }
108
111
  try {
109
112
  const jayHtml = await fs$1.promises.readFile(jayHtmlPath, "utf-8");
110
- const filename = path$1.basename(jayHtmlPath);
111
- const dirname = path$1.dirname(jayHtmlPath);
112
113
  const parsedJayHtml = await parseJayFile(
113
114
  jayHtml,
114
115
  filename,
@@ -132,6 +133,71 @@ async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
132
133
  }
133
134
  }
134
135
  }
136
+ const generatePageDefinitionFiles$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
137
+ __proto__: null,
138
+ generatePageDefinitionFiles
139
+ }, Symbol.toStringTag, { value: "Module" }));
140
+ const IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", "build", "dist", ".git", ".cache"]);
141
+ async function generateContractDefinitionFiles(root) {
142
+ const contractFiles = await collectContractFiles(root);
143
+ for (const contractPath of contractFiles) {
144
+ const definitionFilePath = contractPath + ".d.ts";
145
+ try {
146
+ const [sourceStats, defStats] = await Promise.all([
147
+ fs$1.promises.stat(contractPath),
148
+ fs$1.promises.stat(definitionFilePath).catch(() => null)
149
+ ]);
150
+ if (defStats && defStats.mtime >= sourceStats.mtime) {
151
+ continue;
152
+ }
153
+ } catch (error) {
154
+ }
155
+ try {
156
+ const code = await fs$1.promises.readFile(contractPath, "utf-8");
157
+ const filename = path$1.basename(contractPath);
158
+ const parsed = parseContract(code, filename);
159
+ const tsCode = await compileContract(parsed, contractPath, JAY_IMPORT_RESOLVER);
160
+ if (tsCode.validations.length > 0) {
161
+ getLogger().warn(
162
+ `failed to generate .d.ts for ${contractPath} with validation errors: ${tsCode.validations.join("\n")}`
163
+ );
164
+ continue;
165
+ }
166
+ await fs$1.promises.writeFile(definitionFilePath, checkValidationErrors(tsCode), "utf-8");
167
+ getLogger().info(`📦 Generated contract definition file: ${definitionFilePath}`);
168
+ } catch (error) {
169
+ getLogger().error(`Failed to generate definition file for ${contractPath}: ${error}`);
170
+ }
171
+ }
172
+ }
173
+ async function collectContractFiles(root) {
174
+ const results = [];
175
+ async function walk(dir) {
176
+ let entries;
177
+ try {
178
+ entries = await fs$1.promises.readdir(dir, { withFileTypes: true });
179
+ } catch {
180
+ return;
181
+ }
182
+ for (const entry of entries) {
183
+ const fullPath = path$1.join(dir, entry.name);
184
+ if (entry.isDirectory()) {
185
+ if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith(".")) {
186
+ continue;
187
+ }
188
+ await walk(fullPath);
189
+ } else if (entry.isFile() && entry.name.endsWith(JAY_CONTRACT_EXTENSION$1)) {
190
+ results.push(fullPath);
191
+ }
192
+ }
193
+ }
194
+ await walk(root);
195
+ return results;
196
+ }
197
+ const generateContractDefinitionFiles$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
198
+ __proto__: null,
199
+ generateContractDefinitionFiles
200
+ }, Symbol.toStringTag, { value: "Module" }));
135
201
  async function startDevServer(options = {}) {
136
202
  const projectPath = options.projectPath || process.cwd();
137
203
  if (projectPath !== process.cwd()) {
@@ -177,7 +243,14 @@ async function startDevServer(options = {}) {
177
243
  app.get(route.path, route.handler);
178
244
  });
179
245
  });
180
- generatePageDefinitionFiles(routes, jayOptions.tsConfigFilePath, process.cwd());
246
+ void (async () => {
247
+ await generatePageDefinitionFiles(
248
+ routes.map((route) => route.fsRoute.jayHtmlPath),
249
+ jayOptions.tsConfigFilePath,
250
+ process.cwd()
251
+ );
252
+ await generateContractDefinitionFiles(path$1.resolve("src"));
253
+ })();
181
254
  httpServer.listen(devServerPort, () => {
182
255
  log.important(`🚀 Jay Stack dev server started successfully!`);
183
256
  log.important(`📱 Dev Server: http://localhost:${devServerPort}`);
@@ -231,6 +304,8 @@ createRequire(import.meta.url);
231
304
  var __defProp2 = Object.defineProperty;
232
305
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
233
306
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, key + "", value);
307
+ const JAY_EXTENSION = ".jay-html";
308
+ const JAY_CONTRACT_EXTENSION = ".jay-contract";
234
309
  class JayAtomicType {
235
310
  constructor(name) {
236
311
  __publicField2(this, "kind", 0);
@@ -803,6 +878,172 @@ async function discoverPluginActions(pluginPath, projectRoot, registry = actionR
803
878
  }
804
879
  return registeredActions;
805
880
  }
881
+ function readTemplateMeta(jayHtmlPath) {
882
+ let content;
883
+ try {
884
+ content = fs.readFileSync(jayHtmlPath, "utf-8");
885
+ } catch {
886
+ return {};
887
+ }
888
+ const root = parse(content);
889
+ const ref = root.querySelector('script[type="application/jay-data"]')?.getAttribute("contract");
890
+ const contractRef = ref ? path.resolve(path.dirname(jayHtmlPath), ref) : void 0;
891
+ const rawTitle = root.querySelector("title")?.text?.trim();
892
+ const staticTitle = rawTitle ? rawTitle.replace(/\{[^}]*\}/g, "").trim() : "";
893
+ return { contractRef, title: staticTitle.length > 0 ? staticTitle : void 0 };
894
+ }
895
+ function listTemplatesForContractFile(contractFile) {
896
+ if (!contractFile) return [];
897
+ const dir = path.dirname(contractFile);
898
+ const target = path.resolve(contractFile);
899
+ const contractBasename = path.basename(contractFile, JAY_CONTRACT_EXTENSION);
900
+ let names;
901
+ try {
902
+ names = fs.readdirSync(dir);
903
+ } catch {
904
+ return [];
905
+ }
906
+ const variants = [];
907
+ for (const f of names) {
908
+ if (!f.endsWith(JAY_EXTENSION)) continue;
909
+ const jayHtmlPath = path.join(dir, f);
910
+ const meta = readTemplateMeta(jayHtmlPath);
911
+ if (meta.contractRef !== target) continue;
912
+ const stem = path.basename(f, JAY_EXTENSION);
913
+ variants.push({
914
+ path: jayHtmlPath,
915
+ variant: stem === contractBasename ? "" : stem,
916
+ ...meta.title && { title: meta.title }
917
+ });
918
+ }
919
+ return variants.sort((a, b) => a.variant.localeCompare(b.variant));
920
+ }
921
+ function hasTemplateForContractFile(contractFile) {
922
+ return listTemplatesForContractFile(contractFile).length > 0;
923
+ }
924
+ function readContractField(contractFile, field) {
925
+ try {
926
+ const parsed = YAML.parse(fs.readFileSync(contractFile, "utf-8"));
927
+ const value = parsed?.[field];
928
+ return typeof value === "string" ? value : void 0;
929
+ } catch {
930
+ return void 0;
931
+ }
932
+ }
933
+ function findContractFilesSync(dir) {
934
+ const out = [];
935
+ let entries;
936
+ try {
937
+ entries = fs.readdirSync(dir, { withFileTypes: true });
938
+ } catch {
939
+ return out;
940
+ }
941
+ for (const entry of entries) {
942
+ const full = path.join(dir, entry.name);
943
+ if (entry.isDirectory()) out.push(...findContractFilesSync(full));
944
+ else if (entry.name.endsWith(JAY_CONTRACT_EXTENSION)) out.push(full);
945
+ }
946
+ return out.sort();
947
+ }
948
+ function buildDesignSystemIndex(options) {
949
+ const { projectRoot, plugins, resolvePluginContractPath } = options;
950
+ const componentsBase = options.componentsBase ? path.resolve(projectRoot, options.componentsBase) : path.join(projectRoot, "src", "components");
951
+ const rel = (p) => "./" + path.relative(projectRoot, p).replace(/\\/g, "/");
952
+ const makeEntry = (contractFile, source, pluginName, nameOverride) => {
953
+ const name = nameOverride ?? readContractField(contractFile, "name") ?? path.basename(contractFile, JAY_CONTRACT_EXTENSION);
954
+ const description = readContractField(contractFile, "description");
955
+ const templates = listTemplatesForContractFile(contractFile).map((t) => ({
956
+ ...t,
957
+ path: rel(t.path)
958
+ }));
959
+ return {
960
+ name,
961
+ contractPath: rel(contractFile),
962
+ ...description && { description },
963
+ source,
964
+ ...pluginName && { plugin: pluginName },
965
+ templates
966
+ };
967
+ };
968
+ const components = [];
969
+ for (const contractFile of findContractFilesSync(componentsBase)) {
970
+ components.push(makeEntry(contractFile, "local", void 0));
971
+ }
972
+ for (const [, plugin] of plugins) {
973
+ const contracts = plugin.manifest.contracts;
974
+ if (!Array.isArray(contracts)) continue;
975
+ for (const contract of contracts) {
976
+ const spec = contract.contract;
977
+ if (!spec) continue;
978
+ const contractFile = resolvePluginContractPath(plugin, spec);
979
+ components.push(makeEntry(contractFile, "plugin", plugin.name, contract.name));
980
+ }
981
+ }
982
+ return { components };
983
+ }
984
+ function toKebabCase$1(str) {
985
+ return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[\s_]+/g, "-").replace(/^-/, "");
986
+ }
987
+ function renderAddMenu(index) {
988
+ const lines = [];
989
+ lines.push("# Design-system elements you can add");
990
+ lines.push("");
991
+ lines.push(
992
+ "Generated by `jay-stack agent-kit`. Each component ships a contract and one or more"
993
+ );
994
+ lines.push(
995
+ "`.jay-html` templates you can flatten into a page as a `<jay:X>` region. Pick a variant,"
996
+ );
997
+ lines.push("paste its import + region, then run `jay-stack sync` to flatten the markup. See");
998
+ lines.push("`designer/design-system-guide.md`.");
999
+ lines.push("");
1000
+ if (index.components.length === 0) {
1001
+ lines.push("_No design-system components found._");
1002
+ lines.push("");
1003
+ return lines.join("\n");
1004
+ }
1005
+ for (const component of index.components) {
1006
+ lines.push(
1007
+ component.description ? `## ${component.name} — ${component.description}` : `## ${component.name}`
1008
+ );
1009
+ lines.push("");
1010
+ if (component.templates.length === 0) {
1011
+ lines.push(
1012
+ "_No template yet (COMPONENT-NO-TEMPLATE) — this component can't be flattened until it ships a `.jay-html`._"
1013
+ );
1014
+ lines.push("");
1015
+ continue;
1016
+ }
1017
+ lines.push("| Variant | Title | Template |");
1018
+ lines.push("| --- | --- | --- |");
1019
+ for (const template of component.templates) {
1020
+ const variant = template.variant === "" ? "(default)" : template.variant;
1021
+ lines.push(`| ${variant} | ${template.title ?? ""} | ${template.path} |`);
1022
+ }
1023
+ lines.push("");
1024
+ const first = component.templates[0];
1025
+ const regionTag = toKebabCase$1(component.name);
1026
+ const contractAttr = component.source === "plugin" ? component.name : component.contractPath;
1027
+ lines.push("```html");
1028
+ lines.push("<script");
1029
+ lines.push(' type="application/jay-headless"');
1030
+ if (component.source === "plugin" && component.plugin) {
1031
+ lines.push(` plugin="${component.plugin}"`);
1032
+ }
1033
+ lines.push(` contract="${contractAttr}"`);
1034
+ lines.push(` template="${first.path}"`);
1035
+ lines.push("><\/script>");
1036
+ lines.push("");
1037
+ lines.push(
1038
+ `<jay:${regionTag} ref="${regionTag}"><!-- flattened copy; edit freely --></jay:${regionTag}>`
1039
+ );
1040
+ lines.push("```");
1041
+ lines.push("");
1042
+ lines.push("Then run `jay-stack sync` to flatten it.");
1043
+ lines.push("");
1044
+ }
1045
+ return lines.join("\n");
1046
+ }
806
1047
  const require$1 = createRequire(import.meta.url);
807
1048
  async function executeDynamicGenerator(plugin, config, projectRoot, services, verbose, viteServer) {
808
1049
  const { pluginPath, name: pluginName, isLocal, packageName } = plugin;
@@ -932,6 +1173,15 @@ function resolveActionFilePath(actionPath, packageName, pluginPath, isLocal, pro
932
1173
  function toKebabCase(str) {
933
1174
  return str.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
934
1175
  }
1176
+ function readComponentsBase(projectRoot) {
1177
+ try {
1178
+ const parsed = YAML.parse(fs.readFileSync(path.join(projectRoot, ".jay"), "utf-8"));
1179
+ const base = parsed?.devServer?.componentsBase;
1180
+ if (typeof base === "string" && base.length > 0) return base;
1181
+ } catch {
1182
+ }
1183
+ return "./src/components";
1184
+ }
935
1185
  async function materializeContracts(options, services = /* @__PURE__ */ new Map()) {
936
1186
  const {
937
1187
  projectRoot,
@@ -1151,13 +1401,26 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
1151
1401
  ...data.commands?.length && { commands: data.commands }
1152
1402
  }))
1153
1403
  };
1404
+ const designSystem = buildDesignSystemIndex({
1405
+ projectRoot,
1406
+ componentsBase: readComponentsBase(projectRoot),
1407
+ plugins,
1408
+ resolvePluginContractPath: (plugin, spec) => resolveStaticContractPath(plugin, spec, projectRoot)
1409
+ });
1410
+ pluginsIndex.designSystemIndex = "./design-system-index.yaml";
1154
1411
  fs.mkdirSync(outputDir, { recursive: true });
1155
1412
  const agentKitDir = path.dirname(outputDir);
1156
1413
  const pluginsIndexPath = path.join(agentKitDir, "plugins-index.yaml");
1157
1414
  fs.writeFileSync(pluginsIndexPath, YAML.stringify(pluginsIndex), "utf-8");
1415
+ const designSystemIndexPath = path.join(agentKitDir, "design-system-index.yaml");
1416
+ fs.writeFileSync(designSystemIndexPath, YAML.stringify(designSystem), "utf-8");
1417
+ const addMenuPath = path.join(agentKitDir, "design-system.md");
1418
+ fs.writeFileSync(addMenuPath, renderAddMenu(designSystem), "utf-8");
1158
1419
  if (verbose) {
1159
1420
  getLogger().info(`
1160
1421
  ✅ Plugins index written to: ${pluginsIndexPath}`);
1422
+ getLogger().info(`✅ Design-system index written to: ${designSystemIndexPath}`);
1423
+ getLogger().info(`✅ Design-system add-menu written to: ${addMenuPath}`);
1161
1424
  }
1162
1425
  return {
1163
1426
  pluginsIndex,
@@ -1581,132 +1844,6 @@ async function initializeServicesForCli(projectRoot, viteServer, quiet = false)
1581
1844
  }
1582
1845
  return { services: getServiceRegistry2(), initErrors };
1583
1846
  }
1584
- async function runDev(projectPath, options) {
1585
- const logLevel = options.quiet ? "silent" : options.verbose ? "verbose" : "info";
1586
- setDevLogger(createDevLogger(logLevel));
1587
- const testMode = options.testMode || options.timeout !== void 0;
1588
- await startDevServer({
1589
- projectPath: projectPath || process.cwd(),
1590
- testMode,
1591
- timeout: options.timeout,
1592
- logLevel
1593
- });
1594
- }
1595
- async function resolveProductionContext(projectPath, versionOverride) {
1596
- const resolvedPath = path__default.resolve(projectPath || process.cwd());
1597
- const jayConfigPath = path__default.join(resolvedPath, ".jay");
1598
- let pagesBase = "./src/pages";
1599
- let siteBaseUrl;
1600
- try {
1601
- const jayConfig = YAML.parse(await fs$2.readFile(jayConfigPath, "utf-8"));
1602
- pagesBase = jayConfig?.devServer?.pagesBase || pagesBase;
1603
- siteBaseUrl = jayConfig?.site?.baseUrl;
1604
- } catch {
1605
- }
1606
- const version = versionOverride || await resolveVersionFromPackageJson(resolvedPath);
1607
- return {
1608
- resolvedPath,
1609
- pagesRoot: path__default.resolve(resolvedPath, pagesBase),
1610
- buildRoot: path__default.join(resolvedPath, "build"),
1611
- version,
1612
- tsConfigFilePath: path__default.join(resolvedPath, "tsconfig.json"),
1613
- siteBaseUrl
1614
- };
1615
- }
1616
- async function resolveVersionFromPackageJson(projectRoot) {
1617
- try {
1618
- const pkgJson = JSON.parse(
1619
- await fs$2.readFile(path__default.join(projectRoot, "package.json"), "utf-8")
1620
- );
1621
- if (pkgJson.version) {
1622
- return pkgJson.version;
1623
- }
1624
- } catch {
1625
- }
1626
- return "1";
1627
- }
1628
- function initLogger(verbose) {
1629
- const logLevel = verbose ? "verbose" : "info";
1630
- setDevLogger(createDevLogger(logLevel));
1631
- }
1632
- async function runBuild(projectPath, options) {
1633
- initLogger(options.verbose);
1634
- const ctx = await resolveProductionContext(projectPath, options.version);
1635
- const { buildVersion } = await import("./index-B9vCxAZ1.js");
1636
- await buildVersion({
1637
- version: ctx.version,
1638
- projectRoot: ctx.resolvedPath,
1639
- pagesRoot: ctx.pagesRoot,
1640
- buildRoot: ctx.buildRoot,
1641
- concurrency: 4,
1642
- tsConfigFilePath: ctx.tsConfigFilePath,
1643
- minify: options.minify,
1644
- siteBaseUrl: ctx.siteBaseUrl
1645
- });
1646
- }
1647
- async function runServe(projectPath, options) {
1648
- initLogger(options.verbose);
1649
- const ctx = await resolveProductionContext(projectPath, options.version);
1650
- if (options.role === "renderer") {
1651
- const { startRendererServer } = await import("@jay-framework/production-server");
1652
- await startRendererServer({
1653
- buildRoot: ctx.buildRoot,
1654
- version: ctx.version,
1655
- port: parseInt(options.port, 10),
1656
- projectRoot: ctx.resolvedPath,
1657
- pagesRoot: ctx.pagesRoot,
1658
- tsConfigFilePath: ctx.tsConfigFilePath,
1659
- siteBaseUrl: ctx.siteBaseUrl
1660
- });
1661
- } else {
1662
- const { startMainServer } = await import("@jay-framework/production-server");
1663
- await startMainServer({
1664
- buildRoot: ctx.buildRoot,
1665
- version: ctx.version,
1666
- port: parseInt(options.port, 10),
1667
- testMode: options.testMode,
1668
- publicBasePath: options.staticBaseUrl,
1669
- serveStatic: options.serveStatic
1670
- });
1671
- }
1672
- }
1673
- async function runRebuild(projectPath, options) {
1674
- initLogger(options.verbose);
1675
- const ctx = await resolveProductionContext(projectPath, options.version);
1676
- let params;
1677
- if (options.params) {
1678
- params = JSON.parse(options.params);
1679
- }
1680
- const { rebuild } = await import("@jay-framework/production-server");
1681
- let target;
1682
- if (options.contract) {
1683
- target = { mode: "contract", contractName: options.contract, params };
1684
- } else if (options.route) {
1685
- target = { mode: "route", routePattern: options.route, params };
1686
- } else if (options.url) {
1687
- target = { mode: "url", url: options.url };
1688
- } else {
1689
- getLogger().error(chalk.red("One of --contract, --route, or --url is required"));
1690
- process.exit(1);
1691
- }
1692
- const result = await rebuild({
1693
- projectRoot: ctx.resolvedPath,
1694
- pagesRoot: ctx.pagesRoot,
1695
- buildRoot: ctx.buildRoot,
1696
- version: ctx.version,
1697
- target,
1698
- tsConfigFilePath: ctx.tsConfigFilePath,
1699
- siteBaseUrl: ctx.siteBaseUrl
1700
- });
1701
- if (result.errors.length > 0) {
1702
- for (const err of result.errors) {
1703
- getLogger().error(
1704
- chalk.red(` Error: ${err.route} ${JSON.stringify(err.params)}: ${err.error}`)
1705
- );
1706
- }
1707
- process.exit(1);
1708
- }
1709
- }
1710
1847
  const s = createRequire(import.meta.url), e = s("typescript"), u = e;
1711
1848
  new Proxy(e, {
1712
1849
  get(t, r) {
@@ -1714,7 +1851,7 @@ new Proxy(e, {
1714
1851
  }
1715
1852
  });
1716
1853
  const FRAMEWORK_PROP_TYPES = /* @__PURE__ */ new Set(["PageProps", "RequestQuery"]);
1717
- function checkComponentPropsAndParams(sourceCode, contract, contractName, contractPath, sourcePath) {
1854
+ function checkComponentPropsAndParams(sourceCode, contract, contractName2, contractPath, sourcePath) {
1718
1855
  const errors = [];
1719
1856
  const warnings = [];
1720
1857
  const sourceFile = u.createSourceFile(
@@ -1733,7 +1870,7 @@ function checkComponentPropsAndParams(sourceCode, contract, contractName, contra
1733
1870
  localInterfaces,
1734
1871
  contractImportedTypes,
1735
1872
  contract,
1736
- contractName,
1873
+ contractName2,
1737
1874
  contractPath,
1738
1875
  sourcePath,
1739
1876
  errors,
@@ -1746,7 +1883,7 @@ function checkComponentPropsAndParams(sourceCode, contract, contractName, contra
1746
1883
  localInterfaces,
1747
1884
  contractImportedTypes,
1748
1885
  contract,
1749
- contractName,
1886
+ contractName2,
1750
1887
  contractPath,
1751
1888
  sourcePath,
1752
1889
  errors,
@@ -1865,10 +2002,10 @@ function extractTypeNames(typeNode) {
1865
2002
  }
1866
2003
  return [];
1867
2004
  }
1868
- function checkPropsConsistency(propsTypeName, localInterfaces, contractImportedTypes, contract, contractName, contractPath, sourcePath, errors, warnings) {
2005
+ function checkPropsConsistency(propsTypeName, localInterfaces, contractImportedTypes, contract, contractName2, contractPath, sourcePath, errors, warnings) {
1869
2006
  if (FRAMEWORK_PROP_TYPES.has(propsTypeName)) return;
1870
2007
  if (contractImportedTypes.has(propsTypeName)) return;
1871
- const prefix = `[${contractName}]`;
2008
+ const prefix = `[${contractName2}]`;
1872
2009
  const iface = localInterfaces.get(propsTypeName);
1873
2010
  if (!iface) {
1874
2011
  if (!contract.props || contract.props.length === 0) {
@@ -1916,8 +2053,8 @@ function checkPropsConsistency(propsTypeName, localInterfaces, contractImportedT
1916
2053
  }
1917
2054
  }
1918
2055
  }
1919
- function checkParamsConsistency(paramsTypeNames, localInterfaces, contractImportedTypes, contract, contractName, contractPath, sourcePath, errors, warnings) {
1920
- const prefix = `[${contractName}]`;
2056
+ function checkParamsConsistency(paramsTypeNames, localInterfaces, contractImportedTypes, contract, contractName2, contractPath, sourcePath, errors, warnings) {
2057
+ const prefix = `[${contractName2}]`;
1921
2058
  if (!contract.params || contract.params.length === 0) {
1922
2059
  errors.push({
1923
2060
  type: "contract-invalid",
@@ -3679,14 +3816,14 @@ async function checkComponentContractConsistency(contract, context, result) {
3679
3816
  } catch {
3680
3817
  return;
3681
3818
  }
3682
- const contractName = contract.contract.replace(/\.jay-contract$/, "");
3819
+ const contractName2 = contract.contract.replace(/\.jay-contract$/, "");
3683
3820
  const checkResult = checkComponentPropsAndParams(
3684
3821
  sourceCode,
3685
3822
  {
3686
3823
  props: parsed.val?.props,
3687
3824
  params: parsed.val?.params
3688
3825
  },
3689
- contractName,
3826
+ contractName2,
3690
3827
  contractPath,
3691
3828
  sourcePath
3692
3829
  );
@@ -3958,36 +4095,731 @@ async function validateDynamicContracts(context, result) {
3958
4095
  }
3959
4096
  }
3960
4097
  }
3961
- async function findJayFiles(dir) {
3962
- return await glob(`${dir}/**/*${JAY_EXTENSION}`);
4098
+ function overrideSpecFor(f) {
4099
+ switch (f.kind) {
4100
+ case "attribute":
4101
+ return { target: "markup-attribute", path: f.path, value: f.name };
4102
+ case "style-declaration":
4103
+ return { target: "markup-attribute", path: f.path, value: `style.${f.property}` };
4104
+ case "children":
4105
+ return { target: "markup-attribute", path: f.path, value: "children" };
4106
+ case "css-rule":
4107
+ return { target: "css-comment", value: "" };
4108
+ case "css-declaration":
4109
+ return { target: "css-comment", value: f.property };
4110
+ }
4111
+ }
4112
+ function facetLabel(f) {
4113
+ switch (f.kind) {
4114
+ case "attribute":
4115
+ return `<${f.element.toLowerCase()}> attribute "${f.name}"`;
4116
+ case "style-declaration":
4117
+ return `<${f.element.toLowerCase()}> style "${f.property}"`;
4118
+ case "children":
4119
+ return `<${f.element.toLowerCase()}> children`;
4120
+ case "css-rule":
4121
+ return `css rule "${f.selector}"`;
4122
+ case "css-declaration":
4123
+ return `css "${f.selector}" declaration "${f.property}"`;
4124
+ }
4125
+ }
4126
+ const CONTENT_MARKER = "jay-content";
4127
+ const META_ATTRS = /* @__PURE__ */ new Set(["jc", "override", "page-scope", CONTENT_MARKER]);
4128
+ function isMetaAttr(name) {
4129
+ return META_ATTRS.has(name.toLowerCase());
4130
+ }
4131
+ function isRegionTag(el) {
4132
+ return (el.tagName ?? "").toLowerCase().startsWith("jay:");
4133
+ }
4134
+ function readAttr(el, name) {
4135
+ const v = el.getAttribute(name);
4136
+ if (v !== void 0 && v !== null) return v;
4137
+ const raw = el.rawAttributes ?? {};
4138
+ return Object.prototype.hasOwnProperty.call(raw, name) ? raw[name] ?? "" : void 0;
4139
+ }
4140
+ function isPageScope(el) {
4141
+ return readAttr(el, "page-scope") !== void 0;
4142
+ }
4143
+ const NO_SUPPRESSION = Object.freeze({
4144
+ all: false,
4145
+ attributes: /* @__PURE__ */ new Set(),
4146
+ styleProps: /* @__PURE__ */ new Set(),
4147
+ children: false
4148
+ });
4149
+ function emptySuppression() {
4150
+ return { all: false, attributes: /* @__PURE__ */ new Set(), styleProps: /* @__PURE__ */ new Set(), children: false };
4151
+ }
4152
+ function parseFacetTokens(spec, sup) {
4153
+ for (const token of spec.split(/[\s,]+/)) {
4154
+ const t = token.trim();
4155
+ if (!t) continue;
4156
+ if (t.toLowerCase() === "children") sup.children = true;
4157
+ else if (t.toLowerCase().startsWith("style."))
4158
+ sup.styleProps.add(t.slice("style.".length).toLowerCase());
4159
+ else sup.attributes.add(t.toLowerCase());
4160
+ }
4161
+ }
4162
+ function parseOverride(el) {
4163
+ if (isPageScope(el)) {
4164
+ return { all: true, attributes: /* @__PURE__ */ new Set(), styleProps: /* @__PURE__ */ new Set(), children: false };
4165
+ }
4166
+ const raw = readAttr(el, "override");
4167
+ const sup = emptySuppression();
4168
+ if (raw === void 0) return sup;
4169
+ const spec = raw.trim();
4170
+ if (spec === "" || spec === "*") {
4171
+ sup.all = true;
4172
+ return sup;
4173
+ }
4174
+ parseFacetTokens(spec, sup);
4175
+ return sup;
4176
+ }
4177
+ function parseContent(el) {
4178
+ const raw = readAttr(el, CONTENT_MARKER);
4179
+ const sup = emptySuppression();
4180
+ if (raw === void 0) return sup;
4181
+ const spec = raw.trim();
4182
+ if (spec === "*") {
4183
+ sup.all = true;
4184
+ return sup;
4185
+ }
4186
+ if (spec === "") {
4187
+ sup.children = true;
4188
+ return sup;
4189
+ }
4190
+ parseFacetTokens(spec, sup);
4191
+ return sup;
4192
+ }
4193
+ function unionSuppression(a, b) {
4194
+ return {
4195
+ all: a.all || b.all,
4196
+ attributes: /* @__PURE__ */ new Set([...a.attributes, ...b.attributes]),
4197
+ styleProps: /* @__PURE__ */ new Set([...a.styleProps, ...b.styleProps]),
4198
+ children: a.children || b.children
4199
+ };
3963
4200
  }
3964
- async function findContractFiles(dir) {
3965
- return await glob(`${dir}/**/*${JAY_CONTRACT_EXTENSION}`);
4201
+ const BINDING = /\{[^}]*\}/g;
4202
+ function normalizeExpr(value) {
4203
+ const bindingsCollapsed = value.replace(BINDING, (m) => m.replace(/\s+/g, ""));
4204
+ return bindingsCollapsed.replace(/\s+/g, " ").trim();
4205
+ }
4206
+ function parseInlineStyle(style) {
4207
+ const out = /* @__PURE__ */ new Map();
4208
+ if (!style) return out;
4209
+ for (const decl of splitDeclarations(style)) {
4210
+ const idx = firstColon(decl);
4211
+ if (idx < 0) continue;
4212
+ const property = decl.slice(0, idx).trim().toLowerCase();
4213
+ const value = decl.slice(idx + 1).trim();
4214
+ if (property) out.set(property, value);
4215
+ }
4216
+ return out;
4217
+ }
4218
+ function serializeInlineStyle(decls) {
4219
+ return [...decls].map(([property, value]) => `${property}: ${value}`).join("; ");
4220
+ }
4221
+ function splitDeclarations(style) {
4222
+ const parts = [];
4223
+ let depthParen = 0;
4224
+ let depthBrace = 0;
4225
+ let current = "";
4226
+ for (const ch of style) {
4227
+ if (ch === "(") depthParen++;
4228
+ else if (ch === ")") depthParen = Math.max(0, depthParen - 1);
4229
+ else if (ch === "{") depthBrace++;
4230
+ else if (ch === "}") depthBrace = Math.max(0, depthBrace - 1);
4231
+ if (ch === ";" && depthParen === 0 && depthBrace === 0) {
4232
+ parts.push(current);
4233
+ current = "";
4234
+ } else {
4235
+ current += ch;
4236
+ }
4237
+ }
4238
+ if (current.trim()) parts.push(current);
4239
+ return parts.filter((p) => p.trim());
4240
+ }
4241
+ function firstColon(decl) {
4242
+ let depthParen = 0;
4243
+ let depthBrace = 0;
4244
+ for (let i = 0; i < decl.length; i++) {
4245
+ const ch = decl[i];
4246
+ if (ch === "(") depthParen++;
4247
+ else if (ch === ")") depthParen = Math.max(0, depthParen - 1);
4248
+ else if (ch === "{") depthBrace++;
4249
+ else if (ch === "}") depthBrace = Math.max(0, depthBrace - 1);
4250
+ else if (ch === ":" && depthParen === 0 && depthBrace === 0) return i;
4251
+ }
4252
+ return -1;
4253
+ }
4254
+ function diffBodies(sourceParent, regionParent) {
4255
+ const out = [];
4256
+ const ignoreClass = isRegionTag(regionParent) ? readAttr(regionParent, "ref") : void 0;
4257
+ diffChildren([], sourceParent, regionParent, ignoreClass, out);
4258
+ return out;
4259
+ }
4260
+ function diffChildren(parentPath, sParent, rParent, ignoreClass, out) {
4261
+ const rSup = isElement(rParent) ? parseOverride(rParent) : NO_SUPPRESSION;
4262
+ const sSup = isElement(sParent) ? parseContent(sParent) : NO_SUPPRESSION;
4263
+ const sup = unionSuppression(rSup, sSup);
4264
+ if (sup.all || sup.children) return;
4265
+ const s2 = contentNodes(sParent);
4266
+ const r = contentNodes(rParent);
4267
+ if (!childrenAligned(s2, r)) {
4268
+ const facet = { kind: "children", path: parentPath, element: tagOf(rParent) };
4269
+ out.push({
4270
+ facet,
4271
+ change: "changed",
4272
+ sourceValue: summarize(s2),
4273
+ regionValue: summarize(r),
4274
+ addedElementTags: addedElementTags(s2, r)
4275
+ });
4276
+ return;
4277
+ }
4278
+ for (let i = 0; i < r.length; i++) {
4279
+ const rc = r[i];
4280
+ if (rc.nodeType !== NodeType.ELEMENT_NODE) continue;
4281
+ const re = rc;
4282
+ if (isRegionTag(re)) continue;
4283
+ const childIgnoreClass = parentPath.length === 0 ? ignoreClass : void 0;
4284
+ diffElement([...parentPath, i], s2[i], re, childIgnoreClass, out);
4285
+ }
4286
+ }
4287
+ function diffElement(path2, se, re, ignoreClass, out) {
4288
+ const sup = unionSuppression(parseOverride(re), parseContent(se));
4289
+ if (sup.all) return;
4290
+ compareAttributes(path2, se, re, sup, ignoreClass, out);
4291
+ compareStyle(path2, se, re, sup, out);
4292
+ diffChildren(path2, se, re, void 0, out);
4293
+ }
4294
+ function compareAttributes(path2, se, re, sup, ignoreClass, out) {
4295
+ const sm = attrMap(se);
4296
+ const rm = attrMap(re);
4297
+ const names = [.../* @__PURE__ */ new Set([...sm.keys(), ...rm.keys()])].sort();
4298
+ for (const name of names) {
4299
+ const sv = name === "class" ? stripClassToken(sm.get(name), ignoreClass) : sm.get(name);
4300
+ const rv = name === "class" ? stripClassToken(rm.get(name), ignoreClass) : rm.get(name);
4301
+ const change = scalarChange(sv, rv, (a, b) => normalizeExpr(a) === normalizeExpr(b));
4302
+ if (!change) continue;
4303
+ const facet = { kind: "attribute", path: path2, element: tagOf(re), name };
4304
+ if (!sup.all && !sup.attributes.has(name))
4305
+ out.push({ facet, change, sourceValue: sv, regionValue: rv });
4306
+ }
4307
+ }
4308
+ function compareStyle(path2, se, re, sup, out) {
4309
+ const sm = parseInlineStyle(se.getAttribute("style"));
4310
+ const rm = parseInlineStyle(re.getAttribute("style"));
4311
+ const props = [.../* @__PURE__ */ new Set([...sm.keys(), ...rm.keys()])].sort();
4312
+ for (const property of props) {
4313
+ const sv = sm.get(property);
4314
+ const rv = rm.get(property);
4315
+ const change = scalarChange(sv, rv, (a, b) => normalizeExpr(a) === normalizeExpr(b));
4316
+ if (!change) continue;
4317
+ const facet = { kind: "style-declaration", path: path2, element: tagOf(re), property };
4318
+ if (!sup.all && !sup.styleProps.has(property))
4319
+ out.push({ facet, change, sourceValue: sv, regionValue: rv });
4320
+ }
4321
+ }
4322
+ function stripClassToken(value, token) {
4323
+ if (value === void 0 || !token) return value;
4324
+ const kept = value.split(/\s+/).filter((t) => t && t !== token);
4325
+ return kept.length ? kept.join(" ") : void 0;
4326
+ }
4327
+ function scalarChange(sv, rv, equal) {
4328
+ if (sv === void 0 && rv === void 0) return void 0;
4329
+ if (sv === void 0) return "added";
4330
+ if (rv === void 0) return "removed";
4331
+ return equal(sv, rv) ? void 0 : "changed";
4332
+ }
4333
+ function attrMap(el) {
4334
+ const map = /* @__PURE__ */ new Map();
4335
+ const attrs = el.attributes;
4336
+ for (const name of Object.keys(attrs)) {
4337
+ const lower = name.toLowerCase();
4338
+ if (isMetaAttr(lower) || lower === "style") continue;
4339
+ map.set(lower, attrs[name] ?? "");
4340
+ }
4341
+ return map;
4342
+ }
4343
+ function contentNodes(parent) {
4344
+ return parent.childNodes.filter((n) => {
4345
+ if (n.nodeType === NodeType.TEXT_NODE) return normalizeExpr(nodeText(n)) !== "";
4346
+ if (n.nodeType === NodeType.ELEMENT_NODE) return !isPageScope(n);
4347
+ return false;
4348
+ });
3966
4349
  }
3967
- function flattenContractTags(tags, prefix) {
3968
- const result = [];
3969
- for (const tag of tags) {
3970
- const tagPath = prefix ? `${prefix}.${tag.tag}` : tag.tag;
3971
- result.push({ path: tagPath, required: tag.required === true });
3972
- if (tag.tags) {
3973
- result.push(...flattenContractTags(tag.tags, tagPath));
4350
+ function childrenAligned(s2, r) {
4351
+ if (s2.length !== r.length) return false;
4352
+ for (let i = 0; i < s2.length; i++) {
4353
+ const sk = s2[i].nodeType;
4354
+ const rk = r[i].nodeType;
4355
+ if (sk !== rk) return false;
4356
+ if (rk === NodeType.TEXT_NODE) {
4357
+ if (normalizeExpr(nodeText(s2[i])) !== normalizeExpr(nodeText(r[i]))) return false;
4358
+ } else {
4359
+ if (tagOf(s2[i]).toLowerCase() !== tagOf(r[i]).toLowerCase()) return false;
4360
+ }
4361
+ }
4362
+ return true;
4363
+ }
4364
+ function isElement(n) {
4365
+ return n.nodeType === NodeType.ELEMENT_NODE;
4366
+ }
4367
+ function tagOf(n) {
4368
+ return isElement(n) ? n.tagName ?? "(root)" : "(root)";
4369
+ }
4370
+ function nodeText(n) {
4371
+ return n.rawText ?? n.text ?? "";
4372
+ }
4373
+ function addedElementTags(s2, r) {
4374
+ const srcCounts = /* @__PURE__ */ new Map();
4375
+ for (const n of s2)
4376
+ if (n.nodeType === NodeType.ELEMENT_NODE) {
4377
+ const t = tagOf(n).toLowerCase();
4378
+ srcCounts.set(t, (srcCounts.get(t) ?? 0) + 1);
4379
+ }
4380
+ const added = [];
4381
+ for (const n of r)
4382
+ if (n.nodeType === NodeType.ELEMENT_NODE) {
4383
+ const t = tagOf(n).toLowerCase();
4384
+ const remaining = srcCounts.get(t) ?? 0;
4385
+ if (remaining > 0) srcCounts.set(t, remaining - 1);
4386
+ else added.push(t);
4387
+ }
4388
+ return added;
4389
+ }
4390
+ function summarize(nodes) {
4391
+ return nodes.map(
4392
+ (n) => n.nodeType === NodeType.ELEMENT_NODE ? `<${tagOf(n).toLowerCase()}>` : `"${normalizeExpr(nodeText(n))}"`
4393
+ ).join("");
4394
+ }
4395
+ function diffCss(source, region) {
4396
+ const s2 = buildUnits(source);
4397
+ const r = buildUnits(region);
4398
+ const out = [];
4399
+ for (const key of [.../* @__PURE__ */ new Set([...s2.keys(), ...r.keys()])].sort()) {
4400
+ const su = s2.get(key);
4401
+ const ru = r.get(key);
4402
+ if (su && !ru) {
4403
+ out.push({
4404
+ facet: { kind: "css-rule", selector: su.facetSelector },
4405
+ change: "removed",
4406
+ sourceValue: serialize(su)
4407
+ });
4408
+ continue;
4409
+ }
4410
+ if (!su && ru) {
4411
+ if (ru.ruleOwned) continue;
4412
+ out.push({
4413
+ facet: { kind: "css-rule", selector: ru.facetSelector },
4414
+ change: "added",
4415
+ regionValue: serialize(ru)
4416
+ });
4417
+ continue;
4418
+ }
4419
+ if (!su || !ru) continue;
4420
+ if (ru.ruleOwned) continue;
4421
+ for (const property of [.../* @__PURE__ */ new Set([...su.decls.keys(), ...ru.decls.keys()])].sort()) {
4422
+ const sv = su.decls.get(property);
4423
+ const rv = ru.decls.get(property);
4424
+ const change = declChange(sv, rv);
4425
+ if (!change) continue;
4426
+ if (ru.ownedProps.has(property)) continue;
4427
+ out.push({
4428
+ facet: { kind: "css-declaration", selector: ru.facetSelector, property },
4429
+ change,
4430
+ sourceValue: sv,
4431
+ regionValue: rv
4432
+ });
3974
4433
  }
3975
4434
  }
3976
- return result;
4435
+ return out;
4436
+ }
4437
+ const OVERRIDE_RE = /^jay:override\s*(?::\s*([\w-]+))?\s*$/i;
4438
+ function buildUnits(css) {
4439
+ const map = /* @__PURE__ */ new Map();
4440
+ const root = postcss.parse(css);
4441
+ root.walkRules((rule) => {
4442
+ const scope = scopeOf(rule);
4443
+ const decls = /* @__PURE__ */ new Map();
4444
+ let ruleOwned = false;
4445
+ const ownedProps = /* @__PURE__ */ new Set();
4446
+ const applyComment = (text) => {
4447
+ const m = text.trim().match(OVERRIDE_RE);
4448
+ if (!m) return;
4449
+ if (m[1]) ownedProps.add(m[1].toLowerCase());
4450
+ else ruleOwned = true;
4451
+ };
4452
+ const prev = rule.prev();
4453
+ if (prev?.type === "comment") applyComment(prev.text);
4454
+ for (const node of rule.nodes ?? []) {
4455
+ if (node.type === "decl") {
4456
+ const decl = node;
4457
+ decls.set(decl.prop.toLowerCase(), decl.value);
4458
+ } else if (node.type === "comment") {
4459
+ applyComment(node.text);
4460
+ }
4461
+ }
4462
+ for (const selector of rule.selectors) {
4463
+ const norm = normalizeExpr(selector);
4464
+ if (!norm) continue;
4465
+ const facetSelector = scope ? `${scope} ${norm}` : norm;
4466
+ const key = `${scope}||${norm}`;
4467
+ const existing = map.get(key);
4468
+ if (existing) {
4469
+ for (const [p, v] of decls) existing.decls.set(p, v);
4470
+ existing.ruleOwned || (existing.ruleOwned = ruleOwned);
4471
+ for (const p of ownedProps) existing.ownedProps.add(p);
4472
+ } else {
4473
+ map.set(key, {
4474
+ scope,
4475
+ selector: norm,
4476
+ facetSelector,
4477
+ decls: new Map(decls),
4478
+ ruleOwned,
4479
+ ownedProps: new Set(ownedProps)
4480
+ });
4481
+ }
4482
+ }
4483
+ });
4484
+ return map;
3977
4485
  }
3978
- function extractExpressions(text) {
3979
- const results = [];
3980
- const regex = /\{([^}]+)\}/g;
3981
- let match;
3982
- while ((match = regex.exec(text)) !== null) {
3983
- results.push(match[1].trim());
4486
+ function scopeOf(rule) {
4487
+ const parts = [];
4488
+ let node = rule.parent;
4489
+ while (node) {
4490
+ if (node.type === "atrule") {
4491
+ const at = node;
4492
+ if (at.name.toLowerCase() !== "scope") {
4493
+ parts.unshift(`@${at.name} ${at.params}`.trim());
4494
+ }
4495
+ }
4496
+ node = node.parent;
3984
4497
  }
3985
- return results;
4498
+ return parts.join(" ");
3986
4499
  }
3987
- function extractTagPath(expr) {
3988
- let cleaned = expr.replace(/^!/, "").trim();
3989
- cleaned = cleaned.split(/\s*[!=]==?\s*/)[0].trim();
3990
- if (cleaned === "." || cleaned === "") return null;
4500
+ function declChange(sv, rv) {
4501
+ if (sv === void 0 && rv === void 0) return void 0;
4502
+ if (sv === void 0) return "added";
4503
+ if (rv === void 0) return "removed";
4504
+ return normalizeExpr(sv) === normalizeExpr(rv) ? void 0 : "changed";
4505
+ }
4506
+ function serialize(u2) {
4507
+ const body = [...u2.decls].map(([p, v]) => `${p}: ${v}`).join("; ");
4508
+ return `${u2.selector} { ${body} }`;
4509
+ }
4510
+ function materialise(pageHtml, opts) {
4511
+ const root = parse(pageHtml);
4512
+ const errors = [];
4513
+ const contributions = [];
4514
+ fillRegions(root, [], opts, errors, contributions);
4515
+ stripContentMarkers(root);
4516
+ const serialized = root.toString();
4517
+ return {
4518
+ html: opts.prettify ? opts.prettify(serialized) : serialized,
4519
+ css: coalesceCss(contributions),
4520
+ errors
4521
+ };
4522
+ }
4523
+ function coalesceCss(contributions) {
4524
+ const groups = [];
4525
+ const byKey = /* @__PURE__ */ new Map();
4526
+ for (const { key, selector, css, to } of contributions) {
4527
+ if (selector === null) {
4528
+ groups.push({ selectors: [], css });
4529
+ continue;
4530
+ }
4531
+ const existing = byKey.get(key);
4532
+ if (existing) {
4533
+ if (!existing.selectors.includes(selector)) existing.selectors.push(selector);
4534
+ } else {
4535
+ const group = { selectors: [selector], css, to };
4536
+ byKey.set(key, group);
4537
+ groups.push(group);
4538
+ }
4539
+ }
4540
+ return groups.map(
4541
+ (g) => g.selectors.length > 0 ? scopeWrap(g.css, g.selectors.join(", "), g.to) : g.css
4542
+ ).join("\n\n");
4543
+ }
4544
+ function fillRegions(container, stack, opts, errors, cssBlocks, parentScoped = false) {
4545
+ for (const region of directRegions(container)) {
4546
+ const name = contractName(region);
4547
+ const templatePath = opts.resolveTemplate(name);
4548
+ if (!templatePath) continue;
4549
+ if (stack.includes(templatePath)) {
4550
+ errors.push(
4551
+ `template inclusion cycle: <jay:${name}> (${templatePath}) includes itself via ${[...stack, templatePath].join(" → ")}`
4552
+ );
4553
+ continue;
4554
+ }
4555
+ const template = opts.loadTemplate(templatePath);
4556
+ if (!template) {
4557
+ errors.push(
4558
+ `cannot resolve template "${templatePath}" for <jay:${name}> — check the template= provenance`
4559
+ );
4560
+ continue;
4561
+ }
4562
+ const existing = region.innerHTML;
4563
+ const filled = opts.preserveOverrides ? mergeOverrides(template.body, existing) : template.body;
4564
+ region.set_content(filled);
4565
+ const ref = readAttr(region, "ref");
4566
+ let scoped = false;
4567
+ if (template.css) {
4568
+ const selector = (opts.scopeSelector ?? defaultScopeSelector)(region);
4569
+ if (selector) {
4570
+ scoped = true;
4571
+ const to = [
4572
+ ...new Set(
4573
+ directRegions(region).filter((c) => opts.resolveTemplate(contractName(c)) !== null).map((c) => readAttr(c, "ref")).filter((r) => !!r).map((r) => `.${r}`)
4574
+ )
4575
+ ];
4576
+ cssBlocks.push({
4577
+ key: `${templatePath}\0${[...to].sort().join(",")}`,
4578
+ selector,
4579
+ css: scopeReadyCss(template.body, template.css),
4580
+ to
4581
+ });
4582
+ } else {
4583
+ cssBlocks.push({ key: templatePath, selector: null, css: template.css });
4584
+ }
4585
+ }
4586
+ if (ref && (scoped || parentScoped)) stampScopeAnchor(region, ref);
4587
+ fillRegions(region, [...stack, templatePath], opts, errors, cssBlocks, scoped);
4588
+ }
4589
+ }
4590
+ function directRegions(container) {
4591
+ const out = [];
4592
+ const visit = (el) => {
4593
+ for (const child of el.childNodes) {
4594
+ if (!(child instanceof HTMLElement)) continue;
4595
+ if (isRegionTag(child)) out.push(child);
4596
+ else visit(child);
4597
+ }
4598
+ };
4599
+ visit(container);
4600
+ return out;
4601
+ }
4602
+ function contractName(region) {
4603
+ return (region.tagName ?? "").slice("jay:".length).toLowerCase();
4604
+ }
4605
+ function defaultScopeSelector(region) {
4606
+ const ref = readAttr(region, "ref");
4607
+ return ref ? `.${ref}` : null;
4608
+ }
4609
+ function scopeWrap(css, selector, to) {
4610
+ const limit = to && to.length ? ` to (${to.join(", ")})` : "";
4611
+ return `@scope (${selector})${limit} {
4612
+ ${css}
4613
+ }`;
4614
+ }
4615
+ function rootClassesOf(templateBody) {
4616
+ const classes = /* @__PURE__ */ new Set();
4617
+ for (const child of parse(templateBody).childNodes) {
4618
+ if (child instanceof HTMLElement) {
4619
+ for (const c of child.classList.values()) classes.add(c);
4620
+ }
4621
+ }
4622
+ return classes;
4623
+ }
4624
+ function scopeReadyCss(templateBody, css) {
4625
+ const rootClasses = rootClassesOf(templateBody);
4626
+ if (rootClasses.size === 0) return css;
4627
+ try {
4628
+ const root = postcss.parse(css);
4629
+ root.walkRules((rule) => {
4630
+ rule.selectors = rule.selectors.map((sel) => rewriteRootSelector(sel, rootClasses));
4631
+ });
4632
+ return root.toString();
4633
+ } catch {
4634
+ return css;
4635
+ }
4636
+ }
4637
+ function rewriteRootSelector(selector, rootClasses) {
4638
+ return selector.replace(
4639
+ /\.([A-Za-z0-9_-]+)/g,
4640
+ (match, name) => rootClasses.has(name) ? ":scope" : match
4641
+ );
4642
+ }
4643
+ function stampScopeAnchor(region, className) {
4644
+ for (const child of region.childNodes) {
4645
+ if (child instanceof HTMLElement) child.classList.add(className);
4646
+ }
4647
+ }
4648
+ function mergeOverrides(templateBody, existingBody) {
4649
+ const tRoot = parse(templateBody);
4650
+ const eRoot = parse(existingBody);
4651
+ mergeChildren(tRoot, eRoot);
4652
+ stripContentMarkers(tRoot);
4653
+ return tRoot.toString();
4654
+ }
4655
+ function stripContentMarkers(root) {
4656
+ const visit = (el) => {
4657
+ if (readAttr(el, CONTENT_MARKER) !== void 0) el.removeAttribute(CONTENT_MARKER);
4658
+ for (const child of el.childNodes) if (child instanceof HTMLElement) visit(child);
4659
+ };
4660
+ visit(root);
4661
+ }
4662
+ function mergeChildren(tParent, eParent) {
4663
+ const t = contentElements(tParent);
4664
+ const e2 = contentElements(eParent);
4665
+ if (!aligned(t, e2)) return;
4666
+ for (let i = 0; i < t.length; i++) mergeElement(t[i], e2[i]);
4667
+ }
4668
+ function mergeElement(te, ee) {
4669
+ if (isRegionTag(te)) {
4670
+ te.set_content(ee.innerHTML);
4671
+ return;
4672
+ }
4673
+ const sup = unionSuppression(parseOverride(ee), parseContent(te));
4674
+ if (sup.all) {
4675
+ te.replaceWith(ee.outerHTML);
4676
+ return;
4677
+ }
4678
+ carryOwnedAttributes(te, ee, sup);
4679
+ carryOwnedStyle(te, ee, sup);
4680
+ carryMarkers(te, ee);
4681
+ if (sup.children) {
4682
+ te.set_content(ee.innerHTML);
4683
+ } else {
4684
+ mergeChildren(te, ee);
4685
+ }
4686
+ }
4687
+ function carryOwnedAttributes(te, ee, sup) {
4688
+ for (const name of sup.attributes) {
4689
+ const v = readAttr(ee, name);
4690
+ if (v === void 0) te.removeAttribute(name);
4691
+ else te.setAttribute(name, v);
4692
+ }
4693
+ }
4694
+ function carryOwnedStyle(te, ee, sup) {
4695
+ if (sup.styleProps.size === 0) return;
4696
+ const merged = parseInlineStyle(te.getAttribute("style"));
4697
+ const pageStyle = parseInlineStyle(ee.getAttribute("style"));
4698
+ for (const prop of sup.styleProps) {
4699
+ const v = pageStyle.get(prop);
4700
+ if (v === void 0) merged.delete(prop);
4701
+ else merged.set(prop, v);
4702
+ }
4703
+ const serialized = serializeInlineStyle(merged);
4704
+ if (serialized) te.setAttribute("style", serialized);
4705
+ else te.removeAttribute("style");
4706
+ }
4707
+ function carryMarkers(te, ee) {
4708
+ const override = readAttr(ee, "override");
4709
+ if (override !== void 0) te.setAttribute("override", override);
4710
+ if (isPageScope(ee)) te.setAttribute("page-scope", readAttr(ee, "page-scope") ?? "");
4711
+ }
4712
+ function contentElements(parent) {
4713
+ return parent.childNodes.filter((n) => n instanceof HTMLElement);
4714
+ }
4715
+ function aligned(t, e2) {
4716
+ if (t.length !== e2.length) return false;
4717
+ for (let i = 0; i < t.length; i++) {
4718
+ if ((t[i].tagName ?? "").toLowerCase() !== (e2[i].tagName ?? "").toLowerCase()) return false;
4719
+ }
4720
+ return true;
4721
+ }
4722
+ function buildMaterialiseOptions(pageDir, jayHtml, readFile, extra = {}) {
4723
+ const nameToPath = /* @__PURE__ */ new Map();
4724
+ for (const imp of jayHtml.headlessImports) {
4725
+ if (imp.template) {
4726
+ nameToPath.set(imp.contractName.toLowerCase(), path$1.resolve(pageDir, imp.template));
4727
+ }
4728
+ }
4729
+ return {
4730
+ resolveTemplate: (name) => nameToPath.get(name.toLowerCase()) ?? null,
4731
+ loadTemplate: (absPath) => {
4732
+ const content = readFile(absPath);
4733
+ if (content === void 0) return null;
4734
+ const root = parse(content);
4735
+ const body = root.querySelector("body");
4736
+ const styles = root.querySelectorAll("style");
4737
+ const css = styles.map((s2) => s2.textContent).join("\n\n");
4738
+ return {
4739
+ body: body ? body.innerHTML : content,
4740
+ css: css.trim() ? css : void 0
4741
+ };
4742
+ },
4743
+ ...extra
4744
+ };
4745
+ }
4746
+ function extractScopeBlock(css, selector) {
4747
+ for (const { selectors, block } of splitScopeBlocks(css)) {
4748
+ if (selectors.includes(selector)) return block;
4749
+ }
4750
+ return void 0;
4751
+ }
4752
+ function tokenizeCss(css) {
4753
+ const out = [];
4754
+ if (!css) return out;
4755
+ const header = /@scope\s*\(\s*([^)]+?)\s*\)(?:\s*to\s*\(\s*([^)]+?)\s*\))?\s*\{/g;
4756
+ let last = 0;
4757
+ let m;
4758
+ while ((m = header.exec(css)) !== null) {
4759
+ let depth = 0;
4760
+ let end = -1;
4761
+ for (let i = m.index + m[0].length - 1; i < css.length; i++) {
4762
+ const ch = css[i];
4763
+ if (ch === "{") depth++;
4764
+ else if (ch === "}") {
4765
+ depth--;
4766
+ if (depth === 0) {
4767
+ end = i + 1;
4768
+ break;
4769
+ }
4770
+ }
4771
+ }
4772
+ if (end === -1) break;
4773
+ if (m.index > last) out.push({ type: "raw", text: css.slice(last, m.index) });
4774
+ const selector = m[1];
4775
+ const selectors = selector.split(",").map((s2) => s2.trim()).filter(Boolean);
4776
+ const to = (m[2] ?? "").split(",").map((s2) => s2.trim()).filter(Boolean);
4777
+ out.push({ type: "scope", selector, selectors, to, block: css.slice(m.index, end) });
4778
+ last = end;
4779
+ header.lastIndex = end;
4780
+ }
4781
+ if (last < css.length) out.push({ type: "raw", text: css.slice(last) });
4782
+ return out;
4783
+ }
4784
+ function splitScopeBlocks(css) {
4785
+ return tokenizeCss(css).filter((s2) => s2.type === "scope").map(({ selector, selectors, to, block }) => ({ selector, selectors, to, block }));
4786
+ }
4787
+ function scopeInnerBody(block) {
4788
+ return block.replace(/^@scope\s*\([^)]*\)(?:\s*to\s*\([^)]*\))?\s*\{/, "").replace(/\}\s*$/, "");
4789
+ }
4790
+ function normalizeCssBody(body) {
4791
+ return body.replace(/\s+/g, " ").replace(/\s*([{}:;,])\s*/g, "$1").replace(/;}/g, "}").trim();
4792
+ }
4793
+ async function findJayFiles(dir) {
4794
+ return await glob(`${dir}/**/*${JAY_EXTENSION$1}`);
4795
+ }
4796
+ async function findContractFiles(dir) {
4797
+ return await glob(`${dir}/**/*${JAY_CONTRACT_EXTENSION$1}`);
4798
+ }
4799
+ function flattenContractTags(tags, prefix) {
4800
+ const result = [];
4801
+ for (const tag of tags) {
4802
+ const tagPath = prefix ? `${prefix}.${tag.tag}` : tag.tag;
4803
+ result.push({ path: tagPath, required: tag.required === true });
4804
+ if (tag.tags) {
4805
+ result.push(...flattenContractTags(tag.tags, tagPath));
4806
+ }
4807
+ }
4808
+ return result;
4809
+ }
4810
+ function extractExpressions(text) {
4811
+ const results = [];
4812
+ const regex = /\{([^}]+)\}/g;
4813
+ let match;
4814
+ while ((match = regex.exec(text)) !== null) {
4815
+ results.push(match[1].trim());
4816
+ }
4817
+ return results;
4818
+ }
4819
+ function extractTagPath(expr) {
4820
+ let cleaned = expr.replace(/^!/, "").trim();
4821
+ cleaned = cleaned.split(/\s*[!=]==?\s*/)[0].trim();
4822
+ if (cleaned === "." || cleaned === "") return null;
3991
4823
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(cleaned)) {
3992
4824
  return cleaned;
3993
4825
  }
@@ -4038,9 +4870,9 @@ function collectUsedTags(jayHtml) {
4038
4870
  const tagName = element.rawTagName?.toLowerCase();
4039
4871
  let childScopes = scopes;
4040
4872
  if (tagName?.startsWith("jay:")) {
4041
- const contractName = tagName.substring(4);
4873
+ const contractName2 = tagName.substring(4);
4042
4874
  const idx = imports.findIndex(
4043
- (imp) => imp.contractName === contractName && imp.contract
4875
+ (imp) => imp.contractName === contractName2 && imp.contract
4044
4876
  );
4045
4877
  if (idx !== -1) {
4046
4878
  childScopes = [...scopes, { importIndex: idx, prefix: "" }];
@@ -4204,9 +5036,9 @@ function checkRefElementTypes(jayHtml, file) {
4204
5036
  const tagName = element.rawTagName?.toLowerCase();
4205
5037
  let childScopes = scopes;
4206
5038
  if (tagName?.startsWith("jay:")) {
4207
- const contractName = tagName.substring(4);
5039
+ const contractName2 = tagName.substring(4);
4208
5040
  const idx = imports.findIndex(
4209
- (imp) => imp.contractName === contractName && imp.contract
5041
+ (imp) => imp.contractName === contractName2 && imp.contract
4210
5042
  );
4211
5043
  if (idx !== -1) {
4212
5044
  childScopes = [...scopes, { importIndex: idx, prefix: "" }];
@@ -4292,7 +5124,7 @@ const DOCUMENT_ACCESS_PATTERNS = [
4292
5124
  const DOM_SUPPRESS_COMMENT = "jay-dom: allow";
4293
5125
  function checkDirectDocumentAccess(jayHtmlPath) {
4294
5126
  const dirname = path$1.dirname(jayHtmlPath);
4295
- const basename = path$1.basename(jayHtmlPath, JAY_EXTENSION);
5127
+ const basename = path$1.basename(jayHtmlPath, JAY_EXTENSION$1);
4296
5128
  const candidates = [path$1.join(dirname, `${basename}.ts`), path$1.join(dirname, "page.ts")];
4297
5129
  const compPath = candidates.find((p) => fs$1.existsSync(p));
4298
5130
  if (!compPath) return [];
@@ -4418,39 +5250,410 @@ const HEADLESS_SKIP_ATTRS = /* @__PURE__ */ new Set([
4418
5250
  "props",
4419
5251
  "key",
4420
5252
  "jay-coordinate-base",
4421
- "jay-scope"
5253
+ "jay-scope",
5254
+ "jc"
5255
+ // component-provenance marker stamped on a flattened <jay:X> region (DL#196)
4422
5256
  ]);
4423
5257
  const PHASE_ORDER = {
4424
5258
  slow: 0,
4425
5259
  fast: 1,
4426
5260
  "fast+interactive": 2
4427
5261
  };
4428
- function resolveBindingPhase(bindingPath, jayHtml) {
5262
+ function resolveBindingTag(bindingPath, jayHtml) {
4429
5263
  const segments = bindingPath.split(".");
4430
5264
  const root = segments[0];
4431
5265
  const keyedImport = jayHtml.headlessImports.find((i) => i.key === root && i.contract);
4432
5266
  if (keyedImport?.contract) {
4433
5267
  const tagPath = segments.slice(1).join(".");
4434
5268
  if (!tagPath) return void 0;
4435
- const tag = resolveContractTag(keyedImport.contract, tagPath);
4436
- if (!tag) return void 0;
4437
- return tag.phase || "slow";
5269
+ return resolveContractTag(keyedImport.contract, tagPath);
4438
5270
  }
4439
5271
  if (jayHtml.contract) {
4440
- const tag = resolveContractTag(jayHtml.contract, bindingPath);
4441
- if (!tag) return void 0;
4442
- return tag.phase || "slow";
5272
+ return resolveContractTag(jayHtml.contract, bindingPath);
4443
5273
  }
4444
5274
  return void 0;
4445
5275
  }
5276
+ function resolveBindingPhase(bindingPath, jayHtml) {
5277
+ const tag = resolveBindingTag(bindingPath, jayHtml);
5278
+ return tag ? tag.phase || "slow" : void 0;
5279
+ }
5280
+ function resolveBindingDataType(bindingPath, jayHtml) {
5281
+ return resolveBindingTag(bindingPath, jayHtml)?.dataType;
5282
+ }
5283
+ function typeLabel(t) {
5284
+ return isEnumType$1(t) ? `enum(${t.values.join(" | ")})` : t.name;
5285
+ }
5286
+ function collectRegionElements(root) {
5287
+ const out = [];
5288
+ const visit = (el) => {
5289
+ for (const child of el.childNodes) {
5290
+ if (child.nodeType !== NodeType.ELEMENT_NODE) continue;
5291
+ const childEl = child;
5292
+ if (isRegionTag(childEl)) out.push(childEl);
5293
+ visit(childEl);
5294
+ }
5295
+ };
5296
+ visit(root);
5297
+ return out;
5298
+ }
5299
+ function directChildRegions(region) {
5300
+ const out = [];
5301
+ const visit = (el) => {
5302
+ for (const child of el.childNodes) {
5303
+ if (child.nodeType !== NodeType.ELEMENT_NODE) continue;
5304
+ const childEl = child;
5305
+ if (isRegionTag(childEl)) out.push(childEl);
5306
+ else visit(childEl);
5307
+ }
5308
+ };
5309
+ visit(region);
5310
+ return out;
5311
+ }
5312
+ function formatRegionDrift(contractName2, template, entry) {
5313
+ const label = facetLabel(entry.facet);
5314
+ const values = entry.change === "added" ? ` (now ${entry.regionValue ?? ""})` : entry.change === "removed" ? ` (was ${entry.sourceValue ?? ""})` : ` (${entry.sourceValue ?? ""} → ${entry.regionValue ?? ""})`;
5315
+ const spec = overrideSpecFor(entry.facet);
5316
+ const mark = spec.target === "markup-attribute" ? `mark the node override="${spec.value}"` : spec.value ? `mark the CSS rule /* jay:override: ${spec.value} */` : "mark the CSS rule /* jay:override */";
5317
+ const contentSlotHint = entry.facet.kind === "children" ? ` Or, if these children are a content slot, mark the node jay-content in the template "${template}" so consumer edits are expected.` : "";
5318
+ return {
5319
+ message: `<jay:${contractName2}> region differs from source template "${template}": ${label} ${entry.change}${values}.`,
5320
+ suggestion: `To keep the page's version, ${mark}; to discard it and re-flatten from source, run \`jay-stack sync\`.${contentSlotHint}`
5321
+ };
5322
+ }
5323
+ function checkRegionDrift(jayHtml, loadTemplate) {
5324
+ const importsWithTemplate = jayHtml.headlessImports.filter((imp) => imp.template);
5325
+ if (importsWithTemplate.length === 0) return [];
5326
+ const warnings = [];
5327
+ const templateBodyCache = /* @__PURE__ */ new Map();
5328
+ for (const region of collectRegionElements(jayHtml.body)) {
5329
+ const contractName2 = (region.rawTagName ?? "").toLowerCase().substring(4);
5330
+ const imp = importsWithTemplate.find((i) => i.contractName === contractName2);
5331
+ if (!imp?.template) continue;
5332
+ let templateBody = templateBodyCache.get(imp.template);
5333
+ if (templateBody === void 0) {
5334
+ const content = loadTemplate(imp.template);
5335
+ templateBody = (content ? parse(content).querySelector("body") : null) ?? null;
5336
+ templateBodyCache.set(imp.template, templateBody);
5337
+ if (!templateBody) {
5338
+ warnings.push({
5339
+ message: `<jay:${contractName2}> declares template="${imp.template}" but its source template could not be read.`,
5340
+ suggestion: "Fix the path or remove the attribute."
5341
+ });
5342
+ }
5343
+ }
5344
+ if (!templateBody) continue;
5345
+ for (const entry of diffBodies(templateBody, region)) {
5346
+ warnings.push(formatRegionDrift(contractName2, imp.template, entry));
5347
+ }
5348
+ }
5349
+ return warnings;
5350
+ }
5351
+ function extractStyleCss(templateHtml) {
5352
+ return parse(templateHtml).querySelectorAll("style").map((s2) => s2.textContent).join("\n\n");
5353
+ }
5354
+ function checkRegionCssDrift(jayHtml, loadTemplate) {
5355
+ const importsWithTemplate = jayHtml.headlessImports.filter((imp) => imp.template);
5356
+ if (importsWithTemplate.length === 0) return [];
5357
+ const pageCss = jayHtml.css ?? "";
5358
+ const warnings = [];
5359
+ const templateCssCache = /* @__PURE__ */ new Map();
5360
+ for (const region of collectRegionElements(jayHtml.body)) {
5361
+ const contractName2 = (region.rawTagName ?? "").toLowerCase().substring(4);
5362
+ const imp = importsWithTemplate.find((i) => i.contractName === contractName2);
5363
+ if (!imp?.template) continue;
5364
+ const ref = region.getAttribute("ref");
5365
+ if (!ref) continue;
5366
+ let templateCss = templateCssCache.get(imp.template);
5367
+ if (templateCss === void 0) {
5368
+ const content = loadTemplate(imp.template);
5369
+ if (content === void 0) {
5370
+ templateCss = null;
5371
+ } else {
5372
+ const body = parse(content).querySelector("body");
5373
+ templateCss = scopeReadyCss(body ? body.innerHTML : "", extractStyleCss(content));
5374
+ }
5375
+ templateCssCache.set(imp.template, templateCss);
5376
+ }
5377
+ if (templateCss === null) continue;
5378
+ const pageBlock = extractScopeBlock(pageCss, `.${ref}`) ?? "";
5379
+ if (!templateCss.trim() && !pageBlock.trim()) continue;
5380
+ if (templateCss.trim() && !pageBlock.trim()) {
5381
+ warnings.push({
5382
+ message: `<jay:${contractName2}> (template="${imp.template}") has no @scope (.${ref}) CSS block on the page, but its source template ships CSS.`,
5383
+ suggestion: "Run jay-stack sync to re-flatten the region CSS."
5384
+ });
5385
+ continue;
5386
+ }
5387
+ for (const entry of diffCss(templateCss, pageBlock)) {
5388
+ warnings.push(formatRegionDrift(contractName2, imp.template, entry));
5389
+ }
5390
+ }
5391
+ return warnings;
5392
+ }
5393
+ function checkRegionCssScoping(jayHtml) {
5394
+ const importsWithTemplate = jayHtml.headlessImports.filter((imp) => imp.template);
5395
+ if (importsWithTemplate.length === 0) return [];
5396
+ const pageCss = jayHtml.css ?? "";
5397
+ if (!pageCss.trim()) return [];
5398
+ const templateOf = /* @__PURE__ */ new Map();
5399
+ for (const region of collectRegionElements(jayHtml.body)) {
5400
+ const contractName2 = (region.rawTagName ?? "").toLowerCase().substring(4);
5401
+ const imp = importsWithTemplate.find((i) => i.contractName === contractName2);
5402
+ const ref = region.getAttribute("ref");
5403
+ if (imp?.template && ref) templateOf.set(`.${ref}`, imp.template);
5404
+ }
5405
+ const warnings = [];
5406
+ const groups = /* @__PURE__ */ new Map();
5407
+ for (const { selectors, to, block } of splitScopeBlocks(pageCss)) {
5408
+ const known = selectors.filter((s2) => templateOf.has(s2));
5409
+ if (known.length === 0) continue;
5410
+ const templates = [...new Set(known.map((s2) => templateOf.get(s2)))];
5411
+ if (templates.length > 1) {
5412
+ warnings.push({
5413
+ message: `A single @scope (${selectors.join(", ")}) block mixes refs from different source templates (${templates.join(", ")}); each region's CSS must be scoped to its own template.`,
5414
+ suggestion: "Run jay-stack sync to re-flatten the region CSS."
5415
+ });
5416
+ continue;
5417
+ }
5418
+ const key = `${templates[0]}\0${scopeBody(block)}\0${[...to].sort().join(",")}`;
5419
+ (groups.get(key) ?? groups.set(key, []).get(key)).push(selectors.join(", "));
5420
+ }
5421
+ for (const [key, blockSelectors] of groups) {
5422
+ if (blockSelectors.length < 2) continue;
5423
+ const template = key.split("\0")[0];
5424
+ warnings.push({
5425
+ message: `${blockSelectors.length} @scope blocks (${blockSelectors.join(" ; ")}) share template "${template}" and identical CSS but are not coalesced into one selector-list block.`,
5426
+ suggestion: "Run jay-stack sync to coalesce the region CSS."
5427
+ });
5428
+ }
5429
+ return warnings;
5430
+ }
5431
+ function checkNestedRegionRefs(jayHtml, loadTemplate) {
5432
+ const importsWithTemplate = jayHtml.headlessImports.filter((imp) => imp.template);
5433
+ if (importsWithTemplate.length === 0) return [];
5434
+ const warnings = [];
5435
+ const shipsCssCache = /* @__PURE__ */ new Map();
5436
+ const templateShipsCss = (rel) => {
5437
+ let v = shipsCssCache.get(rel);
5438
+ if (v === void 0) {
5439
+ const content = loadTemplate(rel);
5440
+ v = content !== void 0 && extractStyleCss(content).trim() !== "";
5441
+ shipsCssCache.set(rel, v);
5442
+ }
5443
+ return v;
5444
+ };
5445
+ for (const region of collectRegionElements(jayHtml.body)) {
5446
+ const contractName2 = (region.rawTagName ?? "").toLowerCase().substring(4);
5447
+ const imp = importsWithTemplate.find((i) => i.contractName === contractName2);
5448
+ if (!imp?.template) continue;
5449
+ const parentRef = region.getAttribute("ref");
5450
+ if (!parentRef) continue;
5451
+ if (!templateShipsCss(imp.template)) continue;
5452
+ for (const child of directChildRegions(region)) {
5453
+ const childName = (child.rawTagName ?? "").toLowerCase().substring(4);
5454
+ const childImp = importsWithTemplate.find((i) => i.contractName === childName);
5455
+ if (!childImp?.template) continue;
5456
+ if (child.getAttribute("ref")) continue;
5457
+ warnings.push({
5458
+ message: `<jay:${childName}> is nested inside <jay:${contractName2}> (template="${imp.template}"), whose CSS is scoped as an @scope (.${parentRef}) donut, but <jay:${childName}> has no ref= — so its region cannot be isolated from the parent's styles.`,
5459
+ suggestion: `Add a ref= to the <jay:${childName}> in template "${imp.template}" so sync can stamp its scope-anchor class and emit the \`to (.<ref>)\` boundary.`
5460
+ });
5461
+ }
5462
+ }
5463
+ return warnings;
5464
+ }
5465
+ const CONTENT_TAGS = /* @__PURE__ */ new Set([
5466
+ // inline text semantics
5467
+ "span",
5468
+ "strong",
5469
+ "b",
5470
+ "em",
5471
+ "i",
5472
+ "u",
5473
+ "s",
5474
+ "small",
5475
+ "mark",
5476
+ "sub",
5477
+ "sup",
5478
+ "abbr",
5479
+ "cite",
5480
+ "q",
5481
+ "code",
5482
+ "kbd",
5483
+ "samp",
5484
+ "var",
5485
+ "del",
5486
+ "ins",
5487
+ "time",
5488
+ "data",
5489
+ "bdi",
5490
+ "bdo",
5491
+ "ruby",
5492
+ "rt",
5493
+ "rp",
5494
+ // links & line breaks
5495
+ "a",
5496
+ "br",
5497
+ "wbr",
5498
+ // media (content, not layout)
5499
+ "img",
5500
+ "picture",
5501
+ "source",
5502
+ "svg",
5503
+ "use",
5504
+ "path",
5505
+ "icon",
5506
+ "figure",
5507
+ "figcaption",
5508
+ "audio",
5509
+ "video",
5510
+ // lists
5511
+ "ul",
5512
+ "ol",
5513
+ "li",
5514
+ "dl",
5515
+ "dt",
5516
+ "dd",
5517
+ // tables
5518
+ "table",
5519
+ "thead",
5520
+ "tbody",
5521
+ "tfoot",
5522
+ "tr",
5523
+ "td",
5524
+ "th",
5525
+ "caption",
5526
+ "colgroup",
5527
+ "col",
5528
+ // text blocks & headings
5529
+ "p",
5530
+ "h1",
5531
+ "h2",
5532
+ "h3",
5533
+ "h4",
5534
+ "h5",
5535
+ "h6",
5536
+ "blockquote",
5537
+ "pre",
5538
+ "hr",
5539
+ // form labels/copy (not controls, which imply structure)
5540
+ "label"
5541
+ ]);
5542
+ function isNonContentOverride(entry, contentTags) {
5543
+ switch (entry.facet.kind) {
5544
+ case "style-declaration":
5545
+ case "css-rule":
5546
+ case "css-declaration":
5547
+ return true;
5548
+ case "attribute":
5549
+ return entry.facet.name.toLowerCase() === "class";
5550
+ case "children":
5551
+ return (entry.addedElementTags ?? []).some((tag) => !contentTags.has(tag));
5552
+ }
5553
+ }
5554
+ function regionContractName(region) {
5555
+ return (region.rawTagName ?? "").toLowerCase().substring(4);
5556
+ }
5557
+ function isRegionRuleSuppressed(imp, jayHtml, ruleId) {
5558
+ if (imp?.suppressedValidations?.includes(ruleId)) return true;
5559
+ if (ruleId === "REGION-NOT-LINKED" && imp) {
5560
+ const allow = jayHtml.validationOverrides?.["jay-stack"]?.["allow-inline-region"];
5561
+ const list = Array.isArray(allow) ? allow.map((v) => String(v).toLowerCase()) : [];
5562
+ if (list.includes(imp.contractName.toLowerCase())) return true;
5563
+ }
5564
+ return false;
5565
+ }
5566
+ function checkRegionNotLinked(jayHtml, templatesFor) {
5567
+ const findings = [];
5568
+ const seen = /* @__PURE__ */ new Set();
5569
+ for (const region of collectRegionElements(jayHtml.body)) {
5570
+ const contractName2 = regionContractName(region);
5571
+ if (seen.has(contractName2)) continue;
5572
+ const imp = jayHtml.headlessImports.find((i) => i.contractName === contractName2);
5573
+ if (!imp) continue;
5574
+ if (imp.key || imp.template) continue;
5575
+ const templates = templatesFor(imp);
5576
+ if (templates.length === 0) continue;
5577
+ if (isRegionRuleSuppressed(imp, jayHtml, "REGION-NOT-LINKED")) continue;
5578
+ seen.add(contractName2);
5579
+ const templateHint = templates.length === 1 ? `template="${templates[0].path}"` : `template= one of: ${templates.map((t) => `"${t.path}"`).join(", ")}`;
5580
+ findings.push({
5581
+ message: `<jay:${contractName2}> is hand-authored, but a design-system template exists for contract "${imp.contractName}". Prefer linking it as a design-system element.`,
5582
+ suggestion: `Add ${templateHint} to the <script type="application/jay-headless"> import and run \`jay-stack sync\`. For a deliberate one-off, suppress on the import with jay-validations="REGION-NOT-LINKED" (or list the contract under allow-inline-region in <script type="application/jay-validations">). See agent-kit/designer/design-system-guide.md.`
5583
+ });
5584
+ }
5585
+ return findings;
5586
+ }
5587
+ function checkRegionOverrideNonContent(jayHtml, loadTemplate, contentTags = CONTENT_TAGS) {
5588
+ const importsWithTemplate = jayHtml.headlessImports.filter((imp) => imp.template);
5589
+ if (importsWithTemplate.length === 0) return [];
5590
+ const findings = [];
5591
+ const seen = /* @__PURE__ */ new Set();
5592
+ const templateBodyCache = /* @__PURE__ */ new Map();
5593
+ for (const region of collectRegionElements(jayHtml.body)) {
5594
+ const contractName2 = regionContractName(region);
5595
+ if (seen.has(contractName2)) continue;
5596
+ const imp = importsWithTemplate.find((i) => i.contractName === contractName2);
5597
+ if (!imp?.template) continue;
5598
+ if (isRegionRuleSuppressed(imp, jayHtml, "REGION-OVERRIDE-NON-CONTENT")) continue;
5599
+ let templateBody = templateBodyCache.get(imp.template);
5600
+ if (templateBody === void 0) {
5601
+ const content = loadTemplate(imp.template);
5602
+ templateBody = (content ? parse(content).querySelector("body") : null) ?? null;
5603
+ templateBodyCache.set(imp.template, templateBody);
5604
+ }
5605
+ if (!templateBody) continue;
5606
+ const hasNonContent = diffBodies(templateBody, region).some(
5607
+ (entry) => isNonContentOverride(entry, contentTags)
5608
+ );
5609
+ if (!hasNonContent) continue;
5610
+ seen.add(contractName2);
5611
+ findings.push({
5612
+ message: `<jay:${contractName2}> changes its design-system template's look or structure (style, class, or net-new layout DOM) — that is a different design, not a content tweak.`,
5613
+ suggestion: `Prefer a second design-system template (a new variant) for contract "${imp.contractName}" and link this region to it. Editing text/images, or enriching text with inline markup, is fine; use conditionals only for runtime state changes, not for a different design. To accept this override, suppress on the import with jay-validations="REGION-OVERRIDE-NON-CONTENT". See agent-kit/designer/design-system-guide.md.`
5614
+ });
5615
+ }
5616
+ return findings;
5617
+ }
5618
+ function checkComponentNoTemplate(contractFile, contractName2, hasTemplate, allowNoTemplate) {
5619
+ if (hasTemplate(contractFile)) return void 0;
5620
+ const base = path$1.basename(contractFile, JAY_CONTRACT_EXTENSION$1).toLowerCase();
5621
+ if (allowNoTemplate.has(contractName2.toLowerCase()) || allowNoTemplate.has(base))
5622
+ return void 0;
5623
+ return {
5624
+ message: `Component "${contractName2}" ships no .jay-html template. If it renders UI, create a reusable design-system template so consumers flatten it (template= + jay-stack sync) instead of hand-authoring each usage.`,
5625
+ suggestion: `Author a ${base}.jay-html next to the contract. If this component is intentionally UI-less (data/logic only), suppress project-wide with allow-no-template: ["${contractName2}"] in <script type="application/jay-validations">. See agent-kit/designer/design-system-guide.md.`
5626
+ };
5627
+ }
5628
+ function checkNoDesignSystem(regionsSeen, templateImportsSeen, suppressed) {
5629
+ if (suppressed || templateImportsSeen > 0) return void 0;
5630
+ if (regionsSeen > 0) {
5631
+ return {
5632
+ message: "This project composes components but none are design-system elements (no template= imports). Ship a .jay-html template with a reused component and flatten it so pages share consistent, upgradable UI.",
5633
+ suggestion: 'Add template= to a headless import and run `jay-stack sync`. Suppress project-wide with allow-no-design-system: true in <script type="application/jay-validations">. See agent-kit/designer/design-system-guide.md.'
5634
+ };
5635
+ }
5636
+ return {
5637
+ message: "This project shares no UI through design-system elements. Consider composing reusable sections as components with .jay-html templates — regions can be used without code, purely to flatten and share a design system.",
5638
+ suggestion: 'See agent-kit/designer/design-system-guide.md. Suppress project-wide with allow-no-design-system: true in <script type="application/jay-validations">.'
5639
+ };
5640
+ }
5641
+ function scopeBody(block) {
5642
+ return block.replace(/^@scope\s*\([^)]*\)(?:\s*to\s*\([^)]*\))?\s*\{/, "").replace(/\}\s*$/, "").replace(/\s+/g, " ").trim();
5643
+ }
5644
+ function checkRegionRecursion(pageHtml, pageDir, jayHtml, readFile) {
5645
+ if (!jayHtml.headlessImports.some((imp) => imp.template)) return [];
5646
+ const { errors } = materialise(pageHtml, buildMaterialiseOptions(pageDir, jayHtml, readFile));
5647
+ return errors.filter((e2) => e2.startsWith("template inclusion cycle"));
5648
+ }
4446
5649
  function checkHeadlessInstanceProps(jayHtml, file) {
4447
5650
  const imports = jayHtml.headlessImports;
4448
5651
  const warnings = [];
4449
5652
  function walkElement(element) {
4450
5653
  const tagName = element.rawTagName?.toLowerCase();
4451
5654
  if (tagName?.startsWith("jay:")) {
4452
- const contractName = tagName.substring(4);
4453
- const imp = imports.find((i) => i.contractName === contractName && i.contract);
5655
+ const contractName2 = tagName.substring(4);
5656
+ const imp = imports.find((i) => i.contractName === contractName2 && i.contract);
4454
5657
  if (imp?.contract) {
4455
5658
  const contract = imp.contract;
4456
5659
  const attrs = element.attributes ?? {};
@@ -4467,7 +5670,7 @@ function checkHeadlessInstanceProps(jayHtml, file) {
4467
5670
  for (const prop of passedProps) {
4468
5671
  if (!contractPropNamesLower.has(prop.toLowerCase())) {
4469
5672
  warnings.push(
4470
- `<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 }]`
5673
+ `<jay:${contractName2}> passes attribute "${prop}" but the "${contract.name}" contract does not declare it as a prop. Add to ${contractName2}.jay-contract: props: [{ name: ${prop}, type: string }]`
4471
5674
  );
4472
5675
  }
4473
5676
  }
@@ -4477,7 +5680,7 @@ function checkHeadlessInstanceProps(jayHtml, file) {
4477
5680
  for (const contractProp of contract.props) {
4478
5681
  if (contractProp.required && !passedPropsLower.has(contractProp.name.toLowerCase())) {
4479
5682
  warnings.push(
4480
- `<jay:${contractName}> is missing required prop "${contractProp.name}" declared in the "${contract.name}" contract.`
5683
+ `<jay:${contractName2}> is missing required prop "${contractProp.name}" declared in the "${contract.name}" contract.`
4481
5684
  );
4482
5685
  }
4483
5686
  }
@@ -4489,18 +5692,35 @@ function checkHeadlessInstanceProps(jayHtml, file) {
4489
5692
  }
4490
5693
  for (const contractProp of contract.props) {
4491
5694
  const attrValue = lowerAttrs[contractProp.name.toLowerCase()];
4492
- if (!attrValue) continue;
5695
+ if (attrValue === void 0) continue;
5696
+ const propType = contractProp.dataType;
4493
5697
  const bindingMatch = attrValue.match(/^\{(.+)\}$/);
4494
- if (!bindingMatch) continue;
5698
+ if (!bindingMatch) {
5699
+ if (attrValue.includes("{")) continue;
5700
+ if (propType && isEnumType$1(propType)) {
5701
+ if (!propType.values.includes(attrValue)) {
5702
+ warnings.push(
5703
+ `<jay:${contractName2}> prop "${contractProp.name}" = "${attrValue}" is not a declared value of enum(${propType.values.join(" | ")}). Use one of: ${propType.values.join(", ")}.`
5704
+ );
5705
+ }
5706
+ }
5707
+ continue;
5708
+ }
4495
5709
  const bindingPath = bindingMatch[1];
5710
+ const sourceType = resolveBindingDataType(bindingPath, jayHtml);
5711
+ if (propType && sourceType && !equalJayTypes(propType, sourceType)) {
5712
+ warnings.push(
5713
+ `<jay:${contractName2}> prop "${contractProp.name}" (${typeLabel(propType)}) is bound to {${bindingPath}} (${typeLabel(sourceType)}). The binding source type must match the prop type.`
5714
+ );
5715
+ }
4496
5716
  const sourcePhase = resolveBindingPhase(bindingPath, jayHtml);
4497
5717
  if (!sourcePhase) continue;
4498
- const propPhase = contractProp.phase ?? "slow";
5718
+ const propPhase = imp.structural ? resolveContractTag(contract, contractProp.name)?.phase ?? contractProp.phase ?? "slow" : contractProp.phase ?? "slow";
4499
5719
  const sourceOrder = PHASE_ORDER[sourcePhase] ?? 0;
4500
5720
  const propOrder = PHASE_ORDER[propPhase] ?? 0;
4501
5721
  if (sourceOrder > propOrder) {
4502
5722
  warnings.push(
4503
- `<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.`
5723
+ `<jay:${contractName2}> 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.`
4504
5724
  );
4505
5725
  }
4506
5726
  }
@@ -4666,13 +5886,17 @@ async function validateJayFiles(options = {}) {
4666
5886
  const warnings = [];
4667
5887
  const coverage = [];
4668
5888
  const parsedFiles = [];
5889
+ let regionsSeen = 0;
5890
+ let templateImportsSeen = 0;
4669
5891
  const pageJayHtmlFiles = await findJayFiles(scanDir);
4670
5892
  const componentJayHtmlFiles = await findJayFiles(componentsDir).catch(() => []);
4671
5893
  const jayHtmlFiles = [...pageJayHtmlFiles, ...componentJayHtmlFiles];
4672
- const contractFiles = [
4673
- ...await findContractFiles(scanDir),
4674
- ...await findContractFiles(componentsDir).catch(() => [])
4675
- ];
5894
+ const componentJayHtmlFileSet = new Set(componentJayHtmlFiles);
5895
+ const componentContractFiles = await findContractFiles(componentsDir).catch(
5896
+ () => []
5897
+ );
5898
+ const contractFiles = [...await findContractFiles(scanDir), ...componentContractFiles];
5899
+ const contractNameByFile = /* @__PURE__ */ new Map();
4676
5900
  if (options.verbose) {
4677
5901
  getLogger().info(chalk.gray(`Scanning directory: ${scanDir}`));
4678
5902
  getLogger().info(chalk.gray(`Found ${jayHtmlFiles.length} .jay-html files`));
@@ -4684,6 +5908,7 @@ async function validateJayFiles(options = {}) {
4684
5908
  try {
4685
5909
  const content = await promises.readFile(contractFile, "utf-8");
4686
5910
  const result = parseContract(content, path$1.basename(contractFile));
5911
+ if (result.val?.name) contractNameByFile.set(contractFile, result.val.name);
4687
5912
  if (result.validations.length > 0) {
4688
5913
  for (const validation of result.validations) {
4689
5914
  errors.push({
@@ -4711,7 +5936,7 @@ async function validateJayFiles(options = {}) {
4711
5936
  }
4712
5937
  for (const jayFile of jayHtmlFiles) {
4713
5938
  const relativePath = path$1.relative(projectRoot, jayFile);
4714
- const filename = path$1.basename(jayFile.replace(JAY_EXTENSION, ""));
5939
+ const filename = path$1.basename(jayFile.replace(JAY_EXTENSION$1, ""));
4715
5940
  const dirname = path$1.dirname(jayFile);
4716
5941
  try {
4717
5942
  const content = await promises.readFile(jayFile, "utf-8");
@@ -4774,12 +5999,95 @@ async function validateJayFiles(options = {}) {
4774
5999
  }
4775
6000
  const headlessPropResults = checkHeadlessInstanceProps(parsedFile.val, relativePath);
4776
6001
  for (const msg of headlessPropResults) {
4777
- if (msg.includes("is missing required prop") || msg.includes("source phase must be")) {
6002
+ if (msg.includes("is missing required prop") || msg.includes("source phase must be") || msg.includes("is not a declared value of enum") || msg.includes("binding source type must match")) {
4778
6003
  errors.push({ file: relativePath, message: msg, stage: "generate" });
4779
6004
  } else {
4780
6005
  warnings.push({ file: relativePath, message: msg });
4781
6006
  }
4782
6007
  }
6008
+ const readTemplateRel = (templatePath) => {
6009
+ try {
6010
+ return fs$1.readFileSync(path$1.resolve(dirname, templatePath), "utf-8");
6011
+ } catch {
6012
+ return void 0;
6013
+ }
6014
+ };
6015
+ const driftWarnings = checkRegionDrift(parsedFile.val, readTemplateRel);
6016
+ for (const finding of driftWarnings) {
6017
+ warnings.push({
6018
+ file: relativePath,
6019
+ message: finding.message,
6020
+ suggestion: finding.suggestion
6021
+ });
6022
+ }
6023
+ const cssDriftWarnings = checkRegionCssDrift(parsedFile.val, readTemplateRel);
6024
+ for (const finding of cssDriftWarnings) {
6025
+ warnings.push({
6026
+ file: relativePath,
6027
+ message: finding.message,
6028
+ suggestion: finding.suggestion
6029
+ });
6030
+ }
6031
+ const cssScopingWarnings = checkRegionCssScoping(parsedFile.val);
6032
+ for (const finding of cssScopingWarnings) {
6033
+ warnings.push({
6034
+ file: relativePath,
6035
+ message: finding.message,
6036
+ suggestion: finding.suggestion
6037
+ });
6038
+ }
6039
+ const nestedRefWarnings = checkNestedRegionRefs(parsedFile.val, readTemplateRel);
6040
+ for (const finding of nestedRefWarnings) {
6041
+ warnings.push({
6042
+ file: relativePath,
6043
+ message: finding.message,
6044
+ suggestion: finding.suggestion
6045
+ });
6046
+ }
6047
+ regionsSeen += collectRegionElements(parsedFile.val.body).length;
6048
+ templateImportsSeen += parsedFile.val.headlessImports.filter(
6049
+ (imp) => imp.template
6050
+ ).length;
6051
+ const notLinkedWarnings = componentJayHtmlFileSet.has(jayFile) ? [] : checkRegionNotLinked(
6052
+ parsedFile.val,
6053
+ (imp) => listTemplatesForContractFile(imp.contractPath).map((t) => ({
6054
+ ...t,
6055
+ path: "./" + path$1.relative(projectRoot, t.path).replace(/\\/g, "/")
6056
+ }))
6057
+ );
6058
+ for (const finding of notLinkedWarnings) {
6059
+ warnings.push({
6060
+ file: relativePath,
6061
+ message: finding.message,
6062
+ suggestion: finding.suggestion
6063
+ });
6064
+ }
6065
+ const overrideWarnings = checkRegionOverrideNonContent(
6066
+ parsedFile.val,
6067
+ readTemplateRel
6068
+ );
6069
+ for (const finding of overrideWarnings) {
6070
+ warnings.push({
6071
+ file: relativePath,
6072
+ message: finding.message,
6073
+ suggestion: finding.suggestion
6074
+ });
6075
+ }
6076
+ const recursionErrors = checkRegionRecursion(
6077
+ content,
6078
+ dirname,
6079
+ parsedFile.val,
6080
+ (absPath) => {
6081
+ try {
6082
+ return fs$1.readFileSync(absPath, "utf-8");
6083
+ } catch {
6084
+ return void 0;
6085
+ }
6086
+ }
6087
+ );
6088
+ for (const msg of recursionErrors) {
6089
+ errors.push({ file: relativePath, message: msg, stage: "generate" });
6090
+ }
4783
6091
  const fileCoverage = analyzeTagCoverage(parsedFile.val, relativePath);
4784
6092
  if (fileCoverage) {
4785
6093
  coverage.push(fileCoverage);
@@ -4852,6 +6160,44 @@ async function validateJayFiles(options = {}) {
4852
6160
  suggestion: "Add to .jay config:\n site:\n baseUrl: https://your-domain.com"
4853
6161
  });
4854
6162
  }
6163
+ const allowNoTemplate = /* @__PURE__ */ new Set();
6164
+ let allowNoDesignSystem = false;
6165
+ for (const { parsed } of parsedFiles) {
6166
+ const overrides = parsed.validationOverrides?.["jay-stack"];
6167
+ if (!overrides) continue;
6168
+ const list = overrides["allow-no-template"];
6169
+ if (Array.isArray(list))
6170
+ for (const name of list) allowNoTemplate.add(String(name).toLowerCase());
6171
+ if (overrides["allow-no-design-system"] === true) allowNoDesignSystem = true;
6172
+ }
6173
+ for (const contractFile of componentContractFiles) {
6174
+ const contractName2 = contractNameByFile.get(contractFile) ?? path$1.basename(contractFile, JAY_CONTRACT_EXTENSION$1);
6175
+ const finding = checkComponentNoTemplate(
6176
+ contractFile,
6177
+ contractName2,
6178
+ hasTemplateForContractFile,
6179
+ allowNoTemplate
6180
+ );
6181
+ if (finding) {
6182
+ warnings.push({
6183
+ file: path$1.relative(projectRoot, contractFile),
6184
+ message: finding.message,
6185
+ suggestion: finding.suggestion
6186
+ });
6187
+ }
6188
+ }
6189
+ const noDesignSystem = checkNoDesignSystem(
6190
+ regionsSeen,
6191
+ templateImportsSeen,
6192
+ allowNoDesignSystem
6193
+ );
6194
+ if (noDesignSystem) {
6195
+ warnings.push({
6196
+ file: ".jay",
6197
+ message: noDesignSystem.message,
6198
+ suggestion: noDesignSystem.suggestion
6199
+ });
6200
+ }
4855
6201
  const pluginValidators = await runPluginValidators(projectRoot, parsedFiles, errors, warnings);
4856
6202
  return {
4857
6203
  valid: errors.length === 0,
@@ -4988,6 +6334,20 @@ async function runValidate(scanPath, options) {
4988
6334
  process.exit(1);
4989
6335
  }
4990
6336
  }
6337
+ async function surfaceValidationIssues(projectPath, options) {
6338
+ let result;
6339
+ try {
6340
+ result = await validateJayFiles({ path: projectPath, verbose: options.verbose });
6341
+ } catch (error) {
6342
+ getLogger().warn(chalk.yellow(`Skipped jay-html validation: ${error?.message ?? error}`));
6343
+ return true;
6344
+ }
6345
+ if (!result.valid) {
6346
+ printJayValidationResult(result, { verbose: options.verbose });
6347
+ return false;
6348
+ }
6349
+ return true;
6350
+ }
4991
6351
  async function runValidatePlugin(pluginPath, options) {
4992
6352
  const result = await validatePlugin({
4993
6353
  pluginPath: pluginPath || process.cwd(),
@@ -5048,6 +6408,334 @@ function printPluginValidationResult(result, verbose) {
5048
6408
  logger.important(chalk.red(`${result.errors.length} errors found.`));
5049
6409
  }
5050
6410
  }
6411
+ const runValidate$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
6412
+ __proto__: null,
6413
+ runValidate,
6414
+ runValidatePlugin,
6415
+ surfaceValidationIssues
6416
+ }, Symbol.toStringTag, { value: "Module" }));
6417
+ async function runDev(projectPath, options) {
6418
+ const logLevel = options.quiet ? "silent" : options.verbose ? "verbose" : "info";
6419
+ setDevLogger(createDevLogger(logLevel));
6420
+ if (!options.quiet) {
6421
+ await surfaceValidationIssues(projectPath, { verbose: options.verbose });
6422
+ }
6423
+ const testMode = options.testMode || options.timeout !== void 0;
6424
+ await startDevServer({
6425
+ projectPath: projectPath || process.cwd(),
6426
+ testMode,
6427
+ timeout: options.timeout,
6428
+ logLevel
6429
+ });
6430
+ }
6431
+ async function resolveProductionContext(projectPath, versionOverride) {
6432
+ const resolvedPath = path__default.resolve(projectPath || process.cwd());
6433
+ const jayConfigPath = path__default.join(resolvedPath, ".jay");
6434
+ let pagesBase = "./src/pages";
6435
+ let siteBaseUrl;
6436
+ try {
6437
+ const jayConfig = YAML.parse(await fs$2.readFile(jayConfigPath, "utf-8"));
6438
+ pagesBase = jayConfig?.devServer?.pagesBase || pagesBase;
6439
+ siteBaseUrl = jayConfig?.site?.baseUrl;
6440
+ } catch {
6441
+ }
6442
+ const version = versionOverride || await resolveVersionFromPackageJson(resolvedPath);
6443
+ return {
6444
+ resolvedPath,
6445
+ pagesRoot: path__default.resolve(resolvedPath, pagesBase),
6446
+ buildRoot: path__default.join(resolvedPath, "build"),
6447
+ version,
6448
+ tsConfigFilePath: path__default.join(resolvedPath, "tsconfig.json"),
6449
+ siteBaseUrl
6450
+ };
6451
+ }
6452
+ async function resolveVersionFromPackageJson(projectRoot) {
6453
+ try {
6454
+ const pkgJson = JSON.parse(
6455
+ await fs$2.readFile(path__default.join(projectRoot, "package.json"), "utf-8")
6456
+ );
6457
+ if (pkgJson.version) {
6458
+ return pkgJson.version;
6459
+ }
6460
+ } catch {
6461
+ }
6462
+ return "1";
6463
+ }
6464
+ function initLogger(verbose) {
6465
+ const logLevel = verbose ? "verbose" : "info";
6466
+ setDevLogger(createDevLogger(logLevel));
6467
+ }
6468
+ async function runBuild(projectPath, options) {
6469
+ initLogger(options.verbose);
6470
+ const { surfaceValidationIssues: surfaceValidationIssues2 } = await Promise.resolve().then(() => runValidate$1);
6471
+ const valid = await surfaceValidationIssues2(projectPath, { verbose: options.verbose });
6472
+ if (!valid) {
6473
+ process.exit(1);
6474
+ }
6475
+ const ctx = await resolveProductionContext(projectPath, options.version);
6476
+ await generateDefinitionFiles(ctx);
6477
+ const { buildVersion } = await import("./index-l6QVpvnY.js");
6478
+ await buildVersion({
6479
+ version: ctx.version,
6480
+ projectRoot: ctx.resolvedPath,
6481
+ pagesRoot: ctx.pagesRoot,
6482
+ buildRoot: ctx.buildRoot,
6483
+ concurrency: 4,
6484
+ tsConfigFilePath: ctx.tsConfigFilePath,
6485
+ minify: options.minify,
6486
+ siteBaseUrl: ctx.siteBaseUrl
6487
+ });
6488
+ }
6489
+ async function generateDefinitionFiles(ctx) {
6490
+ const { glob: glob2 } = await import("glob");
6491
+ const { generatePageDefinitionFiles: generatePageDefinitionFiles2 } = await Promise.resolve().then(() => generatePageDefinitionFiles$1);
6492
+ const { generateContractDefinitionFiles: generateContractDefinitionFiles2 } = await Promise.resolve().then(() => generateContractDefinitionFiles$1);
6493
+ const jayHtmlPaths = await glob2("**/page.jay-html", {
6494
+ cwd: ctx.pagesRoot,
6495
+ absolute: true,
6496
+ ignore: ["**/node_modules/**", "**/build/**", "**/dist/**"]
6497
+ });
6498
+ await generatePageDefinitionFiles2(jayHtmlPaths, ctx.tsConfigFilePath, ctx.resolvedPath);
6499
+ await generateContractDefinitionFiles2(path__default.resolve(ctx.resolvedPath, "src"));
6500
+ }
6501
+ async function runServe(projectPath, options) {
6502
+ initLogger(options.verbose);
6503
+ const ctx = await resolveProductionContext(projectPath, options.version);
6504
+ if (options.role === "renderer") {
6505
+ const { startRendererServer } = await import("@jay-framework/production-server");
6506
+ await startRendererServer({
6507
+ buildRoot: ctx.buildRoot,
6508
+ version: ctx.version,
6509
+ port: parseInt(options.port, 10),
6510
+ projectRoot: ctx.resolvedPath,
6511
+ pagesRoot: ctx.pagesRoot,
6512
+ tsConfigFilePath: ctx.tsConfigFilePath,
6513
+ siteBaseUrl: ctx.siteBaseUrl
6514
+ });
6515
+ } else {
6516
+ const { startMainServer } = await import("@jay-framework/production-server");
6517
+ await startMainServer({
6518
+ buildRoot: ctx.buildRoot,
6519
+ version: ctx.version,
6520
+ port: parseInt(options.port, 10),
6521
+ testMode: options.testMode,
6522
+ publicBasePath: options.staticBaseUrl,
6523
+ serveStatic: options.serveStatic
6524
+ });
6525
+ }
6526
+ }
6527
+ async function runRebuild(projectPath, options) {
6528
+ initLogger(options.verbose);
6529
+ const ctx = await resolveProductionContext(projectPath, options.version);
6530
+ let params;
6531
+ if (options.params) {
6532
+ params = JSON.parse(options.params);
6533
+ }
6534
+ const { rebuild } = await import("@jay-framework/production-server");
6535
+ let target;
6536
+ if (options.contract) {
6537
+ target = { mode: "contract", contractName: options.contract, params };
6538
+ } else if (options.route) {
6539
+ target = { mode: "route", routePattern: options.route, params };
6540
+ } else if (options.url) {
6541
+ target = { mode: "url", url: options.url };
6542
+ } else {
6543
+ getLogger().error(chalk.red("One of --contract, --route, or --url is required"));
6544
+ process.exit(1);
6545
+ }
6546
+ const result = await rebuild({
6547
+ projectRoot: ctx.resolvedPath,
6548
+ pagesRoot: ctx.pagesRoot,
6549
+ buildRoot: ctx.buildRoot,
6550
+ version: ctx.version,
6551
+ target,
6552
+ tsConfigFilePath: ctx.tsConfigFilePath,
6553
+ siteBaseUrl: ctx.siteBaseUrl
6554
+ });
6555
+ if (result.errors.length > 0) {
6556
+ for (const err of result.errors) {
6557
+ getLogger().error(
6558
+ chalk.red(` Error: ${err.route} ${JSON.stringify(err.params)}: ${err.error}`)
6559
+ );
6560
+ }
6561
+ process.exit(1);
6562
+ }
6563
+ }
6564
+ function syncPageContent(rawPage, pageDir, jayHtml, readFile) {
6565
+ const regions = countDesignSystemRegions(jayHtml);
6566
+ if (regions === 0) {
6567
+ return { content: rawPage, changed: false, regions: 0, errors: [] };
6568
+ }
6569
+ const opts = buildMaterialiseOptions(pageDir, jayHtml, readFile, { preserveOverrides: true });
6570
+ const { html, css, errors } = materialise(rawPage, opts);
6571
+ const merged = mergeScopeCss(html, css);
6572
+ const content = prettifyHtml(merged);
6573
+ const changed = content !== prettifyHtml(rawPage);
6574
+ return { content, changed, regions, errors };
6575
+ }
6576
+ function countDesignSystemRegions(jayHtml) {
6577
+ const named = new Set(
6578
+ jayHtml.headlessImports.filter((i) => i.template).map((i) => i.contractName.toLowerCase())
6579
+ );
6580
+ if (named.size === 0) return 0;
6581
+ let count = 0;
6582
+ const visit = (el) => {
6583
+ for (const child of el.childNodes ?? []) {
6584
+ if (child.rawTagName === void 0) continue;
6585
+ const tag = (child.rawTagName ?? "").toLowerCase();
6586
+ if (tag.startsWith("jay:") && named.has(tag.substring(4))) count++;
6587
+ visit(child);
6588
+ }
6589
+ };
6590
+ visit(jayHtml.body);
6591
+ return count;
6592
+ }
6593
+ function mergeScopeCss(html, aggregatedCss) {
6594
+ if (!aggregatedCss.trim()) return html;
6595
+ const root = parse(html);
6596
+ const head = root.querySelector("head");
6597
+ if (!head) return html;
6598
+ const styleEl = head.querySelector("style");
6599
+ const existingCss = styleEl ? styleEl.textContent : "";
6600
+ const groups = splitScopeBlocks(aggregatedCss);
6601
+ const groupOf = /* @__PURE__ */ new Map();
6602
+ const canonicalBody = /* @__PURE__ */ new Map();
6603
+ for (const g of groups) {
6604
+ const nb = normalizeCssBody(scopeInnerBody(g.block));
6605
+ for (const s2 of g.selectors) {
6606
+ groupOf.set(s2, g);
6607
+ canonicalBody.set(s2, nb);
6608
+ }
6609
+ }
6610
+ const overridden = /* @__PURE__ */ new Set();
6611
+ const kept = [];
6612
+ for (const seg of tokenizeCss(existingCss)) {
6613
+ if (seg.type === "raw") {
6614
+ if (seg.text.trim()) kept.push(seg.text.trim());
6615
+ continue;
6616
+ }
6617
+ const marked = hasOverrideMarker(seg.block);
6618
+ const body = normalizeCssBody(scopeInnerBody(seg.block));
6619
+ const keepMembers = seg.selectors.filter((s2) => {
6620
+ if (!groupOf.has(s2)) return true;
6621
+ const diverges = marked && body !== canonicalBody.get(s2);
6622
+ if (diverges) overridden.add(s2);
6623
+ return diverges;
6624
+ });
6625
+ if (keepMembers.length === seg.selectors.length) kept.push(seg.block);
6626
+ else if (keepMembers.length > 0) kept.push(narrowScopeBlock(seg.block, keepMembers));
6627
+ }
6628
+ const appended = [];
6629
+ for (const g of groups) {
6630
+ const plain = g.selectors.filter((s2) => !overridden.has(s2));
6631
+ if (plain.length === 0) continue;
6632
+ appended.push(
6633
+ plain.length === g.selectors.length ? g.block : narrowScopeBlock(g.block, plain)
6634
+ );
6635
+ }
6636
+ const newCss = [...kept, ...appended].filter(Boolean).join("\n\n");
6637
+ if (newCss === existingCss.trim()) return html;
6638
+ if (styleEl) styleEl.set_content(newCss);
6639
+ else head.insertAdjacentHTML("beforeend", `<style>${newCss}</style>`);
6640
+ return root.toString();
6641
+ }
6642
+ function hasOverrideMarker(block) {
6643
+ return /\/\*\s*jay:override\b/.test(block);
6644
+ }
6645
+ function narrowScopeBlock(block, selectors) {
6646
+ return block.replace(
6647
+ /^(@scope\s*\()\s*[^)]*?\s*(\)(?:\s*to\s*\([^)]*\))?\s*\{)/,
6648
+ `$1${selectors.join(", ")}$2`
6649
+ );
6650
+ }
6651
+ async function resolveTargets(projectRoot, scanDirs, target, all) {
6652
+ if (target && !all) {
6653
+ const file = target.split("#")[0];
6654
+ const abs = path$1.isAbsolute(file) ? file : path$1.resolve(projectRoot, file);
6655
+ return [abs];
6656
+ }
6657
+ const found = await Promise.all(
6658
+ scanDirs.map((dir) => glob(`${dir}/**/*${JAY_EXTENSION$1}`).catch(() => []))
6659
+ );
6660
+ return [...new Set(found.flat())];
6661
+ }
6662
+ async function runSync(target, options = {}) {
6663
+ const logger = getLogger();
6664
+ const projectRoot = options.path ? path$1.resolve(options.path) : process.cwd();
6665
+ if (target && target.includes("#")) {
6666
+ logger.important(
6667
+ chalk.yellow(
6668
+ `Per-region targeting (#ref) is not yet supported — syncing the whole page "${target.split("#")[0]}".`
6669
+ )
6670
+ );
6671
+ }
6672
+ const resolvedConfig = getConfigWithDefaults(loadConfig(projectRoot));
6673
+ const scanDirs = [
6674
+ path$1.resolve(projectRoot, resolvedConfig.devServer.pagesBase),
6675
+ path$1.resolve(projectRoot, resolvedConfig.devServer.componentsBase)
6676
+ ];
6677
+ const files = await resolveTargets(projectRoot, scanDirs, target, options.all ?? false);
6678
+ let filesChanged = 0;
6679
+ let regionsSynced = 0;
6680
+ let hadError = false;
6681
+ for (const file of files) {
6682
+ const relativePath = path$1.relative(projectRoot, file);
6683
+ let content;
6684
+ try {
6685
+ content = await promises.readFile(file, "utf-8");
6686
+ } catch {
6687
+ logger.error(chalk.red(`✗ ${relativePath}: could not read file`));
6688
+ hadError = true;
6689
+ continue;
6690
+ }
6691
+ const dirname = path$1.dirname(file);
6692
+ const filename = path$1.basename(file.replace(JAY_EXTENSION$1, ""));
6693
+ const parsed = await parseJayFile(
6694
+ content,
6695
+ filename,
6696
+ dirname,
6697
+ {},
6698
+ JAY_IMPORT_RESOLVER,
6699
+ projectRoot
6700
+ );
6701
+ if (parsed.validations.length > 0 || !parsed.val) {
6702
+ logger.error(chalk.red(`✗ ${relativePath}: ${parsed.validations.join("; ")}`));
6703
+ hadError = true;
6704
+ continue;
6705
+ }
6706
+ const result = syncPageContent(content, dirname, parsed.val, (absPath) => {
6707
+ try {
6708
+ return fs$1.readFileSync(absPath, "utf-8");
6709
+ } catch {
6710
+ return void 0;
6711
+ }
6712
+ });
6713
+ if (result.errors.length > 0) {
6714
+ for (const err of result.errors) logger.error(chalk.red(`✗ ${relativePath}: ${err}`));
6715
+ hadError = true;
6716
+ continue;
6717
+ }
6718
+ if (result.regions === 0) continue;
6719
+ if (result.changed) {
6720
+ await promises.writeFile(file, result.content, "utf-8");
6721
+ filesChanged++;
6722
+ regionsSynced += result.regions;
6723
+ logger.important(
6724
+ chalk.green(`✓ synced ${result.regions} region(s) in ${relativePath}`)
6725
+ );
6726
+ } else if (options.verbose) {
6727
+ logger.important(chalk.gray(`• ${relativePath} — already in sync`));
6728
+ }
6729
+ }
6730
+ if (filesChanged === 0) {
6731
+ logger.important("Nothing to sync.");
6732
+ } else {
6733
+ logger.important(
6734
+ chalk.green(`Synced ${regionsSynced} region(s) across ${filesChanged} file(s).`)
6735
+ );
6736
+ }
6737
+ if (hadError) process.exit(1);
6738
+ }
5051
6739
  const ALL_ROLES = ["designer", "developer", "plugin", "devops"];
5052
6740
  async function runAgentKit(options) {
5053
6741
  const projectRoot = process.cwd();
@@ -5122,6 +6810,11 @@ Materialized ${totalContracts} contracts`));
5122
6810
  getLogger().important(` Static: ${result.staticCount}`);
5123
6811
  getLogger().important(` Dynamic: ${result.dynamicCount}`);
5124
6812
  getLogger().important(` Output: ${result.outputDir}`);
6813
+ if (result.pluginsIndex.designSystemIndex) {
6814
+ getLogger().important(
6815
+ ` Design-system catalog: agent-kit/design-system-index.yaml + design-system.md`
6816
+ );
6817
+ }
5125
6818
  }
5126
6819
  return { initErrors, viteServer: keepViteAlive ? viteServer : void 0 };
5127
6820
  } catch (error) {
@@ -5384,7 +7077,7 @@ async function runParams(contractRef, options, projectRoot, initializeServices)
5384
7077
  let viteServer;
5385
7078
  try {
5386
7079
  let pluginName;
5387
- let contractName;
7080
+ let contractName2;
5388
7081
  if (contractRef.startsWith("@")) {
5389
7082
  const secondSlash = contractRef.indexOf("/", contractRef.indexOf("/") + 1);
5390
7083
  if (secondSlash === -1) {
@@ -5394,7 +7087,7 @@ async function runParams(contractRef, options, projectRoot, initializeServices)
5394
7087
  process.exit(1);
5395
7088
  }
5396
7089
  pluginName = contractRef.substring(0, secondSlash);
5397
- contractName = contractRef.substring(secondSlash + 1);
7090
+ contractName2 = contractRef.substring(secondSlash + 1);
5398
7091
  } else {
5399
7092
  const slashIndex = contractRef.indexOf("/");
5400
7093
  if (slashIndex === -1) {
@@ -5404,7 +7097,7 @@ async function runParams(contractRef, options, projectRoot, initializeServices)
5404
7097
  process.exit(1);
5405
7098
  }
5406
7099
  pluginName = contractRef.substring(0, slashIndex);
5407
- contractName = contractRef.substring(slashIndex + 1);
7100
+ contractName2 = contractRef.substring(slashIndex + 1);
5408
7101
  }
5409
7102
  if (options.verbose) {
5410
7103
  getLogger().info("Starting Vite for TypeScript support...");
@@ -5412,10 +7105,10 @@ async function runParams(contractRef, options, projectRoot, initializeServices)
5412
7105
  viteServer = await createViteForCli({ projectRoot });
5413
7106
  await initializeServices(projectRoot, viteServer);
5414
7107
  const { resolvePluginComponent } = await import("@jay-framework/compiler-shared");
5415
- const resolution = resolvePluginComponent(projectRoot, pluginName, contractName);
7108
+ const resolution = resolvePluginComponent(projectRoot, pluginName, contractName2);
5416
7109
  if (resolution.validations.length > 0 || !resolution.val) {
5417
7110
  getLogger().error(
5418
- chalk.red(`❌ Could not resolve plugin "${pluginName}" contract "${contractName}"`)
7111
+ chalk.red(`❌ Could not resolve plugin "${pluginName}" contract "${contractName2}"`)
5419
7112
  );
5420
7113
  for (const msg of resolution.validations) {
5421
7114
  getLogger().error(` ${msg}`);
@@ -5915,6 +7608,17 @@ program.command("validate").description("Validate all .jay-html and .jay-contrac
5915
7608
  process.exit(1);
5916
7609
  }
5917
7610
  });
7611
+ program.command("sync [target]").description(
7612
+ "Re-flatten design-system regions from their source templates (keeps override facets)"
7613
+ ).option("-p, --path <path>", "Project root (default: cwd)").option("--all", "Sync every page in the project (default when no target is given)").option("-v, --verbose", "Show pages that are already in sync").action(async (target, options) => {
7614
+ try {
7615
+ await runSync(target, options);
7616
+ } catch (error) {
7617
+ getLogger().error(chalk.red("Sync failed:") + " " + error.message);
7618
+ if (error.stack) getLogger().error(error.stack);
7619
+ process.exit(1);
7620
+ }
7621
+ });
5918
7622
  program.command("validate-plugin").description("Validate a Jay Stack plugin package").option("-p, --path <path>", "Plugin root (default: cwd)").option("--local", "Validate local plugins in src/plugins/").option("-v, --verbose", "Show detailed validation output").option("--strict", "Treat warnings as errors (for CI)").option("--generate-types", "Generate .d.ts files for contracts").action(async (options) => {
5919
7623
  try {
5920
7624
  await runValidatePlugin(options.path, options);