@ikas/component-cli 1.4.0-beta.114 → 1.4.0-beta.115

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.
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA46DpC,wBAAgB,mBAAmB,IAAI,OAAO,CAuU7C"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwyEpC,wBAAgB,mBAAmB,IAAI,OAAO,CA+U7C"}
@@ -162,7 +162,54 @@ const ALLOWED_PROP_FIELDS = new Set([
162
162
  "enumTypeId",
163
163
  "filteredComponentIds",
164
164
  "privateVarMap",
165
+ "numberRangeData",
165
166
  ]);
167
+ /**
168
+ * Validate/sanitize a numberRangeData object for a NUMBER_RANGE prop. Accepts an object
169
+ * or a JSON string. min/max/interval are truncated to integers; only a positive interval
170
+ * is kept (0/negative would yield an invalid slider step). Exits with a structured error
171
+ * on malformed input.
172
+ */
173
+ function parseNumberRangeData(raw, propName) {
174
+ const fail = (error) => {
175
+ console.log(JSON.stringify({ success: false, error }));
176
+ process.exit(1);
177
+ };
178
+ let obj = raw;
179
+ if (typeof raw === "string") {
180
+ try {
181
+ obj = JSON.parse(raw);
182
+ }
183
+ catch {
184
+ fail(`Invalid --numberRangeData JSON for prop "${propName}": ${raw}`);
185
+ }
186
+ }
187
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
188
+ fail(`numberRangeData for prop "${propName}" must be an object { min, max, interval?, unit? }.`);
189
+ }
190
+ const o = obj;
191
+ const asInt = (v, field) => {
192
+ if (v === undefined || v === null)
193
+ return undefined;
194
+ if (typeof v !== "number" || !Number.isFinite(v)) {
195
+ fail(`numberRangeData.${field} for prop "${propName}" must be a finite number.`);
196
+ }
197
+ return Math.trunc(v);
198
+ };
199
+ const min = asInt(o.min, "min");
200
+ const max = asInt(o.max, "max");
201
+ const interval = asInt(o.interval, "interval");
202
+ const unit = typeof o.unit === "string" && o.unit.trim() ? o.unit.trim() : undefined;
203
+ if (min !== undefined && max !== undefined && min >= max) {
204
+ fail(`numberRangeData for prop "${propName}": min (${min}) must be less than max (${max}).`);
205
+ }
206
+ return {
207
+ ...(min !== undefined ? { min } : {}),
208
+ ...(max !== undefined ? { max } : {}),
209
+ ...(interval !== undefined && interval > 0 ? { interval } : {}),
210
+ ...(unit ? { unit } : {}),
211
+ };
212
+ }
166
213
  /**
167
214
  * Parse and validate the --props JSON flag for add-component.
168
215
  * Returns validated ComponentProp[] or exits with error.
@@ -240,6 +287,8 @@ async function parsePropsFlag(propsJson, config, configPath) {
240
287
  }));
241
288
  process.exit(1);
242
289
  }
290
+ // Dynamic (merchant-data reference) props can't carry a static default.
291
+ rejectDynamicDefault(r.name, propType, r.defaultValue);
243
292
  // Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
244
293
  if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
245
294
  r.defaultValue !== undefined) {
@@ -258,6 +307,15 @@ async function parsePropsFlag(propsJson, config, configPath) {
258
307
  process.exit(1);
259
308
  }
260
309
  }
310
+ // Validate TYPE (structured/style) defaultValue shape — must be an object (or an array for an
311
+ // _array typeId), never a bare string that would be emitted as a wrong-shaped default.
312
+ if (propType === "TYPE" && r.defaultValue !== undefined) {
313
+ const typeError = validateTypeDefaultValue(r.defaultValue, r.name, r.typeId);
314
+ if (typeError) {
315
+ console.log(JSON.stringify({ success: false, error: typeError }));
316
+ process.exit(1);
317
+ }
318
+ }
261
319
  // Auto-generate displayName if omitted
262
320
  const displayName = typeof r.displayName === "string" && r.displayName
263
321
  ? r.displayName
@@ -328,6 +386,9 @@ async function parsePropsFlag(propsJson, config, configPath) {
328
386
  privateVarMap: r.privateVarMap,
329
387
  }
330
388
  : {}),
389
+ ...(propType === "NUMBER_RANGE" && r.numberRangeData !== undefined
390
+ ? { numberRangeData: parseNumberRangeData(r.numberRangeData, r.name) }
391
+ : {}),
331
392
  };
332
393
  props.push(prop);
333
394
  }
@@ -557,6 +618,7 @@ async function addProp(ref, options) {
557
618
  process.exit(1);
558
619
  }
559
620
  }
621
+ rejectDynamicDefault(options.name, propType, options.defaultValue);
560
622
  const parsedDefaultValue = options.defaultValue !== undefined
561
623
  ? parseDefaultValue(options.defaultValue, propType)
562
624
  : undefined;
@@ -576,6 +638,23 @@ async function addProp(ref, options) {
576
638
  process.exit(1);
577
639
  }
578
640
  }
641
+ if (propType === "TYPE" && parsedDefaultValue !== undefined) {
642
+ const typeError = validateTypeDefaultValue(parsedDefaultValue, options.name, options.typeId);
643
+ if (typeError) {
644
+ console.log(JSON.stringify({ success: false, error: typeError }));
645
+ process.exit(1);
646
+ }
647
+ }
648
+ const parsedNumberRangeData = propType === "NUMBER_RANGE" && options.numberRangeData !== undefined
649
+ ? parseNumberRangeData(options.numberRangeData, options.name)
650
+ : undefined;
651
+ if (propType === "NUMBER_RANGE" && parsedDefaultValue !== undefined) {
652
+ const nrError = validateNumberRangeDefaultValue(parsedDefaultValue, parsedNumberRangeData, options.name);
653
+ if (nrError) {
654
+ console.log(JSON.stringify({ success: false, error: nrError }));
655
+ process.exit(1);
656
+ }
657
+ }
579
658
  const newProp = {
580
659
  name: options.name,
581
660
  displayName: options.displayName,
@@ -590,6 +669,9 @@ async function addProp(ref, options) {
590
669
  ...(options.enumTypeId ? { enumTypeId: options.enumTypeId } : {}),
591
670
  ...(parsedFilteredIds ? { filteredComponentIds: parsedFilteredIds } : {}),
592
671
  ...(parsedPrivateVarMap ? { privateVarMap: parsedPrivateVarMap } : {}),
672
+ ...(parsedNumberRangeData
673
+ ? { numberRangeData: parsedNumberRangeData }
674
+ : {}),
593
675
  };
594
676
  component.props.push(newProp);
595
677
  saveConfig(configPath, config);
@@ -612,6 +694,9 @@ async function addProp(ref, options) {
612
694
  ...(newProp.privateVarMap
613
695
  ? { privateVarMap: newProp.privateVarMap }
614
696
  : {}),
697
+ ...(newProp.numberRangeData
698
+ ? { numberRangeData: newProp.numberRangeData }
699
+ : {}),
615
700
  },
616
701
  }));
617
702
  }
@@ -642,10 +727,66 @@ function parseDefaultValue(value, propType) {
642
727
  catch {
643
728
  return value;
644
729
  }
730
+ case "NUMBER_RANGE":
731
+ // The default arrives as a string; a NUMBER_RANGE value is a typed object
732
+ // { value, unit }. Parse it here; if it is not valid JSON, return the raw string
733
+ // so validateNumberRangeDefaultValue can report a precise error.
734
+ try {
735
+ return JSON.parse(value);
736
+ }
737
+ catch {
738
+ return value;
739
+ }
740
+ case "TYPE":
741
+ // A TYPE (structured/style) default is a typed object — e.g. { value, unit } for a size
742
+ // style type — or an array of them for an _array typeId. Parse it here; if it is not valid
743
+ // JSON, return the raw string so validateTypeDefaultValue can report a precise error.
744
+ // (Without this the --defaultValue flag would store the raw string, a wrong-shaped default.)
745
+ try {
746
+ return JSON.parse(value);
747
+ }
748
+ catch {
749
+ return value;
750
+ }
645
751
  default:
646
752
  return value;
647
753
  }
648
754
  }
755
+ // Prop types whose value is a dynamic reference to merchant storefront data (products, media,
756
+ // categories, brands, blogs, raffles). They resolve at storefront render time, so a static config
757
+ // default is invalid and is rejected on add/update-prop. Keep in sync with @ikas/editor-models
758
+ // DYNAMIC_PROP_TYPES (the CLI must not depend on that private package).
759
+ const DYNAMIC_PROP_TYPES = [
760
+ "IMAGE",
761
+ "IMAGE_LIST",
762
+ "VIDEO",
763
+ "PRODUCT",
764
+ "PRODUCT_LIST",
765
+ "PRODUCT_ATTRIBUTE",
766
+ "PRODUCT_ATTRIBUTE_LIST",
767
+ "BRAND",
768
+ "BRAND_LIST",
769
+ "CATEGORY",
770
+ "CATEGORY_LIST",
771
+ "BLOG",
772
+ "BLOG_LIST",
773
+ "BLOG_CATEGORY",
774
+ "BLOG_CATEGORY_LIST",
775
+ "RAFFLE",
776
+ "RAFFLE_LIST",
777
+ ];
778
+ 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.`;
779
+ // Reject a static default on a dynamic (merchant-data reference) prop type: print the standard
780
+ // error and exit. No-op when the type isn't dynamic or no default was supplied.
781
+ const rejectDynamicDefault = (propName, propType, defaultValue) => {
782
+ if (DYNAMIC_PROP_TYPES.includes(propType) && defaultValue !== undefined) {
783
+ console.log(JSON.stringify({
784
+ success: false,
785
+ error: dynamicPropDefaultError(propName, propType),
786
+ }));
787
+ process.exit(1);
788
+ }
789
+ };
649
790
  const VALID_LINK_TYPES = ["PAGE", "EXTERNAL", "FILE"];
650
791
  const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
651
792
  'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
@@ -762,6 +903,141 @@ function validateSvgDefaultValue(propType, value, propName) {
762
903
  : null;
763
904
  });
764
905
  }
906
+ const NUMBER_RANGE_DEFAULT_HELP = 'A NUMBER_RANGE default must be an object { "value": <number>, "unit": "px" | null }. ' +
907
+ "When the prop defines numberRangeData, value must be within [min, max] and on the interval grid (min + k·interval).";
908
+ // Validates a NUMBER_RANGE defaultValue. Returns an error message, or null if valid.
909
+ // Checks the { value, unit } shape and — when the prop carries numberRangeData — that the
910
+ // value is within [min, max] and lands on the interval grid, mirroring the editor's slider
911
+ // so an agent can't author an out-of-range or off-step default.
912
+ function validateNumberRangeDefaultValue(value, numberRangeData, propName) {
913
+ if (value === undefined || value === null)
914
+ return null;
915
+ const where = `defaultValue for prop "${propName}"`;
916
+ if (typeof value === "string") {
917
+ return `${where} is a JSON string but must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
918
+ }
919
+ if (typeof value !== "object" || Array.isArray(value)) {
920
+ return `${where} must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
921
+ }
922
+ const v = value;
923
+ if (typeof v.value !== "number" || !Number.isFinite(v.value)) {
924
+ return `${where}: "value" must be a finite number. ${NUMBER_RANGE_DEFAULT_HELP}`;
925
+ }
926
+ if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
927
+ return `${where}: "unit" must be a string or null. ${NUMBER_RANGE_DEFAULT_HELP}`;
928
+ }
929
+ if (numberRangeData) {
930
+ const { min, max, interval } = numberRangeData;
931
+ if (min !== undefined && v.value < min) {
932
+ return `${where}: value ${v.value} is below the configured min (${min}).`;
933
+ }
934
+ if (max !== undefined && v.value > max) {
935
+ return `${where}: value ${v.value} is above the configured max (${max}).`;
936
+ }
937
+ if (interval !== undefined &&
938
+ interval > 0 &&
939
+ min !== undefined &&
940
+ (v.value - min) % interval !== 0) {
941
+ return `${where}: value ${v.value} is not on the interval grid (min ${min} + k·${interval}).`;
942
+ }
943
+ }
944
+ return null;
945
+ }
946
+ const STYLE_TYPE_DEFAULT_HELP = "A size/style TYPE default must be an object matching the type's shape: " +
947
+ '{ "value": <number>, "unit": "px" | "rem" | "%" | "vh" | "vw" } (or { "css": "<raw css>" }). ' +
948
+ "Author it via add-component --props '[…]' or --defaultValue '<json>'.";
949
+ const BP_STOREFRONT_PREFIX = "@ikas/bp-storefront-models-";
950
+ // The size/style types that share the { css?, value?, unit? } shape. This is EXACTLY the set of
951
+ // TYPE typeIds whose default the generator emits (mirror of DYNAMIC_STYLE_TYPE_IDS in
952
+ // @ikas/blueprint-modules design-generator/generate-helpers/styles.ts — the standalone CLI must not
953
+ // depend on that package, so it is hand-maintained here and kept in sync by a parity test). Every
954
+ // OTHER TYPE typeId (BorderStyleType, BoxShadowStyleType, GridTemplateColumnsStyleType, string-union
955
+ // styles like TextAlignStyleType, IkasProduct, custom types) has a DIFFERENT value shape, so their
956
+ // defaults are not shape-checked (and are dead code in the generator anyway).
957
+ const SIZE_STYLE_TYPE_NAMES = [
958
+ "SizeStyleType",
959
+ "PaddingStyleType",
960
+ "PaddingTopStyleType",
961
+ "PaddingRightStyleType",
962
+ "PaddingBottomStyleType",
963
+ "PaddingLeftStyleType",
964
+ "MarginStyleType",
965
+ "MarginTopStyleType",
966
+ "MarginRightStyleType",
967
+ "MarginBottomStyleType",
968
+ "MarginLeftStyleType",
969
+ "BorderRadiusStyleType",
970
+ "BorderRadiusTopLeftStyleType",
971
+ "BorderRadiusTopRightStyleType",
972
+ "BorderRadiusBottomRightStyleType",
973
+ "BorderRadiusBottomLeftStyleType",
974
+ "FontSizeStyleType",
975
+ "LineHeightStyleType",
976
+ "LetterSpacingStyleType",
977
+ "HeightStyleType",
978
+ "MinHeightStyleType",
979
+ "MaxHeightStyleType",
980
+ "WidthStyleType",
981
+ "MinWidthStyleType",
982
+ "MaxWidthStyleType",
983
+ "GapStyleType",
984
+ "BorderWidthStyleType",
985
+ "TopStyleType",
986
+ "RightStyleType",
987
+ "BottomStyleType",
988
+ "LeftStyleType",
989
+ ];
990
+ // Validates a single style-type default object. Every size style type shares the optional
991
+ // { css?, value?, unit? } shape, so inner fields are checked loosely — the key rejection is a
992
+ // non-object (e.g. a bare JSON string, the common failure when a --defaultValue flag is not parsed).
993
+ function styleDefaultObjectError(value, where) {
994
+ if (typeof value === "string") {
995
+ return `${where} is a JSON string but must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
996
+ }
997
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
998
+ return `${where} must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
999
+ }
1000
+ const v = value;
1001
+ if (v.value !== undefined &&
1002
+ (typeof v.value !== "number" || !Number.isFinite(v.value))) {
1003
+ return `${where}: "value" must be a finite number. ${STYLE_TYPE_DEFAULT_HELP}`;
1004
+ }
1005
+ if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
1006
+ return `${where}: "unit" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
1007
+ }
1008
+ if (v.css !== undefined && typeof v.css !== "string") {
1009
+ return `${where}: "css" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
1010
+ }
1011
+ return null;
1012
+ }
1013
+ // Validates a TYPE prop defaultValue — but ONLY for the size/style types whose shape we know
1014
+ // ({ css?, value?, unit? }). Any other TYPE typeId (color/border/box-shadow/grid/string-union
1015
+ // styles, IkasProduct, custom types) has a different value shape we can't verify offline, so its
1016
+ // default is passed through unchecked. The generator only emits a default for these size/style
1017
+ // types anyway. Returns an error message, or null if valid / not a size-style type.
1018
+ function validateTypeDefaultValue(value, propName, typeId) {
1019
+ if (value === undefined || value === null)
1020
+ return null;
1021
+ if (typeof typeId !== "string" || !typeId.startsWith(BP_STOREFRONT_PREFIX)) {
1022
+ return null;
1023
+ }
1024
+ // Strip the prefix and an optional _array suffix to get the base type name.
1025
+ const withoutPrefix = typeId.slice(BP_STOREFRONT_PREFIX.length);
1026
+ const isArray = withoutPrefix.endsWith("_array");
1027
+ const baseName = isArray
1028
+ ? withoutPrefix.slice(0, -"_array".length)
1029
+ : withoutPrefix;
1030
+ if (!SIZE_STYLE_TYPE_NAMES.includes(baseName))
1031
+ return null;
1032
+ const where = `defaultValue for prop "${propName}"`;
1033
+ if (isArray) {
1034
+ if (!Array.isArray(value)) {
1035
+ return `${where} must be an array for an _array TYPE prop: [ { "value": …, "unit": … }, … ]. ${STYLE_TYPE_DEFAULT_HELP}`;
1036
+ }
1037
+ return firstError(value, (el, i) => styleDefaultObjectError(el, `${where}[${i}]`));
1038
+ }
1039
+ return styleDefaultObjectError(value, where);
1040
+ }
765
1041
  async function updateProp(ref, options) {
766
1042
  const { config, configPath } = loadConfig();
767
1043
  const component = resolveComponent(config, ref);
@@ -794,7 +1070,18 @@ async function updateProp(ref, options) {
794
1070
  if (options.description !== undefined) {
795
1071
  prop.description = options.description || undefined;
796
1072
  }
1073
+ // Resolve numberRangeData before defaultValue so a NUMBER_RANGE default can be validated
1074
+ // against the (possibly just-updated) slider config in the same call.
1075
+ if (options.numberRangeData !== undefined) {
1076
+ if (options.numberRangeData === "" || options.numberRangeData === "none") {
1077
+ delete prop.numberRangeData;
1078
+ }
1079
+ else {
1080
+ prop.numberRangeData = parseNumberRangeData(options.numberRangeData, prop.name);
1081
+ }
1082
+ }
797
1083
  if (options.defaultValue !== undefined) {
1084
+ rejectDynamicDefault(prop.name, prop.type, options.defaultValue);
798
1085
  const parsed = parseDefaultValue(options.defaultValue, prop.type);
799
1086
  if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
800
1087
  const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
@@ -803,6 +1090,21 @@ async function updateProp(ref, options) {
803
1090
  process.exit(1);
804
1091
  }
805
1092
  }
1093
+ if (prop.type === "NUMBER_RANGE") {
1094
+ const nrError = validateNumberRangeDefaultValue(parsed, prop.numberRangeData, prop.name);
1095
+ if (nrError) {
1096
+ console.log(JSON.stringify({ success: false, error: nrError }));
1097
+ process.exit(1);
1098
+ }
1099
+ }
1100
+ if (prop.type === "TYPE") {
1101
+ // Prefer a typeId supplied in this same update over the stored one.
1102
+ const typeError = validateTypeDefaultValue(parsed, prop.name, options.typeId ?? prop.typeId);
1103
+ if (typeError) {
1104
+ console.log(JSON.stringify({ success: false, error: typeError }));
1105
+ process.exit(1);
1106
+ }
1107
+ }
806
1108
  prop.defaultValue = parsed;
807
1109
  }
808
1110
  if (options.group !== undefined) {
@@ -925,6 +1227,9 @@ async function updateProp(ref, options) {
925
1227
  ? { filteredComponentIds: prop.filteredComponentIds }
926
1228
  : {}),
927
1229
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
1230
+ ...(prop.numberRangeData
1231
+ ? { numberRangeData: prop.numberRangeData }
1232
+ : {}),
928
1233
  },
929
1234
  }));
930
1235
  }
@@ -1423,6 +1728,7 @@ export function createConfigCommand() {
1423
1728
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
1424
1729
  .option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
1425
1730
  .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)")
1731
+ .option("--numberRangeData <json>", 'JSON slider config for NUMBER_RANGE props, e.g. \'{"min":0,"max":100,"interval":5,"unit":"px"}\'')
1426
1732
  .action((options) => {
1427
1733
  addProp({ id: options.componentId, name: options.component }, options);
1428
1734
  });
@@ -1442,6 +1748,7 @@ export function createConfigCommand() {
1442
1748
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (use 'none' to clear)")
1443
1749
  .option("--filteredComponentIds <json>", "JSON array of component IDs (use 'none' to clear)")
1444
1750
  .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (use 'none' to clear)")
1751
+ .option("--numberRangeData <json>", "JSON slider config for NUMBER_RANGE props (use 'none' to clear)")
1445
1752
  .action((options) => {
1446
1753
  updateProp({ id: options.componentId, name: options.component }, options);
1447
1754
  });