@ikas/component-cli 2.6.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/build-tools.d.ts +11 -0
  2. package/dist/build-tools.d.ts.map +1 -0
  3. package/dist/build-tools.js +11 -0
  4. package/dist/build-tools.js.map +1 -0
  5. package/dist/commands/config.d.ts +35 -0
  6. package/dist/commands/config.d.ts.map +1 -1
  7. package/dist/commands/config.js +583 -130
  8. package/dist/commands/config.js.map +1 -1
  9. package/dist/commands/create-design-tokens.d.ts.map +1 -1
  10. package/dist/commands/create-design-tokens.js +44 -14
  11. package/dist/commands/create-design-tokens.js.map +1 -1
  12. package/dist/commands/dev.d.ts.map +1 -1
  13. package/dist/commands/dev.js +15 -23
  14. package/dist/commands/dev.js.map +1 -1
  15. package/dist/commands/publish.d.ts.map +1 -1
  16. package/dist/commands/publish.js +1 -0
  17. package/dist/commands/publish.js.map +1 -1
  18. package/dist/commands/update-section-prop.d.ts.map +1 -1
  19. package/dist/commands/update-section-prop.js +24 -8
  20. package/dist/commands/update-section-prop.js.map +1 -1
  21. package/dist/types.d.ts +7 -0
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/utils/compile.d.ts.map +1 -1
  24. package/dist/utils/compile.js +10 -2
  25. package/dist/utils/compile.js.map +1 -1
  26. package/dist/utils/component-helpers.d.ts +13 -0
  27. package/dist/utils/component-helpers.d.ts.map +1 -1
  28. package/dist/utils/component-helpers.js +55 -24
  29. package/dist/utils/component-helpers.js.map +1 -1
  30. package/dist/utils/find-component-for-file.d.ts +12 -0
  31. package/dist/utils/find-component-for-file.d.ts.map +1 -0
  32. package/dist/utils/find-component-for-file.js +24 -0
  33. package/dist/utils/find-component-for-file.js.map +1 -0
  34. package/dist/utils/forbid-external-packages.d.ts.map +1 -1
  35. package/dist/utils/forbid-external-packages.js +8 -0
  36. package/dist/utils/forbid-external-packages.js.map +1 -1
  37. package/dist/utils/websocket-server.d.ts +7 -0
  38. package/dist/utils/websocket-server.d.ts.map +1 -1
  39. package/dist/utils/websocket-server.js.map +1 -1
  40. package/package.json +13 -1
@@ -1,7 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { PROP_TYPES, toPascalCase, generateTypesFile, generateGlobalTypesFile, collectUsedEnumIds, generateComponentFile, generateStylesFile, generateProjectId, isValidProjectId, generateComponentId, generateUniqueId, updateBarrelExport, findPropGroup, collectPropGroupIds, movePropGroupInTree, validateFilteredComponentIds, } from "../utils/component-helpers.js";
4
+ import { PROP_TYPES, toPascalCase, generateTypesFile, isBuiltInEnumTypeId, generateGlobalTypesFile, collectUsedEnumIds, generateComponentFile, generateStylesFile, generateProjectId, isValidProjectId, generateComponentId, generateUniqueId, updateBarrelExport, findPropGroup, collectPropGroupIds, movePropGroupInTree, validateFilteredComponentIds, } from "../utils/component-helpers.js";
5
5
  function loadConfig() {
6
6
  const configPath = path.resolve(process.cwd(), "ikas.config.json");
7
7
  if (!fs.existsSync(configPath)) {
@@ -96,6 +96,11 @@ function assertKnownComponentIds(input, config) {
96
96
  }
97
97
  return input;
98
98
  }
99
+ // The set of component ids that currently exist in the project, for validateComponentDefaultValue's
100
+ // existence check. Undefined when no config is loaded, which makes the check a no-op (back-compat).
101
+ function existingComponentIdSet(config) {
102
+ return config ? new Set(config.components.map((c) => c.id)) : undefined;
103
+ }
99
104
  function regenerateTypes(component, componentType, config) {
100
105
  const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
101
106
  const typesPath = path.join(componentDir, "types.ts");
@@ -162,7 +167,91 @@ const ALLOWED_PROP_FIELDS = new Set([
162
167
  "enumTypeId",
163
168
  "filteredComponentIds",
164
169
  "privateVarMap",
170
+ "numberRangeData",
165
171
  ]);
172
+ // Emit the standard CLI error envelope and exit(1) when a validator returned a message; a no-op
173
+ // when the message is null. Collapses the repeated `if (err) { console.log(...); process.exit(1) }`
174
+ // tail that follows every default-value validator call.
175
+ function failIf(error) {
176
+ if (error) {
177
+ console.log(JSON.stringify({ success: false, error }));
178
+ process.exit(1);
179
+ }
180
+ }
181
+ /**
182
+ * Validate/sanitize a numberRangeData object for a NUMBER_RANGE prop. Accepts an object
183
+ * or a JSON string. min/max/interval are truncated to integers; only a positive interval
184
+ * is kept (0/negative would yield an invalid slider step). Exits with a structured error
185
+ * on malformed input.
186
+ */
187
+ function parseNumberRangeData(raw, propName) {
188
+ const fail = (error) => {
189
+ console.log(JSON.stringify({ success: false, error }));
190
+ process.exit(1);
191
+ };
192
+ let obj = raw;
193
+ if (typeof raw === "string") {
194
+ try {
195
+ obj = JSON.parse(raw);
196
+ }
197
+ catch {
198
+ fail(`Invalid --numberRangeData JSON for prop "${propName}": ${raw}`);
199
+ }
200
+ }
201
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
202
+ fail(`numberRangeData for prop "${propName}" must be an object { min, max, interval?, unit? }.`);
203
+ }
204
+ const o = obj;
205
+ const asInt = (v, field) => {
206
+ if (v === undefined || v === null)
207
+ return undefined;
208
+ if (typeof v !== "number" || !Number.isFinite(v)) {
209
+ fail(`numberRangeData.${field} for prop "${propName}" must be a finite number.`);
210
+ }
211
+ return Math.trunc(v);
212
+ };
213
+ const min = asInt(o.min, "min");
214
+ const max = asInt(o.max, "max");
215
+ const interval = asInt(o.interval, "interval");
216
+ const unit = typeof o.unit === "string" && o.unit.trim() ? o.unit.trim() : undefined;
217
+ if (min !== undefined && max !== undefined && min >= max) {
218
+ fail(`numberRangeData for prop "${propName}": min (${min}) must be less than max (${max}).`);
219
+ }
220
+ return {
221
+ ...(min !== undefined ? { min } : {}),
222
+ ...(max !== undefined ? { max } : {}),
223
+ ...(interval !== undefined && interval > 0 ? { interval } : {}),
224
+ ...(unit ? { unit } : {}),
225
+ };
226
+ }
227
+ const numberRangeDataRequiredError = (propName) => `numberRangeData with min and max is required for NUMBER_RANGE prop "${propName}". ` +
228
+ `Provide it, e.g. --numberRangeData '{"min":0,"max":100,"interval":5,"unit":"px"}' ` +
229
+ `(or a "numberRangeData" field inside --props). interval and unit are optional.`;
230
+ // A NUMBER_RANGE slider config is complete only with both bounds; interval/unit stay optional.
231
+ export const isNumberRangeComplete = (data) => data.min !== undefined && data.max !== undefined;
232
+ /**
233
+ * A NUMBER_RANGE prop must carry a slider config with at least min and max — the editor UI marks
234
+ * Minimum/Maksimum Değer as required, so a prop authored without them yields a half-configured
235
+ * slider. Enforce it at creation time: reject when numberRangeData is absent or missing min/max.
236
+ */
237
+ export function requireNumberRangeData(raw, propName) {
238
+ if (raw === undefined) {
239
+ console.log(JSON.stringify({
240
+ success: false,
241
+ error: numberRangeDataRequiredError(propName),
242
+ }));
243
+ process.exit(1);
244
+ }
245
+ const parsed = parseNumberRangeData(raw, propName);
246
+ if (!isNumberRangeComplete(parsed)) {
247
+ console.log(JSON.stringify({
248
+ success: false,
249
+ error: numberRangeDataRequiredError(propName),
250
+ }));
251
+ process.exit(1);
252
+ }
253
+ return parsed;
254
+ }
166
255
  /**
167
256
  * Parse and validate the --props JSON flag for add-component.
168
257
  * Returns validated ComponentProp[] or exits with error.
@@ -188,6 +277,8 @@ async function parsePropsFlag(propsJson, config, configPath) {
188
277
  }
189
278
  const seenNames = new Set();
190
279
  const props = [];
280
+ // Loop-invariant: build the known-id set once for the whole batch, not per prop.
281
+ const existingComponentIds = existingComponentIdSet(config);
191
282
  for (const raw of rawProps) {
192
283
  const r = raw;
193
284
  const unknownFields = Object.keys(r).filter((k) => !ALLOWED_PROP_FIELDS.has(k));
@@ -240,23 +331,35 @@ async function parsePropsFlag(propsJson, config, configPath) {
240
331
  }));
241
332
  process.exit(1);
242
333
  }
334
+ // Dynamic (merchant-data reference) props can't carry a static default.
335
+ rejectDynamicDefault(r.name, propType, r.defaultValue);
243
336
  // Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
244
337
  if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
245
338
  r.defaultValue !== undefined) {
246
339
  const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
247
- if (linkError) {
248
- console.log(JSON.stringify({ success: false, error: linkError }));
249
- process.exit(1);
250
- }
340
+ failIf(linkError);
251
341
  }
252
342
  // Validate SVG / SVG_LIST defaultValue (reject malformed/oversized/unsafe SVG)
253
343
  if ((propType === "SVG" || propType === "SVG_LIST") &&
254
344
  r.defaultValue !== undefined) {
255
345
  const svgError = validateSvgDefaultValue(propType, r.defaultValue, r.name);
256
- if (svgError) {
257
- console.log(JSON.stringify({ success: false, error: svgError }));
258
- process.exit(1);
259
- }
346
+ failIf(svgError);
347
+ }
348
+ // Validate COMPONENT / COMPONENT_LIST defaultValue (reference-only child code components)
349
+ if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
350
+ r.defaultValue !== undefined) {
351
+ failIf(validateComponentDefaultValue(propType, r.defaultValue, r.name, r.filteredComponentIds, existingComponentIds));
352
+ }
353
+ // Validate TYPE (structured/style) defaultValue shape — must be an object (or an array for an
354
+ // _array typeId), never a bare string that would be emitted as a wrong-shaped default.
355
+ if (propType === "TYPE" && r.defaultValue !== undefined) {
356
+ const typeError = validateTypeDefaultValue(r.defaultValue, r.name, r.typeId);
357
+ failIf(typeError);
358
+ }
359
+ // Validate NUMBER defaultValue — reject empty/non-finite (Number("") silently becomes 0).
360
+ if (propType === "NUMBER" && r.defaultValue !== undefined) {
361
+ const numError = validateNumberDefaultValue(r.defaultValue, r.name);
362
+ failIf(numError);
260
363
  }
261
364
  // Auto-generate displayName if omitted
262
365
  const displayName = typeof r.displayName === "string" && r.displayName
@@ -285,7 +388,7 @@ async function parsePropsFlag(propsJson, config, configPath) {
285
388
  if (config &&
286
389
  configPath &&
287
390
  typeof r.enumTypeId === "string" &&
288
- !r.enumTypeId.startsWith("@ikas/")) {
391
+ !isBuiltInEnumTypeId(r.enumTypeId)) {
289
392
  const resolved = await resolveEnumType(r.enumTypeId, config, configPath);
290
393
  if (!resolved) {
291
394
  console.log(JSON.stringify({
@@ -328,6 +431,9 @@ async function parsePropsFlag(propsJson, config, configPath) {
328
431
  privateVarMap: r.privateVarMap,
329
432
  }
330
433
  : {}),
434
+ ...(propType === "NUMBER_RANGE"
435
+ ? { numberRangeData: requireNumberRangeData(r.numberRangeData, r.name) }
436
+ : {}),
331
437
  };
332
438
  props.push(prop);
333
439
  }
@@ -375,6 +481,8 @@ async function addComponent(name, options) {
375
481
  const props = options.props
376
482
  ? await parsePropsFlag(options.props, config, configPath)
377
483
  : [];
484
+ // Reject section-disallowed TYPE props at definition time (best-effort, needs dev server)
485
+ await assertTypeIdsUsable(componentType, props.flatMap((p) => (p.type === "TYPE" && p.typeId ? [p.typeId] : [])));
378
486
  const componentId = generateComponentId(config.projectId);
379
487
  const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
380
488
  // Create directory
@@ -465,6 +573,10 @@ async function addProp(ref, options) {
465
573
  }));
466
574
  process.exit(1);
467
575
  }
576
+ // Reject section-disallowed types at definition time (best-effort, needs dev server)
577
+ if (propType === "TYPE" && options.typeId) {
578
+ await assertTypeIdsUsable(component.type, [options.typeId]);
579
+ }
468
580
  // Validate enumTypeId for ENUM props
469
581
  if (propType === "ENUM" && !options.enumTypeId) {
470
582
  console.log(JSON.stringify({
@@ -477,7 +589,7 @@ async function addProp(ref, options) {
477
589
  // (checks config.customTypes first, then falls back to the live dev server)
478
590
  if (propType === "ENUM" &&
479
591
  options.enumTypeId &&
480
- !options.enumTypeId.startsWith("@ikas/")) {
592
+ !isBuiltInEnumTypeId(options.enumTypeId)) {
481
593
  const resolved = await resolveEnumType(options.enumTypeId, config, configPath);
482
594
  if (!resolved) {
483
595
  console.log(JSON.stringify({
@@ -551,24 +663,39 @@ async function addProp(ref, options) {
551
663
  process.exit(1);
552
664
  }
553
665
  }
666
+ rejectDynamicDefault(options.name, propType, options.defaultValue);
554
667
  const parsedDefaultValue = options.defaultValue !== undefined
555
668
  ? parseDefaultValue(options.defaultValue, propType)
556
669
  : undefined;
557
670
  if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
558
671
  parsedDefaultValue !== undefined) {
559
672
  const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
560
- if (linkError) {
561
- console.log(JSON.stringify({ success: false, error: linkError }));
562
- process.exit(1);
563
- }
673
+ failIf(linkError);
564
674
  }
565
675
  if ((propType === "SVG" || propType === "SVG_LIST") &&
566
676
  parsedDefaultValue !== undefined) {
567
677
  const svgError = validateSvgDefaultValue(propType, parsedDefaultValue, options.name);
568
- if (svgError) {
569
- console.log(JSON.stringify({ success: false, error: svgError }));
570
- process.exit(1);
571
- }
678
+ failIf(svgError);
679
+ }
680
+ if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
681
+ parsedDefaultValue !== undefined) {
682
+ failIf(validateComponentDefaultValue(propType, parsedDefaultValue, options.name, parsedFilteredIds, existingComponentIdSet(config)));
683
+ }
684
+ if (propType === "TYPE" && parsedDefaultValue !== undefined) {
685
+ const typeError = validateTypeDefaultValue(parsedDefaultValue, options.name, options.typeId);
686
+ failIf(typeError);
687
+ }
688
+ // Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
689
+ if (propType === "NUMBER" && options.defaultValue !== undefined) {
690
+ const numError = validateNumberDefaultValue(options.defaultValue, options.name);
691
+ failIf(numError);
692
+ }
693
+ const parsedNumberRangeData = propType === "NUMBER_RANGE"
694
+ ? requireNumberRangeData(options.numberRangeData, options.name)
695
+ : undefined;
696
+ if (propType === "NUMBER_RANGE" && parsedDefaultValue !== undefined) {
697
+ const nrError = validateNumberRangeDefaultValue(parsedDefaultValue, parsedNumberRangeData, options.name);
698
+ failIf(nrError);
572
699
  }
573
700
  const newProp = {
574
701
  name: options.name,
@@ -584,6 +711,9 @@ async function addProp(ref, options) {
584
711
  ...(options.enumTypeId ? { enumTypeId: options.enumTypeId } : {}),
585
712
  ...(parsedFilteredIds ? { filteredComponentIds: parsedFilteredIds } : {}),
586
713
  ...(parsedPrivateVarMap ? { privateVarMap: parsedPrivateVarMap } : {}),
714
+ ...(parsedNumberRangeData
715
+ ? { numberRangeData: parsedNumberRangeData }
716
+ : {}),
587
717
  };
588
718
  component.props.push(newProp);
589
719
  saveConfig(configPath, config);
@@ -606,30 +736,30 @@ async function addProp(ref, options) {
606
736
  ...(newProp.privateVarMap
607
737
  ? { privateVarMap: newProp.privateVarMap }
608
738
  : {}),
739
+ ...(newProp.numberRangeData
740
+ ? { numberRangeData: newProp.numberRangeData }
741
+ : {}),
609
742
  },
610
743
  }));
611
744
  }
612
- function parseDefaultValue(value, propType) {
745
+ export function parseDefaultValue(value, propType) {
613
746
  switch (propType) {
614
747
  case "NUMBER":
615
748
  return Number(value);
616
749
  case "BOOLEAN":
617
750
  return value === "true";
751
+ case "COMPONENT":
752
+ case "COMPONENT_LIST":
618
753
  case "LINK":
619
754
  case "LIST_OF_LINK":
620
- // The --default-value flag arrives as a string; LINK values must be stored as a
621
- // typed object. Parse it here; if it is not valid JSON, return the raw string so
622
- // validateLinkDefaultValue can report a precise error.
623
- try {
624
- return JSON.parse(value);
625
- }
626
- catch {
627
- return value;
628
- }
629
755
  case "SVG_LIST":
630
- // Same reasoning as LINK_LIST: SVG_LIST must be stored as an array of SVG strings.
631
- // Parse it here; if it is not valid JSON, return the raw string so
632
- // validateSvgDefaultValue can report a precise "must be an array" error.
756
+ case "NUMBER_RANGE":
757
+ case "TYPE":
758
+ // These all carry a typed value (object/array), not a scalar. The --defaultValue flag arrives
759
+ // as a string, so parse it here; if it is not valid JSON, return the raw string so the type's
760
+ // validator (validateLinkDefaultValue / validateSvgDefaultValue / validateNumberRangeDefaultValue
761
+ // / validateTypeDefaultValue / validateComponentDefaultValue) can report a precise error instead
762
+ // of silently storing a bad shape.
633
763
  try {
634
764
  return JSON.parse(value);
635
765
  }
@@ -640,7 +770,46 @@ function parseDefaultValue(value, propType) {
640
770
  return value;
641
771
  }
642
772
  }
643
- const VALID_LINK_TYPES = ["PAGE", "EXTERNAL", "FILE"];
773
+ // Prop types whose value is a dynamic reference to merchant storefront data (products, media,
774
+ // categories, brands, blogs, raffles). They resolve at storefront render time, so a static config
775
+ // default is invalid and is rejected on add/update-prop. Keep in sync with @ikas/editor-models
776
+ // DYNAMIC_PROP_TYPES (the CLI must not depend on that private package).
777
+ export const DYNAMIC_PROP_TYPES = [
778
+ "IMAGE",
779
+ "IMAGE_LIST",
780
+ "VIDEO",
781
+ "PRODUCT",
782
+ "PRODUCT_LIST",
783
+ "PRODUCT_ATTRIBUTE",
784
+ "PRODUCT_ATTRIBUTE_LIST",
785
+ "BRAND",
786
+ "BRAND_LIST",
787
+ "CATEGORY",
788
+ "CATEGORY_LIST",
789
+ "BLOG",
790
+ "BLOG_LIST",
791
+ "BLOG_CATEGORY",
792
+ "BLOG_CATEGORY_LIST",
793
+ "RAFFLE",
794
+ "RAFFLE_LIST",
795
+ ];
796
+ const dynamicPropDefaultError = (propName, propType) => `defaultValue is not allowed for prop "${propName}" of type ${propType}. ${propType} resolves to merchant storefront data at render time, so it can't carry a static default — omit defaultValue.`;
797
+ // Reject a static default on a dynamic (merchant-data reference) prop type: print the standard
798
+ // error and exit. No-op when the type isn't dynamic or no default was supplied.
799
+ export const rejectDynamicDefault = (propName, propType, defaultValue) => {
800
+ if (DYNAMIC_PROP_TYPES.includes(propType) && defaultValue !== undefined) {
801
+ console.log(JSON.stringify({
802
+ success: false,
803
+ error: dynamicPropDefaultError(propName, propType),
804
+ }));
805
+ process.exit(1);
806
+ }
807
+ };
808
+ const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
809
+ // FILE is intentionally excluded from CLI-authored defaults: a code-component / AI-authored default
810
+ // can't reference a store-specific uploaded file, so a "FILE" link default is always rejected. Store
811
+ // owners can still create FILE links manually in the editor (editor-core keeps FILE in its own set).
812
+ const VALID_LINK_TYPES = ["PAGE", "EXTERNAL"];
644
813
  const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
645
814
  'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
646
815
  "Do NOT pass a JSON string or a { label, href } shape.";
@@ -662,6 +831,15 @@ const validateSingleLink = (link, propName, path) => {
662
831
  !VALID_LINK_TYPES.includes(l.linkType)) {
663
832
  return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
664
833
  }
834
+ // Require the destination field for the declared linkType, so a link that points nowhere never
835
+ // reaches a published theme. A PAGE default carries a pageType (pageId is per-store, filled on
836
+ // materialization); pageId alone is also accepted for a specific page.
837
+ if (l.linkType === "PAGE" && !nonEmpty(l.pageType) && !nonEmpty(l.pageId)) {
838
+ return `${where}: a "PAGE" link needs a "pageType" (e.g. "INDEX") — or a "pageId" for a specific page. ${LINK_SHAPE_HELP}`;
839
+ }
840
+ if (l.linkType === "EXTERNAL" && !nonEmpty(l.externalLink)) {
841
+ return `${where}: an "EXTERNAL" link needs a non-empty "externalLink" URL. ${LINK_SHAPE_HELP}`;
842
+ }
665
843
  if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
666
844
  return `${where}: "subLinks" must be an array (use [] when there are none).`;
667
845
  }
@@ -674,7 +852,7 @@ const validateSingleLink = (link, propName, path) => {
674
852
  // These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
675
853
  // legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
676
854
  // out of published themes.
677
- function validateLinkDefaultValue(propType, value, propName) {
855
+ export function validateLinkDefaultValue(propType, value, propName) {
678
856
  if (value === undefined || value === null)
679
857
  return null;
680
858
  if (propType === "LINK") {
@@ -693,8 +871,57 @@ function validateLinkDefaultValue(propType, value, propName) {
693
871
  const links = value.links;
694
872
  return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
695
873
  }
874
+ const COMPONENT_SHAPE_HELP = 'A COMPONENT / COMPONENT_LIST default must be an object listing child code components: ' +
875
+ '{ "components": [ { "codeComponentId": "<id>" } ] }. Only code components are allowed; each entry ' +
876
+ "needs a string \"codeComponentId\" (no componentId, no inline propValues).";
877
+ /**
878
+ * Validate a COMPONENT / COMPONENT_LIST defaultValue. Reference-only: each entry must name a code
879
+ * component via `codeComponentId`. Rejects non-CC (`componentId`) children, missing ids, more than
880
+ * one entry for a single COMPONENT, and ids outside the prop's filteredComponentIds allowlist.
881
+ * Returns an error message, or null if valid.
882
+ */
883
+ export function validateComponentDefaultValue(propType, value, propName, filteredComponentIds, existingComponentIds) {
884
+ if (value === undefined || value === null)
885
+ return null;
886
+ const where = `defaultValue for prop "${propName}"`;
887
+ if (typeof value === "string") {
888
+ return `${where} is a JSON string but must be an object. ${COMPONENT_SHAPE_HELP}`;
889
+ }
890
+ if (!value || typeof value !== "object" || Array.isArray(value) || !Array.isArray(value.components)) {
891
+ return `${where} must be an object with a "components" array. ${COMPONENT_SHAPE_HELP}`;
892
+ }
893
+ const components = value.components;
894
+ if (propType === "COMPONENT" && components.length > 1) {
895
+ return `${where}: a COMPONENT prop holds a single child — provide at most one entry (use COMPONENT_LIST for many).`;
896
+ }
897
+ const allow = filteredComponentIds && filteredComponentIds.length ? new Set(filteredComponentIds) : null;
898
+ return firstError(components, (entry, i) => {
899
+ const at = `${where}.components[${i}]`;
900
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
901
+ return `${at} must be an object { "codeComponentId": "<id>" }. ${COMPONENT_SHAPE_HELP}`;
902
+ }
903
+ const e = entry;
904
+ if ("componentId" in e && !("codeComponentId" in e)) {
905
+ return `${at} references a non-code component (componentId). Only code components are allowed as defaults. ${COMPONENT_SHAPE_HELP}`;
906
+ }
907
+ if (typeof e.codeComponentId !== "string" || e.codeComponentId.trim().length === 0) {
908
+ return `${at} must have a non-empty string "codeComponentId". ${COMPONENT_SHAPE_HELP}`;
909
+ }
910
+ if (allow && !allow.has(e.codeComponentId)) {
911
+ return `${at}: "${e.codeComponentId}" is not in this prop's filteredComponentIds allowlist.`;
912
+ }
913
+ // Reject a reference to a component that does not exist in the project (deleted, renamed, or
914
+ // never created). A dangling default would seed a ghost entry on every placement that renders
915
+ // nothing. Only enforced when the caller supplies the known-component set (the config-backed
916
+ // add-prop / update-prop / batch sites); pure-shape callers pass no set and skip this check.
917
+ if (existingComponentIds && !existingComponentIds.has(e.codeComponentId)) {
918
+ return `${at}: no component with id "${e.codeComponentId}" exists in this project. Create it first (config add-component) or reference an existing component id (config list).`;
919
+ }
920
+ return null;
921
+ });
922
+ }
696
923
  const SVG_DEFAULT_HELP = "An SVG default must be a single well-formed <svg>…</svg> string ≤ 64 KB with no embedded base64 raster images. " +
697
- "Tip: the editor uses each svg's `class` attribute as its display name — set class=\"my-icon\" to name it; " +
924
+ 'Tip: the editor uses each svg\'s `class` attribute as its display name — set class="my-icon" to name it; ' +
698
925
  "SVG_LIST items with no class show as svg-1, svg-2, ….";
699
926
  // Keep the cap and base64 pattern in sync with @ikas/editor-models validate-svg.ts (the
700
927
  // authoritative validator). Duplicated here on purpose — see validateSvgShape below.
@@ -736,7 +963,7 @@ function validateSvgShape(input) {
736
963
  // Validates an SVG / SVG_LIST defaultValue. Returns an error message, or null if valid.
737
964
  // Authoring an SVG default is a common mistake source for agents; rejecting here (with an
738
965
  // instructive message) keeps malformed/oversized SVGs out of published components.
739
- function validateSvgDefaultValue(propType, value, propName) {
966
+ export function validateSvgDefaultValue(propType, value, propName) {
740
967
  if (value === undefined || value === null)
741
968
  return null;
742
969
  if (propType === "SVG") {
@@ -756,7 +983,177 @@ function validateSvgDefaultValue(propType, value, propName) {
756
983
  : null;
757
984
  });
758
985
  }
759
- function updateProp(ref, options) {
986
+ const NUMBER_RANGE_DEFAULT_HELP = 'A NUMBER_RANGE default must be an object { "value": <number>, "unit": "px" | null }. ' +
987
+ "When the prop defines numberRangeData, value must be within [min, max] and on the interval grid (min + k·interval).";
988
+ // Validates a NUMBER_RANGE defaultValue. Returns an error message, or null if valid.
989
+ // Checks the { value, unit } shape and — when the prop carries numberRangeData — that the
990
+ // value is within [min, max] and lands on the interval grid, mirroring the editor's slider
991
+ // so an agent can't author an out-of-range or off-step default.
992
+ export function validateNumberRangeDefaultValue(value, numberRangeData, propName) {
993
+ if (value === undefined || value === null)
994
+ return null;
995
+ const where = `defaultValue for prop "${propName}"`;
996
+ if (typeof value === "string") {
997
+ return `${where} is a JSON string but must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
998
+ }
999
+ if (typeof value !== "object" || Array.isArray(value)) {
1000
+ return `${where} must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
1001
+ }
1002
+ const v = value;
1003
+ if (typeof v.value !== "number" || !Number.isFinite(v.value)) {
1004
+ return `${where}: "value" must be a finite number. ${NUMBER_RANGE_DEFAULT_HELP}`;
1005
+ }
1006
+ if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
1007
+ return `${where}: "unit" must be a string or null. ${NUMBER_RANGE_DEFAULT_HELP}`;
1008
+ }
1009
+ if (numberRangeData) {
1010
+ const { min, max, interval } = numberRangeData;
1011
+ if (min !== undefined && v.value < min) {
1012
+ return `${where}: value ${v.value} is below the configured min (${min}).`;
1013
+ }
1014
+ if (max !== undefined && v.value > max) {
1015
+ return `${where}: value ${v.value} is above the configured max (${max}).`;
1016
+ }
1017
+ if (interval !== undefined &&
1018
+ interval > 0 &&
1019
+ min !== undefined &&
1020
+ (v.value - min) % interval !== 0) {
1021
+ return `${where}: value ${v.value} is not on the interval grid (min ${min} + k·${interval}).`;
1022
+ }
1023
+ }
1024
+ return null;
1025
+ }
1026
+ const NUMBER_DEFAULT_HELP = "A NUMBER default must be a finite number, e.g. --defaultValue 42.";
1027
+ // Validates a NUMBER defaultValue. Returns an error message, or null if valid. Guards two footguns:
1028
+ // an empty/whitespace flag string (Number("") === 0 would silently store 0) and a non-numeric or
1029
+ // non-finite value (e.g. "42px" → NaN, "Infinity"). The flag path passes the raw --defaultValue
1030
+ // string; the --props path passes the already-parsed JSON value — both are handled here.
1031
+ export function validateNumberDefaultValue(value, propName) {
1032
+ if (value === undefined || value === null)
1033
+ return null;
1034
+ const where = `defaultValue for prop "${propName}"`;
1035
+ if (typeof value === "string") {
1036
+ if (value.trim() === "") {
1037
+ return `${where} is empty, but NUMBER requires a finite number. ${NUMBER_DEFAULT_HELP}`;
1038
+ }
1039
+ if (!Number.isFinite(Number(value))) {
1040
+ return `${where} ("${value}") is not a finite number. ${NUMBER_DEFAULT_HELP}`;
1041
+ }
1042
+ return null;
1043
+ }
1044
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1045
+ return `${where} must be a finite number. ${NUMBER_DEFAULT_HELP}`;
1046
+ }
1047
+ return null;
1048
+ }
1049
+ const STYLE_TYPE_DEFAULT_HELP = "A size/style TYPE default must be an object matching the type's shape: " +
1050
+ '{ "value": <number>, "unit": "px" | "rem" | "%" | "vh" | "vw" } (or { "css": "<raw css>" }). ' +
1051
+ "Author it via add-component --props '[…]' or --defaultValue '<json>'.";
1052
+ const BP_STOREFRONT_PREFIX = "@ikas/bp-storefront-models-";
1053
+ // The size/style types that share the { css?, value?, unit? } shape. This is EXACTLY the set of
1054
+ // TYPE typeIds whose default the generator emits (mirror of DYNAMIC_STYLE_TYPE_IDS in
1055
+ // @ikas/blueprint-modules design-generator/generate-helpers/styles.ts — the standalone CLI must not
1056
+ // depend on that package, so it is hand-maintained here and kept in sync by a parity test). Every
1057
+ // OTHER TYPE typeId (BorderStyleType, BoxShadowStyleType, GridTemplateColumnsStyleType, string-union
1058
+ // styles like TextAlignStyleType, IkasProduct, custom types) has a DIFFERENT value shape, so their
1059
+ // defaults are not shape-checked (and are dead code in the generator anyway).
1060
+ const SIZE_STYLE_TYPE_NAMES = [
1061
+ "SizeStyleType",
1062
+ "PaddingStyleType",
1063
+ "PaddingTopStyleType",
1064
+ "PaddingRightStyleType",
1065
+ "PaddingBottomStyleType",
1066
+ "PaddingLeftStyleType",
1067
+ "MarginStyleType",
1068
+ "MarginTopStyleType",
1069
+ "MarginRightStyleType",
1070
+ "MarginBottomStyleType",
1071
+ "MarginLeftStyleType",
1072
+ "BorderRadiusStyleType",
1073
+ "BorderRadiusTopLeftStyleType",
1074
+ "BorderRadiusTopRightStyleType",
1075
+ "BorderRadiusBottomRightStyleType",
1076
+ "BorderRadiusBottomLeftStyleType",
1077
+ "FontSizeStyleType",
1078
+ "LineHeightStyleType",
1079
+ "LetterSpacingStyleType",
1080
+ "HeightStyleType",
1081
+ "MinHeightStyleType",
1082
+ "MaxHeightStyleType",
1083
+ "WidthStyleType",
1084
+ "MinWidthStyleType",
1085
+ "MaxWidthStyleType",
1086
+ "GapStyleType",
1087
+ "BorderWidthStyleType",
1088
+ "TopStyleType",
1089
+ "RightStyleType",
1090
+ "BottomStyleType",
1091
+ "LeftStyleType",
1092
+ ];
1093
+ // Validates a single style-type default object. Every size style type shares the optional
1094
+ // { css?, value?, unit? } shape, so inner fields are checked loosely — the key rejection is a
1095
+ // non-object (e.g. a bare JSON string, the common failure when a --defaultValue flag is not parsed).
1096
+ function styleDefaultObjectError(value, where) {
1097
+ if (typeof value === "string") {
1098
+ return `${where} is a JSON string but must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
1099
+ }
1100
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1101
+ return `${where} must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
1102
+ }
1103
+ const v = value;
1104
+ // css/value/unit are all optional, and the editor treats an explicit null as "unset" (it accepts
1105
+ // { value: null }). So only type-check a field that is present AND non-null — otherwise the CLI
1106
+ // would be stricter than the authoritative editor and reject a value it would happily accept.
1107
+ // Unknown fields ARE rejected below: every size-style type shares exactly this { css?, value?, unit? }
1108
+ // shape, so an extra key is always a mistake (this matches the editor). The per-type value-DOMAIN
1109
+ // (which units a given type allows — BorderWidth also permits em/vmin/vmax while the rest are
1110
+ // px/rem/%/vh/vw) stays editor-side: it is per-type and can't be mirrored offline without drift.
1111
+ if (v.css !== undefined && v.css !== null && typeof v.css !== "string") {
1112
+ return `${where}: "css" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
1113
+ }
1114
+ if (v.value !== undefined &&
1115
+ v.value !== null &&
1116
+ (typeof v.value !== "number" || !Number.isFinite(v.value))) {
1117
+ return `${where}: "value" must be a finite number. ${STYLE_TYPE_DEFAULT_HELP}`;
1118
+ }
1119
+ if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
1120
+ return `${where}: "unit" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
1121
+ }
1122
+ const unknownKey = Object.keys(v).find((k) => k !== "css" && k !== "value" && k !== "unit");
1123
+ if (unknownKey) {
1124
+ return `${where}: unknown field "${unknownKey}". A size/style default allows only "css", "value", "unit". ${STYLE_TYPE_DEFAULT_HELP}`;
1125
+ }
1126
+ return null;
1127
+ }
1128
+ // Validates a TYPE prop defaultValue — but ONLY for the size/style types whose shape we know
1129
+ // ({ css?, value?, unit? }). Any other TYPE typeId (color/border/box-shadow/grid/string-union
1130
+ // styles, IkasProduct, custom types) has a different value shape we can't verify offline, so its
1131
+ // default is passed through unchecked. The generator only emits a default for these size/style
1132
+ // types anyway. Returns an error message, or null if valid / not a size-style type.
1133
+ export function validateTypeDefaultValue(value, propName, typeId) {
1134
+ if (value === undefined || value === null)
1135
+ return null;
1136
+ if (typeof typeId !== "string" || !typeId.startsWith(BP_STOREFRONT_PREFIX)) {
1137
+ return null;
1138
+ }
1139
+ // Strip the prefix and an optional _array suffix to get the base type name.
1140
+ const withoutPrefix = typeId.slice(BP_STOREFRONT_PREFIX.length);
1141
+ const isArray = withoutPrefix.endsWith("_array");
1142
+ const baseName = isArray
1143
+ ? withoutPrefix.slice(0, -"_array".length)
1144
+ : withoutPrefix;
1145
+ if (!SIZE_STYLE_TYPE_NAMES.includes(baseName))
1146
+ return null;
1147
+ const where = `defaultValue for prop "${propName}"`;
1148
+ if (isArray) {
1149
+ if (!Array.isArray(value)) {
1150
+ return `${where} must be an array for an _array TYPE prop: [ { "value": …, "unit": … }, … ]. ${STYLE_TYPE_DEFAULT_HELP}`;
1151
+ }
1152
+ return firstError(value, (el, i) => styleDefaultObjectError(el, `${where}[${i}]`));
1153
+ }
1154
+ return styleDefaultObjectError(value, where);
1155
+ }
1156
+ async function updateProp(ref, options) {
760
1157
  const { config, configPath } = loadConfig();
761
1158
  const component = resolveComponent(config, ref);
762
1159
  const propIndex = component.props.findIndex((p) => p.name === options.prop);
@@ -788,14 +1185,85 @@ function updateProp(ref, options) {
788
1185
  if (options.description !== undefined) {
789
1186
  prop.description = options.description || undefined;
790
1187
  }
1188
+ // Resolve numberRangeData before defaultValue so a NUMBER_RANGE default can be validated
1189
+ // against the (possibly just-updated) slider config in the same call.
1190
+ if (options.numberRangeData !== undefined) {
1191
+ if (options.numberRangeData === "" || options.numberRangeData === "none") {
1192
+ delete prop.numberRangeData;
1193
+ }
1194
+ else {
1195
+ prop.numberRangeData = parseNumberRangeData(options.numberRangeData, prop.name);
1196
+ }
1197
+ }
1198
+ // A NUMBER_RANGE prop must keep a valid slider config (min + max). Enforce the invariant only when
1199
+ // this update touches the type or numberRangeData — a benign update (e.g. displayName) on an
1200
+ // existing prop must still pass. This catches switching a prop TO NUMBER_RANGE without a config, or
1201
+ // clearing/blanking numberRangeData on a NUMBER_RANGE prop.
1202
+ if ((options.type !== undefined || options.numberRangeData !== undefined) &&
1203
+ prop.type === "NUMBER_RANGE" &&
1204
+ (!prop.numberRangeData || !isNumberRangeComplete(prop.numberRangeData))) {
1205
+ console.log(JSON.stringify({
1206
+ success: false,
1207
+ error: numberRangeDataRequiredError(prop.name),
1208
+ }));
1209
+ process.exit(1);
1210
+ }
1211
+ // Resolve filteredComponentIds before defaultValue so a COMPONENT / COMPONENT_LIST default can be
1212
+ // validated against the (possibly just-updated) allowlist in the same call.
1213
+ if (options.filteredComponentIds !== undefined) {
1214
+ if (options.filteredComponentIds === "" ||
1215
+ options.filteredComponentIds === "none") {
1216
+ delete prop.filteredComponentIds;
1217
+ }
1218
+ else {
1219
+ try {
1220
+ const parsed = JSON.parse(options.filteredComponentIds);
1221
+ if (!Array.isArray(parsed)) {
1222
+ console.log(JSON.stringify({
1223
+ success: false,
1224
+ error: "--filteredComponentIds must be a JSON array of strings.",
1225
+ }));
1226
+ process.exit(1);
1227
+ }
1228
+ assertKnownComponentIds(parsed, config);
1229
+ prop.filteredComponentIds = parsed;
1230
+ }
1231
+ catch (err) {
1232
+ if (err instanceof SyntaxError) {
1233
+ console.log(JSON.stringify({
1234
+ success: false,
1235
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
1236
+ }));
1237
+ process.exit(1);
1238
+ }
1239
+ throw err;
1240
+ }
1241
+ }
1242
+ }
791
1243
  if (options.defaultValue !== undefined) {
1244
+ rejectDynamicDefault(prop.name, prop.type, options.defaultValue);
792
1245
  const parsed = parseDefaultValue(options.defaultValue, prop.type);
1246
+ // Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
1247
+ if (prop.type === "NUMBER") {
1248
+ const numError = validateNumberDefaultValue(options.defaultValue, prop.name);
1249
+ failIf(numError);
1250
+ }
793
1251
  if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
794
1252
  const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
795
- if (linkError) {
796
- console.log(JSON.stringify({ success: false, error: linkError }));
797
- process.exit(1);
798
- }
1253
+ failIf(linkError);
1254
+ }
1255
+ if ((prop.type === "COMPONENT" || prop.type === "COMPONENT_LIST") &&
1256
+ parsed !== undefined) {
1257
+ failIf(validateComponentDefaultValue(prop.type, parsed, prop.name, prop.filteredComponentIds, existingComponentIdSet(config)));
1258
+ }
1259
+ if (prop.type === "NUMBER_RANGE") {
1260
+ const nrError = validateNumberRangeDefaultValue(parsed, prop.numberRangeData, prop.name);
1261
+ failIf(nrError);
1262
+ }
1263
+ if (prop.type === "TYPE") {
1264
+ // Prefer a typeId supplied in this same update over the stored one.
1265
+ const typeError = validateTypeDefaultValue(parsed, prop.name, options.typeId ?? prop.typeId);
1266
+ failIf(typeError);
799
1267
  }
800
1268
  prop.defaultValue = parsed;
801
1269
  }
@@ -830,6 +1298,13 @@ function updateProp(ref, options) {
830
1298
  prop.typeId = options.typeId;
831
1299
  }
832
1300
  }
1301
+ // Reject section-disallowed types when the caller changes type/typeId;
1302
+ // untouched legacy props stay editable (displayName, group, ...).
1303
+ if ((options.type !== undefined || options.typeId !== undefined) &&
1304
+ prop.type === "TYPE" &&
1305
+ prop.typeId) {
1306
+ await assertTypeIdsUsable(component.type, [prop.typeId]);
1307
+ }
833
1308
  if (options.enumTypeId !== undefined) {
834
1309
  if (options.enumTypeId === "" || options.enumTypeId === "none") {
835
1310
  delete prop.enumTypeId;
@@ -838,36 +1313,6 @@ function updateProp(ref, options) {
838
1313
  prop.enumTypeId = options.enumTypeId;
839
1314
  }
840
1315
  }
841
- if (options.filteredComponentIds !== undefined) {
842
- if (options.filteredComponentIds === "" ||
843
- options.filteredComponentIds === "none") {
844
- delete prop.filteredComponentIds;
845
- }
846
- else {
847
- try {
848
- const parsed = JSON.parse(options.filteredComponentIds);
849
- if (!Array.isArray(parsed)) {
850
- console.log(JSON.stringify({
851
- success: false,
852
- error: "--filteredComponentIds must be a JSON array of strings.",
853
- }));
854
- process.exit(1);
855
- }
856
- assertKnownComponentIds(parsed, config);
857
- prop.filteredComponentIds = parsed;
858
- }
859
- catch (err) {
860
- if (err instanceof SyntaxError) {
861
- console.log(JSON.stringify({
862
- success: false,
863
- error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
864
- }));
865
- process.exit(1);
866
- }
867
- throw err;
868
- }
869
- }
870
- }
871
1316
  if (options.privateVarMap !== undefined) {
872
1317
  if (options.privateVarMap === "" || options.privateVarMap === "none") {
873
1318
  delete prop.privateVarMap;
@@ -912,6 +1357,9 @@ function updateProp(ref, options) {
912
1357
  ? { filteredComponentIds: prop.filteredComponentIds }
913
1358
  : {}),
914
1359
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
1360
+ ...(prop.numberRangeData
1361
+ ? { numberRangeData: prop.numberRangeData }
1362
+ : {}),
915
1363
  },
916
1364
  }));
917
1365
  }
@@ -1410,6 +1858,7 @@ export function createConfigCommand() {
1410
1858
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
1411
1859
  .option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
1412
1860
  .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)")
1861
+ .option("--numberRangeData <json>", 'JSON slider config for NUMBER_RANGE props, e.g. \'{"min":0,"max":100,"interval":5,"unit":"px"}\'')
1413
1862
  .action((options) => {
1414
1863
  addProp({ id: options.componentId, name: options.component }, options);
1415
1864
  });
@@ -1429,6 +1878,7 @@ export function createConfigCommand() {
1429
1878
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (use 'none' to clear)")
1430
1879
  .option("--filteredComponentIds <json>", "JSON array of component IDs (use 'none' to clear)")
1431
1880
  .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (use 'none' to clear)")
1881
+ .option("--numberRangeData <json>", "JSON slider config for NUMBER_RANGE props (use 'none' to clear)")
1432
1882
  .action((options) => {
1433
1883
  updateProp({ id: options.componentId, name: options.component }, options);
1434
1884
  });
@@ -1560,62 +2010,15 @@ export function createConfigCommand() {
1560
2010
  * Returns the matching type (with enumOptions) if found, null otherwise.
1561
2011
  */
1562
2012
  async function fetchLiveEditorEnumById(enumId) {
1563
- const { WebSocket } = await import("ws");
1564
- const WS_URL = "ws://localhost:5201";
1565
- const TIMEOUT_MS = 1500;
1566
- return new Promise((resolve) => {
1567
- let ws;
1568
- let settled = false;
1569
- const finish = (value) => {
1570
- if (settled)
1571
- return;
1572
- settled = true;
1573
- try {
1574
- ws?.close();
1575
- }
1576
- catch {
1577
- /* ignore */
1578
- }
1579
- resolve(value);
1580
- };
1581
- try {
1582
- ws = new WebSocket(WS_URL);
1583
- }
1584
- catch {
1585
- resolve(null);
1586
- return;
2013
+ const types = await fetchLiveEditorTypes();
2014
+ const match = types?.find((t) => t.category === "enum" && t.id === enumId);
2015
+ return match
2016
+ ? {
2017
+ id: match.id,
2018
+ name: match.name,
2019
+ ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}),
1587
2020
  }
1588
- const timeout = setTimeout(() => finish(null), TIMEOUT_MS);
1589
- ws.on("open", () => {
1590
- ws.send(JSON.stringify({ type: "request-types" }));
1591
- });
1592
- ws.on("message", (data) => {
1593
- try {
1594
- const message = JSON.parse(data.toString());
1595
- if (message.type === "types-list") {
1596
- clearTimeout(timeout);
1597
- const types = message.payload?.types || [];
1598
- const match = types.find((t) => t.category === "enum" && t.id === enumId);
1599
- finish(match
1600
- ? {
1601
- id: match.id,
1602
- name: match.name,
1603
- ...(match.enumOptions
1604
- ? { enumOptions: match.enumOptions }
1605
- : {}),
1606
- }
1607
- : null);
1608
- }
1609
- }
1610
- catch {
1611
- // Ignore non-JSON messages
1612
- }
1613
- });
1614
- ws.on("error", () => {
1615
- clearTimeout(timeout);
1616
- finish(null);
1617
- });
1618
- });
2021
+ : null;
1619
2022
  }
1620
2023
  /**
1621
2024
  * Best-effort lookup of an enum by PascalCase name in the live editor session.
@@ -1625,6 +2028,16 @@ async function fetchLiveEditorEnumById(enumId) {
1625
2028
  * must treat null as "could not verify, fall back to on-disk check only".
1626
2029
  */
1627
2030
  async function fetchLiveEditorEnumByName(pascalName) {
2031
+ const types = await fetchLiveEditorTypes();
2032
+ const match = types?.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
2033
+ return match ? { id: match.id, name: match.name } : null;
2034
+ }
2035
+ /**
2036
+ * Best-effort fetch of the editor's synced type list (with category /
2037
+ * sectionAllowed / enumOptions metadata). Returns null on any failure (no dev
2038
+ * server, timeout) — callers must treat null as "could not verify, allow".
2039
+ */
2040
+ async function fetchLiveEditorTypes() {
1628
2041
  const { WebSocket } = await import("ws");
1629
2042
  const WS_URL = "ws://localhost:5201";
1630
2043
  const TIMEOUT_MS = 1500;
@@ -1659,9 +2072,7 @@ async function fetchLiveEditorEnumByName(pascalName) {
1659
2072
  const message = JSON.parse(data.toString());
1660
2073
  if (message.type === "types-list") {
1661
2074
  clearTimeout(timeout);
1662
- const types = message.payload?.types || [];
1663
- const match = types.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
1664
- finish(match ? { id: match.id, name: match.name } : null);
2075
+ finish(message.payload?.types || []);
1665
2076
  }
1666
2077
  }
1667
2078
  catch {
@@ -1674,6 +2085,47 @@ async function fetchLiveEditorEnumByName(pascalName) {
1674
2085
  });
1675
2086
  });
1676
2087
  }
2088
+ /**
2089
+ * Definition-time guard for TYPE-prop typeIds, verified against the live
2090
+ * editor's type list (skipped when the dev server is unreachable or the type
2091
+ * is unknown to the editor). Two rules:
2092
+ * - deprecated types are rejected on ANY component: they are legacy
2093
+ * render-only types — old themes may still carry values, but new props must
2094
+ * not be defined with them.
2095
+ * - on section components, only section-allowed (style) types pass — any other
2096
+ * type renders an empty placeholder merchants cannot fill, and section-level
2097
+ * bindings cannot supply a value either, so the prop would be dead weight.
2098
+ * On violation prints the standard { success: false } envelope and exits.
2099
+ */
2100
+ async function assertTypeIdsUsable(componentType, typeIds) {
2101
+ if (typeIds.length === 0)
2102
+ return;
2103
+ const types = await fetchLiveEditorTypes();
2104
+ if (!types)
2105
+ return;
2106
+ const matches = typeIds.flatMap((typeId) => {
2107
+ const match = types.find((t) => t.id === typeId);
2108
+ return match ? [match] : [];
2109
+ });
2110
+ const legacy = matches.find((t) => t.deprecated === true);
2111
+ if (legacy) {
2112
+ console.log(JSON.stringify({
2113
+ success: false,
2114
+ error: `Type "${legacy.id}" is a legacy render-only type — values of it may still exist in old themes, but NEW props must not use it. Pick a current style type instead (run "config list-types" to see them).`,
2115
+ }));
2116
+ process.exit(1);
2117
+ }
2118
+ if (componentType !== "section")
2119
+ return;
2120
+ const disallowed = matches.find((t) => t.sectionAllowed === false);
2121
+ if (!disallowed)
2122
+ return;
2123
+ console.log(JSON.stringify({
2124
+ success: false,
2125
+ error: `Type "${disallowed.id}" is not allowed for TYPE props on section components — merchants cannot fill it in the editor (sections only support style types). Run "config list-types --component-type section" to see allowed types, or use a dedicated prop type (e.g. PRODUCT, CATEGORY) for domain data.`,
2126
+ }));
2127
+ process.exit(1);
2128
+ }
1677
2129
  async function listTypes(componentType) {
1678
2130
  const { WebSocket } = await import("ws");
1679
2131
  const WS_URL = "ws://localhost:5201";
@@ -1707,7 +2159,8 @@ async function listTypes(componentType) {
1707
2159
  const message = JSON.parse(data.toString());
1708
2160
  if (message.type === "types-list") {
1709
2161
  clearTimeout(timeout);
1710
- let types = message.payload.types;
2162
+ // Legacy render-only types are never offered for new props.
2163
+ let types = message.payload.types.filter((t) => !t.deprecated);
1711
2164
  if (componentType === "section") {
1712
2165
  types = types.filter((t) => t.sectionAllowed);
1713
2166
  }