@ikas/component-cli 1.4.0-beta.14 → 1.4.0-beta.141

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 (127) 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 +35 -0
  16. package/dist/commands/config.d.ts.map +1 -1
  17. package/dist/commands/config.js +831 -116
  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 +17 -0
  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 +69 -2
  103. package/dist/utils/component-helpers.d.ts.map +1 -1
  104. package/dist/utils/component-helpers.js +191 -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
@@ -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
  }));
@@ -90,6 +96,11 @@ function assertKnownComponentIds(input, config) {
90
96
  }
91
97
  return input;
92
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
+ }
93
104
  function regenerateTypes(component, componentType, config) {
94
105
  const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
95
106
  const typesPath = path.join(componentDir, "types.ts");
@@ -156,7 +167,91 @@ const ALLOWED_PROP_FIELDS = new Set([
156
167
  "enumTypeId",
157
168
  "filteredComponentIds",
158
169
  "privateVarMap",
170
+ "numberRangeData",
159
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
+ }
160
255
  /**
161
256
  * Parse and validate the --props JSON flag for add-component.
162
257
  * Returns validated ComponentProp[] or exits with error.
@@ -182,6 +277,8 @@ async function parsePropsFlag(propsJson, config, configPath) {
182
277
  }
183
278
  const seenNames = new Set();
184
279
  const props = [];
280
+ // Loop-invariant: build the known-id set once for the whole batch, not per prop.
281
+ const existingComponentIds = existingComponentIdSet(config);
185
282
  for (const raw of rawProps) {
186
283
  const r = raw;
187
284
  const unknownFields = Object.keys(r).filter((k) => !ALLOWED_PROP_FIELDS.has(k));
@@ -234,6 +331,36 @@ async function parsePropsFlag(propsJson, config, configPath) {
234
331
  }));
235
332
  process.exit(1);
236
333
  }
334
+ // Dynamic (merchant-data reference) props can't carry a static default.
335
+ rejectDynamicDefault(r.name, propType, r.defaultValue);
336
+ // Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
337
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
338
+ r.defaultValue !== undefined) {
339
+ const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
340
+ failIf(linkError);
341
+ }
342
+ // Validate SVG / SVG_LIST defaultValue (reject malformed/oversized/unsafe SVG)
343
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
344
+ r.defaultValue !== undefined) {
345
+ const svgError = validateSvgDefaultValue(propType, r.defaultValue, r.name);
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);
363
+ }
237
364
  // Auto-generate displayName if omitted
238
365
  const displayName = typeof r.displayName === "string" && r.displayName
239
366
  ? r.displayName
@@ -258,7 +385,10 @@ async function parsePropsFlag(propsJson, config, configPath) {
258
385
  process.exit(1);
259
386
  }
260
387
  // Validate enum type exists (checks config then live dev server)
261
- if (config && configPath && typeof r.enumTypeId === "string" && !r.enumTypeId.startsWith("@ikas/")) {
388
+ if (config &&
389
+ configPath &&
390
+ typeof r.enumTypeId === "string" &&
391
+ !isBuiltInEnumTypeId(r.enumTypeId)) {
262
392
  const resolved = await resolveEnumType(r.enumTypeId, config, configPath);
263
393
  if (!resolved) {
264
394
  console.log(JSON.stringify({
@@ -277,9 +407,7 @@ async function parsePropsFlag(propsJson, config, configPath) {
277
407
  ...(typeof r.description === "string" && r.description
278
408
  ? { description: r.description }
279
409
  : {}),
280
- ...(r.defaultValue !== undefined
281
- ? { defaultValue: r.defaultValue }
282
- : {}),
410
+ ...(r.defaultValue !== undefined ? { defaultValue: r.defaultValue } : {}),
283
411
  ...(typeof r.groupId === "string" && r.groupId
284
412
  ? { groupId: r.groupId }
285
413
  : {}),
@@ -289,11 +417,22 @@ async function parsePropsFlag(propsJson, config, configPath) {
289
417
  ...(propType === "ENUM" && typeof r.enumTypeId === "string"
290
418
  ? { enumTypeId: r.enumTypeId }
291
419
  : {}),
292
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.filteredComponentIds && Array.isArray(r.filteredComponentIds)
293
- ? { filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config) }
420
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
421
+ r.filteredComponentIds &&
422
+ Array.isArray(r.filteredComponentIds)
423
+ ? {
424
+ filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config),
425
+ }
294
426
  : {}),
295
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.privateVarMap && typeof r.privateVarMap === "object"
296
- ? { privateVarMap: r.privateVarMap }
427
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
428
+ r.privateVarMap &&
429
+ typeof r.privateVarMap === "object"
430
+ ? {
431
+ privateVarMap: r.privateVarMap,
432
+ }
433
+ : {}),
434
+ ...(propType === "NUMBER_RANGE"
435
+ ? { numberRangeData: requireNumberRangeData(r.numberRangeData, r.name) }
297
436
  : {}),
298
437
  };
299
438
  props.push(prop);
@@ -325,11 +464,25 @@ async function addComponent(name, options) {
325
464
  if (!config.projectId) {
326
465
  config.projectId = generateProjectId();
327
466
  }
467
+ else if (!isValidProjectId(config.projectId)) {
468
+ // A malformed projectId (hand-edited config) breaks the editor's republish
469
+ // stability: published/local id prefixes stop matching and every publish
470
+ // INSERTs duplicate design elements. Fail fast instead of corrupting data.
471
+ console.log(JSON.stringify({
472
+ success: false,
473
+ error: `Invalid projectId "${config.projectId}" in ikas.config.json. ` +
474
+ "projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
475
+ "Restore the original generated value — do not hand-edit it.",
476
+ }));
477
+ process.exit(1);
478
+ }
328
479
  // Parse --props (and validate filteredComponentIds) BEFORE any filesystem side-effects
329
480
  // so a bad payload doesn't leave an empty component directory behind.
330
481
  const props = options.props
331
482
  ? await parsePropsFlag(options.props, config, configPath)
332
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] : [])));
333
486
  const componentId = generateComponentId(config.projectId);
334
487
  const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
335
488
  // Create directory
@@ -420,6 +573,10 @@ async function addProp(ref, options) {
420
573
  }));
421
574
  process.exit(1);
422
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
+ }
423
580
  // Validate enumTypeId for ENUM props
424
581
  if (propType === "ENUM" && !options.enumTypeId) {
425
582
  console.log(JSON.stringify({
@@ -430,7 +587,9 @@ async function addProp(ref, options) {
430
587
  }
431
588
  // Validate that enumTypeId references an existing enum type
432
589
  // (checks config.customTypes first, then falls back to the live dev server)
433
- if (propType === "ENUM" && options.enumTypeId && !options.enumTypeId.startsWith("@ikas/")) {
590
+ if (propType === "ENUM" &&
591
+ options.enumTypeId &&
592
+ !isBuiltInEnumTypeId(options.enumTypeId)) {
434
593
  const resolved = await resolveEnumType(options.enumTypeId, config, configPath);
435
594
  if (!resolved) {
436
595
  console.log(JSON.stringify({
@@ -460,49 +619,101 @@ async function addProp(ref, options) {
460
619
  }
461
620
  // Parse filteredComponentIds JSON for COMPONENT/COMPONENT_LIST
462
621
  let parsedFilteredIds;
463
- if (options.filteredComponentIds && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
622
+ if (options.filteredComponentIds &&
623
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
464
624
  try {
465
625
  parsedFilteredIds = JSON.parse(options.filteredComponentIds);
466
626
  if (!Array.isArray(parsedFilteredIds)) {
467
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
627
+ console.log(JSON.stringify({
628
+ success: false,
629
+ error: "--filteredComponentIds must be a JSON array of strings.",
630
+ }));
468
631
  process.exit(1);
469
632
  }
470
633
  }
471
634
  catch {
472
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
635
+ console.log(JSON.stringify({
636
+ success: false,
637
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
638
+ }));
473
639
  process.exit(1);
474
640
  }
475
641
  assertKnownComponentIds(parsedFilteredIds, config);
476
642
  }
477
643
  // Parse privateVarMap JSON for COMPONENT/COMPONENT_LIST
478
644
  let parsedPrivateVarMap;
479
- if (options.privateVarMap && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
645
+ if (options.privateVarMap &&
646
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
480
647
  try {
481
648
  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}." }));
649
+ if (typeof parsedPrivateVarMap !== "object" ||
650
+ Array.isArray(parsedPrivateVarMap)) {
651
+ console.log(JSON.stringify({
652
+ success: false,
653
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
654
+ }));
484
655
  process.exit(1);
485
656
  }
486
657
  }
487
658
  catch {
488
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
659
+ console.log(JSON.stringify({
660
+ success: false,
661
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
662
+ }));
489
663
  process.exit(1);
490
664
  }
491
665
  }
666
+ rejectDynamicDefault(options.name, propType, options.defaultValue);
667
+ const parsedDefaultValue = options.defaultValue !== undefined
668
+ ? parseDefaultValue(options.defaultValue, propType)
669
+ : undefined;
670
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
671
+ parsedDefaultValue !== undefined) {
672
+ const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
673
+ failIf(linkError);
674
+ }
675
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
676
+ parsedDefaultValue !== undefined) {
677
+ const svgError = validateSvgDefaultValue(propType, parsedDefaultValue, options.name);
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);
699
+ }
492
700
  const newProp = {
493
701
  name: options.name,
494
702
  displayName: options.displayName,
495
703
  type: propType,
496
704
  required: options.required ?? false,
497
705
  ...(options.description ? { description: options.description } : {}),
498
- ...(options.defaultValue !== undefined
499
- ? { defaultValue: parseDefaultValue(options.defaultValue, propType) }
706
+ ...(parsedDefaultValue !== undefined
707
+ ? { defaultValue: parsedDefaultValue }
500
708
  : {}),
501
709
  ...(options.group ? { groupId: options.group } : {}),
502
710
  ...(options.typeId ? { typeId: options.typeId } : {}),
503
711
  ...(options.enumTypeId ? { enumTypeId: options.enumTypeId } : {}),
504
712
  ...(parsedFilteredIds ? { filteredComponentIds: parsedFilteredIds } : {}),
505
713
  ...(parsedPrivateVarMap ? { privateVarMap: parsedPrivateVarMap } : {}),
714
+ ...(parsedNumberRangeData
715
+ ? { numberRangeData: parsedNumberRangeData }
716
+ : {}),
506
717
  };
507
718
  component.props.push(newProp);
508
719
  saveConfig(configPath, config);
@@ -519,22 +730,430 @@ async function addProp(ref, options) {
519
730
  required: newProp.required,
520
731
  ...(newProp.groupId ? { groupId: newProp.groupId } : {}),
521
732
  ...(newProp.enumTypeId ? { enumTypeId: newProp.enumTypeId } : {}),
522
- ...(newProp.filteredComponentIds ? { filteredComponentIds: newProp.filteredComponentIds } : {}),
523
- ...(newProp.privateVarMap ? { privateVarMap: newProp.privateVarMap } : {}),
733
+ ...(newProp.filteredComponentIds
734
+ ? { filteredComponentIds: newProp.filteredComponentIds }
735
+ : {}),
736
+ ...(newProp.privateVarMap
737
+ ? { privateVarMap: newProp.privateVarMap }
738
+ : {}),
739
+ ...(newProp.numberRangeData
740
+ ? { numberRangeData: newProp.numberRangeData }
741
+ : {}),
524
742
  },
525
743
  }));
526
744
  }
527
- function parseDefaultValue(value, propType) {
745
+ export function parseDefaultValue(value, propType) {
528
746
  switch (propType) {
529
747
  case "NUMBER":
530
748
  return Number(value);
531
749
  case "BOOLEAN":
532
750
  return value === "true";
751
+ case "COMPONENT":
752
+ case "COMPONENT_LIST":
753
+ case "LINK":
754
+ case "LIST_OF_LINK":
755
+ case "SVG_LIST":
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.
763
+ try {
764
+ return JSON.parse(value);
765
+ }
766
+ catch {
767
+ return value;
768
+ }
533
769
  default:
534
770
  return value;
535
771
  }
536
772
  }
537
- function updateProp(ref, options) {
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"];
813
+ const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
814
+ 'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
815
+ "Do NOT pass a JSON string or a { label, href } shape.";
816
+ // Returns the index of the first array element that fails `fn`, as an error string, or null.
817
+ const firstError = (arr, fn) => arr.reduce((acc, item, index) => acc ?? fn(item, index), null);
818
+ const validateSingleLink = (link, propName, path) => {
819
+ const where = `defaultValue for prop "${propName}"${path}`;
820
+ if (typeof link === "string") {
821
+ return `${where} is a JSON string but must be an object. ${LINK_SHAPE_HELP}`;
822
+ }
823
+ if (!link || typeof link !== "object" || Array.isArray(link)) {
824
+ return `${where} must be a link object. ${LINK_SHAPE_HELP}`;
825
+ }
826
+ const l = link;
827
+ if ("href" in l && !("linkType" in l)) {
828
+ return `${where} uses a legacy { href } shape without "linkType". Use "linkType" + "externalLink"/"pageType" instead. ${LINK_SHAPE_HELP}`;
829
+ }
830
+ if (typeof l.linkType !== "string" ||
831
+ !VALID_LINK_TYPES.includes(l.linkType)) {
832
+ return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
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
+ }
843
+ if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
844
+ return `${where}: "subLinks" must be an array (use [] when there are none).`;
845
+ }
846
+ if (Array.isArray(l.subLinks)) {
847
+ return firstError(l.subLinks, (sl, i) => validateSingleLink(sl, propName, `${path}.subLinks[${i}]`));
848
+ }
849
+ return null;
850
+ };
851
+ // Validates a LINK / LIST_OF_LINK defaultValue. Returns an error message, or null if valid.
852
+ // These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
853
+ // legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
854
+ // out of published themes.
855
+ export function validateLinkDefaultValue(propType, value, propName) {
856
+ if (value === undefined || value === null)
857
+ return null;
858
+ if (propType === "LINK") {
859
+ return validateSingleLink(value, propName, "");
860
+ }
861
+ // LIST_OF_LINK
862
+ if (typeof value === "string") {
863
+ return `defaultValue for prop "${propName}" is a JSON string but must be an object { "links": [...] }. ${LINK_SHAPE_HELP}`;
864
+ }
865
+ if (!value ||
866
+ typeof value !== "object" ||
867
+ Array.isArray(value) ||
868
+ !Array.isArray(value.links)) {
869
+ return `defaultValue for prop "${propName}" must be an object with a "links" array: { "links": [ <link>, ... ] }. ${LINK_SHAPE_HELP}`;
870
+ }
871
+ const links = value.links;
872
+ return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
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
+ }
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. " +
924
+ 'Tip: the editor uses each svg\'s `class` attribute as its display name — set class="my-icon" to name it; ' +
925
+ "SVG_LIST items with no class show as svg-1, svg-2, ….";
926
+ // Keep the cap and base64 pattern in sync with @ikas/editor-models validate-svg.ts (the
927
+ // authoritative validator). Duplicated here on purpose — see validateSvgShape below.
928
+ const SVG_MAX_SIZE_KB = 64;
929
+ const SVG_BASE64_RASTER_RE = /data:image\/[a-z0-9.+-]+;base64/i;
930
+ const SVG_SHAPE_RE = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>][\s\S]*<\/svg>\s*$/i;
931
+ // Count TOP-LEVEL <svg> roots without a DOM: scan svg open/close tags in order and track
932
+ // nesting depth. A root is any <svg> opened at depth 0. This matches the editor's DOM-based
933
+ // single-root check — it rejects siblings (`<svg/><svg/>`) while allowing a nested <svg>.
934
+ // Self-closing <svg .../> counts as a root but opens no nesting level.
935
+ function svgRootCount(input) {
936
+ const tags = input.match(/<svg\b[^>]*>|<\/svg\s*>/gi) || [];
937
+ return tags.reduce((acc, tag) => {
938
+ if (tag.startsWith("</"))
939
+ return { depth: Math.max(0, acc.depth - 1), roots: acc.roots };
940
+ const roots = acc.depth === 0 ? acc.roots + 1 : acc.roots;
941
+ const depth = /\/>\s*$/.test(tag) ? acc.depth : acc.depth + 1;
942
+ return { depth, roots };
943
+ }, { depth: 0, roots: 0 }).roots;
944
+ }
945
+ // Lightweight, dependency-free SVG structure check. This standalone CLI is publishable and
946
+ // must not depend on the private @ikas/editor-models package (nor pull a DOM/DOMPurify into a
947
+ // CLI), so it mirrors the RULES of the editor's validateSvg (size, base64, single <svg> root)
948
+ // for fast authoring feedback. The authoritative sanitize runs in the editor/upload path.
949
+ // Returns a rejection reason, or null if the shape is acceptable.
950
+ function validateSvgShape(input) {
951
+ if (typeof input !== "string" || input.trim() === "")
952
+ return "empty or not a string";
953
+ if (Buffer.byteLength(input, "utf8") > SVG_MAX_SIZE_KB * 1024)
954
+ return `exceeds ${SVG_MAX_SIZE_KB} KB`;
955
+ if (SVG_BASE64_RASTER_RE.test(input))
956
+ return "embedded base64 raster image not allowed";
957
+ if (!SVG_SHAPE_RE.test(input))
958
+ return "not a single-root <svg> document";
959
+ if (svgRootCount(input) !== 1)
960
+ return "must be exactly one <svg> root";
961
+ return null;
962
+ }
963
+ // Validates an SVG / SVG_LIST defaultValue. Returns an error message, or null if valid.
964
+ // Authoring an SVG default is a common mistake source for agents; rejecting here (with an
965
+ // instructive message) keeps malformed/oversized SVGs out of published components.
966
+ export function validateSvgDefaultValue(propType, value, propName) {
967
+ if (value === undefined || value === null)
968
+ return null;
969
+ if (propType === "SVG") {
970
+ const reason = validateSvgShape(value);
971
+ return reason
972
+ ? `defaultValue for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
973
+ : null;
974
+ }
975
+ // SVG_LIST
976
+ if (!Array.isArray(value)) {
977
+ return `defaultValue for prop "${propName}" must be an array of SVG strings: [ "<svg>…</svg>", ... ]. ${SVG_DEFAULT_HELP}`;
978
+ }
979
+ return firstError(value, (svg, i) => {
980
+ const reason = validateSvgShape(svg);
981
+ return reason
982
+ ? `defaultValue[${i}] for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
983
+ : null;
984
+ });
985
+ }
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) {
538
1157
  const { config, configPath } = loadConfig();
539
1158
  const component = resolveComponent(config, ref);
540
1159
  const propIndex = component.props.findIndex((p) => p.name === options.prop);
@@ -566,8 +1185,87 @@ function updateProp(ref, options) {
566
1185
  if (options.description !== undefined) {
567
1186
  prop.description = options.description || undefined;
568
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
+ }
569
1243
  if (options.defaultValue !== undefined) {
570
- prop.defaultValue = parseDefaultValue(options.defaultValue, prop.type);
1244
+ rejectDynamicDefault(prop.name, prop.type, options.defaultValue);
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
+ }
1251
+ if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
1252
+ const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
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);
1267
+ }
1268
+ prop.defaultValue = parsed;
571
1269
  }
572
1270
  if (options.group !== undefined) {
573
1271
  if (options.group === "" || options.group === "none") {
@@ -600,6 +1298,13 @@ function updateProp(ref, options) {
600
1298
  prop.typeId = options.typeId;
601
1299
  }
602
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
+ }
603
1308
  if (options.enumTypeId !== undefined) {
604
1309
  if (options.enumTypeId === "" || options.enumTypeId === "none") {
605
1310
  delete prop.enumTypeId;
@@ -608,29 +1313,6 @@ function updateProp(ref, options) {
608
1313
  prop.enumTypeId = options.enumTypeId;
609
1314
  }
610
1315
  }
611
- if (options.filteredComponentIds !== undefined) {
612
- if (options.filteredComponentIds === "" || options.filteredComponentIds === "none") {
613
- delete prop.filteredComponentIds;
614
- }
615
- else {
616
- try {
617
- const parsed = JSON.parse(options.filteredComponentIds);
618
- if (!Array.isArray(parsed)) {
619
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
620
- process.exit(1);
621
- }
622
- assertKnownComponentIds(parsed, config);
623
- prop.filteredComponentIds = parsed;
624
- }
625
- catch (err) {
626
- if (err instanceof SyntaxError) {
627
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
628
- process.exit(1);
629
- }
630
- throw err;
631
- }
632
- }
633
- }
634
1316
  if (options.privateVarMap !== undefined) {
635
1317
  if (options.privateVarMap === "" || options.privateVarMap === "none") {
636
1318
  delete prop.privateVarMap;
@@ -639,13 +1321,19 @@ function updateProp(ref, options) {
639
1321
  try {
640
1322
  const parsed = JSON.parse(options.privateVarMap);
641
1323
  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}." }));
1324
+ console.log(JSON.stringify({
1325
+ success: false,
1326
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
1327
+ }));
643
1328
  process.exit(1);
644
1329
  }
645
1330
  prop.privateVarMap = parsed;
646
1331
  }
647
1332
  catch {
648
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
1333
+ console.log(JSON.stringify({
1334
+ success: false,
1335
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
1336
+ }));
649
1337
  process.exit(1);
650
1338
  }
651
1339
  }
@@ -665,8 +1353,13 @@ function updateProp(ref, options) {
665
1353
  ...(prop.groupId ? { groupId: prop.groupId } : {}),
666
1354
  ...(prop.typeId ? { typeId: prop.typeId } : {}),
667
1355
  ...(prop.enumTypeId ? { enumTypeId: prop.enumTypeId } : {}),
668
- ...(prop.filteredComponentIds ? { filteredComponentIds: prop.filteredComponentIds } : {}),
1356
+ ...(prop.filteredComponentIds
1357
+ ? { filteredComponentIds: prop.filteredComponentIds }
1358
+ : {}),
669
1359
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
1360
+ ...(prop.numberRangeData
1361
+ ? { numberRangeData: prop.numberRangeData }
1362
+ : {}),
670
1363
  },
671
1364
  }));
672
1365
  }
@@ -729,7 +1422,10 @@ function removeComponent(ref) {
729
1422
  removedComponentId: component.id,
730
1423
  removedComponentName: component.name,
731
1424
  removedDirectory: path.relative(process.cwd(), componentDir),
732
- remainingComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
1425
+ remainingComponents: config.components.map((c) => ({
1426
+ id: c.id,
1427
+ name: c.name,
1428
+ })),
733
1429
  }));
734
1430
  }
735
1431
  function addPropGroup(ref, options) {
@@ -836,10 +1532,10 @@ function removePropGroup(ref, options) {
836
1532
  process.exit(1);
837
1533
  }
838
1534
  if (found.parent) {
839
- found.parent.children = found.parent.children.filter(c => c.id !== options.id);
1535
+ found.parent.children = found.parent.children.filter((c) => c.id !== options.id);
840
1536
  }
841
1537
  else {
842
- component.propGroups = component.propGroups.filter(g => g.id !== options.id);
1538
+ component.propGroups = component.propGroups.filter((g) => g.id !== options.id);
843
1539
  }
844
1540
  // Clear groupId from props referencing the deleted group
845
1541
  for (const prop of component.props) {
@@ -852,7 +1548,7 @@ function removePropGroup(ref, options) {
852
1548
  success: true,
853
1549
  componentName: component.name,
854
1550
  removedPropGroup: options.id,
855
- remainingPropGroups: component.propGroups.map(g => g.id),
1551
+ remainingPropGroups: component.propGroups.map((g) => g.id),
856
1552
  }));
857
1553
  }
858
1554
  function movePropGroup(ref, options) {
@@ -869,7 +1565,10 @@ function movePropGroup(ref, options) {
869
1565
  component.propGroups = movePropGroupInTree(component.propGroups, options.id, options.parent || null, options.index);
870
1566
  }
871
1567
  catch (e) {
872
- console.log(JSON.stringify({ success: false, error: e.message || "Failed to move prop group" }));
1568
+ console.log(JSON.stringify({
1569
+ success: false,
1570
+ error: e.message || "Failed to move prop group",
1571
+ }));
873
1572
  process.exit(1);
874
1573
  }
875
1574
  saveConfig(configPath, config);
@@ -925,7 +1624,9 @@ function listComponents() {
925
1624
  required: p.required,
926
1625
  ...(p.groupId ? { groupId: p.groupId } : {}),
927
1626
  })),
928
- ...(c.propGroups && c.propGroups.length > 0 ? { propGroups: c.propGroups } : {}),
1627
+ ...(c.propGroups && c.propGroups.length > 0
1628
+ ? { propGroups: c.propGroups }
1629
+ : {}),
929
1630
  }));
930
1631
  console.log(JSON.stringify({
931
1632
  success: true,
@@ -939,7 +1640,9 @@ function listComponents() {
939
1640
  async function addEnum(options) {
940
1641
  const { config, configPath } = loadConfig();
941
1642
  // Validate name
942
- if (!options.name || typeof options.name !== "string" || !options.name.trim()) {
1643
+ if (!options.name ||
1644
+ typeof options.name !== "string" ||
1645
+ !options.name.trim()) {
943
1646
  console.log(JSON.stringify({ success: false, error: "Enum name is required." }));
944
1647
  process.exit(1);
945
1648
  }
@@ -1053,7 +1756,7 @@ function updateEnum(options) {
1053
1756
  Object.keys(enumOptions).length === 0) {
1054
1757
  console.log(JSON.stringify({
1055
1758
  success: false,
1056
- error: 'Options must be a non-empty JSON object.',
1759
+ error: "Options must be a non-empty JSON object.",
1057
1760
  }));
1058
1761
  process.exit(1);
1059
1762
  }
@@ -1126,8 +1829,7 @@ function listEnums() {
1126
1829
  }
1127
1830
  // --- Command registration ---
1128
1831
  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)");
1832
+ const config = new Command("config").description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
1131
1833
  config
1132
1834
  .command("add-component")
1133
1835
  .description("Add a new component to the project")
@@ -1136,7 +1838,7 @@ export function createConfigCommand() {
1136
1838
  .option("--isHeader", "Mark this section as the store header (only for type: section)")
1137
1839
  .option("--isFooter", "Mark this section as the store footer (only for type: section)")
1138
1840
  .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\"}]'")
1841
+ 'Example: \'[{"name":"title","type":"TEXT","required":true,"defaultValue":"Hello","groupId":"basic"}]\'')
1140
1842
  .action((options) => {
1141
1843
  addComponent(options.name, options);
1142
1844
  });
@@ -1155,7 +1857,8 @@ export function createConfigCommand() {
1155
1857
  .option("--typeId <typeId>", "Type ID for TYPE props (required when type is TYPE)")
1156
1858
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
1157
1859
  .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)')
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"}\'')
1159
1862
  .action((options) => {
1160
1863
  addProp({ id: options.componentId, name: options.component }, options);
1161
1864
  });
@@ -1175,6 +1878,7 @@ export function createConfigCommand() {
1175
1878
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (use 'none' to clear)")
1176
1879
  .option("--filteredComponentIds <json>", "JSON array of component IDs (use 'none' to clear)")
1177
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)")
1178
1882
  .action((options) => {
1179
1883
  updateProp({ id: options.componentId, name: options.component }, options);
1180
1884
  });
@@ -1246,7 +1950,7 @@ export function createConfigCommand() {
1246
1950
  .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1247
1951
  .requiredOption("--id <id>", "Group ID to move")
1248
1952
  .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))
1953
+ .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", (v) => parseInt(v, 10))
1250
1954
  .action((options) => {
1251
1955
  movePropGroup({ id: options.componentId, name: options.component }, options);
1252
1956
  });
@@ -1306,54 +2010,15 @@ export function createConfigCommand() {
1306
2010
  * Returns the matching type (with enumOptions) if found, null otherwise.
1307
2011
  */
1308
2012
  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;
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 } : {}),
1333
2020
  }
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
- });
2021
+ : null;
1357
2022
  }
1358
2023
  /**
1359
2024
  * Best-effort lookup of an enum by PascalCase name in the live editor session.
@@ -1363,6 +2028,16 @@ async function fetchLiveEditorEnumById(enumId) {
1363
2028
  * must treat null as "could not verify, fall back to on-disk check only".
1364
2029
  */
1365
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() {
1366
2041
  const { WebSocket } = await import("ws");
1367
2042
  const WS_URL = "ws://localhost:5201";
1368
2043
  const TIMEOUT_MS = 1500;
@@ -1397,9 +2072,7 @@ async function fetchLiveEditorEnumByName(pascalName) {
1397
2072
  const message = JSON.parse(data.toString());
1398
2073
  if (message.type === "types-list") {
1399
2074
  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);
2075
+ finish(message.payload?.types || []);
1403
2076
  }
1404
2077
  }
1405
2078
  catch {
@@ -1412,6 +2085,47 @@ async function fetchLiveEditorEnumByName(pascalName) {
1412
2085
  });
1413
2086
  });
1414
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
+ }
1415
2129
  async function listTypes(componentType) {
1416
2130
  const { WebSocket } = await import("ws");
1417
2131
  const WS_URL = "ws://localhost:5201";
@@ -1445,7 +2159,8 @@ async function listTypes(componentType) {
1445
2159
  const message = JSON.parse(data.toString());
1446
2160
  if (message.type === "types-list") {
1447
2161
  clearTimeout(timeout);
1448
- 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);
1449
2164
  if (componentType === "section") {
1450
2165
  types = types.filter((t) => t.sectionAllowed);
1451
2166
  }