@ikas/component-cli 1.4.0-beta.13 → 1.4.0-beta.130

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 (135) 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/add-sections-to-page.d.ts +3 -0
  6. package/dist/commands/add-sections-to-page.d.ts.map +1 -0
  7. package/dist/commands/add-sections-to-page.js +39 -0
  8. package/dist/commands/add-sections-to-page.js.map +1 -0
  9. package/dist/commands/add.d.ts.map +1 -1
  10. package/dist/commands/add.js +10 -1
  11. package/dist/commands/add.js.map +1 -1
  12. package/dist/commands/build.d.ts.map +1 -1
  13. package/dist/commands/build.js +5 -165
  14. package/dist/commands/build.js.map +1 -1
  15. package/dist/commands/config.d.ts +28 -0
  16. package/dist/commands/config.d.ts.map +1 -1
  17. package/dist/commands/config.js +737 -96
  18. package/dist/commands/config.js.map +1 -1
  19. package/dist/commands/create-design-tokens.d.ts +24 -0
  20. package/dist/commands/create-design-tokens.d.ts.map +1 -0
  21. package/dist/commands/create-design-tokens.js +327 -0
  22. package/dist/commands/create-design-tokens.js.map +1 -0
  23. package/dist/commands/create-global-variable.d.ts +3 -0
  24. package/dist/commands/create-global-variable.d.ts.map +1 -0
  25. package/dist/commands/create-global-variable.js +53 -0
  26. package/dist/commands/create-global-variable.js.map +1 -0
  27. package/dist/commands/create-page.d.ts +3 -0
  28. package/dist/commands/create-page.d.ts.map +1 -0
  29. package/dist/commands/create-page.js +31 -0
  30. package/dist/commands/create-page.js.map +1 -0
  31. package/dist/commands/delete-theme-globals.d.ts +4 -0
  32. package/dist/commands/delete-theme-globals.d.ts.map +1 -0
  33. package/dist/commands/delete-theme-globals.js +48 -0
  34. package/dist/commands/delete-theme-globals.js.map +1 -0
  35. package/dist/commands/dev.d.ts.map +1 -1
  36. package/dist/commands/dev.js +82 -12
  37. package/dist/commands/dev.js.map +1 -1
  38. package/dist/commands/get-component-props.d.ts +3 -0
  39. package/dist/commands/get-component-props.d.ts.map +1 -0
  40. package/dist/commands/get-component-props.js +32 -0
  41. package/dist/commands/get-component-props.js.map +1 -0
  42. package/dist/commands/get-page-by-type.d.ts +3 -0
  43. package/dist/commands/get-page-by-type.d.ts.map +1 -0
  44. package/dist/commands/get-page-by-type.js +25 -0
  45. package/dist/commands/get-page-by-type.js.map +1 -0
  46. package/dist/commands/get-section-values.d.ts +3 -0
  47. package/dist/commands/get-section-values.d.ts.map +1 -0
  48. package/dist/commands/get-section-values.js +39 -0
  49. package/dist/commands/get-section-values.js.map +1 -0
  50. package/dist/commands/list-entities.d.ts +3 -0
  51. package/dist/commands/list-entities.d.ts.map +1 -0
  52. package/dist/commands/list-entities.js +32 -0
  53. package/dist/commands/list-entities.js.map +1 -0
  54. package/dist/commands/list-page-sections.d.ts +3 -0
  55. package/dist/commands/list-page-sections.d.ts.map +1 -0
  56. package/dist/commands/list-page-sections.js +25 -0
  57. package/dist/commands/list-page-sections.js.map +1 -0
  58. package/dist/commands/list-theme-globals.d.ts +3 -0
  59. package/dist/commands/list-theme-globals.d.ts.map +1 -0
  60. package/dist/commands/list-theme-globals.js +22 -0
  61. package/dist/commands/list-theme-globals.js.map +1 -0
  62. package/dist/commands/publish-theme.d.ts +3 -0
  63. package/dist/commands/publish-theme.d.ts.map +1 -0
  64. package/dist/commands/publish-theme.js +29 -0
  65. package/dist/commands/publish-theme.js.map +1 -0
  66. package/dist/commands/publish.d.ts.map +1 -1
  67. package/dist/commands/publish.js +24 -2
  68. package/dist/commands/publish.js.map +1 -1
  69. package/dist/commands/search-products.d.ts +3 -0
  70. package/dist/commands/search-products.d.ts.map +1 -0
  71. package/dist/commands/search-products.js +40 -0
  72. package/dist/commands/search-products.js.map +1 -0
  73. package/dist/commands/update-global-variable.d.ts +3 -0
  74. package/dist/commands/update-global-variable.d.ts.map +1 -0
  75. package/dist/commands/update-global-variable.js +47 -0
  76. package/dist/commands/update-global-variable.js.map +1 -0
  77. package/dist/commands/update-page-sections.d.ts +3 -0
  78. package/dist/commands/update-page-sections.d.ts.map +1 -0
  79. package/dist/commands/update-page-sections.js +39 -0
  80. package/dist/commands/update-page-sections.js.map +1 -0
  81. package/dist/commands/update-section-prop.d.ts +3 -0
  82. package/dist/commands/update-section-prop.d.ts.map +1 -0
  83. package/dist/commands/update-section-prop.js +75 -0
  84. package/dist/commands/update-section-prop.js.map +1 -0
  85. package/dist/commands/upload-image.d.ts +3 -0
  86. package/dist/commands/upload-image.d.ts.map +1 -0
  87. package/dist/commands/upload-image.js +38 -0
  88. package/dist/commands/upload-image.js.map +1 -0
  89. package/dist/commands/upload-images.d.ts +3 -0
  90. package/dist/commands/upload-images.d.ts.map +1 -0
  91. package/dist/commands/upload-images.js +48 -0
  92. package/dist/commands/upload-images.js.map +1 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +48 -1
  95. package/dist/index.js.map +1 -1
  96. package/dist/types.d.ts +18 -1
  97. package/dist/types.d.ts.map +1 -1
  98. package/dist/utils/compile.d.ts +4 -1
  99. package/dist/utils/compile.d.ts.map +1 -1
  100. package/dist/utils/compile.js +630 -102
  101. package/dist/utils/compile.js.map +1 -1
  102. package/dist/utils/component-helpers.d.ts +70 -3
  103. package/dist/utils/component-helpers.d.ts.map +1 -1
  104. package/dist/utils/component-helpers.js +195 -23
  105. package/dist/utils/component-helpers.js.map +1 -1
  106. package/dist/utils/editor-action-client.d.ts +1 -1
  107. package/dist/utils/editor-action-client.d.ts.map +1 -1
  108. package/dist/utils/editor-action-client.js +23 -8
  109. package/dist/utils/editor-action-client.js.map +1 -1
  110. package/dist/utils/forbid-deep-imports.d.ts +3 -0
  111. package/dist/utils/forbid-deep-imports.d.ts.map +1 -0
  112. package/dist/utils/forbid-deep-imports.js +54 -0
  113. package/dist/utils/forbid-deep-imports.js.map +1 -0
  114. package/dist/utils/forbid-external-packages.d.ts +16 -0
  115. package/dist/utils/forbid-external-packages.d.ts.map +1 -0
  116. package/dist/utils/forbid-external-packages.js +89 -0
  117. package/dist/utils/forbid-external-packages.js.map +1 -0
  118. package/dist/utils/load-image.d.ts +16 -0
  119. package/dist/utils/load-image.d.ts.map +1 -0
  120. package/dist/utils/load-image.js +50 -0
  121. package/dist/utils/load-image.js.map +1 -0
  122. package/dist/utils/websocket-server.d.ts +13 -4
  123. package/dist/utils/websocket-server.d.ts.map +1 -1
  124. package/dist/utils/websocket-server.js +1 -1
  125. package/dist/utils/websocket-server.js.map +1 -1
  126. package/package.json +13 -1
  127. package/templates/add/section.css +11 -0
  128. package/dist/commands/create.d.ts +0 -9
  129. package/dist/commands/create.d.ts.map +0 -1
  130. package/dist/commands/create.js +0 -9
  131. package/dist/commands/create.js.map +0 -1
  132. package/dist/commands/proxy.d.ts +0 -39
  133. package/dist/commands/proxy.d.ts.map +0 -1
  134. package/dist/commands/proxy.js +0 -212
  135. package/dist/commands/proxy.js.map +0 -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, 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)) {
@@ -31,7 +31,10 @@ function resolveComponent(config, ref) {
31
31
  success: false,
32
32
  error: "Component reference required: pass --id (preferred) or --name (exact PascalCase). " +
33
33
  "Run `ikas-component config list-components` to see canonical ids.",
34
- validComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
34
+ validComponents: config.components.map((c) => ({
35
+ id: c.id,
36
+ name: c.name,
37
+ })),
35
38
  }));
36
39
  process.exit(1);
37
40
  }
@@ -52,7 +55,10 @@ function resolveComponent(config, ref) {
52
55
  ? `Component not found: id="${id}".`
53
56
  : `Component not found: name="${name}". Names are matched exactly (PascalCase, no normalization).`,
54
57
  lookup: id ? { id } : { name },
55
- validComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
58
+ validComponents: config.components.map((c) => ({
59
+ id: c.id,
60
+ name: c.name,
61
+ })),
56
62
  hint: "Use --id (preferred) or exact PascalCase --name. " +
57
63
  "Run `ikas-component config list-components` to see canonical ids.",
58
64
  }));
@@ -156,7 +162,91 @@ const ALLOWED_PROP_FIELDS = new Set([
156
162
  "enumTypeId",
157
163
  "filteredComponentIds",
158
164
  "privateVarMap",
165
+ "numberRangeData",
159
166
  ]);
167
+ // Emit the standard CLI error envelope and exit(1) when a validator returned a message; a no-op
168
+ // when the message is null. Collapses the repeated `if (err) { console.log(...); process.exit(1) }`
169
+ // tail that follows every default-value validator call.
170
+ function failIf(error) {
171
+ if (error) {
172
+ console.log(JSON.stringify({ success: false, error }));
173
+ process.exit(1);
174
+ }
175
+ }
176
+ /**
177
+ * Validate/sanitize a numberRangeData object for a NUMBER_RANGE prop. Accepts an object
178
+ * or a JSON string. min/max/interval are truncated to integers; only a positive interval
179
+ * is kept (0/negative would yield an invalid slider step). Exits with a structured error
180
+ * on malformed input.
181
+ */
182
+ function parseNumberRangeData(raw, propName) {
183
+ const fail = (error) => {
184
+ console.log(JSON.stringify({ success: false, error }));
185
+ process.exit(1);
186
+ };
187
+ let obj = raw;
188
+ if (typeof raw === "string") {
189
+ try {
190
+ obj = JSON.parse(raw);
191
+ }
192
+ catch {
193
+ fail(`Invalid --numberRangeData JSON for prop "${propName}": ${raw}`);
194
+ }
195
+ }
196
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
197
+ fail(`numberRangeData for prop "${propName}" must be an object { min, max, interval?, unit? }.`);
198
+ }
199
+ const o = obj;
200
+ const asInt = (v, field) => {
201
+ if (v === undefined || v === null)
202
+ return undefined;
203
+ if (typeof v !== "number" || !Number.isFinite(v)) {
204
+ fail(`numberRangeData.${field} for prop "${propName}" must be a finite number.`);
205
+ }
206
+ return Math.trunc(v);
207
+ };
208
+ const min = asInt(o.min, "min");
209
+ const max = asInt(o.max, "max");
210
+ const interval = asInt(o.interval, "interval");
211
+ const unit = typeof o.unit === "string" && o.unit.trim() ? o.unit.trim() : undefined;
212
+ if (min !== undefined && max !== undefined && min >= max) {
213
+ fail(`numberRangeData for prop "${propName}": min (${min}) must be less than max (${max}).`);
214
+ }
215
+ return {
216
+ ...(min !== undefined ? { min } : {}),
217
+ ...(max !== undefined ? { max } : {}),
218
+ ...(interval !== undefined && interval > 0 ? { interval } : {}),
219
+ ...(unit ? { unit } : {}),
220
+ };
221
+ }
222
+ const numberRangeDataRequiredError = (propName) => `numberRangeData with min and max is required for NUMBER_RANGE prop "${propName}". ` +
223
+ `Provide it, e.g. --numberRangeData '{"min":0,"max":100,"interval":5,"unit":"px"}' ` +
224
+ `(or a "numberRangeData" field inside --props). interval and unit are optional.`;
225
+ // A NUMBER_RANGE slider config is complete only with both bounds; interval/unit stay optional.
226
+ export const isNumberRangeComplete = (data) => data.min !== undefined && data.max !== undefined;
227
+ /**
228
+ * A NUMBER_RANGE prop must carry a slider config with at least min and max — the editor UI marks
229
+ * Minimum/Maksimum Değer as required, so a prop authored without them yields a half-configured
230
+ * slider. Enforce it at creation time: reject when numberRangeData is absent or missing min/max.
231
+ */
232
+ export function requireNumberRangeData(raw, propName) {
233
+ if (raw === undefined) {
234
+ console.log(JSON.stringify({
235
+ success: false,
236
+ error: numberRangeDataRequiredError(propName),
237
+ }));
238
+ process.exit(1);
239
+ }
240
+ const parsed = parseNumberRangeData(raw, propName);
241
+ if (!isNumberRangeComplete(parsed)) {
242
+ console.log(JSON.stringify({
243
+ success: false,
244
+ error: numberRangeDataRequiredError(propName),
245
+ }));
246
+ process.exit(1);
247
+ }
248
+ return parsed;
249
+ }
160
250
  /**
161
251
  * Parse and validate the --props JSON flag for add-component.
162
252
  * Returns validated ComponentProp[] or exits with error.
@@ -234,6 +324,31 @@ async function parsePropsFlag(propsJson, config, configPath) {
234
324
  }));
235
325
  process.exit(1);
236
326
  }
327
+ // Dynamic (merchant-data reference) props can't carry a static default.
328
+ rejectDynamicDefault(r.name, propType, r.defaultValue);
329
+ // Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
330
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
331
+ r.defaultValue !== undefined) {
332
+ const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
333
+ failIf(linkError);
334
+ }
335
+ // Validate SVG / SVG_LIST defaultValue (reject malformed/oversized/unsafe SVG)
336
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
337
+ r.defaultValue !== undefined) {
338
+ const svgError = validateSvgDefaultValue(propType, r.defaultValue, r.name);
339
+ failIf(svgError);
340
+ }
341
+ // Validate TYPE (structured/style) defaultValue shape — must be an object (or an array for an
342
+ // _array typeId), never a bare string that would be emitted as a wrong-shaped default.
343
+ if (propType === "TYPE" && r.defaultValue !== undefined) {
344
+ const typeError = validateTypeDefaultValue(r.defaultValue, r.name, r.typeId);
345
+ failIf(typeError);
346
+ }
347
+ // Validate NUMBER defaultValue — reject empty/non-finite (Number("") silently becomes 0).
348
+ if (propType === "NUMBER" && r.defaultValue !== undefined) {
349
+ const numError = validateNumberDefaultValue(r.defaultValue, r.name);
350
+ failIf(numError);
351
+ }
237
352
  // Auto-generate displayName if omitted
238
353
  const displayName = typeof r.displayName === "string" && r.displayName
239
354
  ? r.displayName
@@ -258,7 +373,10 @@ async function parsePropsFlag(propsJson, config, configPath) {
258
373
  process.exit(1);
259
374
  }
260
375
  // Validate enum type exists (checks config then live dev server)
261
- if (config && configPath && typeof r.enumTypeId === "string" && !r.enumTypeId.startsWith("@ikas/")) {
376
+ if (config &&
377
+ configPath &&
378
+ typeof r.enumTypeId === "string" &&
379
+ !isBuiltInEnumTypeId(r.enumTypeId)) {
262
380
  const resolved = await resolveEnumType(r.enumTypeId, config, configPath);
263
381
  if (!resolved) {
264
382
  console.log(JSON.stringify({
@@ -277,9 +395,7 @@ async function parsePropsFlag(propsJson, config, configPath) {
277
395
  ...(typeof r.description === "string" && r.description
278
396
  ? { description: r.description }
279
397
  : {}),
280
- ...(r.defaultValue !== undefined
281
- ? { defaultValue: r.defaultValue }
282
- : {}),
398
+ ...(r.defaultValue !== undefined ? { defaultValue: r.defaultValue } : {}),
283
399
  ...(typeof r.groupId === "string" && r.groupId
284
400
  ? { groupId: r.groupId }
285
401
  : {}),
@@ -289,11 +405,22 @@ async function parsePropsFlag(propsJson, config, configPath) {
289
405
  ...(propType === "ENUM" && typeof r.enumTypeId === "string"
290
406
  ? { enumTypeId: r.enumTypeId }
291
407
  : {}),
292
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.filteredComponentIds && Array.isArray(r.filteredComponentIds)
293
- ? { filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config) }
408
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
409
+ r.filteredComponentIds &&
410
+ Array.isArray(r.filteredComponentIds)
411
+ ? {
412
+ filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config),
413
+ }
414
+ : {}),
415
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
416
+ r.privateVarMap &&
417
+ typeof r.privateVarMap === "object"
418
+ ? {
419
+ privateVarMap: r.privateVarMap,
420
+ }
294
421
  : {}),
295
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.privateVarMap && typeof r.privateVarMap === "object"
296
- ? { privateVarMap: r.privateVarMap }
422
+ ...(propType === "NUMBER_RANGE"
423
+ ? { numberRangeData: requireNumberRangeData(r.numberRangeData, r.name) }
297
424
  : {}),
298
425
  };
299
426
  props.push(prop);
@@ -325,11 +452,25 @@ async function addComponent(name, options) {
325
452
  if (!config.projectId) {
326
453
  config.projectId = generateProjectId();
327
454
  }
455
+ else if (!isValidProjectId(config.projectId)) {
456
+ // A malformed projectId (hand-edited config) breaks the editor's republish
457
+ // stability: published/local id prefixes stop matching and every publish
458
+ // INSERTs duplicate design elements. Fail fast instead of corrupting data.
459
+ console.log(JSON.stringify({
460
+ success: false,
461
+ error: `Invalid projectId "${config.projectId}" in ikas.config.json. ` +
462
+ "projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
463
+ "Restore the original generated value — do not hand-edit it.",
464
+ }));
465
+ process.exit(1);
466
+ }
328
467
  // Parse --props (and validate filteredComponentIds) BEFORE any filesystem side-effects
329
468
  // so a bad payload doesn't leave an empty component directory behind.
330
469
  const props = options.props
331
470
  ? await parsePropsFlag(options.props, config, configPath)
332
471
  : [];
472
+ // Reject section-disallowed TYPE props at definition time (best-effort, needs dev server)
473
+ await assertTypeIdsUsable(componentType, props.flatMap((p) => (p.type === "TYPE" && p.typeId ? [p.typeId] : [])));
333
474
  const componentId = generateComponentId(config.projectId);
334
475
  const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
335
476
  // Create directory
@@ -420,6 +561,10 @@ async function addProp(ref, options) {
420
561
  }));
421
562
  process.exit(1);
422
563
  }
564
+ // Reject section-disallowed types at definition time (best-effort, needs dev server)
565
+ if (propType === "TYPE" && options.typeId) {
566
+ await assertTypeIdsUsable(component.type, [options.typeId]);
567
+ }
423
568
  // Validate enumTypeId for ENUM props
424
569
  if (propType === "ENUM" && !options.enumTypeId) {
425
570
  console.log(JSON.stringify({
@@ -430,7 +575,9 @@ async function addProp(ref, options) {
430
575
  }
431
576
  // Validate that enumTypeId references an existing enum type
432
577
  // (checks config.customTypes first, then falls back to the live dev server)
433
- if (propType === "ENUM" && options.enumTypeId && !options.enumTypeId.startsWith("@ikas/")) {
578
+ if (propType === "ENUM" &&
579
+ options.enumTypeId &&
580
+ !isBuiltInEnumTypeId(options.enumTypeId)) {
434
581
  const resolved = await resolveEnumType(options.enumTypeId, config, configPath);
435
582
  if (!resolved) {
436
583
  console.log(JSON.stringify({
@@ -460,49 +607,97 @@ async function addProp(ref, options) {
460
607
  }
461
608
  // Parse filteredComponentIds JSON for COMPONENT/COMPONENT_LIST
462
609
  let parsedFilteredIds;
463
- if (options.filteredComponentIds && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
610
+ if (options.filteredComponentIds &&
611
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
464
612
  try {
465
613
  parsedFilteredIds = JSON.parse(options.filteredComponentIds);
466
614
  if (!Array.isArray(parsedFilteredIds)) {
467
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
615
+ console.log(JSON.stringify({
616
+ success: false,
617
+ error: "--filteredComponentIds must be a JSON array of strings.",
618
+ }));
468
619
  process.exit(1);
469
620
  }
470
621
  }
471
622
  catch {
472
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
623
+ console.log(JSON.stringify({
624
+ success: false,
625
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
626
+ }));
473
627
  process.exit(1);
474
628
  }
475
629
  assertKnownComponentIds(parsedFilteredIds, config);
476
630
  }
477
631
  // Parse privateVarMap JSON for COMPONENT/COMPONENT_LIST
478
632
  let parsedPrivateVarMap;
479
- if (options.privateVarMap && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
633
+ if (options.privateVarMap &&
634
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
480
635
  try {
481
636
  parsedPrivateVarMap = JSON.parse(options.privateVarMap);
482
- if (typeof parsedPrivateVarMap !== "object" || Array.isArray(parsedPrivateVarMap)) {
483
- console.log(JSON.stringify({ success: false, error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}." }));
637
+ if (typeof parsedPrivateVarMap !== "object" ||
638
+ Array.isArray(parsedPrivateVarMap)) {
639
+ console.log(JSON.stringify({
640
+ success: false,
641
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
642
+ }));
484
643
  process.exit(1);
485
644
  }
486
645
  }
487
646
  catch {
488
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
647
+ console.log(JSON.stringify({
648
+ success: false,
649
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
650
+ }));
489
651
  process.exit(1);
490
652
  }
491
653
  }
654
+ rejectDynamicDefault(options.name, propType, options.defaultValue);
655
+ const parsedDefaultValue = options.defaultValue !== undefined
656
+ ? parseDefaultValue(options.defaultValue, propType)
657
+ : undefined;
658
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
659
+ parsedDefaultValue !== undefined) {
660
+ const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
661
+ failIf(linkError);
662
+ }
663
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
664
+ parsedDefaultValue !== undefined) {
665
+ const svgError = validateSvgDefaultValue(propType, parsedDefaultValue, options.name);
666
+ failIf(svgError);
667
+ }
668
+ if (propType === "TYPE" && parsedDefaultValue !== undefined) {
669
+ const typeError = validateTypeDefaultValue(parsedDefaultValue, options.name, options.typeId);
670
+ failIf(typeError);
671
+ }
672
+ // Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
673
+ if (propType === "NUMBER" && options.defaultValue !== undefined) {
674
+ const numError = validateNumberDefaultValue(options.defaultValue, options.name);
675
+ failIf(numError);
676
+ }
677
+ const parsedNumberRangeData = propType === "NUMBER_RANGE"
678
+ ? requireNumberRangeData(options.numberRangeData, options.name)
679
+ : undefined;
680
+ if (propType === "NUMBER_RANGE" && parsedDefaultValue !== undefined) {
681
+ const nrError = validateNumberRangeDefaultValue(parsedDefaultValue, parsedNumberRangeData, options.name);
682
+ failIf(nrError);
683
+ }
492
684
  const newProp = {
493
685
  name: options.name,
494
686
  displayName: options.displayName,
495
687
  type: propType,
496
688
  required: options.required ?? false,
497
689
  ...(options.description ? { description: options.description } : {}),
498
- ...(options.defaultValue !== undefined
499
- ? { defaultValue: parseDefaultValue(options.defaultValue, propType) }
690
+ ...(parsedDefaultValue !== undefined
691
+ ? { defaultValue: parsedDefaultValue }
500
692
  : {}),
501
693
  ...(options.group ? { groupId: options.group } : {}),
502
694
  ...(options.typeId ? { typeId: options.typeId } : {}),
503
695
  ...(options.enumTypeId ? { enumTypeId: options.enumTypeId } : {}),
504
696
  ...(parsedFilteredIds ? { filteredComponentIds: parsedFilteredIds } : {}),
505
697
  ...(parsedPrivateVarMap ? { privateVarMap: parsedPrivateVarMap } : {}),
698
+ ...(parsedNumberRangeData
699
+ ? { numberRangeData: parsedNumberRangeData }
700
+ : {}),
506
701
  };
507
702
  component.props.push(newProp);
508
703
  saveConfig(configPath, config);
@@ -519,22 +714,378 @@ async function addProp(ref, options) {
519
714
  required: newProp.required,
520
715
  ...(newProp.groupId ? { groupId: newProp.groupId } : {}),
521
716
  ...(newProp.enumTypeId ? { enumTypeId: newProp.enumTypeId } : {}),
522
- ...(newProp.filteredComponentIds ? { filteredComponentIds: newProp.filteredComponentIds } : {}),
523
- ...(newProp.privateVarMap ? { privateVarMap: newProp.privateVarMap } : {}),
717
+ ...(newProp.filteredComponentIds
718
+ ? { filteredComponentIds: newProp.filteredComponentIds }
719
+ : {}),
720
+ ...(newProp.privateVarMap
721
+ ? { privateVarMap: newProp.privateVarMap }
722
+ : {}),
723
+ ...(newProp.numberRangeData
724
+ ? { numberRangeData: newProp.numberRangeData }
725
+ : {}),
524
726
  },
525
727
  }));
526
728
  }
527
- function parseDefaultValue(value, propType) {
729
+ export function parseDefaultValue(value, propType) {
528
730
  switch (propType) {
529
731
  case "NUMBER":
530
732
  return Number(value);
531
733
  case "BOOLEAN":
532
734
  return value === "true";
735
+ case "LINK":
736
+ case "LIST_OF_LINK":
737
+ case "SVG_LIST":
738
+ case "NUMBER_RANGE":
739
+ case "TYPE":
740
+ // These all carry a typed value (object/array), not a scalar. The --defaultValue flag arrives
741
+ // as a string, so parse it here; if it is not valid JSON, return the raw string so the type's
742
+ // validator (validateLinkDefaultValue / validateSvgDefaultValue / validateNumberRangeDefaultValue
743
+ // / validateTypeDefaultValue) can report a precise error instead of silently storing a bad shape.
744
+ try {
745
+ return JSON.parse(value);
746
+ }
747
+ catch {
748
+ return value;
749
+ }
533
750
  default:
534
751
  return value;
535
752
  }
536
753
  }
537
- function updateProp(ref, options) {
754
+ // Prop types whose value is a dynamic reference to merchant storefront data (products, media,
755
+ // categories, brands, blogs, raffles). They resolve at storefront render time, so a static config
756
+ // default is invalid and is rejected on add/update-prop. Keep in sync with @ikas/editor-models
757
+ // DYNAMIC_PROP_TYPES (the CLI must not depend on that private package).
758
+ export const DYNAMIC_PROP_TYPES = [
759
+ "IMAGE",
760
+ "IMAGE_LIST",
761
+ "VIDEO",
762
+ "PRODUCT",
763
+ "PRODUCT_LIST",
764
+ "PRODUCT_ATTRIBUTE",
765
+ "PRODUCT_ATTRIBUTE_LIST",
766
+ "BRAND",
767
+ "BRAND_LIST",
768
+ "CATEGORY",
769
+ "CATEGORY_LIST",
770
+ "BLOG",
771
+ "BLOG_LIST",
772
+ "BLOG_CATEGORY",
773
+ "BLOG_CATEGORY_LIST",
774
+ "RAFFLE",
775
+ "RAFFLE_LIST",
776
+ ];
777
+ 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.`;
778
+ // Reject a static default on a dynamic (merchant-data reference) prop type: print the standard
779
+ // error and exit. No-op when the type isn't dynamic or no default was supplied.
780
+ export const rejectDynamicDefault = (propName, propType, defaultValue) => {
781
+ if (DYNAMIC_PROP_TYPES.includes(propType) && defaultValue !== undefined) {
782
+ console.log(JSON.stringify({
783
+ success: false,
784
+ error: dynamicPropDefaultError(propName, propType),
785
+ }));
786
+ process.exit(1);
787
+ }
788
+ };
789
+ const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
790
+ // FILE is intentionally excluded from CLI-authored defaults: a code-component / AI-authored default
791
+ // can't reference a store-specific uploaded file, so a "FILE" link default is always rejected. Store
792
+ // owners can still create FILE links manually in the editor (editor-core keeps FILE in its own set).
793
+ const VALID_LINK_TYPES = ["PAGE", "EXTERNAL"];
794
+ const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
795
+ 'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
796
+ "Do NOT pass a JSON string or a { label, href } shape.";
797
+ // Returns the index of the first array element that fails `fn`, as an error string, or null.
798
+ const firstError = (arr, fn) => arr.reduce((acc, item, index) => acc ?? fn(item, index), null);
799
+ const validateSingleLink = (link, propName, path) => {
800
+ const where = `defaultValue for prop "${propName}"${path}`;
801
+ if (typeof link === "string") {
802
+ return `${where} is a JSON string but must be an object. ${LINK_SHAPE_HELP}`;
803
+ }
804
+ if (!link || typeof link !== "object" || Array.isArray(link)) {
805
+ return `${where} must be a link object. ${LINK_SHAPE_HELP}`;
806
+ }
807
+ const l = link;
808
+ if ("href" in l && !("linkType" in l)) {
809
+ return `${where} uses a legacy { href } shape without "linkType". Use "linkType" + "externalLink"/"pageType" instead. ${LINK_SHAPE_HELP}`;
810
+ }
811
+ if (typeof l.linkType !== "string" ||
812
+ !VALID_LINK_TYPES.includes(l.linkType)) {
813
+ return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
814
+ }
815
+ // Require the destination field for the declared linkType, so a link that points nowhere never
816
+ // reaches a published theme. A PAGE default carries a pageType (pageId is per-store, filled on
817
+ // materialization); pageId alone is also accepted for a specific page.
818
+ if (l.linkType === "PAGE" && !nonEmpty(l.pageType) && !nonEmpty(l.pageId)) {
819
+ return `${where}: a "PAGE" link needs a "pageType" (e.g. "INDEX") — or a "pageId" for a specific page. ${LINK_SHAPE_HELP}`;
820
+ }
821
+ if (l.linkType === "EXTERNAL" && !nonEmpty(l.externalLink)) {
822
+ return `${where}: an "EXTERNAL" link needs a non-empty "externalLink" URL. ${LINK_SHAPE_HELP}`;
823
+ }
824
+ if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
825
+ return `${where}: "subLinks" must be an array (use [] when there are none).`;
826
+ }
827
+ if (Array.isArray(l.subLinks)) {
828
+ return firstError(l.subLinks, (sl, i) => validateSingleLink(sl, propName, `${path}.subLinks[${i}]`));
829
+ }
830
+ return null;
831
+ };
832
+ // Validates a LINK / LIST_OF_LINK defaultValue. Returns an error message, or null if valid.
833
+ // These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
834
+ // legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
835
+ // out of published themes.
836
+ export function validateLinkDefaultValue(propType, value, propName) {
837
+ if (value === undefined || value === null)
838
+ return null;
839
+ if (propType === "LINK") {
840
+ return validateSingleLink(value, propName, "");
841
+ }
842
+ // LIST_OF_LINK
843
+ if (typeof value === "string") {
844
+ return `defaultValue for prop "${propName}" is a JSON string but must be an object { "links": [...] }. ${LINK_SHAPE_HELP}`;
845
+ }
846
+ if (!value ||
847
+ typeof value !== "object" ||
848
+ Array.isArray(value) ||
849
+ !Array.isArray(value.links)) {
850
+ return `defaultValue for prop "${propName}" must be an object with a "links" array: { "links": [ <link>, ... ] }. ${LINK_SHAPE_HELP}`;
851
+ }
852
+ const links = value.links;
853
+ return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
854
+ }
855
+ const SVG_DEFAULT_HELP = "An SVG default must be a single well-formed <svg>…</svg> string ≤ 64 KB with no embedded base64 raster images. " +
856
+ 'Tip: the editor uses each svg\'s `class` attribute as its display name — set class="my-icon" to name it; ' +
857
+ "SVG_LIST items with no class show as svg-1, svg-2, ….";
858
+ // Keep the cap and base64 pattern in sync with @ikas/editor-models validate-svg.ts (the
859
+ // authoritative validator). Duplicated here on purpose — see validateSvgShape below.
860
+ const SVG_MAX_SIZE_KB = 64;
861
+ const SVG_BASE64_RASTER_RE = /data:image\/[a-z0-9.+-]+;base64/i;
862
+ const SVG_SHAPE_RE = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>][\s\S]*<\/svg>\s*$/i;
863
+ // Count TOP-LEVEL <svg> roots without a DOM: scan svg open/close tags in order and track
864
+ // nesting depth. A root is any <svg> opened at depth 0. This matches the editor's DOM-based
865
+ // single-root check — it rejects siblings (`<svg/><svg/>`) while allowing a nested <svg>.
866
+ // Self-closing <svg .../> counts as a root but opens no nesting level.
867
+ function svgRootCount(input) {
868
+ const tags = input.match(/<svg\b[^>]*>|<\/svg\s*>/gi) || [];
869
+ return tags.reduce((acc, tag) => {
870
+ if (tag.startsWith("</"))
871
+ return { depth: Math.max(0, acc.depth - 1), roots: acc.roots };
872
+ const roots = acc.depth === 0 ? acc.roots + 1 : acc.roots;
873
+ const depth = /\/>\s*$/.test(tag) ? acc.depth : acc.depth + 1;
874
+ return { depth, roots };
875
+ }, { depth: 0, roots: 0 }).roots;
876
+ }
877
+ // Lightweight, dependency-free SVG structure check. This standalone CLI is publishable and
878
+ // must not depend on the private @ikas/editor-models package (nor pull a DOM/DOMPurify into a
879
+ // CLI), so it mirrors the RULES of the editor's validateSvg (size, base64, single <svg> root)
880
+ // for fast authoring feedback. The authoritative sanitize runs in the editor/upload path.
881
+ // Returns a rejection reason, or null if the shape is acceptable.
882
+ function validateSvgShape(input) {
883
+ if (typeof input !== "string" || input.trim() === "")
884
+ return "empty or not a string";
885
+ if (Buffer.byteLength(input, "utf8") > SVG_MAX_SIZE_KB * 1024)
886
+ return `exceeds ${SVG_MAX_SIZE_KB} KB`;
887
+ if (SVG_BASE64_RASTER_RE.test(input))
888
+ return "embedded base64 raster image not allowed";
889
+ if (!SVG_SHAPE_RE.test(input))
890
+ return "not a single-root <svg> document";
891
+ if (svgRootCount(input) !== 1)
892
+ return "must be exactly one <svg> root";
893
+ return null;
894
+ }
895
+ // Validates an SVG / SVG_LIST defaultValue. Returns an error message, or null if valid.
896
+ // Authoring an SVG default is a common mistake source for agents; rejecting here (with an
897
+ // instructive message) keeps malformed/oversized SVGs out of published components.
898
+ export function validateSvgDefaultValue(propType, value, propName) {
899
+ if (value === undefined || value === null)
900
+ return null;
901
+ if (propType === "SVG") {
902
+ const reason = validateSvgShape(value);
903
+ return reason
904
+ ? `defaultValue for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
905
+ : null;
906
+ }
907
+ // SVG_LIST
908
+ if (!Array.isArray(value)) {
909
+ return `defaultValue for prop "${propName}" must be an array of SVG strings: [ "<svg>…</svg>", ... ]. ${SVG_DEFAULT_HELP}`;
910
+ }
911
+ return firstError(value, (svg, i) => {
912
+ const reason = validateSvgShape(svg);
913
+ return reason
914
+ ? `defaultValue[${i}] for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
915
+ : null;
916
+ });
917
+ }
918
+ const NUMBER_RANGE_DEFAULT_HELP = 'A NUMBER_RANGE default must be an object { "value": <number>, "unit": "px" | null }. ' +
919
+ "When the prop defines numberRangeData, value must be within [min, max] and on the interval grid (min + k·interval).";
920
+ // Validates a NUMBER_RANGE defaultValue. Returns an error message, or null if valid.
921
+ // Checks the { value, unit } shape and — when the prop carries numberRangeData — that the
922
+ // value is within [min, max] and lands on the interval grid, mirroring the editor's slider
923
+ // so an agent can't author an out-of-range or off-step default.
924
+ export function validateNumberRangeDefaultValue(value, numberRangeData, propName) {
925
+ if (value === undefined || value === null)
926
+ return null;
927
+ const where = `defaultValue for prop "${propName}"`;
928
+ if (typeof value === "string") {
929
+ return `${where} is a JSON string but must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
930
+ }
931
+ if (typeof value !== "object" || Array.isArray(value)) {
932
+ return `${where} must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
933
+ }
934
+ const v = value;
935
+ if (typeof v.value !== "number" || !Number.isFinite(v.value)) {
936
+ return `${where}: "value" must be a finite number. ${NUMBER_RANGE_DEFAULT_HELP}`;
937
+ }
938
+ if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
939
+ return `${where}: "unit" must be a string or null. ${NUMBER_RANGE_DEFAULT_HELP}`;
940
+ }
941
+ if (numberRangeData) {
942
+ const { min, max, interval } = numberRangeData;
943
+ if (min !== undefined && v.value < min) {
944
+ return `${where}: value ${v.value} is below the configured min (${min}).`;
945
+ }
946
+ if (max !== undefined && v.value > max) {
947
+ return `${where}: value ${v.value} is above the configured max (${max}).`;
948
+ }
949
+ if (interval !== undefined &&
950
+ interval > 0 &&
951
+ min !== undefined &&
952
+ (v.value - min) % interval !== 0) {
953
+ return `${where}: value ${v.value} is not on the interval grid (min ${min} + k·${interval}).`;
954
+ }
955
+ }
956
+ return null;
957
+ }
958
+ const NUMBER_DEFAULT_HELP = "A NUMBER default must be a finite number, e.g. --defaultValue 42.";
959
+ // Validates a NUMBER defaultValue. Returns an error message, or null if valid. Guards two footguns:
960
+ // an empty/whitespace flag string (Number("") === 0 would silently store 0) and a non-numeric or
961
+ // non-finite value (e.g. "42px" → NaN, "Infinity"). The flag path passes the raw --defaultValue
962
+ // string; the --props path passes the already-parsed JSON value — both are handled here.
963
+ export function validateNumberDefaultValue(value, propName) {
964
+ if (value === undefined || value === null)
965
+ return null;
966
+ const where = `defaultValue for prop "${propName}"`;
967
+ if (typeof value === "string") {
968
+ if (value.trim() === "") {
969
+ return `${where} is empty, but NUMBER requires a finite number. ${NUMBER_DEFAULT_HELP}`;
970
+ }
971
+ if (!Number.isFinite(Number(value))) {
972
+ return `${where} ("${value}") is not a finite number. ${NUMBER_DEFAULT_HELP}`;
973
+ }
974
+ return null;
975
+ }
976
+ if (typeof value !== "number" || !Number.isFinite(value)) {
977
+ return `${where} must be a finite number. ${NUMBER_DEFAULT_HELP}`;
978
+ }
979
+ return null;
980
+ }
981
+ const STYLE_TYPE_DEFAULT_HELP = "A size/style TYPE default must be an object matching the type's shape: " +
982
+ '{ "value": <number>, "unit": "px" | "rem" | "%" | "vh" | "vw" } (or { "css": "<raw css>" }). ' +
983
+ "Author it via add-component --props '[…]' or --defaultValue '<json>'.";
984
+ const BP_STOREFRONT_PREFIX = "@ikas/bp-storefront-models-";
985
+ // The size/style types that share the { css?, value?, unit? } shape. This is EXACTLY the set of
986
+ // TYPE typeIds whose default the generator emits (mirror of DYNAMIC_STYLE_TYPE_IDS in
987
+ // @ikas/blueprint-modules design-generator/generate-helpers/styles.ts — the standalone CLI must not
988
+ // depend on that package, so it is hand-maintained here and kept in sync by a parity test). Every
989
+ // OTHER TYPE typeId (BorderStyleType, BoxShadowStyleType, GridTemplateColumnsStyleType, string-union
990
+ // styles like TextAlignStyleType, IkasProduct, custom types) has a DIFFERENT value shape, so their
991
+ // defaults are not shape-checked (and are dead code in the generator anyway).
992
+ const SIZE_STYLE_TYPE_NAMES = [
993
+ "SizeStyleType",
994
+ "PaddingStyleType",
995
+ "PaddingTopStyleType",
996
+ "PaddingRightStyleType",
997
+ "PaddingBottomStyleType",
998
+ "PaddingLeftStyleType",
999
+ "MarginStyleType",
1000
+ "MarginTopStyleType",
1001
+ "MarginRightStyleType",
1002
+ "MarginBottomStyleType",
1003
+ "MarginLeftStyleType",
1004
+ "BorderRadiusStyleType",
1005
+ "BorderRadiusTopLeftStyleType",
1006
+ "BorderRadiusTopRightStyleType",
1007
+ "BorderRadiusBottomRightStyleType",
1008
+ "BorderRadiusBottomLeftStyleType",
1009
+ "FontSizeStyleType",
1010
+ "LineHeightStyleType",
1011
+ "LetterSpacingStyleType",
1012
+ "HeightStyleType",
1013
+ "MinHeightStyleType",
1014
+ "MaxHeightStyleType",
1015
+ "WidthStyleType",
1016
+ "MinWidthStyleType",
1017
+ "MaxWidthStyleType",
1018
+ "GapStyleType",
1019
+ "BorderWidthStyleType",
1020
+ "TopStyleType",
1021
+ "RightStyleType",
1022
+ "BottomStyleType",
1023
+ "LeftStyleType",
1024
+ ];
1025
+ // Validates a single style-type default object. Every size style type shares the optional
1026
+ // { css?, value?, unit? } shape, so inner fields are checked loosely — the key rejection is a
1027
+ // non-object (e.g. a bare JSON string, the common failure when a --defaultValue flag is not parsed).
1028
+ function styleDefaultObjectError(value, where) {
1029
+ if (typeof value === "string") {
1030
+ return `${where} is a JSON string but must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
1031
+ }
1032
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1033
+ return `${where} must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
1034
+ }
1035
+ const v = value;
1036
+ // css/value/unit are all optional, and the editor treats an explicit null as "unset" (it accepts
1037
+ // { value: null }). So only type-check a field that is present AND non-null — otherwise the CLI
1038
+ // would be stricter than the authoritative editor and reject a value it would happily accept.
1039
+ // Unknown fields ARE rejected below: every size-style type shares exactly this { css?, value?, unit? }
1040
+ // shape, so an extra key is always a mistake (this matches the editor). The per-type value-DOMAIN
1041
+ // (which units a given type allows — BorderWidth also permits em/vmin/vmax while the rest are
1042
+ // px/rem/%/vh/vw) stays editor-side: it is per-type and can't be mirrored offline without drift.
1043
+ if (v.css !== undefined && v.css !== null && typeof v.css !== "string") {
1044
+ return `${where}: "css" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
1045
+ }
1046
+ if (v.value !== undefined &&
1047
+ v.value !== null &&
1048
+ (typeof v.value !== "number" || !Number.isFinite(v.value))) {
1049
+ return `${where}: "value" must be a finite number. ${STYLE_TYPE_DEFAULT_HELP}`;
1050
+ }
1051
+ if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
1052
+ return `${where}: "unit" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
1053
+ }
1054
+ const unknownKey = Object.keys(v).find((k) => k !== "css" && k !== "value" && k !== "unit");
1055
+ if (unknownKey) {
1056
+ return `${where}: unknown field "${unknownKey}". A size/style default allows only "css", "value", "unit". ${STYLE_TYPE_DEFAULT_HELP}`;
1057
+ }
1058
+ return null;
1059
+ }
1060
+ // Validates a TYPE prop defaultValue — but ONLY for the size/style types whose shape we know
1061
+ // ({ css?, value?, unit? }). Any other TYPE typeId (color/border/box-shadow/grid/string-union
1062
+ // styles, IkasProduct, custom types) has a different value shape we can't verify offline, so its
1063
+ // default is passed through unchecked. The generator only emits a default for these size/style
1064
+ // types anyway. Returns an error message, or null if valid / not a size-style type.
1065
+ export function validateTypeDefaultValue(value, propName, typeId) {
1066
+ if (value === undefined || value === null)
1067
+ return null;
1068
+ if (typeof typeId !== "string" || !typeId.startsWith(BP_STOREFRONT_PREFIX)) {
1069
+ return null;
1070
+ }
1071
+ // Strip the prefix and an optional _array suffix to get the base type name.
1072
+ const withoutPrefix = typeId.slice(BP_STOREFRONT_PREFIX.length);
1073
+ const isArray = withoutPrefix.endsWith("_array");
1074
+ const baseName = isArray
1075
+ ? withoutPrefix.slice(0, -"_array".length)
1076
+ : withoutPrefix;
1077
+ if (!SIZE_STYLE_TYPE_NAMES.includes(baseName))
1078
+ return null;
1079
+ const where = `defaultValue for prop "${propName}"`;
1080
+ if (isArray) {
1081
+ if (!Array.isArray(value)) {
1082
+ return `${where} must be an array for an _array TYPE prop: [ { "value": …, "unit": … }, … ]. ${STYLE_TYPE_DEFAULT_HELP}`;
1083
+ }
1084
+ return firstError(value, (el, i) => styleDefaultObjectError(el, `${where}[${i}]`));
1085
+ }
1086
+ return styleDefaultObjectError(value, where);
1087
+ }
1088
+ async function updateProp(ref, options) {
538
1089
  const { config, configPath } = loadConfig();
539
1090
  const component = resolveComponent(config, ref);
540
1091
  const propIndex = component.props.findIndex((p) => p.name === options.prop);
@@ -566,8 +1117,51 @@ function updateProp(ref, options) {
566
1117
  if (options.description !== undefined) {
567
1118
  prop.description = options.description || undefined;
568
1119
  }
1120
+ // Resolve numberRangeData before defaultValue so a NUMBER_RANGE default can be validated
1121
+ // against the (possibly just-updated) slider config in the same call.
1122
+ if (options.numberRangeData !== undefined) {
1123
+ if (options.numberRangeData === "" || options.numberRangeData === "none") {
1124
+ delete prop.numberRangeData;
1125
+ }
1126
+ else {
1127
+ prop.numberRangeData = parseNumberRangeData(options.numberRangeData, prop.name);
1128
+ }
1129
+ }
1130
+ // A NUMBER_RANGE prop must keep a valid slider config (min + max). Enforce the invariant only when
1131
+ // this update touches the type or numberRangeData — a benign update (e.g. displayName) on an
1132
+ // existing prop must still pass. This catches switching a prop TO NUMBER_RANGE without a config, or
1133
+ // clearing/blanking numberRangeData on a NUMBER_RANGE prop.
1134
+ if ((options.type !== undefined || options.numberRangeData !== undefined) &&
1135
+ prop.type === "NUMBER_RANGE" &&
1136
+ (!prop.numberRangeData || !isNumberRangeComplete(prop.numberRangeData))) {
1137
+ console.log(JSON.stringify({
1138
+ success: false,
1139
+ error: numberRangeDataRequiredError(prop.name),
1140
+ }));
1141
+ process.exit(1);
1142
+ }
569
1143
  if (options.defaultValue !== undefined) {
570
- prop.defaultValue = parseDefaultValue(options.defaultValue, prop.type);
1144
+ rejectDynamicDefault(prop.name, prop.type, options.defaultValue);
1145
+ const parsed = parseDefaultValue(options.defaultValue, prop.type);
1146
+ // Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
1147
+ if (prop.type === "NUMBER") {
1148
+ const numError = validateNumberDefaultValue(options.defaultValue, prop.name);
1149
+ failIf(numError);
1150
+ }
1151
+ if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
1152
+ const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
1153
+ failIf(linkError);
1154
+ }
1155
+ if (prop.type === "NUMBER_RANGE") {
1156
+ const nrError = validateNumberRangeDefaultValue(parsed, prop.numberRangeData, prop.name);
1157
+ failIf(nrError);
1158
+ }
1159
+ if (prop.type === "TYPE") {
1160
+ // Prefer a typeId supplied in this same update over the stored one.
1161
+ const typeError = validateTypeDefaultValue(parsed, prop.name, options.typeId ?? prop.typeId);
1162
+ failIf(typeError);
1163
+ }
1164
+ prop.defaultValue = parsed;
571
1165
  }
572
1166
  if (options.group !== undefined) {
573
1167
  if (options.group === "" || options.group === "none") {
@@ -600,6 +1194,13 @@ function updateProp(ref, options) {
600
1194
  prop.typeId = options.typeId;
601
1195
  }
602
1196
  }
1197
+ // Reject section-disallowed types when the caller changes type/typeId;
1198
+ // untouched legacy props stay editable (displayName, group, ...).
1199
+ if ((options.type !== undefined || options.typeId !== undefined) &&
1200
+ prop.type === "TYPE" &&
1201
+ prop.typeId) {
1202
+ await assertTypeIdsUsable(component.type, [prop.typeId]);
1203
+ }
603
1204
  if (options.enumTypeId !== undefined) {
604
1205
  if (options.enumTypeId === "" || options.enumTypeId === "none") {
605
1206
  delete prop.enumTypeId;
@@ -609,14 +1210,18 @@ function updateProp(ref, options) {
609
1210
  }
610
1211
  }
611
1212
  if (options.filteredComponentIds !== undefined) {
612
- if (options.filteredComponentIds === "" || options.filteredComponentIds === "none") {
1213
+ if (options.filteredComponentIds === "" ||
1214
+ options.filteredComponentIds === "none") {
613
1215
  delete prop.filteredComponentIds;
614
1216
  }
615
1217
  else {
616
1218
  try {
617
1219
  const parsed = JSON.parse(options.filteredComponentIds);
618
1220
  if (!Array.isArray(parsed)) {
619
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
1221
+ console.log(JSON.stringify({
1222
+ success: false,
1223
+ error: "--filteredComponentIds must be a JSON array of strings.",
1224
+ }));
620
1225
  process.exit(1);
621
1226
  }
622
1227
  assertKnownComponentIds(parsed, config);
@@ -624,7 +1229,10 @@ function updateProp(ref, options) {
624
1229
  }
625
1230
  catch (err) {
626
1231
  if (err instanceof SyntaxError) {
627
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
1232
+ console.log(JSON.stringify({
1233
+ success: false,
1234
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
1235
+ }));
628
1236
  process.exit(1);
629
1237
  }
630
1238
  throw err;
@@ -639,13 +1247,19 @@ function updateProp(ref, options) {
639
1247
  try {
640
1248
  const parsed = JSON.parse(options.privateVarMap);
641
1249
  if (typeof parsed !== "object" || Array.isArray(parsed)) {
642
- console.log(JSON.stringify({ success: false, error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}." }));
1250
+ console.log(JSON.stringify({
1251
+ success: false,
1252
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
1253
+ }));
643
1254
  process.exit(1);
644
1255
  }
645
1256
  prop.privateVarMap = parsed;
646
1257
  }
647
1258
  catch {
648
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
1259
+ console.log(JSON.stringify({
1260
+ success: false,
1261
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
1262
+ }));
649
1263
  process.exit(1);
650
1264
  }
651
1265
  }
@@ -665,8 +1279,13 @@ function updateProp(ref, options) {
665
1279
  ...(prop.groupId ? { groupId: prop.groupId } : {}),
666
1280
  ...(prop.typeId ? { typeId: prop.typeId } : {}),
667
1281
  ...(prop.enumTypeId ? { enumTypeId: prop.enumTypeId } : {}),
668
- ...(prop.filteredComponentIds ? { filteredComponentIds: prop.filteredComponentIds } : {}),
1282
+ ...(prop.filteredComponentIds
1283
+ ? { filteredComponentIds: prop.filteredComponentIds }
1284
+ : {}),
669
1285
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
1286
+ ...(prop.numberRangeData
1287
+ ? { numberRangeData: prop.numberRangeData }
1288
+ : {}),
670
1289
  },
671
1290
  }));
672
1291
  }
@@ -729,7 +1348,10 @@ function removeComponent(ref) {
729
1348
  removedComponentId: component.id,
730
1349
  removedComponentName: component.name,
731
1350
  removedDirectory: path.relative(process.cwd(), componentDir),
732
- remainingComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
1351
+ remainingComponents: config.components.map((c) => ({
1352
+ id: c.id,
1353
+ name: c.name,
1354
+ })),
733
1355
  }));
734
1356
  }
735
1357
  function addPropGroup(ref, options) {
@@ -836,10 +1458,10 @@ function removePropGroup(ref, options) {
836
1458
  process.exit(1);
837
1459
  }
838
1460
  if (found.parent) {
839
- found.parent.children = found.parent.children.filter(c => c.id !== options.id);
1461
+ found.parent.children = found.parent.children.filter((c) => c.id !== options.id);
840
1462
  }
841
1463
  else {
842
- component.propGroups = component.propGroups.filter(g => g.id !== options.id);
1464
+ component.propGroups = component.propGroups.filter((g) => g.id !== options.id);
843
1465
  }
844
1466
  // Clear groupId from props referencing the deleted group
845
1467
  for (const prop of component.props) {
@@ -852,7 +1474,7 @@ function removePropGroup(ref, options) {
852
1474
  success: true,
853
1475
  componentName: component.name,
854
1476
  removedPropGroup: options.id,
855
- remainingPropGroups: component.propGroups.map(g => g.id),
1477
+ remainingPropGroups: component.propGroups.map((g) => g.id),
856
1478
  }));
857
1479
  }
858
1480
  function movePropGroup(ref, options) {
@@ -869,7 +1491,10 @@ function movePropGroup(ref, options) {
869
1491
  component.propGroups = movePropGroupInTree(component.propGroups, options.id, options.parent || null, options.index);
870
1492
  }
871
1493
  catch (e) {
872
- console.log(JSON.stringify({ success: false, error: e.message || "Failed to move prop group" }));
1494
+ console.log(JSON.stringify({
1495
+ success: false,
1496
+ error: e.message || "Failed to move prop group",
1497
+ }));
873
1498
  process.exit(1);
874
1499
  }
875
1500
  saveConfig(configPath, config);
@@ -925,7 +1550,9 @@ function listComponents() {
925
1550
  required: p.required,
926
1551
  ...(p.groupId ? { groupId: p.groupId } : {}),
927
1552
  })),
928
- ...(c.propGroups && c.propGroups.length > 0 ? { propGroups: c.propGroups } : {}),
1553
+ ...(c.propGroups && c.propGroups.length > 0
1554
+ ? { propGroups: c.propGroups }
1555
+ : {}),
929
1556
  }));
930
1557
  console.log(JSON.stringify({
931
1558
  success: true,
@@ -939,7 +1566,9 @@ function listComponents() {
939
1566
  async function addEnum(options) {
940
1567
  const { config, configPath } = loadConfig();
941
1568
  // Validate name
942
- if (!options.name || typeof options.name !== "string" || !options.name.trim()) {
1569
+ if (!options.name ||
1570
+ typeof options.name !== "string" ||
1571
+ !options.name.trim()) {
943
1572
  console.log(JSON.stringify({ success: false, error: "Enum name is required." }));
944
1573
  process.exit(1);
945
1574
  }
@@ -1053,7 +1682,7 @@ function updateEnum(options) {
1053
1682
  Object.keys(enumOptions).length === 0) {
1054
1683
  console.log(JSON.stringify({
1055
1684
  success: false,
1056
- error: 'Options must be a non-empty JSON object.',
1685
+ error: "Options must be a non-empty JSON object.",
1057
1686
  }));
1058
1687
  process.exit(1);
1059
1688
  }
@@ -1126,8 +1755,7 @@ function listEnums() {
1126
1755
  }
1127
1756
  // --- Command registration ---
1128
1757
  export function createConfigCommand() {
1129
- const config = new Command("config")
1130
- .description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
1758
+ const config = new Command("config").description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
1131
1759
  config
1132
1760
  .command("add-component")
1133
1761
  .description("Add a new component to the project")
@@ -1136,7 +1764,7 @@ export function createConfigCommand() {
1136
1764
  .option("--isHeader", "Mark this section as the store header (only for type: section)")
1137
1765
  .option("--isFooter", "Mark this section as the store footer (only for type: section)")
1138
1766
  .option("--props <json>", "JSON array of props. Required per entry: name, type. Optional: displayName (auto from name), required, description, defaultValue, groupId, typeId (TYPE props), enumTypeId (ENUM props), filteredComponentIds, privateVarMap. " +
1139
- "Example: '[{\"name\":\"title\",\"type\":\"TEXT\",\"required\":true,\"defaultValue\":\"Hello\",\"groupId\":\"basic\"}]'")
1767
+ 'Example: \'[{"name":"title","type":"TEXT","required":true,"defaultValue":"Hello","groupId":"basic"}]\'')
1140
1768
  .action((options) => {
1141
1769
  addComponent(options.name, options);
1142
1770
  });
@@ -1155,7 +1783,8 @@ export function createConfigCommand() {
1155
1783
  .option("--typeId <typeId>", "Type ID for TYPE props (required when type is TYPE)")
1156
1784
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
1157
1785
  .option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
1158
- .option("--privateVarMap <json>", 'JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)')
1786
+ .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)")
1787
+ .option("--numberRangeData <json>", 'JSON slider config for NUMBER_RANGE props, e.g. \'{"min":0,"max":100,"interval":5,"unit":"px"}\'')
1159
1788
  .action((options) => {
1160
1789
  addProp({ id: options.componentId, name: options.component }, options);
1161
1790
  });
@@ -1175,6 +1804,7 @@ export function createConfigCommand() {
1175
1804
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (use 'none' to clear)")
1176
1805
  .option("--filteredComponentIds <json>", "JSON array of component IDs (use 'none' to clear)")
1177
1806
  .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (use 'none' to clear)")
1807
+ .option("--numberRangeData <json>", "JSON slider config for NUMBER_RANGE props (use 'none' to clear)")
1178
1808
  .action((options) => {
1179
1809
  updateProp({ id: options.componentId, name: options.component }, options);
1180
1810
  });
@@ -1246,7 +1876,7 @@ export function createConfigCommand() {
1246
1876
  .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1247
1877
  .requiredOption("--id <id>", "Group ID to move")
1248
1878
  .option("--parent <parentId>", "Target parent group ID (omit to move to root)")
1249
- .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", v => parseInt(v, 10))
1879
+ .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", (v) => parseInt(v, 10))
1250
1880
  .action((options) => {
1251
1881
  movePropGroup({ id: options.componentId, name: options.component }, options);
1252
1882
  });
@@ -1306,54 +1936,15 @@ export function createConfigCommand() {
1306
1936
  * Returns the matching type (with enumOptions) if found, null otherwise.
1307
1937
  */
1308
1938
  async function fetchLiveEditorEnumById(enumId) {
1309
- const { WebSocket } = await import("ws");
1310
- const WS_URL = "ws://localhost:5201";
1311
- const TIMEOUT_MS = 1500;
1312
- return new Promise((resolve) => {
1313
- let ws;
1314
- let settled = false;
1315
- const finish = (value) => {
1316
- if (settled)
1317
- return;
1318
- settled = true;
1319
- try {
1320
- ws?.close();
1321
- }
1322
- catch {
1323
- /* ignore */
1324
- }
1325
- resolve(value);
1326
- };
1327
- try {
1328
- ws = new WebSocket(WS_URL);
1329
- }
1330
- catch {
1331
- resolve(null);
1332
- return;
1939
+ const types = await fetchLiveEditorTypes();
1940
+ const match = types?.find((t) => t.category === "enum" && t.id === enumId);
1941
+ return match
1942
+ ? {
1943
+ id: match.id,
1944
+ name: match.name,
1945
+ ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}),
1333
1946
  }
1334
- const timeout = setTimeout(() => finish(null), TIMEOUT_MS);
1335
- ws.on("open", () => {
1336
- ws.send(JSON.stringify({ type: "request-types" }));
1337
- });
1338
- ws.on("message", (data) => {
1339
- try {
1340
- const message = JSON.parse(data.toString());
1341
- if (message.type === "types-list") {
1342
- clearTimeout(timeout);
1343
- const types = message.payload?.types || [];
1344
- const match = types.find((t) => t.category === "enum" && t.id === enumId);
1345
- finish(match ? { id: match.id, name: match.name, ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}) } : null);
1346
- }
1347
- }
1348
- catch {
1349
- // Ignore non-JSON messages
1350
- }
1351
- });
1352
- ws.on("error", () => {
1353
- clearTimeout(timeout);
1354
- finish(null);
1355
- });
1356
- });
1947
+ : null;
1357
1948
  }
1358
1949
  /**
1359
1950
  * Best-effort lookup of an enum by PascalCase name in the live editor session.
@@ -1363,6 +1954,16 @@ async function fetchLiveEditorEnumById(enumId) {
1363
1954
  * must treat null as "could not verify, fall back to on-disk check only".
1364
1955
  */
1365
1956
  async function fetchLiveEditorEnumByName(pascalName) {
1957
+ const types = await fetchLiveEditorTypes();
1958
+ const match = types?.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
1959
+ return match ? { id: match.id, name: match.name } : null;
1960
+ }
1961
+ /**
1962
+ * Best-effort fetch of the editor's synced type list (with category /
1963
+ * sectionAllowed / enumOptions metadata). Returns null on any failure (no dev
1964
+ * server, timeout) — callers must treat null as "could not verify, allow".
1965
+ */
1966
+ async function fetchLiveEditorTypes() {
1366
1967
  const { WebSocket } = await import("ws");
1367
1968
  const WS_URL = "ws://localhost:5201";
1368
1969
  const TIMEOUT_MS = 1500;
@@ -1397,9 +1998,7 @@ async function fetchLiveEditorEnumByName(pascalName) {
1397
1998
  const message = JSON.parse(data.toString());
1398
1999
  if (message.type === "types-list") {
1399
2000
  clearTimeout(timeout);
1400
- const types = message.payload?.types || [];
1401
- const match = types.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
1402
- finish(match ? { id: match.id, name: match.name } : null);
2001
+ finish(message.payload?.types || []);
1403
2002
  }
1404
2003
  }
1405
2004
  catch {
@@ -1412,6 +2011,47 @@ async function fetchLiveEditorEnumByName(pascalName) {
1412
2011
  });
1413
2012
  });
1414
2013
  }
2014
+ /**
2015
+ * Definition-time guard for TYPE-prop typeIds, verified against the live
2016
+ * editor's type list (skipped when the dev server is unreachable or the type
2017
+ * is unknown to the editor). Two rules:
2018
+ * - deprecated types are rejected on ANY component: they are legacy
2019
+ * render-only types — old themes may still carry values, but new props must
2020
+ * not be defined with them.
2021
+ * - on section components, only section-allowed (style) types pass — any other
2022
+ * type renders an empty placeholder merchants cannot fill, and section-level
2023
+ * bindings cannot supply a value either, so the prop would be dead weight.
2024
+ * On violation prints the standard { success: false } envelope and exits.
2025
+ */
2026
+ async function assertTypeIdsUsable(componentType, typeIds) {
2027
+ if (typeIds.length === 0)
2028
+ return;
2029
+ const types = await fetchLiveEditorTypes();
2030
+ if (!types)
2031
+ return;
2032
+ const matches = typeIds.flatMap((typeId) => {
2033
+ const match = types.find((t) => t.id === typeId);
2034
+ return match ? [match] : [];
2035
+ });
2036
+ const legacy = matches.find((t) => t.deprecated === true);
2037
+ if (legacy) {
2038
+ console.log(JSON.stringify({
2039
+ success: false,
2040
+ 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).`,
2041
+ }));
2042
+ process.exit(1);
2043
+ }
2044
+ if (componentType !== "section")
2045
+ return;
2046
+ const disallowed = matches.find((t) => t.sectionAllowed === false);
2047
+ if (!disallowed)
2048
+ return;
2049
+ console.log(JSON.stringify({
2050
+ success: false,
2051
+ 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.`,
2052
+ }));
2053
+ process.exit(1);
2054
+ }
1415
2055
  async function listTypes(componentType) {
1416
2056
  const { WebSocket } = await import("ws");
1417
2057
  const WS_URL = "ws://localhost:5201";
@@ -1445,7 +2085,8 @@ async function listTypes(componentType) {
1445
2085
  const message = JSON.parse(data.toString());
1446
2086
  if (message.type === "types-list") {
1447
2087
  clearTimeout(timeout);
1448
- let types = message.payload.types;
2088
+ // Legacy render-only types are never offered for new props.
2089
+ let types = message.payload.types.filter((t) => !t.deprecated);
1449
2090
  if (componentType === "section") {
1450
2091
  types = types.filter((t) => t.sectionAllowed);
1451
2092
  }