@ikas/component-cli 1.4.0-beta.11 → 1.4.0-beta.111

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 (150) 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-to-page.d.ts +3 -0
  10. package/dist/commands/add-to-page.d.ts.map +1 -0
  11. package/dist/commands/add-to-page.js +41 -0
  12. package/dist/commands/add-to-page.js.map +1 -0
  13. package/dist/commands/add.d.ts.map +1 -1
  14. package/dist/commands/add.js +10 -1
  15. package/dist/commands/add.js.map +1 -1
  16. package/dist/commands/build.d.ts.map +1 -1
  17. package/dist/commands/build.js +5 -165
  18. package/dist/commands/build.js.map +1 -1
  19. package/dist/commands/config.d.ts.map +1 -1
  20. package/dist/commands/config.js +397 -95
  21. package/dist/commands/config.js.map +1 -1
  22. package/dist/commands/create-design-tokens.d.ts +24 -0
  23. package/dist/commands/create-design-tokens.d.ts.map +1 -0
  24. package/dist/commands/create-design-tokens.js +327 -0
  25. package/dist/commands/create-design-tokens.js.map +1 -0
  26. package/dist/commands/create-global-variable.d.ts +3 -0
  27. package/dist/commands/create-global-variable.d.ts.map +1 -0
  28. package/dist/commands/create-global-variable.js +53 -0
  29. package/dist/commands/create-global-variable.js.map +1 -0
  30. package/dist/commands/create-page.d.ts +3 -0
  31. package/dist/commands/create-page.d.ts.map +1 -0
  32. package/dist/commands/create-page.js +31 -0
  33. package/dist/commands/create-page.js.map +1 -0
  34. package/dist/commands/delete-theme-globals.d.ts +4 -0
  35. package/dist/commands/delete-theme-globals.d.ts.map +1 -0
  36. package/dist/commands/delete-theme-globals.js +48 -0
  37. package/dist/commands/delete-theme-globals.js.map +1 -0
  38. package/dist/commands/dev.d.ts.map +1 -1
  39. package/dist/commands/dev.js +80 -12
  40. package/dist/commands/dev.js.map +1 -1
  41. package/dist/commands/get-component-props.d.ts +3 -0
  42. package/dist/commands/get-component-props.d.ts.map +1 -0
  43. package/dist/commands/get-component-props.js +32 -0
  44. package/dist/commands/get-component-props.js.map +1 -0
  45. package/dist/commands/get-page-by-type.d.ts +3 -0
  46. package/dist/commands/get-page-by-type.d.ts.map +1 -0
  47. package/dist/commands/get-page-by-type.js +25 -0
  48. package/dist/commands/get-page-by-type.js.map +1 -0
  49. package/dist/commands/get-section-values.d.ts +3 -0
  50. package/dist/commands/get-section-values.d.ts.map +1 -0
  51. package/dist/commands/get-section-values.js +39 -0
  52. package/dist/commands/get-section-values.js.map +1 -0
  53. package/dist/commands/import.d.ts +3 -0
  54. package/dist/commands/import.d.ts.map +1 -0
  55. package/dist/commands/import.js +25 -0
  56. package/dist/commands/import.js.map +1 -0
  57. package/dist/commands/list-entities.d.ts +3 -0
  58. package/dist/commands/list-entities.d.ts.map +1 -0
  59. package/dist/commands/list-entities.js +32 -0
  60. package/dist/commands/list-entities.js.map +1 -0
  61. package/dist/commands/list-imported.d.ts +3 -0
  62. package/dist/commands/list-imported.d.ts.map +1 -0
  63. package/dist/commands/list-imported.js +25 -0
  64. package/dist/commands/list-imported.js.map +1 -0
  65. package/dist/commands/list-page-sections.d.ts +3 -0
  66. package/dist/commands/list-page-sections.d.ts.map +1 -0
  67. package/dist/commands/list-page-sections.js +25 -0
  68. package/dist/commands/list-page-sections.js.map +1 -0
  69. package/dist/commands/list-pages.d.ts +3 -0
  70. package/dist/commands/list-pages.d.ts.map +1 -0
  71. package/dist/commands/list-pages.js +21 -0
  72. package/dist/commands/list-pages.js.map +1 -0
  73. package/dist/commands/list-theme-globals.d.ts +3 -0
  74. package/dist/commands/list-theme-globals.d.ts.map +1 -0
  75. package/dist/commands/list-theme-globals.js +22 -0
  76. package/dist/commands/list-theme-globals.js.map +1 -0
  77. package/dist/commands/publish-theme.d.ts +3 -0
  78. package/dist/commands/publish-theme.d.ts.map +1 -0
  79. package/dist/commands/publish-theme.js +29 -0
  80. package/dist/commands/publish-theme.js.map +1 -0
  81. package/dist/commands/publish.d.ts.map +1 -1
  82. package/dist/commands/publish.js +23 -2
  83. package/dist/commands/publish.js.map +1 -1
  84. package/dist/commands/search-products.d.ts +3 -0
  85. package/dist/commands/search-products.d.ts.map +1 -0
  86. package/dist/commands/search-products.js +40 -0
  87. package/dist/commands/search-products.js.map +1 -0
  88. package/dist/commands/update-global-variable.d.ts +3 -0
  89. package/dist/commands/update-global-variable.d.ts.map +1 -0
  90. package/dist/commands/update-global-variable.js +47 -0
  91. package/dist/commands/update-global-variable.js.map +1 -0
  92. package/dist/commands/update-page-sections.d.ts +3 -0
  93. package/dist/commands/update-page-sections.d.ts.map +1 -0
  94. package/dist/commands/update-page-sections.js +39 -0
  95. package/dist/commands/update-page-sections.js.map +1 -0
  96. package/dist/commands/update-section-prop.d.ts +3 -0
  97. package/dist/commands/update-section-prop.d.ts.map +1 -0
  98. package/dist/commands/update-section-prop.js +75 -0
  99. package/dist/commands/update-section-prop.js.map +1 -0
  100. package/dist/commands/upload-image.d.ts +3 -0
  101. package/dist/commands/upload-image.d.ts.map +1 -0
  102. package/dist/commands/upload-image.js +38 -0
  103. package/dist/commands/upload-image.js.map +1 -0
  104. package/dist/commands/upload-images.d.ts +3 -0
  105. package/dist/commands/upload-images.d.ts.map +1 -0
  106. package/dist/commands/upload-images.js +48 -0
  107. package/dist/commands/upload-images.js.map +1 -0
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +55 -0
  110. package/dist/index.js.map +1 -1
  111. package/dist/types.d.ts +11 -1
  112. package/dist/types.d.ts.map +1 -1
  113. package/dist/utils/compile.d.ts +4 -1
  114. package/dist/utils/compile.d.ts.map +1 -1
  115. package/dist/utils/compile.js +630 -102
  116. package/dist/utils/compile.js.map +1 -1
  117. package/dist/utils/component-helpers.d.ts +64 -3
  118. package/dist/utils/component-helpers.d.ts.map +1 -1
  119. package/dist/utils/component-helpers.js +189 -23
  120. package/dist/utils/component-helpers.js.map +1 -1
  121. package/dist/utils/editor-action-client.d.ts +28 -0
  122. package/dist/utils/editor-action-client.d.ts.map +1 -0
  123. package/dist/utils/editor-action-client.js +116 -0
  124. package/dist/utils/editor-action-client.js.map +1 -0
  125. package/dist/utils/forbid-deep-imports.d.ts +3 -0
  126. package/dist/utils/forbid-deep-imports.d.ts.map +1 -0
  127. package/dist/utils/forbid-deep-imports.js +54 -0
  128. package/dist/utils/forbid-deep-imports.js.map +1 -0
  129. package/dist/utils/forbid-external-packages.d.ts +16 -0
  130. package/dist/utils/forbid-external-packages.d.ts.map +1 -0
  131. package/dist/utils/forbid-external-packages.js +89 -0
  132. package/dist/utils/forbid-external-packages.js.map +1 -0
  133. package/dist/utils/load-image.d.ts +16 -0
  134. package/dist/utils/load-image.d.ts.map +1 -0
  135. package/dist/utils/load-image.js +50 -0
  136. package/dist/utils/load-image.js.map +1 -0
  137. package/dist/utils/websocket-server.d.ts +33 -2
  138. package/dist/utils/websocket-server.d.ts.map +1 -1
  139. package/dist/utils/websocket-server.js +59 -1
  140. package/dist/utils/websocket-server.js.map +1 -1
  141. package/package.json +9 -1
  142. package/templates/add/section.css +11 -0
  143. package/dist/commands/create.d.ts +0 -9
  144. package/dist/commands/create.d.ts.map +0 -1
  145. package/dist/commands/create.js +0 -9
  146. package/dist/commands/create.js.map +0 -1
  147. package/dist/commands/proxy.d.ts +0 -39
  148. package/dist/commands/proxy.d.ts.map +0 -1
  149. package/dist/commands/proxy.js +0 -212
  150. 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
  }));
@@ -144,6 +150,19 @@ function camelCaseToDisplayName(name) {
144
150
  .replace(/([a-z])([A-Z])/g, "$1 $2")
145
151
  .replace(/^./, (c) => c.toUpperCase());
146
152
  }
153
+ const ALLOWED_PROP_FIELDS = new Set([
154
+ "name",
155
+ "displayName",
156
+ "type",
157
+ "required",
158
+ "description",
159
+ "defaultValue",
160
+ "groupId",
161
+ "typeId",
162
+ "enumTypeId",
163
+ "filteredComponentIds",
164
+ "privateVarMap",
165
+ ]);
147
166
  /**
148
167
  * Parse and validate the --props JSON flag for add-component.
149
168
  * Returns validated ComponentProp[] or exits with error.
@@ -171,6 +190,16 @@ async function parsePropsFlag(propsJson, config, configPath) {
171
190
  const props = [];
172
191
  for (const raw of rawProps) {
173
192
  const r = raw;
193
+ const unknownFields = Object.keys(r).filter((k) => !ALLOWED_PROP_FIELDS.has(k));
194
+ if (unknownFields.length > 0) {
195
+ console.log(JSON.stringify({
196
+ success: false,
197
+ error: `Unknown field(s) in --props entry: ${unknownFields.join(", ")}. ` +
198
+ `Allowed: ${[...ALLOWED_PROP_FIELDS].join(", ")}. ` +
199
+ `Note: use "groupId" (not "group") and "defaultValue" (not "default") in --props JSON.`,
200
+ }));
201
+ process.exit(1);
202
+ }
174
203
  if (!r.name || typeof r.name !== "string") {
175
204
  console.log(JSON.stringify({
176
205
  success: false,
@@ -211,6 +240,24 @@ async function parsePropsFlag(propsJson, config, configPath) {
211
240
  }));
212
241
  process.exit(1);
213
242
  }
243
+ // Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
244
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
245
+ r.defaultValue !== undefined) {
246
+ const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
247
+ if (linkError) {
248
+ console.log(JSON.stringify({ success: false, error: linkError }));
249
+ process.exit(1);
250
+ }
251
+ }
252
+ // Validate SVG / SVG_LIST defaultValue (reject malformed/oversized/unsafe SVG)
253
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
254
+ r.defaultValue !== undefined) {
255
+ const svgError = validateSvgDefaultValue(propType, r.defaultValue, r.name);
256
+ if (svgError) {
257
+ console.log(JSON.stringify({ success: false, error: svgError }));
258
+ process.exit(1);
259
+ }
260
+ }
214
261
  // Auto-generate displayName if omitted
215
262
  const displayName = typeof r.displayName === "string" && r.displayName
216
263
  ? r.displayName
@@ -235,7 +282,10 @@ async function parsePropsFlag(propsJson, config, configPath) {
235
282
  process.exit(1);
236
283
  }
237
284
  // Validate enum type exists (checks config then live dev server)
238
- if (config && configPath && typeof r.enumTypeId === "string" && !r.enumTypeId.startsWith("@ikas/")) {
285
+ if (config &&
286
+ configPath &&
287
+ typeof r.enumTypeId === "string" &&
288
+ !isBuiltInEnumTypeId(r.enumTypeId)) {
239
289
  const resolved = await resolveEnumType(r.enumTypeId, config, configPath);
240
290
  if (!resolved) {
241
291
  console.log(JSON.stringify({
@@ -254,9 +304,7 @@ async function parsePropsFlag(propsJson, config, configPath) {
254
304
  ...(typeof r.description === "string" && r.description
255
305
  ? { description: r.description }
256
306
  : {}),
257
- ...(r.defaultValue !== undefined
258
- ? { defaultValue: r.defaultValue }
259
- : {}),
307
+ ...(r.defaultValue !== undefined ? { defaultValue: r.defaultValue } : {}),
260
308
  ...(typeof r.groupId === "string" && r.groupId
261
309
  ? { groupId: r.groupId }
262
310
  : {}),
@@ -266,11 +314,19 @@ async function parsePropsFlag(propsJson, config, configPath) {
266
314
  ...(propType === "ENUM" && typeof r.enumTypeId === "string"
267
315
  ? { enumTypeId: r.enumTypeId }
268
316
  : {}),
269
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.filteredComponentIds && Array.isArray(r.filteredComponentIds)
270
- ? { filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config) }
317
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
318
+ r.filteredComponentIds &&
319
+ Array.isArray(r.filteredComponentIds)
320
+ ? {
321
+ filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config),
322
+ }
271
323
  : {}),
272
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.privateVarMap && typeof r.privateVarMap === "object"
273
- ? { privateVarMap: r.privateVarMap }
324
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
325
+ r.privateVarMap &&
326
+ typeof r.privateVarMap === "object"
327
+ ? {
328
+ privateVarMap: r.privateVarMap,
329
+ }
274
330
  : {}),
275
331
  };
276
332
  props.push(prop);
@@ -302,11 +358,25 @@ async function addComponent(name, options) {
302
358
  if (!config.projectId) {
303
359
  config.projectId = generateProjectId();
304
360
  }
361
+ else if (!isValidProjectId(config.projectId)) {
362
+ // A malformed projectId (hand-edited config) breaks the editor's republish
363
+ // stability: published/local id prefixes stop matching and every publish
364
+ // INSERTs duplicate design elements. Fail fast instead of corrupting data.
365
+ console.log(JSON.stringify({
366
+ success: false,
367
+ error: `Invalid projectId "${config.projectId}" in ikas.config.json. ` +
368
+ "projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
369
+ "Restore the original generated value — do not hand-edit it.",
370
+ }));
371
+ process.exit(1);
372
+ }
305
373
  // Parse --props (and validate filteredComponentIds) BEFORE any filesystem side-effects
306
374
  // so a bad payload doesn't leave an empty component directory behind.
307
375
  const props = options.props
308
376
  ? await parsePropsFlag(options.props, config, configPath)
309
377
  : [];
378
+ // Reject section-disallowed TYPE props at definition time (best-effort, needs dev server)
379
+ await assertTypeIdsUsable(componentType, props.flatMap((p) => (p.type === "TYPE" && p.typeId ? [p.typeId] : [])));
310
380
  const componentId = generateComponentId(config.projectId);
311
381
  const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
312
382
  // Create directory
@@ -397,6 +467,10 @@ async function addProp(ref, options) {
397
467
  }));
398
468
  process.exit(1);
399
469
  }
470
+ // Reject section-disallowed types at definition time (best-effort, needs dev server)
471
+ if (propType === "TYPE" && options.typeId) {
472
+ await assertTypeIdsUsable(component.type, [options.typeId]);
473
+ }
400
474
  // Validate enumTypeId for ENUM props
401
475
  if (propType === "ENUM" && !options.enumTypeId) {
402
476
  console.log(JSON.stringify({
@@ -407,7 +481,9 @@ async function addProp(ref, options) {
407
481
  }
408
482
  // Validate that enumTypeId references an existing enum type
409
483
  // (checks config.customTypes first, then falls back to the live dev server)
410
- if (propType === "ENUM" && options.enumTypeId && !options.enumTypeId.startsWith("@ikas/")) {
484
+ if (propType === "ENUM" &&
485
+ options.enumTypeId &&
486
+ !isBuiltInEnumTypeId(options.enumTypeId)) {
411
487
  const resolved = await resolveEnumType(options.enumTypeId, config, configPath);
412
488
  if (!resolved) {
413
489
  console.log(JSON.stringify({
@@ -437,32 +513,66 @@ async function addProp(ref, options) {
437
513
  }
438
514
  // Parse filteredComponentIds JSON for COMPONENT/COMPONENT_LIST
439
515
  let parsedFilteredIds;
440
- if (options.filteredComponentIds && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
516
+ if (options.filteredComponentIds &&
517
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
441
518
  try {
442
519
  parsedFilteredIds = JSON.parse(options.filteredComponentIds);
443
520
  if (!Array.isArray(parsedFilteredIds)) {
444
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
521
+ console.log(JSON.stringify({
522
+ success: false,
523
+ error: "--filteredComponentIds must be a JSON array of strings.",
524
+ }));
445
525
  process.exit(1);
446
526
  }
447
527
  }
448
528
  catch {
449
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
529
+ console.log(JSON.stringify({
530
+ success: false,
531
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
532
+ }));
450
533
  process.exit(1);
451
534
  }
452
535
  assertKnownComponentIds(parsedFilteredIds, config);
453
536
  }
454
537
  // Parse privateVarMap JSON for COMPONENT/COMPONENT_LIST
455
538
  let parsedPrivateVarMap;
456
- if (options.privateVarMap && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
539
+ if (options.privateVarMap &&
540
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
457
541
  try {
458
542
  parsedPrivateVarMap = JSON.parse(options.privateVarMap);
459
- if (typeof parsedPrivateVarMap !== "object" || Array.isArray(parsedPrivateVarMap)) {
460
- console.log(JSON.stringify({ success: false, error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}." }));
543
+ if (typeof parsedPrivateVarMap !== "object" ||
544
+ Array.isArray(parsedPrivateVarMap)) {
545
+ console.log(JSON.stringify({
546
+ success: false,
547
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
548
+ }));
461
549
  process.exit(1);
462
550
  }
463
551
  }
464
552
  catch {
465
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
553
+ console.log(JSON.stringify({
554
+ success: false,
555
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
556
+ }));
557
+ process.exit(1);
558
+ }
559
+ }
560
+ const parsedDefaultValue = options.defaultValue !== undefined
561
+ ? parseDefaultValue(options.defaultValue, propType)
562
+ : undefined;
563
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
564
+ parsedDefaultValue !== undefined) {
565
+ const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
566
+ if (linkError) {
567
+ console.log(JSON.stringify({ success: false, error: linkError }));
568
+ process.exit(1);
569
+ }
570
+ }
571
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
572
+ parsedDefaultValue !== undefined) {
573
+ const svgError = validateSvgDefaultValue(propType, parsedDefaultValue, options.name);
574
+ if (svgError) {
575
+ console.log(JSON.stringify({ success: false, error: svgError }));
466
576
  process.exit(1);
467
577
  }
468
578
  }
@@ -472,8 +582,8 @@ async function addProp(ref, options) {
472
582
  type: propType,
473
583
  required: options.required ?? false,
474
584
  ...(options.description ? { description: options.description } : {}),
475
- ...(options.defaultValue !== undefined
476
- ? { defaultValue: parseDefaultValue(options.defaultValue, propType) }
585
+ ...(parsedDefaultValue !== undefined
586
+ ? { defaultValue: parsedDefaultValue }
477
587
  : {}),
478
588
  ...(options.group ? { groupId: options.group } : {}),
479
589
  ...(options.typeId ? { typeId: options.typeId } : {}),
@@ -496,8 +606,12 @@ async function addProp(ref, options) {
496
606
  required: newProp.required,
497
607
  ...(newProp.groupId ? { groupId: newProp.groupId } : {}),
498
608
  ...(newProp.enumTypeId ? { enumTypeId: newProp.enumTypeId } : {}),
499
- ...(newProp.filteredComponentIds ? { filteredComponentIds: newProp.filteredComponentIds } : {}),
500
- ...(newProp.privateVarMap ? { privateVarMap: newProp.privateVarMap } : {}),
609
+ ...(newProp.filteredComponentIds
610
+ ? { filteredComponentIds: newProp.filteredComponentIds }
611
+ : {}),
612
+ ...(newProp.privateVarMap
613
+ ? { privateVarMap: newProp.privateVarMap }
614
+ : {}),
501
615
  },
502
616
  }));
503
617
  }
@@ -507,11 +621,148 @@ function parseDefaultValue(value, propType) {
507
621
  return Number(value);
508
622
  case "BOOLEAN":
509
623
  return value === "true";
624
+ case "LINK":
625
+ case "LIST_OF_LINK":
626
+ // The --default-value flag arrives as a string; LINK values must be stored as a
627
+ // typed object. Parse it here; if it is not valid JSON, return the raw string so
628
+ // validateLinkDefaultValue can report a precise error.
629
+ try {
630
+ return JSON.parse(value);
631
+ }
632
+ catch {
633
+ return value;
634
+ }
635
+ case "SVG_LIST":
636
+ // Same reasoning as LINK_LIST: SVG_LIST must be stored as an array of SVG strings.
637
+ // Parse it here; if it is not valid JSON, return the raw string so
638
+ // validateSvgDefaultValue can report a precise "must be an array" error.
639
+ try {
640
+ return JSON.parse(value);
641
+ }
642
+ catch {
643
+ return value;
644
+ }
510
645
  default:
511
646
  return value;
512
647
  }
513
648
  }
514
- function updateProp(ref, options) {
649
+ const VALID_LINK_TYPES = ["PAGE", "EXTERNAL", "FILE"];
650
+ const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
651
+ 'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
652
+ "Do NOT pass a JSON string or a { label, href } shape.";
653
+ // Returns the index of the first array element that fails `fn`, as an error string, or null.
654
+ const firstError = (arr, fn) => arr.reduce((acc, item, index) => acc ?? fn(item, index), null);
655
+ const validateSingleLink = (link, propName, path) => {
656
+ const where = `defaultValue for prop "${propName}"${path}`;
657
+ if (typeof link === "string") {
658
+ return `${where} is a JSON string but must be an object. ${LINK_SHAPE_HELP}`;
659
+ }
660
+ if (!link || typeof link !== "object" || Array.isArray(link)) {
661
+ return `${where} must be a link object. ${LINK_SHAPE_HELP}`;
662
+ }
663
+ const l = link;
664
+ if ("href" in l && !("linkType" in l)) {
665
+ return `${where} uses a legacy { href } shape without "linkType". Use "linkType" + "externalLink"/"pageType" instead. ${LINK_SHAPE_HELP}`;
666
+ }
667
+ if (typeof l.linkType !== "string" ||
668
+ !VALID_LINK_TYPES.includes(l.linkType)) {
669
+ return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
670
+ }
671
+ if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
672
+ return `${where}: "subLinks" must be an array (use [] when there are none).`;
673
+ }
674
+ if (Array.isArray(l.subLinks)) {
675
+ return firstError(l.subLinks, (sl, i) => validateSingleLink(sl, propName, `${path}.subLinks[${i}]`));
676
+ }
677
+ return null;
678
+ };
679
+ // Validates a LINK / LIST_OF_LINK defaultValue. Returns an error message, or null if valid.
680
+ // These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
681
+ // legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
682
+ // out of published themes.
683
+ function validateLinkDefaultValue(propType, value, propName) {
684
+ if (value === undefined || value === null)
685
+ return null;
686
+ if (propType === "LINK") {
687
+ return validateSingleLink(value, propName, "");
688
+ }
689
+ // LIST_OF_LINK
690
+ if (typeof value === "string") {
691
+ return `defaultValue for prop "${propName}" is a JSON string but must be an object { "links": [...] }. ${LINK_SHAPE_HELP}`;
692
+ }
693
+ if (!value ||
694
+ typeof value !== "object" ||
695
+ Array.isArray(value) ||
696
+ !Array.isArray(value.links)) {
697
+ return `defaultValue for prop "${propName}" must be an object with a "links" array: { "links": [ <link>, ... ] }. ${LINK_SHAPE_HELP}`;
698
+ }
699
+ const links = value.links;
700
+ return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
701
+ }
702
+ const SVG_DEFAULT_HELP = "An SVG default must be a single well-formed <svg>…</svg> string ≤ 64 KB with no embedded base64 raster images. " +
703
+ 'Tip: the editor uses each svg\'s `class` attribute as its display name — set class="my-icon" to name it; ' +
704
+ "SVG_LIST items with no class show as svg-1, svg-2, ….";
705
+ // Keep the cap and base64 pattern in sync with @ikas/editor-models validate-svg.ts (the
706
+ // authoritative validator). Duplicated here on purpose — see validateSvgShape below.
707
+ const SVG_MAX_SIZE_KB = 64;
708
+ const SVG_BASE64_RASTER_RE = /data:image\/[a-z0-9.+-]+;base64/i;
709
+ const SVG_SHAPE_RE = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>][\s\S]*<\/svg>\s*$/i;
710
+ // Count TOP-LEVEL <svg> roots without a DOM: scan svg open/close tags in order and track
711
+ // nesting depth. A root is any <svg> opened at depth 0. This matches the editor's DOM-based
712
+ // single-root check — it rejects siblings (`<svg/><svg/>`) while allowing a nested <svg>.
713
+ // Self-closing <svg .../> counts as a root but opens no nesting level.
714
+ function svgRootCount(input) {
715
+ const tags = input.match(/<svg\b[^>]*>|<\/svg\s*>/gi) || [];
716
+ return tags.reduce((acc, tag) => {
717
+ if (tag.startsWith("</"))
718
+ return { depth: Math.max(0, acc.depth - 1), roots: acc.roots };
719
+ const roots = acc.depth === 0 ? acc.roots + 1 : acc.roots;
720
+ const depth = /\/>\s*$/.test(tag) ? acc.depth : acc.depth + 1;
721
+ return { depth, roots };
722
+ }, { depth: 0, roots: 0 }).roots;
723
+ }
724
+ // Lightweight, dependency-free SVG structure check. This standalone CLI is publishable and
725
+ // must not depend on the private @ikas/editor-models package (nor pull a DOM/DOMPurify into a
726
+ // CLI), so it mirrors the RULES of the editor's validateSvg (size, base64, single <svg> root)
727
+ // for fast authoring feedback. The authoritative sanitize runs in the editor/upload path.
728
+ // Returns a rejection reason, or null if the shape is acceptable.
729
+ function validateSvgShape(input) {
730
+ if (typeof input !== "string" || input.trim() === "")
731
+ return "empty or not a string";
732
+ if (Buffer.byteLength(input, "utf8") > SVG_MAX_SIZE_KB * 1024)
733
+ return `exceeds ${SVG_MAX_SIZE_KB} KB`;
734
+ if (SVG_BASE64_RASTER_RE.test(input))
735
+ return "embedded base64 raster image not allowed";
736
+ if (!SVG_SHAPE_RE.test(input))
737
+ return "not a single-root <svg> document";
738
+ if (svgRootCount(input) !== 1)
739
+ return "must be exactly one <svg> root";
740
+ return null;
741
+ }
742
+ // Validates an SVG / SVG_LIST defaultValue. Returns an error message, or null if valid.
743
+ // Authoring an SVG default is a common mistake source for agents; rejecting here (with an
744
+ // instructive message) keeps malformed/oversized SVGs out of published components.
745
+ function validateSvgDefaultValue(propType, value, propName) {
746
+ if (value === undefined || value === null)
747
+ return null;
748
+ if (propType === "SVG") {
749
+ const reason = validateSvgShape(value);
750
+ return reason
751
+ ? `defaultValue for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
752
+ : null;
753
+ }
754
+ // SVG_LIST
755
+ if (!Array.isArray(value)) {
756
+ return `defaultValue for prop "${propName}" must be an array of SVG strings: [ "<svg>…</svg>", ... ]. ${SVG_DEFAULT_HELP}`;
757
+ }
758
+ return firstError(value, (svg, i) => {
759
+ const reason = validateSvgShape(svg);
760
+ return reason
761
+ ? `defaultValue[${i}] for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
762
+ : null;
763
+ });
764
+ }
765
+ async function updateProp(ref, options) {
515
766
  const { config, configPath } = loadConfig();
516
767
  const component = resolveComponent(config, ref);
517
768
  const propIndex = component.props.findIndex((p) => p.name === options.prop);
@@ -544,7 +795,15 @@ function updateProp(ref, options) {
544
795
  prop.description = options.description || undefined;
545
796
  }
546
797
  if (options.defaultValue !== undefined) {
547
- prop.defaultValue = parseDefaultValue(options.defaultValue, prop.type);
798
+ const parsed = parseDefaultValue(options.defaultValue, prop.type);
799
+ if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
800
+ const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
801
+ if (linkError) {
802
+ console.log(JSON.stringify({ success: false, error: linkError }));
803
+ process.exit(1);
804
+ }
805
+ }
806
+ prop.defaultValue = parsed;
548
807
  }
549
808
  if (options.group !== undefined) {
550
809
  if (options.group === "" || options.group === "none") {
@@ -577,6 +836,13 @@ function updateProp(ref, options) {
577
836
  prop.typeId = options.typeId;
578
837
  }
579
838
  }
839
+ // Reject section-disallowed types when the caller changes type/typeId;
840
+ // untouched legacy props stay editable (displayName, group, ...).
841
+ if ((options.type !== undefined || options.typeId !== undefined) &&
842
+ prop.type === "TYPE" &&
843
+ prop.typeId) {
844
+ await assertTypeIdsUsable(component.type, [prop.typeId]);
845
+ }
580
846
  if (options.enumTypeId !== undefined) {
581
847
  if (options.enumTypeId === "" || options.enumTypeId === "none") {
582
848
  delete prop.enumTypeId;
@@ -586,14 +852,18 @@ function updateProp(ref, options) {
586
852
  }
587
853
  }
588
854
  if (options.filteredComponentIds !== undefined) {
589
- if (options.filteredComponentIds === "" || options.filteredComponentIds === "none") {
855
+ if (options.filteredComponentIds === "" ||
856
+ options.filteredComponentIds === "none") {
590
857
  delete prop.filteredComponentIds;
591
858
  }
592
859
  else {
593
860
  try {
594
861
  const parsed = JSON.parse(options.filteredComponentIds);
595
862
  if (!Array.isArray(parsed)) {
596
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
863
+ console.log(JSON.stringify({
864
+ success: false,
865
+ error: "--filteredComponentIds must be a JSON array of strings.",
866
+ }));
597
867
  process.exit(1);
598
868
  }
599
869
  assertKnownComponentIds(parsed, config);
@@ -601,7 +871,10 @@ function updateProp(ref, options) {
601
871
  }
602
872
  catch (err) {
603
873
  if (err instanceof SyntaxError) {
604
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
874
+ console.log(JSON.stringify({
875
+ success: false,
876
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
877
+ }));
605
878
  process.exit(1);
606
879
  }
607
880
  throw err;
@@ -616,13 +889,19 @@ function updateProp(ref, options) {
616
889
  try {
617
890
  const parsed = JSON.parse(options.privateVarMap);
618
891
  if (typeof parsed !== "object" || Array.isArray(parsed)) {
619
- console.log(JSON.stringify({ success: false, error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}." }));
892
+ console.log(JSON.stringify({
893
+ success: false,
894
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
895
+ }));
620
896
  process.exit(1);
621
897
  }
622
898
  prop.privateVarMap = parsed;
623
899
  }
624
900
  catch {
625
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
901
+ console.log(JSON.stringify({
902
+ success: false,
903
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
904
+ }));
626
905
  process.exit(1);
627
906
  }
628
907
  }
@@ -642,7 +921,9 @@ function updateProp(ref, options) {
642
921
  ...(prop.groupId ? { groupId: prop.groupId } : {}),
643
922
  ...(prop.typeId ? { typeId: prop.typeId } : {}),
644
923
  ...(prop.enumTypeId ? { enumTypeId: prop.enumTypeId } : {}),
645
- ...(prop.filteredComponentIds ? { filteredComponentIds: prop.filteredComponentIds } : {}),
924
+ ...(prop.filteredComponentIds
925
+ ? { filteredComponentIds: prop.filteredComponentIds }
926
+ : {}),
646
927
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
647
928
  },
648
929
  }));
@@ -706,7 +987,10 @@ function removeComponent(ref) {
706
987
  removedComponentId: component.id,
707
988
  removedComponentName: component.name,
708
989
  removedDirectory: path.relative(process.cwd(), componentDir),
709
- remainingComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
990
+ remainingComponents: config.components.map((c) => ({
991
+ id: c.id,
992
+ name: c.name,
993
+ })),
710
994
  }));
711
995
  }
712
996
  function addPropGroup(ref, options) {
@@ -813,10 +1097,10 @@ function removePropGroup(ref, options) {
813
1097
  process.exit(1);
814
1098
  }
815
1099
  if (found.parent) {
816
- found.parent.children = found.parent.children.filter(c => c.id !== options.id);
1100
+ found.parent.children = found.parent.children.filter((c) => c.id !== options.id);
817
1101
  }
818
1102
  else {
819
- component.propGroups = component.propGroups.filter(g => g.id !== options.id);
1103
+ component.propGroups = component.propGroups.filter((g) => g.id !== options.id);
820
1104
  }
821
1105
  // Clear groupId from props referencing the deleted group
822
1106
  for (const prop of component.props) {
@@ -829,7 +1113,7 @@ function removePropGroup(ref, options) {
829
1113
  success: true,
830
1114
  componentName: component.name,
831
1115
  removedPropGroup: options.id,
832
- remainingPropGroups: component.propGroups.map(g => g.id),
1116
+ remainingPropGroups: component.propGroups.map((g) => g.id),
833
1117
  }));
834
1118
  }
835
1119
  function movePropGroup(ref, options) {
@@ -846,7 +1130,10 @@ function movePropGroup(ref, options) {
846
1130
  component.propGroups = movePropGroupInTree(component.propGroups, options.id, options.parent || null, options.index);
847
1131
  }
848
1132
  catch (e) {
849
- console.log(JSON.stringify({ success: false, error: e.message || "Failed to move prop group" }));
1133
+ console.log(JSON.stringify({
1134
+ success: false,
1135
+ error: e.message || "Failed to move prop group",
1136
+ }));
850
1137
  process.exit(1);
851
1138
  }
852
1139
  saveConfig(configPath, config);
@@ -902,7 +1189,9 @@ function listComponents() {
902
1189
  required: p.required,
903
1190
  ...(p.groupId ? { groupId: p.groupId } : {}),
904
1191
  })),
905
- ...(c.propGroups && c.propGroups.length > 0 ? { propGroups: c.propGroups } : {}),
1192
+ ...(c.propGroups && c.propGroups.length > 0
1193
+ ? { propGroups: c.propGroups }
1194
+ : {}),
906
1195
  }));
907
1196
  console.log(JSON.stringify({
908
1197
  success: true,
@@ -916,7 +1205,9 @@ function listComponents() {
916
1205
  async function addEnum(options) {
917
1206
  const { config, configPath } = loadConfig();
918
1207
  // Validate name
919
- if (!options.name || typeof options.name !== "string" || !options.name.trim()) {
1208
+ if (!options.name ||
1209
+ typeof options.name !== "string" ||
1210
+ !options.name.trim()) {
920
1211
  console.log(JSON.stringify({ success: false, error: "Enum name is required." }));
921
1212
  process.exit(1);
922
1213
  }
@@ -1030,7 +1321,7 @@ function updateEnum(options) {
1030
1321
  Object.keys(enumOptions).length === 0) {
1031
1322
  console.log(JSON.stringify({
1032
1323
  success: false,
1033
- error: 'Options must be a non-empty JSON object.',
1324
+ error: "Options must be a non-empty JSON object.",
1034
1325
  }));
1035
1326
  process.exit(1);
1036
1327
  }
@@ -1103,8 +1394,7 @@ function listEnums() {
1103
1394
  }
1104
1395
  // --- Command registration ---
1105
1396
  export function createConfigCommand() {
1106
- const config = new Command("config")
1107
- .description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
1397
+ const config = new Command("config").description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
1108
1398
  config
1109
1399
  .command("add-component")
1110
1400
  .description("Add a new component to the project")
@@ -1112,7 +1402,8 @@ export function createConfigCommand() {
1112
1402
  .option("--type <type>", "Component type: section or component", "component")
1113
1403
  .option("--isHeader", "Mark this section as the store header (only for type: section)")
1114
1404
  .option("--isFooter", "Mark this section as the store footer (only for type: section)")
1115
- .option("--props <json>", "JSON array of props, e.g. '[{\"name\":\"title\",\"type\":\"TEXT\",\"required\":true}]'. Each prop needs name+type; displayName is auto-generated from name if omitted.")
1405
+ .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. " +
1406
+ 'Example: \'[{"name":"title","type":"TEXT","required":true,"defaultValue":"Hello","groupId":"basic"}]\'')
1116
1407
  .action((options) => {
1117
1408
  addComponent(options.name, options);
1118
1409
  });
@@ -1131,7 +1422,7 @@ export function createConfigCommand() {
1131
1422
  .option("--typeId <typeId>", "Type ID for TYPE props (required when type is TYPE)")
1132
1423
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
1133
1424
  .option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
1134
- .option("--privateVarMap <json>", 'JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)')
1425
+ .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)")
1135
1426
  .action((options) => {
1136
1427
  addProp({ id: options.componentId, name: options.component }, options);
1137
1428
  });
@@ -1222,7 +1513,7 @@ export function createConfigCommand() {
1222
1513
  .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1223
1514
  .requiredOption("--id <id>", "Group ID to move")
1224
1515
  .option("--parent <parentId>", "Target parent group ID (omit to move to root)")
1225
- .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", v => parseInt(v, 10))
1516
+ .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", (v) => parseInt(v, 10))
1226
1517
  .action((options) => {
1227
1518
  movePropGroup({ id: options.componentId, name: options.component }, options);
1228
1519
  });
@@ -1282,54 +1573,15 @@ export function createConfigCommand() {
1282
1573
  * Returns the matching type (with enumOptions) if found, null otherwise.
1283
1574
  */
1284
1575
  async function fetchLiveEditorEnumById(enumId) {
1285
- const { WebSocket } = await import("ws");
1286
- const WS_URL = "ws://localhost:5201";
1287
- const TIMEOUT_MS = 1500;
1288
- return new Promise((resolve) => {
1289
- let ws;
1290
- let settled = false;
1291
- const finish = (value) => {
1292
- if (settled)
1293
- return;
1294
- settled = true;
1295
- try {
1296
- ws?.close();
1297
- }
1298
- catch {
1299
- /* ignore */
1300
- }
1301
- resolve(value);
1302
- };
1303
- try {
1304
- ws = new WebSocket(WS_URL);
1305
- }
1306
- catch {
1307
- resolve(null);
1308
- return;
1576
+ const types = await fetchLiveEditorTypes();
1577
+ const match = types?.find((t) => t.category === "enum" && t.id === enumId);
1578
+ return match
1579
+ ? {
1580
+ id: match.id,
1581
+ name: match.name,
1582
+ ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}),
1309
1583
  }
1310
- const timeout = setTimeout(() => finish(null), TIMEOUT_MS);
1311
- ws.on("open", () => {
1312
- ws.send(JSON.stringify({ type: "request-types" }));
1313
- });
1314
- ws.on("message", (data) => {
1315
- try {
1316
- const message = JSON.parse(data.toString());
1317
- if (message.type === "types-list") {
1318
- clearTimeout(timeout);
1319
- const types = message.payload?.types || [];
1320
- const match = types.find((t) => t.category === "enum" && t.id === enumId);
1321
- finish(match ? { id: match.id, name: match.name, ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}) } : null);
1322
- }
1323
- }
1324
- catch {
1325
- // Ignore non-JSON messages
1326
- }
1327
- });
1328
- ws.on("error", () => {
1329
- clearTimeout(timeout);
1330
- finish(null);
1331
- });
1332
- });
1584
+ : null;
1333
1585
  }
1334
1586
  /**
1335
1587
  * Best-effort lookup of an enum by PascalCase name in the live editor session.
@@ -1339,6 +1591,16 @@ async function fetchLiveEditorEnumById(enumId) {
1339
1591
  * must treat null as "could not verify, fall back to on-disk check only".
1340
1592
  */
1341
1593
  async function fetchLiveEditorEnumByName(pascalName) {
1594
+ const types = await fetchLiveEditorTypes();
1595
+ const match = types?.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
1596
+ return match ? { id: match.id, name: match.name } : null;
1597
+ }
1598
+ /**
1599
+ * Best-effort fetch of the editor's synced type list (with category /
1600
+ * sectionAllowed / enumOptions metadata). Returns null on any failure (no dev
1601
+ * server, timeout) — callers must treat null as "could not verify, allow".
1602
+ */
1603
+ async function fetchLiveEditorTypes() {
1342
1604
  const { WebSocket } = await import("ws");
1343
1605
  const WS_URL = "ws://localhost:5201";
1344
1606
  const TIMEOUT_MS = 1500;
@@ -1373,9 +1635,7 @@ async function fetchLiveEditorEnumByName(pascalName) {
1373
1635
  const message = JSON.parse(data.toString());
1374
1636
  if (message.type === "types-list") {
1375
1637
  clearTimeout(timeout);
1376
- const types = message.payload?.types || [];
1377
- const match = types.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
1378
- finish(match ? { id: match.id, name: match.name } : null);
1638
+ finish(message.payload?.types || []);
1379
1639
  }
1380
1640
  }
1381
1641
  catch {
@@ -1388,6 +1648,47 @@ async function fetchLiveEditorEnumByName(pascalName) {
1388
1648
  });
1389
1649
  });
1390
1650
  }
1651
+ /**
1652
+ * Definition-time guard for TYPE-prop typeIds, verified against the live
1653
+ * editor's type list (skipped when the dev server is unreachable or the type
1654
+ * is unknown to the editor). Two rules:
1655
+ * - deprecated types are rejected on ANY component: they are legacy
1656
+ * render-only types — old themes may still carry values, but new props must
1657
+ * not be defined with them.
1658
+ * - on section components, only section-allowed (style) types pass — any other
1659
+ * type renders an empty placeholder merchants cannot fill, and section-level
1660
+ * bindings cannot supply a value either, so the prop would be dead weight.
1661
+ * On violation prints the standard { success: false } envelope and exits.
1662
+ */
1663
+ async function assertTypeIdsUsable(componentType, typeIds) {
1664
+ if (typeIds.length === 0)
1665
+ return;
1666
+ const types = await fetchLiveEditorTypes();
1667
+ if (!types)
1668
+ return;
1669
+ const matches = typeIds.flatMap((typeId) => {
1670
+ const match = types.find((t) => t.id === typeId);
1671
+ return match ? [match] : [];
1672
+ });
1673
+ const legacy = matches.find((t) => t.deprecated === true);
1674
+ if (legacy) {
1675
+ console.log(JSON.stringify({
1676
+ success: false,
1677
+ 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).`,
1678
+ }));
1679
+ process.exit(1);
1680
+ }
1681
+ if (componentType !== "section")
1682
+ return;
1683
+ const disallowed = matches.find((t) => t.sectionAllowed === false);
1684
+ if (!disallowed)
1685
+ return;
1686
+ console.log(JSON.stringify({
1687
+ success: false,
1688
+ 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.`,
1689
+ }));
1690
+ process.exit(1);
1691
+ }
1391
1692
  async function listTypes(componentType) {
1392
1693
  const { WebSocket } = await import("ws");
1393
1694
  const WS_URL = "ws://localhost:5201";
@@ -1421,7 +1722,8 @@ async function listTypes(componentType) {
1421
1722
  const message = JSON.parse(data.toString());
1422
1723
  if (message.type === "types-list") {
1423
1724
  clearTimeout(timeout);
1424
- let types = message.payload.types;
1725
+ // Legacy render-only types are never offered for new props.
1726
+ let types = message.payload.types.filter((t) => !t.deprecated);
1425
1727
  if (componentType === "section") {
1426
1728
  types = types.filter((t) => t.sectionAllowed);
1427
1729
  }