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