@ikas/component-cli 1.4.0-beta.15 → 1.4.0-beta.150
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 +35 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +831 -116
- 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/create.d.ts +4 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +878 -0
- package/dist/commands/create.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 +76 -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 +17 -0
- 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 +69 -2
- package/dist/utils/component-helpers.d.ts.map +1 -1
- package/dist/utils/component-helpers.js +193 -25
- 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/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
|
}));
|
|
@@ -90,6 +96,11 @@ function assertKnownComponentIds(input, config) {
|
|
|
90
96
|
}
|
|
91
97
|
return input;
|
|
92
98
|
}
|
|
99
|
+
// The set of component ids that currently exist in the project, for validateComponentDefaultValue's
|
|
100
|
+
// existence check. Undefined when no config is loaded, which makes the check a no-op (back-compat).
|
|
101
|
+
function existingComponentIdSet(config) {
|
|
102
|
+
return config ? new Set(config.components.map((c) => c.id)) : undefined;
|
|
103
|
+
}
|
|
93
104
|
function regenerateTypes(component, componentType, config) {
|
|
94
105
|
const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
|
|
95
106
|
const typesPath = path.join(componentDir, "types.ts");
|
|
@@ -156,7 +167,91 @@ const ALLOWED_PROP_FIELDS = new Set([
|
|
|
156
167
|
"enumTypeId",
|
|
157
168
|
"filteredComponentIds",
|
|
158
169
|
"privateVarMap",
|
|
170
|
+
"numberRangeData",
|
|
159
171
|
]);
|
|
172
|
+
// Emit the standard CLI error envelope and exit(1) when a validator returned a message; a no-op
|
|
173
|
+
// when the message is null. Collapses the repeated `if (err) { console.log(...); process.exit(1) }`
|
|
174
|
+
// tail that follows every default-value validator call.
|
|
175
|
+
function failIf(error) {
|
|
176
|
+
if (error) {
|
|
177
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Validate/sanitize a numberRangeData object for a NUMBER_RANGE prop. Accepts an object
|
|
183
|
+
* or a JSON string. min/max/interval are truncated to integers; only a positive interval
|
|
184
|
+
* is kept (0/negative would yield an invalid slider step). Exits with a structured error
|
|
185
|
+
* on malformed input.
|
|
186
|
+
*/
|
|
187
|
+
function parseNumberRangeData(raw, propName) {
|
|
188
|
+
const fail = (error) => {
|
|
189
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
190
|
+
process.exit(1);
|
|
191
|
+
};
|
|
192
|
+
let obj = raw;
|
|
193
|
+
if (typeof raw === "string") {
|
|
194
|
+
try {
|
|
195
|
+
obj = JSON.parse(raw);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
fail(`Invalid --numberRangeData JSON for prop "${propName}": ${raw}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
202
|
+
fail(`numberRangeData for prop "${propName}" must be an object { min, max, interval?, unit? }.`);
|
|
203
|
+
}
|
|
204
|
+
const o = obj;
|
|
205
|
+
const asInt = (v, field) => {
|
|
206
|
+
if (v === undefined || v === null)
|
|
207
|
+
return undefined;
|
|
208
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
209
|
+
fail(`numberRangeData.${field} for prop "${propName}" must be a finite number.`);
|
|
210
|
+
}
|
|
211
|
+
return Math.trunc(v);
|
|
212
|
+
};
|
|
213
|
+
const min = asInt(o.min, "min");
|
|
214
|
+
const max = asInt(o.max, "max");
|
|
215
|
+
const interval = asInt(o.interval, "interval");
|
|
216
|
+
const unit = typeof o.unit === "string" && o.unit.trim() ? o.unit.trim() : undefined;
|
|
217
|
+
if (min !== undefined && max !== undefined && min >= max) {
|
|
218
|
+
fail(`numberRangeData for prop "${propName}": min (${min}) must be less than max (${max}).`);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
...(min !== undefined ? { min } : {}),
|
|
222
|
+
...(max !== undefined ? { max } : {}),
|
|
223
|
+
...(interval !== undefined && interval > 0 ? { interval } : {}),
|
|
224
|
+
...(unit ? { unit } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const numberRangeDataRequiredError = (propName) => `numberRangeData with min and max is required for NUMBER_RANGE prop "${propName}". ` +
|
|
228
|
+
`Provide it, e.g. --numberRangeData '{"min":0,"max":100,"interval":5,"unit":"px"}' ` +
|
|
229
|
+
`(or a "numberRangeData" field inside --props). interval and unit are optional.`;
|
|
230
|
+
// A NUMBER_RANGE slider config is complete only with both bounds; interval/unit stay optional.
|
|
231
|
+
export const isNumberRangeComplete = (data) => data.min !== undefined && data.max !== undefined;
|
|
232
|
+
/**
|
|
233
|
+
* A NUMBER_RANGE prop must carry a slider config with at least min and max — the editor UI marks
|
|
234
|
+
* Minimum/Maksimum Değer as required, so a prop authored without them yields a half-configured
|
|
235
|
+
* slider. Enforce it at creation time: reject when numberRangeData is absent or missing min/max.
|
|
236
|
+
*/
|
|
237
|
+
export function requireNumberRangeData(raw, propName) {
|
|
238
|
+
if (raw === undefined) {
|
|
239
|
+
console.log(JSON.stringify({
|
|
240
|
+
success: false,
|
|
241
|
+
error: numberRangeDataRequiredError(propName),
|
|
242
|
+
}));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
const parsed = parseNumberRangeData(raw, propName);
|
|
246
|
+
if (!isNumberRangeComplete(parsed)) {
|
|
247
|
+
console.log(JSON.stringify({
|
|
248
|
+
success: false,
|
|
249
|
+
error: numberRangeDataRequiredError(propName),
|
|
250
|
+
}));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
return parsed;
|
|
254
|
+
}
|
|
160
255
|
/**
|
|
161
256
|
* Parse and validate the --props JSON flag for add-component.
|
|
162
257
|
* Returns validated ComponentProp[] or exits with error.
|
|
@@ -182,6 +277,8 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
182
277
|
}
|
|
183
278
|
const seenNames = new Set();
|
|
184
279
|
const props = [];
|
|
280
|
+
// Loop-invariant: build the known-id set once for the whole batch, not per prop.
|
|
281
|
+
const existingComponentIds = existingComponentIdSet(config);
|
|
185
282
|
for (const raw of rawProps) {
|
|
186
283
|
const r = raw;
|
|
187
284
|
const unknownFields = Object.keys(r).filter((k) => !ALLOWED_PROP_FIELDS.has(k));
|
|
@@ -234,6 +331,36 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
234
331
|
}));
|
|
235
332
|
process.exit(1);
|
|
236
333
|
}
|
|
334
|
+
// Dynamic (merchant-data reference) props can't carry a static default.
|
|
335
|
+
rejectDynamicDefault(r.name, propType, r.defaultValue);
|
|
336
|
+
// Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
|
|
337
|
+
if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
|
|
338
|
+
r.defaultValue !== undefined) {
|
|
339
|
+
const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
|
|
340
|
+
failIf(linkError);
|
|
341
|
+
}
|
|
342
|
+
// Validate SVG / SVG_LIST defaultValue (reject malformed/oversized/unsafe SVG)
|
|
343
|
+
if ((propType === "SVG" || propType === "SVG_LIST") &&
|
|
344
|
+
r.defaultValue !== undefined) {
|
|
345
|
+
const svgError = validateSvgDefaultValue(propType, r.defaultValue, r.name);
|
|
346
|
+
failIf(svgError);
|
|
347
|
+
}
|
|
348
|
+
// Validate COMPONENT / COMPONENT_LIST defaultValue (reference-only child code components)
|
|
349
|
+
if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
350
|
+
r.defaultValue !== undefined) {
|
|
351
|
+
failIf(validateComponentDefaultValue(propType, r.defaultValue, r.name, r.filteredComponentIds, existingComponentIds));
|
|
352
|
+
}
|
|
353
|
+
// Validate TYPE (structured/style) defaultValue shape — must be an object (or an array for an
|
|
354
|
+
// _array typeId), never a bare string that would be emitted as a wrong-shaped default.
|
|
355
|
+
if (propType === "TYPE" && r.defaultValue !== undefined) {
|
|
356
|
+
const typeError = validateTypeDefaultValue(r.defaultValue, r.name, r.typeId);
|
|
357
|
+
failIf(typeError);
|
|
358
|
+
}
|
|
359
|
+
// Validate NUMBER defaultValue — reject empty/non-finite (Number("") silently becomes 0).
|
|
360
|
+
if (propType === "NUMBER" && r.defaultValue !== undefined) {
|
|
361
|
+
const numError = validateNumberDefaultValue(r.defaultValue, r.name);
|
|
362
|
+
failIf(numError);
|
|
363
|
+
}
|
|
237
364
|
// Auto-generate displayName if omitted
|
|
238
365
|
const displayName = typeof r.displayName === "string" && r.displayName
|
|
239
366
|
? r.displayName
|
|
@@ -258,7 +385,10 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
258
385
|
process.exit(1);
|
|
259
386
|
}
|
|
260
387
|
// Validate enum type exists (checks config then live dev server)
|
|
261
|
-
if (config &&
|
|
388
|
+
if (config &&
|
|
389
|
+
configPath &&
|
|
390
|
+
typeof r.enumTypeId === "string" &&
|
|
391
|
+
!isBuiltInEnumTypeId(r.enumTypeId)) {
|
|
262
392
|
const resolved = await resolveEnumType(r.enumTypeId, config, configPath);
|
|
263
393
|
if (!resolved) {
|
|
264
394
|
console.log(JSON.stringify({
|
|
@@ -277,9 +407,7 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
277
407
|
...(typeof r.description === "string" && r.description
|
|
278
408
|
? { description: r.description }
|
|
279
409
|
: {}),
|
|
280
|
-
...(r.defaultValue !== undefined
|
|
281
|
-
? { defaultValue: r.defaultValue }
|
|
282
|
-
: {}),
|
|
410
|
+
...(r.defaultValue !== undefined ? { defaultValue: r.defaultValue } : {}),
|
|
283
411
|
...(typeof r.groupId === "string" && r.groupId
|
|
284
412
|
? { groupId: r.groupId }
|
|
285
413
|
: {}),
|
|
@@ -289,11 +417,22 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
289
417
|
...(propType === "ENUM" && typeof r.enumTypeId === "string"
|
|
290
418
|
? { enumTypeId: r.enumTypeId }
|
|
291
419
|
: {}),
|
|
292
|
-
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
293
|
-
|
|
420
|
+
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
421
|
+
r.filteredComponentIds &&
|
|
422
|
+
Array.isArray(r.filteredComponentIds)
|
|
423
|
+
? {
|
|
424
|
+
filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config),
|
|
425
|
+
}
|
|
294
426
|
: {}),
|
|
295
|
-
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
296
|
-
|
|
427
|
+
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
428
|
+
r.privateVarMap &&
|
|
429
|
+
typeof r.privateVarMap === "object"
|
|
430
|
+
? {
|
|
431
|
+
privateVarMap: r.privateVarMap,
|
|
432
|
+
}
|
|
433
|
+
: {}),
|
|
434
|
+
...(propType === "NUMBER_RANGE"
|
|
435
|
+
? { numberRangeData: requireNumberRangeData(r.numberRangeData, r.name) }
|
|
297
436
|
: {}),
|
|
298
437
|
};
|
|
299
438
|
props.push(prop);
|
|
@@ -325,11 +464,25 @@ async function addComponent(name, options) {
|
|
|
325
464
|
if (!config.projectId) {
|
|
326
465
|
config.projectId = generateProjectId();
|
|
327
466
|
}
|
|
467
|
+
else if (!isValidProjectId(config.projectId)) {
|
|
468
|
+
// A malformed projectId (hand-edited config) breaks the editor's republish
|
|
469
|
+
// stability: published/local id prefixes stop matching and every publish
|
|
470
|
+
// INSERTs duplicate design elements. Fail fast instead of corrupting data.
|
|
471
|
+
console.log(JSON.stringify({
|
|
472
|
+
success: false,
|
|
473
|
+
error: `Invalid projectId "${config.projectId}" in ikas.config.json. ` +
|
|
474
|
+
"projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
|
|
475
|
+
"Restore the original generated value — do not hand-edit it.",
|
|
476
|
+
}));
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
328
479
|
// Parse --props (and validate filteredComponentIds) BEFORE any filesystem side-effects
|
|
329
480
|
// so a bad payload doesn't leave an empty component directory behind.
|
|
330
481
|
const props = options.props
|
|
331
482
|
? await parsePropsFlag(options.props, config, configPath)
|
|
332
483
|
: [];
|
|
484
|
+
// Reject section-disallowed TYPE props at definition time (best-effort, needs dev server)
|
|
485
|
+
await assertTypeIdsUsable(componentType, props.flatMap((p) => (p.type === "TYPE" && p.typeId ? [p.typeId] : [])));
|
|
333
486
|
const componentId = generateComponentId(config.projectId);
|
|
334
487
|
const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
|
|
335
488
|
// Create directory
|
|
@@ -420,6 +573,10 @@ async function addProp(ref, options) {
|
|
|
420
573
|
}));
|
|
421
574
|
process.exit(1);
|
|
422
575
|
}
|
|
576
|
+
// Reject section-disallowed types at definition time (best-effort, needs dev server)
|
|
577
|
+
if (propType === "TYPE" && options.typeId) {
|
|
578
|
+
await assertTypeIdsUsable(component.type, [options.typeId]);
|
|
579
|
+
}
|
|
423
580
|
// Validate enumTypeId for ENUM props
|
|
424
581
|
if (propType === "ENUM" && !options.enumTypeId) {
|
|
425
582
|
console.log(JSON.stringify({
|
|
@@ -430,7 +587,9 @@ async function addProp(ref, options) {
|
|
|
430
587
|
}
|
|
431
588
|
// Validate that enumTypeId references an existing enum type
|
|
432
589
|
// (checks config.customTypes first, then falls back to the live dev server)
|
|
433
|
-
if (propType === "ENUM" &&
|
|
590
|
+
if (propType === "ENUM" &&
|
|
591
|
+
options.enumTypeId &&
|
|
592
|
+
!isBuiltInEnumTypeId(options.enumTypeId)) {
|
|
434
593
|
const resolved = await resolveEnumType(options.enumTypeId, config, configPath);
|
|
435
594
|
if (!resolved) {
|
|
436
595
|
console.log(JSON.stringify({
|
|
@@ -460,49 +619,101 @@ async function addProp(ref, options) {
|
|
|
460
619
|
}
|
|
461
620
|
// Parse filteredComponentIds JSON for COMPONENT/COMPONENT_LIST
|
|
462
621
|
let parsedFilteredIds;
|
|
463
|
-
if (options.filteredComponentIds &&
|
|
622
|
+
if (options.filteredComponentIds &&
|
|
623
|
+
(propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
|
|
464
624
|
try {
|
|
465
625
|
parsedFilteredIds = JSON.parse(options.filteredComponentIds);
|
|
466
626
|
if (!Array.isArray(parsedFilteredIds)) {
|
|
467
|
-
console.log(JSON.stringify({
|
|
627
|
+
console.log(JSON.stringify({
|
|
628
|
+
success: false,
|
|
629
|
+
error: "--filteredComponentIds must be a JSON array of strings.",
|
|
630
|
+
}));
|
|
468
631
|
process.exit(1);
|
|
469
632
|
}
|
|
470
633
|
}
|
|
471
634
|
catch {
|
|
472
|
-
console.log(JSON.stringify({
|
|
635
|
+
console.log(JSON.stringify({
|
|
636
|
+
success: false,
|
|
637
|
+
error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
|
|
638
|
+
}));
|
|
473
639
|
process.exit(1);
|
|
474
640
|
}
|
|
475
641
|
assertKnownComponentIds(parsedFilteredIds, config);
|
|
476
642
|
}
|
|
477
643
|
// Parse privateVarMap JSON for COMPONENT/COMPONENT_LIST
|
|
478
644
|
let parsedPrivateVarMap;
|
|
479
|
-
if (options.privateVarMap &&
|
|
645
|
+
if (options.privateVarMap &&
|
|
646
|
+
(propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
|
|
480
647
|
try {
|
|
481
648
|
parsedPrivateVarMap = JSON.parse(options.privateVarMap);
|
|
482
|
-
if (typeof parsedPrivateVarMap !== "object" ||
|
|
483
|
-
|
|
649
|
+
if (typeof parsedPrivateVarMap !== "object" ||
|
|
650
|
+
Array.isArray(parsedPrivateVarMap)) {
|
|
651
|
+
console.log(JSON.stringify({
|
|
652
|
+
success: false,
|
|
653
|
+
error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
|
|
654
|
+
}));
|
|
484
655
|
process.exit(1);
|
|
485
656
|
}
|
|
486
657
|
}
|
|
487
658
|
catch {
|
|
488
|
-
console.log(JSON.stringify({
|
|
659
|
+
console.log(JSON.stringify({
|
|
660
|
+
success: false,
|
|
661
|
+
error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
|
|
662
|
+
}));
|
|
489
663
|
process.exit(1);
|
|
490
664
|
}
|
|
491
665
|
}
|
|
666
|
+
rejectDynamicDefault(options.name, propType, options.defaultValue);
|
|
667
|
+
const parsedDefaultValue = options.defaultValue !== undefined
|
|
668
|
+
? parseDefaultValue(options.defaultValue, propType)
|
|
669
|
+
: undefined;
|
|
670
|
+
if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
|
|
671
|
+
parsedDefaultValue !== undefined) {
|
|
672
|
+
const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
|
|
673
|
+
failIf(linkError);
|
|
674
|
+
}
|
|
675
|
+
if ((propType === "SVG" || propType === "SVG_LIST") &&
|
|
676
|
+
parsedDefaultValue !== undefined) {
|
|
677
|
+
const svgError = validateSvgDefaultValue(propType, parsedDefaultValue, options.name);
|
|
678
|
+
failIf(svgError);
|
|
679
|
+
}
|
|
680
|
+
if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
681
|
+
parsedDefaultValue !== undefined) {
|
|
682
|
+
failIf(validateComponentDefaultValue(propType, parsedDefaultValue, options.name, parsedFilteredIds, existingComponentIdSet(config)));
|
|
683
|
+
}
|
|
684
|
+
if (propType === "TYPE" && parsedDefaultValue !== undefined) {
|
|
685
|
+
const typeError = validateTypeDefaultValue(parsedDefaultValue, options.name, options.typeId);
|
|
686
|
+
failIf(typeError);
|
|
687
|
+
}
|
|
688
|
+
// Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
|
|
689
|
+
if (propType === "NUMBER" && options.defaultValue !== undefined) {
|
|
690
|
+
const numError = validateNumberDefaultValue(options.defaultValue, options.name);
|
|
691
|
+
failIf(numError);
|
|
692
|
+
}
|
|
693
|
+
const parsedNumberRangeData = propType === "NUMBER_RANGE"
|
|
694
|
+
? requireNumberRangeData(options.numberRangeData, options.name)
|
|
695
|
+
: undefined;
|
|
696
|
+
if (propType === "NUMBER_RANGE" && parsedDefaultValue !== undefined) {
|
|
697
|
+
const nrError = validateNumberRangeDefaultValue(parsedDefaultValue, parsedNumberRangeData, options.name);
|
|
698
|
+
failIf(nrError);
|
|
699
|
+
}
|
|
492
700
|
const newProp = {
|
|
493
701
|
name: options.name,
|
|
494
702
|
displayName: options.displayName,
|
|
495
703
|
type: propType,
|
|
496
704
|
required: options.required ?? false,
|
|
497
705
|
...(options.description ? { description: options.description } : {}),
|
|
498
|
-
...(
|
|
499
|
-
? { defaultValue:
|
|
706
|
+
...(parsedDefaultValue !== undefined
|
|
707
|
+
? { defaultValue: parsedDefaultValue }
|
|
500
708
|
: {}),
|
|
501
709
|
...(options.group ? { groupId: options.group } : {}),
|
|
502
710
|
...(options.typeId ? { typeId: options.typeId } : {}),
|
|
503
711
|
...(options.enumTypeId ? { enumTypeId: options.enumTypeId } : {}),
|
|
504
712
|
...(parsedFilteredIds ? { filteredComponentIds: parsedFilteredIds } : {}),
|
|
505
713
|
...(parsedPrivateVarMap ? { privateVarMap: parsedPrivateVarMap } : {}),
|
|
714
|
+
...(parsedNumberRangeData
|
|
715
|
+
? { numberRangeData: parsedNumberRangeData }
|
|
716
|
+
: {}),
|
|
506
717
|
};
|
|
507
718
|
component.props.push(newProp);
|
|
508
719
|
saveConfig(configPath, config);
|
|
@@ -519,22 +730,430 @@ async function addProp(ref, options) {
|
|
|
519
730
|
required: newProp.required,
|
|
520
731
|
...(newProp.groupId ? { groupId: newProp.groupId } : {}),
|
|
521
732
|
...(newProp.enumTypeId ? { enumTypeId: newProp.enumTypeId } : {}),
|
|
522
|
-
...(newProp.filteredComponentIds
|
|
523
|
-
|
|
733
|
+
...(newProp.filteredComponentIds
|
|
734
|
+
? { filteredComponentIds: newProp.filteredComponentIds }
|
|
735
|
+
: {}),
|
|
736
|
+
...(newProp.privateVarMap
|
|
737
|
+
? { privateVarMap: newProp.privateVarMap }
|
|
738
|
+
: {}),
|
|
739
|
+
...(newProp.numberRangeData
|
|
740
|
+
? { numberRangeData: newProp.numberRangeData }
|
|
741
|
+
: {}),
|
|
524
742
|
},
|
|
525
743
|
}));
|
|
526
744
|
}
|
|
527
|
-
function parseDefaultValue(value, propType) {
|
|
745
|
+
export function parseDefaultValue(value, propType) {
|
|
528
746
|
switch (propType) {
|
|
529
747
|
case "NUMBER":
|
|
530
748
|
return Number(value);
|
|
531
749
|
case "BOOLEAN":
|
|
532
750
|
return value === "true";
|
|
751
|
+
case "COMPONENT":
|
|
752
|
+
case "COMPONENT_LIST":
|
|
753
|
+
case "LINK":
|
|
754
|
+
case "LIST_OF_LINK":
|
|
755
|
+
case "SVG_LIST":
|
|
756
|
+
case "NUMBER_RANGE":
|
|
757
|
+
case "TYPE":
|
|
758
|
+
// These all carry a typed value (object/array), not a scalar. The --defaultValue flag arrives
|
|
759
|
+
// as a string, so parse it here; if it is not valid JSON, return the raw string so the type's
|
|
760
|
+
// validator (validateLinkDefaultValue / validateSvgDefaultValue / validateNumberRangeDefaultValue
|
|
761
|
+
// / validateTypeDefaultValue / validateComponentDefaultValue) can report a precise error instead
|
|
762
|
+
// of silently storing a bad shape.
|
|
763
|
+
try {
|
|
764
|
+
return JSON.parse(value);
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
return value;
|
|
768
|
+
}
|
|
533
769
|
default:
|
|
534
770
|
return value;
|
|
535
771
|
}
|
|
536
772
|
}
|
|
537
|
-
|
|
773
|
+
// Prop types whose value is a dynamic reference to merchant storefront data (products, media,
|
|
774
|
+
// categories, brands, blogs, raffles). They resolve at storefront render time, so a static config
|
|
775
|
+
// default is invalid and is rejected on add/update-prop. Keep in sync with @ikas/editor-models
|
|
776
|
+
// DYNAMIC_PROP_TYPES (the CLI must not depend on that private package).
|
|
777
|
+
export const DYNAMIC_PROP_TYPES = [
|
|
778
|
+
"IMAGE",
|
|
779
|
+
"IMAGE_LIST",
|
|
780
|
+
"VIDEO",
|
|
781
|
+
"PRODUCT",
|
|
782
|
+
"PRODUCT_LIST",
|
|
783
|
+
"PRODUCT_ATTRIBUTE",
|
|
784
|
+
"PRODUCT_ATTRIBUTE_LIST",
|
|
785
|
+
"BRAND",
|
|
786
|
+
"BRAND_LIST",
|
|
787
|
+
"CATEGORY",
|
|
788
|
+
"CATEGORY_LIST",
|
|
789
|
+
"BLOG",
|
|
790
|
+
"BLOG_LIST",
|
|
791
|
+
"BLOG_CATEGORY",
|
|
792
|
+
"BLOG_CATEGORY_LIST",
|
|
793
|
+
"RAFFLE",
|
|
794
|
+
"RAFFLE_LIST",
|
|
795
|
+
];
|
|
796
|
+
const dynamicPropDefaultError = (propName, propType) => `defaultValue is not allowed for prop "${propName}" of type ${propType}. ${propType} resolves to merchant storefront data at render time, so it can't carry a static default — omit defaultValue.`;
|
|
797
|
+
// Reject a static default on a dynamic (merchant-data reference) prop type: print the standard
|
|
798
|
+
// error and exit. No-op when the type isn't dynamic or no default was supplied.
|
|
799
|
+
export const rejectDynamicDefault = (propName, propType, defaultValue) => {
|
|
800
|
+
if (DYNAMIC_PROP_TYPES.includes(propType) && defaultValue !== undefined) {
|
|
801
|
+
console.log(JSON.stringify({
|
|
802
|
+
success: false,
|
|
803
|
+
error: dynamicPropDefaultError(propName, propType),
|
|
804
|
+
}));
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
|
|
809
|
+
// FILE is intentionally excluded from CLI-authored defaults: a code-component / AI-authored default
|
|
810
|
+
// can't reference a store-specific uploaded file, so a "FILE" link default is always rejected. Store
|
|
811
|
+
// owners can still create FILE links manually in the editor (editor-core keeps FILE in its own set).
|
|
812
|
+
const VALID_LINK_TYPES = ["PAGE", "EXTERNAL"];
|
|
813
|
+
const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
|
|
814
|
+
'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
|
|
815
|
+
"Do NOT pass a JSON string or a { label, href } shape.";
|
|
816
|
+
// Returns the index of the first array element that fails `fn`, as an error string, or null.
|
|
817
|
+
const firstError = (arr, fn) => arr.reduce((acc, item, index) => acc ?? fn(item, index), null);
|
|
818
|
+
const validateSingleLink = (link, propName, path) => {
|
|
819
|
+
const where = `defaultValue for prop "${propName}"${path}`;
|
|
820
|
+
if (typeof link === "string") {
|
|
821
|
+
return `${where} is a JSON string but must be an object. ${LINK_SHAPE_HELP}`;
|
|
822
|
+
}
|
|
823
|
+
if (!link || typeof link !== "object" || Array.isArray(link)) {
|
|
824
|
+
return `${where} must be a link object. ${LINK_SHAPE_HELP}`;
|
|
825
|
+
}
|
|
826
|
+
const l = link;
|
|
827
|
+
if ("href" in l && !("linkType" in l)) {
|
|
828
|
+
return `${where} uses a legacy { href } shape without "linkType". Use "linkType" + "externalLink"/"pageType" instead. ${LINK_SHAPE_HELP}`;
|
|
829
|
+
}
|
|
830
|
+
if (typeof l.linkType !== "string" ||
|
|
831
|
+
!VALID_LINK_TYPES.includes(l.linkType)) {
|
|
832
|
+
return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
|
|
833
|
+
}
|
|
834
|
+
// Require the destination field for the declared linkType, so a link that points nowhere never
|
|
835
|
+
// reaches a published theme. A PAGE default carries a pageType (pageId is per-store, filled on
|
|
836
|
+
// materialization); pageId alone is also accepted for a specific page.
|
|
837
|
+
if (l.linkType === "PAGE" && !nonEmpty(l.pageType) && !nonEmpty(l.pageId)) {
|
|
838
|
+
return `${where}: a "PAGE" link needs a "pageType" (e.g. "INDEX") — or a "pageId" for a specific page. ${LINK_SHAPE_HELP}`;
|
|
839
|
+
}
|
|
840
|
+
if (l.linkType === "EXTERNAL" && !nonEmpty(l.externalLink)) {
|
|
841
|
+
return `${where}: an "EXTERNAL" link needs a non-empty "externalLink" URL. ${LINK_SHAPE_HELP}`;
|
|
842
|
+
}
|
|
843
|
+
if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
|
|
844
|
+
return `${where}: "subLinks" must be an array (use [] when there are none).`;
|
|
845
|
+
}
|
|
846
|
+
if (Array.isArray(l.subLinks)) {
|
|
847
|
+
return firstError(l.subLinks, (sl, i) => validateSingleLink(sl, propName, `${path}.subLinks[${i}]`));
|
|
848
|
+
}
|
|
849
|
+
return null;
|
|
850
|
+
};
|
|
851
|
+
// Validates a LINK / LIST_OF_LINK defaultValue. Returns an error message, or null if valid.
|
|
852
|
+
// These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
|
|
853
|
+
// legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
|
|
854
|
+
// out of published themes.
|
|
855
|
+
export function validateLinkDefaultValue(propType, value, propName) {
|
|
856
|
+
if (value === undefined || value === null)
|
|
857
|
+
return null;
|
|
858
|
+
if (propType === "LINK") {
|
|
859
|
+
return validateSingleLink(value, propName, "");
|
|
860
|
+
}
|
|
861
|
+
// LIST_OF_LINK
|
|
862
|
+
if (typeof value === "string") {
|
|
863
|
+
return `defaultValue for prop "${propName}" is a JSON string but must be an object { "links": [...] }. ${LINK_SHAPE_HELP}`;
|
|
864
|
+
}
|
|
865
|
+
if (!value ||
|
|
866
|
+
typeof value !== "object" ||
|
|
867
|
+
Array.isArray(value) ||
|
|
868
|
+
!Array.isArray(value.links)) {
|
|
869
|
+
return `defaultValue for prop "${propName}" must be an object with a "links" array: { "links": [ <link>, ... ] }. ${LINK_SHAPE_HELP}`;
|
|
870
|
+
}
|
|
871
|
+
const links = value.links;
|
|
872
|
+
return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
|
|
873
|
+
}
|
|
874
|
+
const COMPONENT_SHAPE_HELP = 'A COMPONENT / COMPONENT_LIST default must be an object listing child code components: ' +
|
|
875
|
+
'{ "components": [ { "codeComponentId": "<id>" } ] }. Only code components are allowed; each entry ' +
|
|
876
|
+
"needs a string \"codeComponentId\" (no componentId, no inline propValues).";
|
|
877
|
+
/**
|
|
878
|
+
* Validate a COMPONENT / COMPONENT_LIST defaultValue. Reference-only: each entry must name a code
|
|
879
|
+
* component via `codeComponentId`. Rejects non-CC (`componentId`) children, missing ids, more than
|
|
880
|
+
* one entry for a single COMPONENT, and ids outside the prop's filteredComponentIds allowlist.
|
|
881
|
+
* Returns an error message, or null if valid.
|
|
882
|
+
*/
|
|
883
|
+
export function validateComponentDefaultValue(propType, value, propName, filteredComponentIds, existingComponentIds) {
|
|
884
|
+
if (value === undefined || value === null)
|
|
885
|
+
return null;
|
|
886
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
887
|
+
if (typeof value === "string") {
|
|
888
|
+
return `${where} is a JSON string but must be an object. ${COMPONENT_SHAPE_HELP}`;
|
|
889
|
+
}
|
|
890
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || !Array.isArray(value.components)) {
|
|
891
|
+
return `${where} must be an object with a "components" array. ${COMPONENT_SHAPE_HELP}`;
|
|
892
|
+
}
|
|
893
|
+
const components = value.components;
|
|
894
|
+
if (propType === "COMPONENT" && components.length > 1) {
|
|
895
|
+
return `${where}: a COMPONENT prop holds a single child — provide at most one entry (use COMPONENT_LIST for many).`;
|
|
896
|
+
}
|
|
897
|
+
const allow = filteredComponentIds && filteredComponentIds.length ? new Set(filteredComponentIds) : null;
|
|
898
|
+
return firstError(components, (entry, i) => {
|
|
899
|
+
const at = `${where}.components[${i}]`;
|
|
900
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
901
|
+
return `${at} must be an object { "codeComponentId": "<id>" }. ${COMPONENT_SHAPE_HELP}`;
|
|
902
|
+
}
|
|
903
|
+
const e = entry;
|
|
904
|
+
if ("componentId" in e && !("codeComponentId" in e)) {
|
|
905
|
+
return `${at} references a non-code component (componentId). Only code components are allowed as defaults. ${COMPONENT_SHAPE_HELP}`;
|
|
906
|
+
}
|
|
907
|
+
if (typeof e.codeComponentId !== "string" || e.codeComponentId.trim().length === 0) {
|
|
908
|
+
return `${at} must have a non-empty string "codeComponentId". ${COMPONENT_SHAPE_HELP}`;
|
|
909
|
+
}
|
|
910
|
+
if (allow && !allow.has(e.codeComponentId)) {
|
|
911
|
+
return `${at}: "${e.codeComponentId}" is not in this prop's filteredComponentIds allowlist.`;
|
|
912
|
+
}
|
|
913
|
+
// Reject a reference to a component that does not exist in the project (deleted, renamed, or
|
|
914
|
+
// never created). A dangling default would seed a ghost entry on every placement that renders
|
|
915
|
+
// nothing. Only enforced when the caller supplies the known-component set (the config-backed
|
|
916
|
+
// add-prop / update-prop / batch sites); pure-shape callers pass no set and skip this check.
|
|
917
|
+
if (existingComponentIds && !existingComponentIds.has(e.codeComponentId)) {
|
|
918
|
+
return `${at}: no component with id "${e.codeComponentId}" exists in this project. Create it first (config add-component) or reference an existing component id (config list).`;
|
|
919
|
+
}
|
|
920
|
+
return null;
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
const SVG_DEFAULT_HELP = "An SVG default must be a single well-formed <svg>…</svg> string ≤ 64 KB with no embedded base64 raster images. " +
|
|
924
|
+
'Tip: the editor uses each svg\'s `class` attribute as its display name — set class="my-icon" to name it; ' +
|
|
925
|
+
"SVG_LIST items with no class show as svg-1, svg-2, ….";
|
|
926
|
+
// Keep the cap and base64 pattern in sync with @ikas/editor-models validate-svg.ts (the
|
|
927
|
+
// authoritative validator). Duplicated here on purpose — see validateSvgShape below.
|
|
928
|
+
const SVG_MAX_SIZE_KB = 64;
|
|
929
|
+
const SVG_BASE64_RASTER_RE = /data:image\/[a-z0-9.+-]+;base64/i;
|
|
930
|
+
const SVG_SHAPE_RE = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>][\s\S]*<\/svg>\s*$/i;
|
|
931
|
+
// Count TOP-LEVEL <svg> roots without a DOM: scan svg open/close tags in order and track
|
|
932
|
+
// nesting depth. A root is any <svg> opened at depth 0. This matches the editor's DOM-based
|
|
933
|
+
// single-root check — it rejects siblings (`<svg/><svg/>`) while allowing a nested <svg>.
|
|
934
|
+
// Self-closing <svg .../> counts as a root but opens no nesting level.
|
|
935
|
+
function svgRootCount(input) {
|
|
936
|
+
const tags = input.match(/<svg\b[^>]*>|<\/svg\s*>/gi) || [];
|
|
937
|
+
return tags.reduce((acc, tag) => {
|
|
938
|
+
if (tag.startsWith("</"))
|
|
939
|
+
return { depth: Math.max(0, acc.depth - 1), roots: acc.roots };
|
|
940
|
+
const roots = acc.depth === 0 ? acc.roots + 1 : acc.roots;
|
|
941
|
+
const depth = /\/>\s*$/.test(tag) ? acc.depth : acc.depth + 1;
|
|
942
|
+
return { depth, roots };
|
|
943
|
+
}, { depth: 0, roots: 0 }).roots;
|
|
944
|
+
}
|
|
945
|
+
// Lightweight, dependency-free SVG structure check. This standalone CLI is publishable and
|
|
946
|
+
// must not depend on the private @ikas/editor-models package (nor pull a DOM/DOMPurify into a
|
|
947
|
+
// CLI), so it mirrors the RULES of the editor's validateSvg (size, base64, single <svg> root)
|
|
948
|
+
// for fast authoring feedback. The authoritative sanitize runs in the editor/upload path.
|
|
949
|
+
// Returns a rejection reason, or null if the shape is acceptable.
|
|
950
|
+
function validateSvgShape(input) {
|
|
951
|
+
if (typeof input !== "string" || input.trim() === "")
|
|
952
|
+
return "empty or not a string";
|
|
953
|
+
if (Buffer.byteLength(input, "utf8") > SVG_MAX_SIZE_KB * 1024)
|
|
954
|
+
return `exceeds ${SVG_MAX_SIZE_KB} KB`;
|
|
955
|
+
if (SVG_BASE64_RASTER_RE.test(input))
|
|
956
|
+
return "embedded base64 raster image not allowed";
|
|
957
|
+
if (!SVG_SHAPE_RE.test(input))
|
|
958
|
+
return "not a single-root <svg> document";
|
|
959
|
+
if (svgRootCount(input) !== 1)
|
|
960
|
+
return "must be exactly one <svg> root";
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
// Validates an SVG / SVG_LIST defaultValue. Returns an error message, or null if valid.
|
|
964
|
+
// Authoring an SVG default is a common mistake source for agents; rejecting here (with an
|
|
965
|
+
// instructive message) keeps malformed/oversized SVGs out of published components.
|
|
966
|
+
export function validateSvgDefaultValue(propType, value, propName) {
|
|
967
|
+
if (value === undefined || value === null)
|
|
968
|
+
return null;
|
|
969
|
+
if (propType === "SVG") {
|
|
970
|
+
const reason = validateSvgShape(value);
|
|
971
|
+
return reason
|
|
972
|
+
? `defaultValue for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
|
|
973
|
+
: null;
|
|
974
|
+
}
|
|
975
|
+
// SVG_LIST
|
|
976
|
+
if (!Array.isArray(value)) {
|
|
977
|
+
return `defaultValue for prop "${propName}" must be an array of SVG strings: [ "<svg>…</svg>", ... ]. ${SVG_DEFAULT_HELP}`;
|
|
978
|
+
}
|
|
979
|
+
return firstError(value, (svg, i) => {
|
|
980
|
+
const reason = validateSvgShape(svg);
|
|
981
|
+
return reason
|
|
982
|
+
? `defaultValue[${i}] for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
|
|
983
|
+
: null;
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
const NUMBER_RANGE_DEFAULT_HELP = 'A NUMBER_RANGE default must be an object { "value": <number>, "unit": "px" | null }. ' +
|
|
987
|
+
"When the prop defines numberRangeData, value must be within [min, max] and on the interval grid (min + k·interval).";
|
|
988
|
+
// Validates a NUMBER_RANGE defaultValue. Returns an error message, or null if valid.
|
|
989
|
+
// Checks the { value, unit } shape and — when the prop carries numberRangeData — that the
|
|
990
|
+
// value is within [min, max] and lands on the interval grid, mirroring the editor's slider
|
|
991
|
+
// so an agent can't author an out-of-range or off-step default.
|
|
992
|
+
export function validateNumberRangeDefaultValue(value, numberRangeData, propName) {
|
|
993
|
+
if (value === undefined || value === null)
|
|
994
|
+
return null;
|
|
995
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
996
|
+
if (typeof value === "string") {
|
|
997
|
+
return `${where} is a JSON string but must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
998
|
+
}
|
|
999
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
1000
|
+
return `${where} must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1001
|
+
}
|
|
1002
|
+
const v = value;
|
|
1003
|
+
if (typeof v.value !== "number" || !Number.isFinite(v.value)) {
|
|
1004
|
+
return `${where}: "value" must be a finite number. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1005
|
+
}
|
|
1006
|
+
if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
|
|
1007
|
+
return `${where}: "unit" must be a string or null. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1008
|
+
}
|
|
1009
|
+
if (numberRangeData) {
|
|
1010
|
+
const { min, max, interval } = numberRangeData;
|
|
1011
|
+
if (min !== undefined && v.value < min) {
|
|
1012
|
+
return `${where}: value ${v.value} is below the configured min (${min}).`;
|
|
1013
|
+
}
|
|
1014
|
+
if (max !== undefined && v.value > max) {
|
|
1015
|
+
return `${where}: value ${v.value} is above the configured max (${max}).`;
|
|
1016
|
+
}
|
|
1017
|
+
if (interval !== undefined &&
|
|
1018
|
+
interval > 0 &&
|
|
1019
|
+
min !== undefined &&
|
|
1020
|
+
(v.value - min) % interval !== 0) {
|
|
1021
|
+
return `${where}: value ${v.value} is not on the interval grid (min ${min} + k·${interval}).`;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
const NUMBER_DEFAULT_HELP = "A NUMBER default must be a finite number, e.g. --defaultValue 42.";
|
|
1027
|
+
// Validates a NUMBER defaultValue. Returns an error message, or null if valid. Guards two footguns:
|
|
1028
|
+
// an empty/whitespace flag string (Number("") === 0 would silently store 0) and a non-numeric or
|
|
1029
|
+
// non-finite value (e.g. "42px" → NaN, "Infinity"). The flag path passes the raw --defaultValue
|
|
1030
|
+
// string; the --props path passes the already-parsed JSON value — both are handled here.
|
|
1031
|
+
export function validateNumberDefaultValue(value, propName) {
|
|
1032
|
+
if (value === undefined || value === null)
|
|
1033
|
+
return null;
|
|
1034
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
1035
|
+
if (typeof value === "string") {
|
|
1036
|
+
if (value.trim() === "") {
|
|
1037
|
+
return `${where} is empty, but NUMBER requires a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1038
|
+
}
|
|
1039
|
+
if (!Number.isFinite(Number(value))) {
|
|
1040
|
+
return `${where} ("${value}") is not a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1041
|
+
}
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1045
|
+
return `${where} must be a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1046
|
+
}
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
const STYLE_TYPE_DEFAULT_HELP = "A size/style TYPE default must be an object matching the type's shape: " +
|
|
1050
|
+
'{ "value": <number>, "unit": "px" | "rem" | "%" | "vh" | "vw" } (or { "css": "<raw css>" }). ' +
|
|
1051
|
+
"Author it via add-component --props '[…]' or --defaultValue '<json>'.";
|
|
1052
|
+
const BP_STOREFRONT_PREFIX = "@ikas/bp-storefront-models-";
|
|
1053
|
+
// The size/style types that share the { css?, value?, unit? } shape. This is EXACTLY the set of
|
|
1054
|
+
// TYPE typeIds whose default the generator emits (mirror of DYNAMIC_STYLE_TYPE_IDS in
|
|
1055
|
+
// @ikas/blueprint-modules design-generator/generate-helpers/styles.ts — the standalone CLI must not
|
|
1056
|
+
// depend on that package, so it is hand-maintained here and kept in sync by a parity test). Every
|
|
1057
|
+
// OTHER TYPE typeId (BorderStyleType, BoxShadowStyleType, GridTemplateColumnsStyleType, string-union
|
|
1058
|
+
// styles like TextAlignStyleType, IkasProduct, custom types) has a DIFFERENT value shape, so their
|
|
1059
|
+
// defaults are not shape-checked (and are dead code in the generator anyway).
|
|
1060
|
+
const SIZE_STYLE_TYPE_NAMES = [
|
|
1061
|
+
"SizeStyleType",
|
|
1062
|
+
"PaddingStyleType",
|
|
1063
|
+
"PaddingTopStyleType",
|
|
1064
|
+
"PaddingRightStyleType",
|
|
1065
|
+
"PaddingBottomStyleType",
|
|
1066
|
+
"PaddingLeftStyleType",
|
|
1067
|
+
"MarginStyleType",
|
|
1068
|
+
"MarginTopStyleType",
|
|
1069
|
+
"MarginRightStyleType",
|
|
1070
|
+
"MarginBottomStyleType",
|
|
1071
|
+
"MarginLeftStyleType",
|
|
1072
|
+
"BorderRadiusStyleType",
|
|
1073
|
+
"BorderRadiusTopLeftStyleType",
|
|
1074
|
+
"BorderRadiusTopRightStyleType",
|
|
1075
|
+
"BorderRadiusBottomRightStyleType",
|
|
1076
|
+
"BorderRadiusBottomLeftStyleType",
|
|
1077
|
+
"FontSizeStyleType",
|
|
1078
|
+
"LineHeightStyleType",
|
|
1079
|
+
"LetterSpacingStyleType",
|
|
1080
|
+
"HeightStyleType",
|
|
1081
|
+
"MinHeightStyleType",
|
|
1082
|
+
"MaxHeightStyleType",
|
|
1083
|
+
"WidthStyleType",
|
|
1084
|
+
"MinWidthStyleType",
|
|
1085
|
+
"MaxWidthStyleType",
|
|
1086
|
+
"GapStyleType",
|
|
1087
|
+
"BorderWidthStyleType",
|
|
1088
|
+
"TopStyleType",
|
|
1089
|
+
"RightStyleType",
|
|
1090
|
+
"BottomStyleType",
|
|
1091
|
+
"LeftStyleType",
|
|
1092
|
+
];
|
|
1093
|
+
// Validates a single style-type default object. Every size style type shares the optional
|
|
1094
|
+
// { css?, value?, unit? } shape, so inner fields are checked loosely — the key rejection is a
|
|
1095
|
+
// non-object (e.g. a bare JSON string, the common failure when a --defaultValue flag is not parsed).
|
|
1096
|
+
function styleDefaultObjectError(value, where) {
|
|
1097
|
+
if (typeof value === "string") {
|
|
1098
|
+
return `${where} is a JSON string but must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1099
|
+
}
|
|
1100
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1101
|
+
return `${where} must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1102
|
+
}
|
|
1103
|
+
const v = value;
|
|
1104
|
+
// css/value/unit are all optional, and the editor treats an explicit null as "unset" (it accepts
|
|
1105
|
+
// { value: null }). So only type-check a field that is present AND non-null — otherwise the CLI
|
|
1106
|
+
// would be stricter than the authoritative editor and reject a value it would happily accept.
|
|
1107
|
+
// Unknown fields ARE rejected below: every size-style type shares exactly this { css?, value?, unit? }
|
|
1108
|
+
// shape, so an extra key is always a mistake (this matches the editor). The per-type value-DOMAIN
|
|
1109
|
+
// (which units a given type allows — BorderWidth also permits em/vmin/vmax while the rest are
|
|
1110
|
+
// px/rem/%/vh/vw) stays editor-side: it is per-type and can't be mirrored offline without drift.
|
|
1111
|
+
if (v.css !== undefined && v.css !== null && typeof v.css !== "string") {
|
|
1112
|
+
return `${where}: "css" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1113
|
+
}
|
|
1114
|
+
if (v.value !== undefined &&
|
|
1115
|
+
v.value !== null &&
|
|
1116
|
+
(typeof v.value !== "number" || !Number.isFinite(v.value))) {
|
|
1117
|
+
return `${where}: "value" must be a finite number. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1118
|
+
}
|
|
1119
|
+
if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
|
|
1120
|
+
return `${where}: "unit" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1121
|
+
}
|
|
1122
|
+
const unknownKey = Object.keys(v).find((k) => k !== "css" && k !== "value" && k !== "unit");
|
|
1123
|
+
if (unknownKey) {
|
|
1124
|
+
return `${where}: unknown field "${unknownKey}". A size/style default allows only "css", "value", "unit". ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1125
|
+
}
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
// Validates a TYPE prop defaultValue — but ONLY for the size/style types whose shape we know
|
|
1129
|
+
// ({ css?, value?, unit? }). Any other TYPE typeId (color/border/box-shadow/grid/string-union
|
|
1130
|
+
// styles, IkasProduct, custom types) has a different value shape we can't verify offline, so its
|
|
1131
|
+
// default is passed through unchecked. The generator only emits a default for these size/style
|
|
1132
|
+
// types anyway. Returns an error message, or null if valid / not a size-style type.
|
|
1133
|
+
export function validateTypeDefaultValue(value, propName, typeId) {
|
|
1134
|
+
if (value === undefined || value === null)
|
|
1135
|
+
return null;
|
|
1136
|
+
if (typeof typeId !== "string" || !typeId.startsWith(BP_STOREFRONT_PREFIX)) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
// Strip the prefix and an optional _array suffix to get the base type name.
|
|
1140
|
+
const withoutPrefix = typeId.slice(BP_STOREFRONT_PREFIX.length);
|
|
1141
|
+
const isArray = withoutPrefix.endsWith("_array");
|
|
1142
|
+
const baseName = isArray
|
|
1143
|
+
? withoutPrefix.slice(0, -"_array".length)
|
|
1144
|
+
: withoutPrefix;
|
|
1145
|
+
if (!SIZE_STYLE_TYPE_NAMES.includes(baseName))
|
|
1146
|
+
return null;
|
|
1147
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
1148
|
+
if (isArray) {
|
|
1149
|
+
if (!Array.isArray(value)) {
|
|
1150
|
+
return `${where} must be an array for an _array TYPE prop: [ { "value": …, "unit": … }, … ]. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1151
|
+
}
|
|
1152
|
+
return firstError(value, (el, i) => styleDefaultObjectError(el, `${where}[${i}]`));
|
|
1153
|
+
}
|
|
1154
|
+
return styleDefaultObjectError(value, where);
|
|
1155
|
+
}
|
|
1156
|
+
async function updateProp(ref, options) {
|
|
538
1157
|
const { config, configPath } = loadConfig();
|
|
539
1158
|
const component = resolveComponent(config, ref);
|
|
540
1159
|
const propIndex = component.props.findIndex((p) => p.name === options.prop);
|
|
@@ -566,8 +1185,87 @@ function updateProp(ref, options) {
|
|
|
566
1185
|
if (options.description !== undefined) {
|
|
567
1186
|
prop.description = options.description || undefined;
|
|
568
1187
|
}
|
|
1188
|
+
// Resolve numberRangeData before defaultValue so a NUMBER_RANGE default can be validated
|
|
1189
|
+
// against the (possibly just-updated) slider config in the same call.
|
|
1190
|
+
if (options.numberRangeData !== undefined) {
|
|
1191
|
+
if (options.numberRangeData === "" || options.numberRangeData === "none") {
|
|
1192
|
+
delete prop.numberRangeData;
|
|
1193
|
+
}
|
|
1194
|
+
else {
|
|
1195
|
+
prop.numberRangeData = parseNumberRangeData(options.numberRangeData, prop.name);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
// A NUMBER_RANGE prop must keep a valid slider config (min + max). Enforce the invariant only when
|
|
1199
|
+
// this update touches the type or numberRangeData — a benign update (e.g. displayName) on an
|
|
1200
|
+
// existing prop must still pass. This catches switching a prop TO NUMBER_RANGE without a config, or
|
|
1201
|
+
// clearing/blanking numberRangeData on a NUMBER_RANGE prop.
|
|
1202
|
+
if ((options.type !== undefined || options.numberRangeData !== undefined) &&
|
|
1203
|
+
prop.type === "NUMBER_RANGE" &&
|
|
1204
|
+
(!prop.numberRangeData || !isNumberRangeComplete(prop.numberRangeData))) {
|
|
1205
|
+
console.log(JSON.stringify({
|
|
1206
|
+
success: false,
|
|
1207
|
+
error: numberRangeDataRequiredError(prop.name),
|
|
1208
|
+
}));
|
|
1209
|
+
process.exit(1);
|
|
1210
|
+
}
|
|
1211
|
+
// Resolve filteredComponentIds before defaultValue so a COMPONENT / COMPONENT_LIST default can be
|
|
1212
|
+
// validated against the (possibly just-updated) allowlist in the same call.
|
|
1213
|
+
if (options.filteredComponentIds !== undefined) {
|
|
1214
|
+
if (options.filteredComponentIds === "" ||
|
|
1215
|
+
options.filteredComponentIds === "none") {
|
|
1216
|
+
delete prop.filteredComponentIds;
|
|
1217
|
+
}
|
|
1218
|
+
else {
|
|
1219
|
+
try {
|
|
1220
|
+
const parsed = JSON.parse(options.filteredComponentIds);
|
|
1221
|
+
if (!Array.isArray(parsed)) {
|
|
1222
|
+
console.log(JSON.stringify({
|
|
1223
|
+
success: false,
|
|
1224
|
+
error: "--filteredComponentIds must be a JSON array of strings.",
|
|
1225
|
+
}));
|
|
1226
|
+
process.exit(1);
|
|
1227
|
+
}
|
|
1228
|
+
assertKnownComponentIds(parsed, config);
|
|
1229
|
+
prop.filteredComponentIds = parsed;
|
|
1230
|
+
}
|
|
1231
|
+
catch (err) {
|
|
1232
|
+
if (err instanceof SyntaxError) {
|
|
1233
|
+
console.log(JSON.stringify({
|
|
1234
|
+
success: false,
|
|
1235
|
+
error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
|
|
1236
|
+
}));
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
throw err;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
569
1243
|
if (options.defaultValue !== undefined) {
|
|
570
|
-
prop.
|
|
1244
|
+
rejectDynamicDefault(prop.name, prop.type, options.defaultValue);
|
|
1245
|
+
const parsed = parseDefaultValue(options.defaultValue, prop.type);
|
|
1246
|
+
// Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
|
|
1247
|
+
if (prop.type === "NUMBER") {
|
|
1248
|
+
const numError = validateNumberDefaultValue(options.defaultValue, prop.name);
|
|
1249
|
+
failIf(numError);
|
|
1250
|
+
}
|
|
1251
|
+
if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
|
|
1252
|
+
const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
|
|
1253
|
+
failIf(linkError);
|
|
1254
|
+
}
|
|
1255
|
+
if ((prop.type === "COMPONENT" || prop.type === "COMPONENT_LIST") &&
|
|
1256
|
+
parsed !== undefined) {
|
|
1257
|
+
failIf(validateComponentDefaultValue(prop.type, parsed, prop.name, prop.filteredComponentIds, existingComponentIdSet(config)));
|
|
1258
|
+
}
|
|
1259
|
+
if (prop.type === "NUMBER_RANGE") {
|
|
1260
|
+
const nrError = validateNumberRangeDefaultValue(parsed, prop.numberRangeData, prop.name);
|
|
1261
|
+
failIf(nrError);
|
|
1262
|
+
}
|
|
1263
|
+
if (prop.type === "TYPE") {
|
|
1264
|
+
// Prefer a typeId supplied in this same update over the stored one.
|
|
1265
|
+
const typeError = validateTypeDefaultValue(parsed, prop.name, options.typeId ?? prop.typeId);
|
|
1266
|
+
failIf(typeError);
|
|
1267
|
+
}
|
|
1268
|
+
prop.defaultValue = parsed;
|
|
571
1269
|
}
|
|
572
1270
|
if (options.group !== undefined) {
|
|
573
1271
|
if (options.group === "" || options.group === "none") {
|
|
@@ -600,6 +1298,13 @@ function updateProp(ref, options) {
|
|
|
600
1298
|
prop.typeId = options.typeId;
|
|
601
1299
|
}
|
|
602
1300
|
}
|
|
1301
|
+
// Reject section-disallowed types when the caller changes type/typeId;
|
|
1302
|
+
// untouched legacy props stay editable (displayName, group, ...).
|
|
1303
|
+
if ((options.type !== undefined || options.typeId !== undefined) &&
|
|
1304
|
+
prop.type === "TYPE" &&
|
|
1305
|
+
prop.typeId) {
|
|
1306
|
+
await assertTypeIdsUsable(component.type, [prop.typeId]);
|
|
1307
|
+
}
|
|
603
1308
|
if (options.enumTypeId !== undefined) {
|
|
604
1309
|
if (options.enumTypeId === "" || options.enumTypeId === "none") {
|
|
605
1310
|
delete prop.enumTypeId;
|
|
@@ -608,29 +1313,6 @@ function updateProp(ref, options) {
|
|
|
608
1313
|
prop.enumTypeId = options.enumTypeId;
|
|
609
1314
|
}
|
|
610
1315
|
}
|
|
611
|
-
if (options.filteredComponentIds !== undefined) {
|
|
612
|
-
if (options.filteredComponentIds === "" || options.filteredComponentIds === "none") {
|
|
613
|
-
delete prop.filteredComponentIds;
|
|
614
|
-
}
|
|
615
|
-
else {
|
|
616
|
-
try {
|
|
617
|
-
const parsed = JSON.parse(options.filteredComponentIds);
|
|
618
|
-
if (!Array.isArray(parsed)) {
|
|
619
|
-
console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
|
|
620
|
-
process.exit(1);
|
|
621
|
-
}
|
|
622
|
-
assertKnownComponentIds(parsed, config);
|
|
623
|
-
prop.filteredComponentIds = parsed;
|
|
624
|
-
}
|
|
625
|
-
catch (err) {
|
|
626
|
-
if (err instanceof SyntaxError) {
|
|
627
|
-
console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
|
|
628
|
-
process.exit(1);
|
|
629
|
-
}
|
|
630
|
-
throw err;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
1316
|
if (options.privateVarMap !== undefined) {
|
|
635
1317
|
if (options.privateVarMap === "" || options.privateVarMap === "none") {
|
|
636
1318
|
delete prop.privateVarMap;
|
|
@@ -639,13 +1321,19 @@ function updateProp(ref, options) {
|
|
|
639
1321
|
try {
|
|
640
1322
|
const parsed = JSON.parse(options.privateVarMap);
|
|
641
1323
|
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
642
|
-
console.log(JSON.stringify({
|
|
1324
|
+
console.log(JSON.stringify({
|
|
1325
|
+
success: false,
|
|
1326
|
+
error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
|
|
1327
|
+
}));
|
|
643
1328
|
process.exit(1);
|
|
644
1329
|
}
|
|
645
1330
|
prop.privateVarMap = parsed;
|
|
646
1331
|
}
|
|
647
1332
|
catch {
|
|
648
|
-
console.log(JSON.stringify({
|
|
1333
|
+
console.log(JSON.stringify({
|
|
1334
|
+
success: false,
|
|
1335
|
+
error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
|
|
1336
|
+
}));
|
|
649
1337
|
process.exit(1);
|
|
650
1338
|
}
|
|
651
1339
|
}
|
|
@@ -665,8 +1353,13 @@ function updateProp(ref, options) {
|
|
|
665
1353
|
...(prop.groupId ? { groupId: prop.groupId } : {}),
|
|
666
1354
|
...(prop.typeId ? { typeId: prop.typeId } : {}),
|
|
667
1355
|
...(prop.enumTypeId ? { enumTypeId: prop.enumTypeId } : {}),
|
|
668
|
-
...(prop.filteredComponentIds
|
|
1356
|
+
...(prop.filteredComponentIds
|
|
1357
|
+
? { filteredComponentIds: prop.filteredComponentIds }
|
|
1358
|
+
: {}),
|
|
669
1359
|
...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
|
|
1360
|
+
...(prop.numberRangeData
|
|
1361
|
+
? { numberRangeData: prop.numberRangeData }
|
|
1362
|
+
: {}),
|
|
670
1363
|
},
|
|
671
1364
|
}));
|
|
672
1365
|
}
|
|
@@ -729,7 +1422,10 @@ function removeComponent(ref) {
|
|
|
729
1422
|
removedComponentId: component.id,
|
|
730
1423
|
removedComponentName: component.name,
|
|
731
1424
|
removedDirectory: path.relative(process.cwd(), componentDir),
|
|
732
|
-
remainingComponents: config.components.map((c) => ({
|
|
1425
|
+
remainingComponents: config.components.map((c) => ({
|
|
1426
|
+
id: c.id,
|
|
1427
|
+
name: c.name,
|
|
1428
|
+
})),
|
|
733
1429
|
}));
|
|
734
1430
|
}
|
|
735
1431
|
function addPropGroup(ref, options) {
|
|
@@ -836,10 +1532,10 @@ function removePropGroup(ref, options) {
|
|
|
836
1532
|
process.exit(1);
|
|
837
1533
|
}
|
|
838
1534
|
if (found.parent) {
|
|
839
|
-
found.parent.children = found.parent.children.filter(c => c.id !== options.id);
|
|
1535
|
+
found.parent.children = found.parent.children.filter((c) => c.id !== options.id);
|
|
840
1536
|
}
|
|
841
1537
|
else {
|
|
842
|
-
component.propGroups = component.propGroups.filter(g => g.id !== options.id);
|
|
1538
|
+
component.propGroups = component.propGroups.filter((g) => g.id !== options.id);
|
|
843
1539
|
}
|
|
844
1540
|
// Clear groupId from props referencing the deleted group
|
|
845
1541
|
for (const prop of component.props) {
|
|
@@ -852,7 +1548,7 @@ function removePropGroup(ref, options) {
|
|
|
852
1548
|
success: true,
|
|
853
1549
|
componentName: component.name,
|
|
854
1550
|
removedPropGroup: options.id,
|
|
855
|
-
remainingPropGroups: component.propGroups.map(g => g.id),
|
|
1551
|
+
remainingPropGroups: component.propGroups.map((g) => g.id),
|
|
856
1552
|
}));
|
|
857
1553
|
}
|
|
858
1554
|
function movePropGroup(ref, options) {
|
|
@@ -869,7 +1565,10 @@ function movePropGroup(ref, options) {
|
|
|
869
1565
|
component.propGroups = movePropGroupInTree(component.propGroups, options.id, options.parent || null, options.index);
|
|
870
1566
|
}
|
|
871
1567
|
catch (e) {
|
|
872
|
-
console.log(JSON.stringify({
|
|
1568
|
+
console.log(JSON.stringify({
|
|
1569
|
+
success: false,
|
|
1570
|
+
error: e.message || "Failed to move prop group",
|
|
1571
|
+
}));
|
|
873
1572
|
process.exit(1);
|
|
874
1573
|
}
|
|
875
1574
|
saveConfig(configPath, config);
|
|
@@ -925,7 +1624,9 @@ function listComponents() {
|
|
|
925
1624
|
required: p.required,
|
|
926
1625
|
...(p.groupId ? { groupId: p.groupId } : {}),
|
|
927
1626
|
})),
|
|
928
|
-
...(c.propGroups && c.propGroups.length > 0
|
|
1627
|
+
...(c.propGroups && c.propGroups.length > 0
|
|
1628
|
+
? { propGroups: c.propGroups }
|
|
1629
|
+
: {}),
|
|
929
1630
|
}));
|
|
930
1631
|
console.log(JSON.stringify({
|
|
931
1632
|
success: true,
|
|
@@ -939,7 +1640,9 @@ function listComponents() {
|
|
|
939
1640
|
async function addEnum(options) {
|
|
940
1641
|
const { config, configPath } = loadConfig();
|
|
941
1642
|
// Validate name
|
|
942
|
-
if (!options.name ||
|
|
1643
|
+
if (!options.name ||
|
|
1644
|
+
typeof options.name !== "string" ||
|
|
1645
|
+
!options.name.trim()) {
|
|
943
1646
|
console.log(JSON.stringify({ success: false, error: "Enum name is required." }));
|
|
944
1647
|
process.exit(1);
|
|
945
1648
|
}
|
|
@@ -1053,7 +1756,7 @@ function updateEnum(options) {
|
|
|
1053
1756
|
Object.keys(enumOptions).length === 0) {
|
|
1054
1757
|
console.log(JSON.stringify({
|
|
1055
1758
|
success: false,
|
|
1056
|
-
error:
|
|
1759
|
+
error: "Options must be a non-empty JSON object.",
|
|
1057
1760
|
}));
|
|
1058
1761
|
process.exit(1);
|
|
1059
1762
|
}
|
|
@@ -1126,8 +1829,7 @@ function listEnums() {
|
|
|
1126
1829
|
}
|
|
1127
1830
|
// --- Command registration ---
|
|
1128
1831
|
export function createConfigCommand() {
|
|
1129
|
-
const config = new Command("config")
|
|
1130
|
-
.description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
|
|
1832
|
+
const config = new Command("config").description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
|
|
1131
1833
|
config
|
|
1132
1834
|
.command("add-component")
|
|
1133
1835
|
.description("Add a new component to the project")
|
|
@@ -1136,7 +1838,7 @@ export function createConfigCommand() {
|
|
|
1136
1838
|
.option("--isHeader", "Mark this section as the store header (only for type: section)")
|
|
1137
1839
|
.option("--isFooter", "Mark this section as the store footer (only for type: section)")
|
|
1138
1840
|
.option("--props <json>", "JSON array of props. Required per entry: name, type. Optional: displayName (auto from name), required, description, defaultValue, groupId, typeId (TYPE props), enumTypeId (ENUM props), filteredComponentIds, privateVarMap. " +
|
|
1139
|
-
|
|
1841
|
+
'Example: \'[{"name":"title","type":"TEXT","required":true,"defaultValue":"Hello","groupId":"basic"}]\'')
|
|
1140
1842
|
.action((options) => {
|
|
1141
1843
|
addComponent(options.name, options);
|
|
1142
1844
|
});
|
|
@@ -1155,7 +1857,8 @@ export function createConfigCommand() {
|
|
|
1155
1857
|
.option("--typeId <typeId>", "Type ID for TYPE props (required when type is TYPE)")
|
|
1156
1858
|
.option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
|
|
1157
1859
|
.option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
|
|
1158
|
-
.option("--privateVarMap <json>",
|
|
1860
|
+
.option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)")
|
|
1861
|
+
.option("--numberRangeData <json>", 'JSON slider config for NUMBER_RANGE props, e.g. \'{"min":0,"max":100,"interval":5,"unit":"px"}\'')
|
|
1159
1862
|
.action((options) => {
|
|
1160
1863
|
addProp({ id: options.componentId, name: options.component }, options);
|
|
1161
1864
|
});
|
|
@@ -1175,6 +1878,7 @@ export function createConfigCommand() {
|
|
|
1175
1878
|
.option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (use 'none' to clear)")
|
|
1176
1879
|
.option("--filteredComponentIds <json>", "JSON array of component IDs (use 'none' to clear)")
|
|
1177
1880
|
.option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (use 'none' to clear)")
|
|
1881
|
+
.option("--numberRangeData <json>", "JSON slider config for NUMBER_RANGE props (use 'none' to clear)")
|
|
1178
1882
|
.action((options) => {
|
|
1179
1883
|
updateProp({ id: options.componentId, name: options.component }, options);
|
|
1180
1884
|
});
|
|
@@ -1246,7 +1950,7 @@ export function createConfigCommand() {
|
|
|
1246
1950
|
.option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1247
1951
|
.requiredOption("--id <id>", "Group ID to move")
|
|
1248
1952
|
.option("--parent <parentId>", "Target parent group ID (omit to move to root)")
|
|
1249
|
-
.option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", v => parseInt(v, 10))
|
|
1953
|
+
.option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", (v) => parseInt(v, 10))
|
|
1250
1954
|
.action((options) => {
|
|
1251
1955
|
movePropGroup({ id: options.componentId, name: options.component }, options);
|
|
1252
1956
|
});
|
|
@@ -1306,54 +2010,15 @@ export function createConfigCommand() {
|
|
|
1306
2010
|
* Returns the matching type (with enumOptions) if found, null otherwise.
|
|
1307
2011
|
*/
|
|
1308
2012
|
async function fetchLiveEditorEnumById(enumId) {
|
|
1309
|
-
const
|
|
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;
|
|
2013
|
+
const types = await fetchLiveEditorTypes();
|
|
2014
|
+
const match = types?.find((t) => t.category === "enum" && t.id === enumId);
|
|
2015
|
+
return match
|
|
2016
|
+
? {
|
|
2017
|
+
id: match.id,
|
|
2018
|
+
name: match.name,
|
|
2019
|
+
...(match.enumOptions ? { enumOptions: match.enumOptions } : {}),
|
|
1333
2020
|
}
|
|
1334
|
-
|
|
1335
|
-
ws.on("open", () => {
|
|
1336
|
-
ws.send(JSON.stringify({ type: "request-types" }));
|
|
1337
|
-
});
|
|
1338
|
-
ws.on("message", (data) => {
|
|
1339
|
-
try {
|
|
1340
|
-
const message = JSON.parse(data.toString());
|
|
1341
|
-
if (message.type === "types-list") {
|
|
1342
|
-
clearTimeout(timeout);
|
|
1343
|
-
const types = message.payload?.types || [];
|
|
1344
|
-
const match = types.find((t) => t.category === "enum" && t.id === enumId);
|
|
1345
|
-
finish(match ? { id: match.id, name: match.name, ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}) } : null);
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
catch {
|
|
1349
|
-
// Ignore non-JSON messages
|
|
1350
|
-
}
|
|
1351
|
-
});
|
|
1352
|
-
ws.on("error", () => {
|
|
1353
|
-
clearTimeout(timeout);
|
|
1354
|
-
finish(null);
|
|
1355
|
-
});
|
|
1356
|
-
});
|
|
2021
|
+
: null;
|
|
1357
2022
|
}
|
|
1358
2023
|
/**
|
|
1359
2024
|
* Best-effort lookup of an enum by PascalCase name in the live editor session.
|
|
@@ -1363,6 +2028,16 @@ async function fetchLiveEditorEnumById(enumId) {
|
|
|
1363
2028
|
* must treat null as "could not verify, fall back to on-disk check only".
|
|
1364
2029
|
*/
|
|
1365
2030
|
async function fetchLiveEditorEnumByName(pascalName) {
|
|
2031
|
+
const types = await fetchLiveEditorTypes();
|
|
2032
|
+
const match = types?.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
|
|
2033
|
+
return match ? { id: match.id, name: match.name } : null;
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Best-effort fetch of the editor's synced type list (with category /
|
|
2037
|
+
* sectionAllowed / enumOptions metadata). Returns null on any failure (no dev
|
|
2038
|
+
* server, timeout) — callers must treat null as "could not verify, allow".
|
|
2039
|
+
*/
|
|
2040
|
+
async function fetchLiveEditorTypes() {
|
|
1366
2041
|
const { WebSocket } = await import("ws");
|
|
1367
2042
|
const WS_URL = "ws://localhost:5201";
|
|
1368
2043
|
const TIMEOUT_MS = 1500;
|
|
@@ -1397,9 +2072,7 @@ async function fetchLiveEditorEnumByName(pascalName) {
|
|
|
1397
2072
|
const message = JSON.parse(data.toString());
|
|
1398
2073
|
if (message.type === "types-list") {
|
|
1399
2074
|
clearTimeout(timeout);
|
|
1400
|
-
|
|
1401
|
-
const match = types.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
|
|
1402
|
-
finish(match ? { id: match.id, name: match.name } : null);
|
|
2075
|
+
finish(message.payload?.types || []);
|
|
1403
2076
|
}
|
|
1404
2077
|
}
|
|
1405
2078
|
catch {
|
|
@@ -1412,6 +2085,47 @@ async function fetchLiveEditorEnumByName(pascalName) {
|
|
|
1412
2085
|
});
|
|
1413
2086
|
});
|
|
1414
2087
|
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Definition-time guard for TYPE-prop typeIds, verified against the live
|
|
2090
|
+
* editor's type list (skipped when the dev server is unreachable or the type
|
|
2091
|
+
* is unknown to the editor). Two rules:
|
|
2092
|
+
* - deprecated types are rejected on ANY component: they are legacy
|
|
2093
|
+
* render-only types — old themes may still carry values, but new props must
|
|
2094
|
+
* not be defined with them.
|
|
2095
|
+
* - on section components, only section-allowed (style) types pass — any other
|
|
2096
|
+
* type renders an empty placeholder merchants cannot fill, and section-level
|
|
2097
|
+
* bindings cannot supply a value either, so the prop would be dead weight.
|
|
2098
|
+
* On violation prints the standard { success: false } envelope and exits.
|
|
2099
|
+
*/
|
|
2100
|
+
async function assertTypeIdsUsable(componentType, typeIds) {
|
|
2101
|
+
if (typeIds.length === 0)
|
|
2102
|
+
return;
|
|
2103
|
+
const types = await fetchLiveEditorTypes();
|
|
2104
|
+
if (!types)
|
|
2105
|
+
return;
|
|
2106
|
+
const matches = typeIds.flatMap((typeId) => {
|
|
2107
|
+
const match = types.find((t) => t.id === typeId);
|
|
2108
|
+
return match ? [match] : [];
|
|
2109
|
+
});
|
|
2110
|
+
const legacy = matches.find((t) => t.deprecated === true);
|
|
2111
|
+
if (legacy) {
|
|
2112
|
+
console.log(JSON.stringify({
|
|
2113
|
+
success: false,
|
|
2114
|
+
error: `Type "${legacy.id}" is a legacy render-only type — values of it may still exist in old themes, but NEW props must not use it. Pick a current style type instead (run "config list-types" to see them).`,
|
|
2115
|
+
}));
|
|
2116
|
+
process.exit(1);
|
|
2117
|
+
}
|
|
2118
|
+
if (componentType !== "section")
|
|
2119
|
+
return;
|
|
2120
|
+
const disallowed = matches.find((t) => t.sectionAllowed === false);
|
|
2121
|
+
if (!disallowed)
|
|
2122
|
+
return;
|
|
2123
|
+
console.log(JSON.stringify({
|
|
2124
|
+
success: false,
|
|
2125
|
+
error: `Type "${disallowed.id}" is not allowed for TYPE props on section components — merchants cannot fill it in the editor (sections only support style types). Run "config list-types --component-type section" to see allowed types, or use a dedicated prop type (e.g. PRODUCT, CATEGORY) for domain data.`,
|
|
2126
|
+
}));
|
|
2127
|
+
process.exit(1);
|
|
2128
|
+
}
|
|
1415
2129
|
async function listTypes(componentType) {
|
|
1416
2130
|
const { WebSocket } = await import("ws");
|
|
1417
2131
|
const WS_URL = "ws://localhost:5201";
|
|
@@ -1445,7 +2159,8 @@ async function listTypes(componentType) {
|
|
|
1445
2159
|
const message = JSON.parse(data.toString());
|
|
1446
2160
|
if (message.type === "types-list") {
|
|
1447
2161
|
clearTimeout(timeout);
|
|
1448
|
-
|
|
2162
|
+
// Legacy render-only types are never offered for new props.
|
|
2163
|
+
let types = message.payload.types.filter((t) => !t.deprecated);
|
|
1449
2164
|
if (componentType === "section") {
|
|
1450
2165
|
types = types.filter((t) => t.sectionAllowed);
|
|
1451
2166
|
}
|