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