@ikas/component-cli 1.4.0-beta.19 → 1.4.0-beta.191
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 +37 -0
- package/dist/build-tools.d.ts.map +1 -0
- package/dist/build-tools.js +36 -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 +11 -2
- 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 +1021 -122
- 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-page.d.ts +3 -0
- package/dist/commands/delete-page.d.ts.map +1 -0
- package/dist/commands/delete-page.js +28 -0
- package/dist/commands/delete-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 +170 -31
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/enable-builtin-pages.d.ts +3 -0
- package/dist/commands/enable-builtin-pages.d.ts.map +1 -0
- package/dist/commands/enable-builtin-pages.js +27 -0
- package/dist/commands/enable-builtin-pages.js.map +1 -0
- 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-builtin-pages.d.ts +3 -0
- package/dist/commands/list-builtin-pages.d.ts.map +1 -0
- package/dist/commands/list-builtin-pages.js +28 -0
- package/dist/commands/list-builtin-pages.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.map +1 -1
- package/dist/commands/list-page-sections.js +8 -2
- package/dist/commands/list-page-sections.js.map +1 -1
- 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/move-page-section.d.ts +3 -0
- package/dist/commands/move-page-section.d.ts.map +1 -0
- package/dist/commands/move-page-section.js +62 -0
- package/dist/commands/move-page-section.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 +54 -3
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/remove-page-section.d.ts +3 -0
- package/dist/commands/remove-page-section.d.ts.map +1 -0
- package/dist/commands/remove-page-section.js +40 -0
- package/dist/commands/remove-page-section.js.map +1 -0
- 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-builtin-page-prop.d.ts +3 -0
- package/dist/commands/update-builtin-page-prop.d.ts.map +1 -0
- package/dist/commands/update-builtin-page-prop.js +49 -0
- package/dist/commands/update-builtin-page-prop.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.map +1 -1
- package/dist/commands/update-section-prop.js +27 -9
- package/dist/commands/update-section-prop.js.map +1 -1
- package/dist/commands/upload-image.d.ts.map +1 -1
- package/dist/commands/upload-image.js +5 -46
- package/dist/commands/upload-image.js.map +1 -1
- 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 +54 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/compile.d.ts +17 -2
- package/dist/utils/compile.d.ts.map +1 -1
- package/dist/utils/compile.js +746 -117
- package/dist/utils/compile.js.map +1 -1
- package/dist/utils/component-helpers.d.ts +128 -2
- package/dist/utils/component-helpers.d.ts.map +1 -1
- package/dist/utils/component-helpers.js +330 -27
- package/dist/utils/component-helpers.js.map +1 -1
- package/dist/utils/css-import-resolver.d.ts +4 -1
- package/dist/utils/css-import-resolver.d.ts.map +1 -1
- package/dist/utils/css-import-resolver.js +9 -1
- package/dist/utils/css-import-resolver.js.map +1 -1
- package/dist/utils/editor-action-client.d.ts +4 -3
- package/dist/utils/editor-action-client.d.ts.map +1 -1
- package/dist/utils/editor-action-client.js +55 -19
- package/dist/utils/editor-action-client.js.map +1 -1
- package/dist/utils/esm-transform.d.ts +30 -0
- package/dist/utils/esm-transform.d.ts.map +1 -1
- package/dist/utils/esm-transform.js +169 -2
- package/dist/utils/esm-transform.js.map +1 -1
- package/dist/utils/find-component-for-file.d.ts +12 -0
- package/dist/utils/find-component-for-file.d.ts.map +1 -0
- package/dist/utils/find-component-for-file.js +24 -0
- package/dist/utils/find-component-for-file.js.map +1 -0
- 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 +26 -0
- package/dist/utils/forbid-external-packages.d.ts.map +1 -0
- package/dist/utils/forbid-external-packages.js +99 -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/vendor-storefront.d.ts +13 -0
- package/dist/utils/vendor-storefront.d.ts.map +1 -0
- package/dist/utils/vendor-storefront.js +91 -0
- package/dist/utils/vendor-storefront.js.map +1 -0
- package/dist/utils/websocket-server.d.ts +88 -5
- package/dist/utils/websocket-server.d.ts.map +1 -1
- package/dist/utils/websocket-server.js +233 -43
- package/dist/utils/websocket-server.js.map +1 -1
- package/package.json +17 -2
- package/templates/add/section.css +11 -0
- package/dist/commands/create.d.ts +0 -4
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js +0 -228
- 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 -210
- 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, visibilityGateError, } 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,113 @@ const ALLOWED_PROP_FIELDS = new Set([
|
|
|
156
167
|
"enumTypeId",
|
|
157
168
|
"filteredComponentIds",
|
|
158
169
|
"privateVarMap",
|
|
170
|
+
"numberRangeData",
|
|
171
|
+
"requiredProp",
|
|
172
|
+
"requiredPropValue",
|
|
173
|
+
"requiredEnumOptionId",
|
|
174
|
+
"requiredEnumOptionValue",
|
|
159
175
|
]);
|
|
176
|
+
// Emit the standard CLI error envelope and exit(1) when a validator returned a message; a no-op
|
|
177
|
+
// when the message is null. Collapses the repeated `if (err) { console.log(...); process.exit(1) }`
|
|
178
|
+
// tail that follows every default-value validator call.
|
|
179
|
+
function failIf(error) {
|
|
180
|
+
if (error) {
|
|
181
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* A `--requiredPropValue` flag as a boolean. Commander hands every flag over as a string, and this is
|
|
187
|
+
* the one place that turns it back; anything other than true/false is refused rather than coerced,
|
|
188
|
+
* since "0" or "off" silently becoming `true` is exactly the kind of gate nobody would think to
|
|
189
|
+
* check.
|
|
190
|
+
*/
|
|
191
|
+
function parseRequiredPropValue(raw, where) {
|
|
192
|
+
if (raw === undefined)
|
|
193
|
+
return undefined;
|
|
194
|
+
if (typeof raw === "boolean")
|
|
195
|
+
return raw;
|
|
196
|
+
if (raw === "true")
|
|
197
|
+
return true;
|
|
198
|
+
if (raw === "false")
|
|
199
|
+
return false;
|
|
200
|
+
failIf(`--requiredPropValue for ${where} must be "true" or "false" (got "${raw}").`);
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Validate/sanitize a numberRangeData object for a NUMBER_RANGE prop. Accepts an object
|
|
205
|
+
* or a JSON string. min/max/interval are truncated to integers; only a positive interval
|
|
206
|
+
* is kept (0/negative would yield an invalid slider step). Exits with a structured error
|
|
207
|
+
* on malformed input.
|
|
208
|
+
*/
|
|
209
|
+
function parseNumberRangeData(raw, propName) {
|
|
210
|
+
const fail = (error) => {
|
|
211
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
};
|
|
214
|
+
let obj = raw;
|
|
215
|
+
if (typeof raw === "string") {
|
|
216
|
+
try {
|
|
217
|
+
obj = JSON.parse(raw);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
fail(`Invalid --numberRangeData JSON for prop "${propName}": ${raw}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
224
|
+
fail(`numberRangeData for prop "${propName}" must be an object { min, max, interval?, unit? }.`);
|
|
225
|
+
}
|
|
226
|
+
const o = obj;
|
|
227
|
+
const asInt = (v, field) => {
|
|
228
|
+
if (v === undefined || v === null)
|
|
229
|
+
return undefined;
|
|
230
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
231
|
+
fail(`numberRangeData.${field} for prop "${propName}" must be a finite number.`);
|
|
232
|
+
}
|
|
233
|
+
return Math.trunc(v);
|
|
234
|
+
};
|
|
235
|
+
const min = asInt(o.min, "min");
|
|
236
|
+
const max = asInt(o.max, "max");
|
|
237
|
+
const interval = asInt(o.interval, "interval");
|
|
238
|
+
const unit = typeof o.unit === "string" && o.unit.trim() ? o.unit.trim() : undefined;
|
|
239
|
+
if (min !== undefined && max !== undefined && min >= max) {
|
|
240
|
+
fail(`numberRangeData for prop "${propName}": min (${min}) must be less than max (${max}).`);
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
...(min !== undefined ? { min } : {}),
|
|
244
|
+
...(max !== undefined ? { max } : {}),
|
|
245
|
+
...(interval !== undefined && interval > 0 ? { interval } : {}),
|
|
246
|
+
...(unit ? { unit } : {}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const numberRangeDataRequiredError = (propName) => `numberRangeData with min and max is required for NUMBER_RANGE prop "${propName}". ` +
|
|
250
|
+
`Provide it, e.g. --numberRangeData '{"min":0,"max":100,"interval":5,"unit":"px"}' ` +
|
|
251
|
+
`(or a "numberRangeData" field inside --props). interval and unit are optional.`;
|
|
252
|
+
// A NUMBER_RANGE slider config is complete only with both bounds; interval/unit stay optional.
|
|
253
|
+
export const isNumberRangeComplete = (data) => data.min !== undefined && data.max !== undefined;
|
|
254
|
+
/**
|
|
255
|
+
* A NUMBER_RANGE prop must carry a slider config with at least min and max — the editor UI marks
|
|
256
|
+
* Minimum/Maksimum Değer as required, so a prop authored without them yields a half-configured
|
|
257
|
+
* slider. Enforce it at creation time: reject when numberRangeData is absent or missing min/max.
|
|
258
|
+
*/
|
|
259
|
+
export function requireNumberRangeData(raw, propName) {
|
|
260
|
+
if (raw === undefined) {
|
|
261
|
+
console.log(JSON.stringify({
|
|
262
|
+
success: false,
|
|
263
|
+
error: numberRangeDataRequiredError(propName),
|
|
264
|
+
}));
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
const parsed = parseNumberRangeData(raw, propName);
|
|
268
|
+
if (!isNumberRangeComplete(parsed)) {
|
|
269
|
+
console.log(JSON.stringify({
|
|
270
|
+
success: false,
|
|
271
|
+
error: numberRangeDataRequiredError(propName),
|
|
272
|
+
}));
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
return parsed;
|
|
276
|
+
}
|
|
160
277
|
/**
|
|
161
278
|
* Parse and validate the --props JSON flag for add-component.
|
|
162
279
|
* Returns validated ComponentProp[] or exits with error.
|
|
@@ -182,6 +299,8 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
182
299
|
}
|
|
183
300
|
const seenNames = new Set();
|
|
184
301
|
const props = [];
|
|
302
|
+
// Loop-invariant: build the known-id set once for the whole batch, not per prop.
|
|
303
|
+
const existingComponentIds = existingComponentIdSet(config);
|
|
185
304
|
for (const raw of rawProps) {
|
|
186
305
|
const r = raw;
|
|
187
306
|
const unknownFields = Object.keys(r).filter((k) => !ALLOWED_PROP_FIELDS.has(k));
|
|
@@ -234,6 +353,36 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
234
353
|
}));
|
|
235
354
|
process.exit(1);
|
|
236
355
|
}
|
|
356
|
+
// Dynamic (merchant-data reference) props can't carry a static default.
|
|
357
|
+
rejectDynamicDefault(r.name, propType, r.defaultValue);
|
|
358
|
+
// Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
|
|
359
|
+
if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
|
|
360
|
+
r.defaultValue !== undefined) {
|
|
361
|
+
const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
|
|
362
|
+
failIf(linkError);
|
|
363
|
+
}
|
|
364
|
+
// Validate SVG / SVG_LIST defaultValue (reject malformed/oversized/unsafe SVG)
|
|
365
|
+
if ((propType === "SVG" || propType === "SVG_LIST") &&
|
|
366
|
+
r.defaultValue !== undefined) {
|
|
367
|
+
const svgError = validateSvgDefaultValue(propType, r.defaultValue, r.name);
|
|
368
|
+
failIf(svgError);
|
|
369
|
+
}
|
|
370
|
+
// Validate COMPONENT / COMPONENT_LIST defaultValue (reference-only child code components)
|
|
371
|
+
if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
372
|
+
r.defaultValue !== undefined) {
|
|
373
|
+
failIf(validateComponentDefaultValue(propType, r.defaultValue, r.name, r.filteredComponentIds, existingComponentIds));
|
|
374
|
+
}
|
|
375
|
+
// Validate TYPE (structured/style) defaultValue shape — must be an object (or an array for an
|
|
376
|
+
// _array typeId), never a bare string that would be emitted as a wrong-shaped default.
|
|
377
|
+
if (propType === "TYPE" && r.defaultValue !== undefined) {
|
|
378
|
+
const typeError = validateTypeDefaultValue(r.defaultValue, r.name, r.typeId);
|
|
379
|
+
failIf(typeError);
|
|
380
|
+
}
|
|
381
|
+
// Validate NUMBER defaultValue — reject empty/non-finite (Number("") silently becomes 0).
|
|
382
|
+
if (propType === "NUMBER" && r.defaultValue !== undefined) {
|
|
383
|
+
const numError = validateNumberDefaultValue(r.defaultValue, r.name);
|
|
384
|
+
failIf(numError);
|
|
385
|
+
}
|
|
237
386
|
// Auto-generate displayName if omitted
|
|
238
387
|
const displayName = typeof r.displayName === "string" && r.displayName
|
|
239
388
|
? r.displayName
|
|
@@ -258,7 +407,10 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
258
407
|
process.exit(1);
|
|
259
408
|
}
|
|
260
409
|
// Validate enum type exists (checks config then live dev server)
|
|
261
|
-
if (config &&
|
|
410
|
+
if (config &&
|
|
411
|
+
configPath &&
|
|
412
|
+
typeof r.enumTypeId === "string" &&
|
|
413
|
+
!isBuiltInEnumTypeId(r.enumTypeId)) {
|
|
262
414
|
const resolved = await resolveEnumType(r.enumTypeId, config, configPath);
|
|
263
415
|
if (!resolved) {
|
|
264
416
|
console.log(JSON.stringify({
|
|
@@ -277,9 +429,7 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
277
429
|
...(typeof r.description === "string" && r.description
|
|
278
430
|
? { description: r.description }
|
|
279
431
|
: {}),
|
|
280
|
-
...(r.defaultValue !== undefined
|
|
281
|
-
? { defaultValue: r.defaultValue }
|
|
282
|
-
: {}),
|
|
432
|
+
...(r.defaultValue !== undefined ? { defaultValue: r.defaultValue } : {}),
|
|
283
433
|
...(typeof r.groupId === "string" && r.groupId
|
|
284
434
|
? { groupId: r.groupId }
|
|
285
435
|
: {}),
|
|
@@ -289,15 +439,47 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
289
439
|
...(propType === "ENUM" && typeof r.enumTypeId === "string"
|
|
290
440
|
? { enumTypeId: r.enumTypeId }
|
|
291
441
|
: {}),
|
|
292
|
-
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
293
|
-
|
|
442
|
+
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
443
|
+
r.filteredComponentIds &&
|
|
444
|
+
Array.isArray(r.filteredComponentIds)
|
|
445
|
+
? {
|
|
446
|
+
filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config),
|
|
447
|
+
}
|
|
294
448
|
: {}),
|
|
295
|
-
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
296
|
-
|
|
449
|
+
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
450
|
+
r.privateVarMap &&
|
|
451
|
+
typeof r.privateVarMap === "object"
|
|
452
|
+
? {
|
|
453
|
+
privateVarMap: r.privateVarMap,
|
|
454
|
+
}
|
|
455
|
+
: {}),
|
|
456
|
+
...(propType === "NUMBER_RANGE"
|
|
457
|
+
? { numberRangeData: requireNumberRangeData(r.numberRangeData, r.name) }
|
|
458
|
+
: {}),
|
|
459
|
+
...(r.requiredProp !== undefined
|
|
460
|
+
? { requiredProp: r.requiredProp }
|
|
461
|
+
: {}),
|
|
462
|
+
...(r.requiredPropValue !== undefined
|
|
463
|
+
? { requiredPropValue: r.requiredPropValue }
|
|
464
|
+
: {}),
|
|
465
|
+
...(r.requiredEnumOptionId !== undefined
|
|
466
|
+
? { requiredEnumOptionId: r.requiredEnumOptionId }
|
|
467
|
+
: {}),
|
|
468
|
+
...(r.requiredEnumOptionValue !== undefined
|
|
469
|
+
? { requiredEnumOptionValue: r.requiredEnumOptionValue }
|
|
297
470
|
: {}),
|
|
298
471
|
};
|
|
299
472
|
props.push(prop);
|
|
300
473
|
}
|
|
474
|
+
// Gates are checked once the whole batch is parsed: a prop may name one declared after it, and
|
|
475
|
+
// cycle detection needs every prop in hand. The batch IS the component's prop list here — the
|
|
476
|
+
// component itself does not exist yet — so it stands in as one.
|
|
477
|
+
const batch = { name: "the new component", props };
|
|
478
|
+
for (const prop of props) {
|
|
479
|
+
failIf(visibilityGateError(batch, prop, `prop "${prop.name}"`, {
|
|
480
|
+
selfPropName: prop.name,
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
301
483
|
return props;
|
|
302
484
|
}
|
|
303
485
|
// --- Subcommands ---
|
|
@@ -325,11 +507,25 @@ async function addComponent(name, options) {
|
|
|
325
507
|
if (!config.projectId) {
|
|
326
508
|
config.projectId = generateProjectId();
|
|
327
509
|
}
|
|
510
|
+
else if (!isValidProjectId(config.projectId)) {
|
|
511
|
+
// A malformed projectId (hand-edited config) breaks the editor's republish
|
|
512
|
+
// stability: published/local id prefixes stop matching and every publish
|
|
513
|
+
// INSERTs duplicate design elements. Fail fast instead of corrupting data.
|
|
514
|
+
console.log(JSON.stringify({
|
|
515
|
+
success: false,
|
|
516
|
+
error: `Invalid projectId "${config.projectId}" in ikas.config.json. ` +
|
|
517
|
+
"projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
|
|
518
|
+
"Restore the original generated value — do not hand-edit it.",
|
|
519
|
+
}));
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
328
522
|
// Parse --props (and validate filteredComponentIds) BEFORE any filesystem side-effects
|
|
329
523
|
// so a bad payload doesn't leave an empty component directory behind.
|
|
330
524
|
const props = options.props
|
|
331
525
|
? await parsePropsFlag(options.props, config, configPath)
|
|
332
526
|
: [];
|
|
527
|
+
// Reject section-disallowed TYPE props at definition time (best-effort, needs dev server)
|
|
528
|
+
await assertTypeIdsUsable(componentType, props.flatMap((p) => (p.type === "TYPE" && p.typeId ? [p.typeId] : [])));
|
|
333
529
|
const componentId = generateComponentId(config.projectId);
|
|
334
530
|
const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
|
|
335
531
|
// Create directory
|
|
@@ -420,6 +616,10 @@ async function addProp(ref, options) {
|
|
|
420
616
|
}));
|
|
421
617
|
process.exit(1);
|
|
422
618
|
}
|
|
619
|
+
// Reject section-disallowed types at definition time (best-effort, needs dev server)
|
|
620
|
+
if (propType === "TYPE" && options.typeId) {
|
|
621
|
+
await assertTypeIdsUsable(component.type, [options.typeId]);
|
|
622
|
+
}
|
|
423
623
|
// Validate enumTypeId for ENUM props
|
|
424
624
|
if (propType === "ENUM" && !options.enumTypeId) {
|
|
425
625
|
console.log(JSON.stringify({
|
|
@@ -430,7 +630,9 @@ async function addProp(ref, options) {
|
|
|
430
630
|
}
|
|
431
631
|
// Validate that enumTypeId references an existing enum type
|
|
432
632
|
// (checks config.customTypes first, then falls back to the live dev server)
|
|
433
|
-
if (propType === "ENUM" &&
|
|
633
|
+
if (propType === "ENUM" &&
|
|
634
|
+
options.enumTypeId &&
|
|
635
|
+
!isBuiltInEnumTypeId(options.enumTypeId)) {
|
|
434
636
|
const resolved = await resolveEnumType(options.enumTypeId, config, configPath);
|
|
435
637
|
if (!resolved) {
|
|
436
638
|
console.log(JSON.stringify({
|
|
@@ -460,50 +662,117 @@ async function addProp(ref, options) {
|
|
|
460
662
|
}
|
|
461
663
|
// Parse filteredComponentIds JSON for COMPONENT/COMPONENT_LIST
|
|
462
664
|
let parsedFilteredIds;
|
|
463
|
-
if (options.filteredComponentIds &&
|
|
665
|
+
if (options.filteredComponentIds &&
|
|
666
|
+
(propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
|
|
464
667
|
try {
|
|
465
668
|
parsedFilteredIds = JSON.parse(options.filteredComponentIds);
|
|
466
669
|
if (!Array.isArray(parsedFilteredIds)) {
|
|
467
|
-
console.log(JSON.stringify({
|
|
670
|
+
console.log(JSON.stringify({
|
|
671
|
+
success: false,
|
|
672
|
+
error: "--filteredComponentIds must be a JSON array of strings.",
|
|
673
|
+
}));
|
|
468
674
|
process.exit(1);
|
|
469
675
|
}
|
|
470
676
|
}
|
|
471
677
|
catch {
|
|
472
|
-
console.log(JSON.stringify({
|
|
678
|
+
console.log(JSON.stringify({
|
|
679
|
+
success: false,
|
|
680
|
+
error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
|
|
681
|
+
}));
|
|
473
682
|
process.exit(1);
|
|
474
683
|
}
|
|
475
684
|
assertKnownComponentIds(parsedFilteredIds, config);
|
|
476
685
|
}
|
|
477
686
|
// Parse privateVarMap JSON for COMPONENT/COMPONENT_LIST
|
|
478
687
|
let parsedPrivateVarMap;
|
|
479
|
-
if (options.privateVarMap &&
|
|
688
|
+
if (options.privateVarMap &&
|
|
689
|
+
(propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
|
|
480
690
|
try {
|
|
481
691
|
parsedPrivateVarMap = JSON.parse(options.privateVarMap);
|
|
482
|
-
if (typeof parsedPrivateVarMap !== "object" ||
|
|
483
|
-
|
|
692
|
+
if (typeof parsedPrivateVarMap !== "object" ||
|
|
693
|
+
Array.isArray(parsedPrivateVarMap)) {
|
|
694
|
+
console.log(JSON.stringify({
|
|
695
|
+
success: false,
|
|
696
|
+
error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
|
|
697
|
+
}));
|
|
484
698
|
process.exit(1);
|
|
485
699
|
}
|
|
486
700
|
}
|
|
487
701
|
catch {
|
|
488
|
-
console.log(JSON.stringify({
|
|
702
|
+
console.log(JSON.stringify({
|
|
703
|
+
success: false,
|
|
704
|
+
error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
|
|
705
|
+
}));
|
|
489
706
|
process.exit(1);
|
|
490
707
|
}
|
|
491
708
|
}
|
|
709
|
+
rejectDynamicDefault(options.name, propType, options.defaultValue);
|
|
710
|
+
const parsedDefaultValue = options.defaultValue !== undefined
|
|
711
|
+
? parseDefaultValue(options.defaultValue, propType)
|
|
712
|
+
: undefined;
|
|
713
|
+
if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
|
|
714
|
+
parsedDefaultValue !== undefined) {
|
|
715
|
+
const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
|
|
716
|
+
failIf(linkError);
|
|
717
|
+
}
|
|
718
|
+
if ((propType === "SVG" || propType === "SVG_LIST") &&
|
|
719
|
+
parsedDefaultValue !== undefined) {
|
|
720
|
+
const svgError = validateSvgDefaultValue(propType, parsedDefaultValue, options.name);
|
|
721
|
+
failIf(svgError);
|
|
722
|
+
}
|
|
723
|
+
if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
724
|
+
parsedDefaultValue !== undefined) {
|
|
725
|
+
failIf(validateComponentDefaultValue(propType, parsedDefaultValue, options.name, parsedFilteredIds, existingComponentIdSet(config)));
|
|
726
|
+
}
|
|
727
|
+
if (propType === "TYPE" && parsedDefaultValue !== undefined) {
|
|
728
|
+
const typeError = validateTypeDefaultValue(parsedDefaultValue, options.name, options.typeId);
|
|
729
|
+
failIf(typeError);
|
|
730
|
+
}
|
|
731
|
+
// Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
|
|
732
|
+
if (propType === "NUMBER" && options.defaultValue !== undefined) {
|
|
733
|
+
const numError = validateNumberDefaultValue(options.defaultValue, options.name);
|
|
734
|
+
failIf(numError);
|
|
735
|
+
}
|
|
736
|
+
const parsedVisibilityValue = parseRequiredPropValue(options.requiredPropValue, `prop "${options.name}"`);
|
|
737
|
+
const parsedNumberRangeData = propType === "NUMBER_RANGE"
|
|
738
|
+
? requireNumberRangeData(options.numberRangeData, options.name)
|
|
739
|
+
: undefined;
|
|
740
|
+
if (propType === "NUMBER_RANGE" && parsedDefaultValue !== undefined) {
|
|
741
|
+
const nrError = validateNumberRangeDefaultValue(parsedDefaultValue, parsedNumberRangeData, options.name);
|
|
742
|
+
failIf(nrError);
|
|
743
|
+
}
|
|
492
744
|
const newProp = {
|
|
493
745
|
name: options.name,
|
|
494
746
|
displayName: options.displayName,
|
|
495
747
|
type: propType,
|
|
496
748
|
required: options.required ?? false,
|
|
497
749
|
...(options.description ? { description: options.description } : {}),
|
|
498
|
-
...(
|
|
499
|
-
? { defaultValue:
|
|
750
|
+
...(parsedDefaultValue !== undefined
|
|
751
|
+
? { defaultValue: parsedDefaultValue }
|
|
500
752
|
: {}),
|
|
501
753
|
...(options.group ? { groupId: options.group } : {}),
|
|
502
754
|
...(options.typeId ? { typeId: options.typeId } : {}),
|
|
503
755
|
...(options.enumTypeId ? { enumTypeId: options.enumTypeId } : {}),
|
|
504
756
|
...(parsedFilteredIds ? { filteredComponentIds: parsedFilteredIds } : {}),
|
|
505
757
|
...(parsedPrivateVarMap ? { privateVarMap: parsedPrivateVarMap } : {}),
|
|
758
|
+
...(parsedNumberRangeData
|
|
759
|
+
? { numberRangeData: parsedNumberRangeData }
|
|
760
|
+
: {}),
|
|
761
|
+
...(options.requiredProp ? { requiredProp: options.requiredProp } : {}),
|
|
762
|
+
...(parsedVisibilityValue !== undefined
|
|
763
|
+
? { requiredPropValue: parsedVisibilityValue }
|
|
764
|
+
: {}),
|
|
765
|
+
...(options.requiredProp && options.requiredEnumOptionValue !== undefined
|
|
766
|
+
? {
|
|
767
|
+
// The editor has no separate option id; the value doubles as one.
|
|
768
|
+
requiredEnumOptionId: options.requiredEnumOptionValue,
|
|
769
|
+
requiredEnumOptionValue: options.requiredEnumOptionValue,
|
|
770
|
+
}
|
|
771
|
+
: {}),
|
|
506
772
|
};
|
|
773
|
+
failIf(visibilityGateError(component, newProp, `prop "${newProp.name}"`, {
|
|
774
|
+
selfPropName: newProp.name,
|
|
775
|
+
}));
|
|
507
776
|
component.props.push(newProp);
|
|
508
777
|
saveConfig(configPath, config);
|
|
509
778
|
// Regenerate types.ts and global types
|
|
@@ -516,25 +785,446 @@ async function addProp(ref, options) {
|
|
|
516
785
|
name: newProp.name,
|
|
517
786
|
displayName: newProp.displayName,
|
|
518
787
|
type: newProp.type,
|
|
519
|
-
required: newProp.required,
|
|
520
788
|
...(newProp.groupId ? { groupId: newProp.groupId } : {}),
|
|
521
789
|
...(newProp.enumTypeId ? { enumTypeId: newProp.enumTypeId } : {}),
|
|
522
|
-
...(newProp.filteredComponentIds
|
|
523
|
-
|
|
790
|
+
...(newProp.filteredComponentIds
|
|
791
|
+
? { filteredComponentIds: newProp.filteredComponentIds }
|
|
792
|
+
: {}),
|
|
793
|
+
...(newProp.privateVarMap
|
|
794
|
+
? { privateVarMap: newProp.privateVarMap }
|
|
795
|
+
: {}),
|
|
796
|
+
...(newProp.numberRangeData
|
|
797
|
+
? { numberRangeData: newProp.numberRangeData }
|
|
798
|
+
: {}),
|
|
799
|
+
...(newProp.requiredProp
|
|
800
|
+
? {
|
|
801
|
+
requiredProp: newProp.requiredProp,
|
|
802
|
+
...(newProp.requiredEnumOptionValue !== undefined
|
|
803
|
+
? { requiredEnumOptionValue: newProp.requiredEnumOptionValue }
|
|
804
|
+
: { requiredPropValue: newProp.requiredPropValue ?? true }),
|
|
805
|
+
}
|
|
806
|
+
: {}),
|
|
524
807
|
},
|
|
525
808
|
}));
|
|
526
809
|
}
|
|
527
|
-
function parseDefaultValue(value, propType) {
|
|
810
|
+
export function parseDefaultValue(value, propType) {
|
|
528
811
|
switch (propType) {
|
|
529
812
|
case "NUMBER":
|
|
530
813
|
return Number(value);
|
|
531
814
|
case "BOOLEAN":
|
|
532
815
|
return value === "true";
|
|
816
|
+
case "COMPONENT":
|
|
817
|
+
case "COMPONENT_LIST":
|
|
818
|
+
case "LINK":
|
|
819
|
+
case "LIST_OF_LINK":
|
|
820
|
+
case "SVG_LIST":
|
|
821
|
+
case "NUMBER_RANGE":
|
|
822
|
+
case "TYPE":
|
|
823
|
+
// These all carry a typed value (object/array), not a scalar. The --defaultValue flag arrives
|
|
824
|
+
// as a string, so parse it here; if it is not valid JSON, return the raw string so the type's
|
|
825
|
+
// validator (validateLinkDefaultValue / validateSvgDefaultValue / validateNumberRangeDefaultValue
|
|
826
|
+
// / validateTypeDefaultValue / validateComponentDefaultValue) can report a precise error instead
|
|
827
|
+
// of silently storing a bad shape.
|
|
828
|
+
try {
|
|
829
|
+
return JSON.parse(value);
|
|
830
|
+
}
|
|
831
|
+
catch {
|
|
832
|
+
return value;
|
|
833
|
+
}
|
|
533
834
|
default:
|
|
534
835
|
return value;
|
|
535
836
|
}
|
|
536
837
|
}
|
|
537
|
-
|
|
838
|
+
// Prop types whose value is a dynamic reference to merchant storefront data (products, media,
|
|
839
|
+
// categories, brands, blogs, raffles). They resolve at storefront render time, so a static config
|
|
840
|
+
// default is invalid and is rejected on add/update-prop. Keep in sync with @ikas/editor-models
|
|
841
|
+
// DYNAMIC_PROP_TYPES (the CLI must not depend on that private package).
|
|
842
|
+
export const DYNAMIC_PROP_TYPES = [
|
|
843
|
+
"IMAGE",
|
|
844
|
+
"IMAGE_LIST",
|
|
845
|
+
"VIDEO",
|
|
846
|
+
"PRODUCT",
|
|
847
|
+
"PRODUCT_LIST",
|
|
848
|
+
"PRODUCT_ATTRIBUTE",
|
|
849
|
+
"PRODUCT_ATTRIBUTE_LIST",
|
|
850
|
+
"BRAND",
|
|
851
|
+
"BRAND_LIST",
|
|
852
|
+
"CATEGORY",
|
|
853
|
+
"CATEGORY_LIST",
|
|
854
|
+
"BLOG",
|
|
855
|
+
"BLOG_LIST",
|
|
856
|
+
"BLOG_CATEGORY",
|
|
857
|
+
"BLOG_CATEGORY_LIST",
|
|
858
|
+
"RAFFLE",
|
|
859
|
+
"RAFFLE_LIST",
|
|
860
|
+
];
|
|
861
|
+
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.`;
|
|
862
|
+
// Reject a static default on a dynamic (merchant-data reference) prop type: print the standard
|
|
863
|
+
// error and exit. No-op when the type isn't dynamic or no default was supplied.
|
|
864
|
+
export const rejectDynamicDefault = (propName, propType, defaultValue) => {
|
|
865
|
+
if (DYNAMIC_PROP_TYPES.includes(propType) && defaultValue !== undefined) {
|
|
866
|
+
console.log(JSON.stringify({
|
|
867
|
+
success: false,
|
|
868
|
+
error: dynamicPropDefaultError(propName, propType),
|
|
869
|
+
}));
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
|
|
874
|
+
// FILE is intentionally excluded from CLI-authored defaults: a code-component / AI-authored default
|
|
875
|
+
// can't reference a store-specific uploaded file, so a "FILE" link default is always rejected. Store
|
|
876
|
+
// owners can still create FILE links manually in the editor (editor-core keeps FILE in its own set).
|
|
877
|
+
const VALID_LINK_TYPES = ["PAGE", "EXTERNAL"];
|
|
878
|
+
const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
|
|
879
|
+
'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
|
|
880
|
+
"Do NOT pass a JSON string or a { label, href } shape.";
|
|
881
|
+
// Returns the index of the first array element that fails `fn`, as an error string, or null.
|
|
882
|
+
const firstError = (arr, fn) => arr.reduce((acc, item, index) => acc ?? fn(item, index), null);
|
|
883
|
+
const validateSingleLink = (link, propName, path) => {
|
|
884
|
+
const where = `defaultValue for prop "${propName}"${path}`;
|
|
885
|
+
if (typeof link === "string") {
|
|
886
|
+
return `${where} is a JSON string but must be an object. ${LINK_SHAPE_HELP}`;
|
|
887
|
+
}
|
|
888
|
+
if (!link || typeof link !== "object" || Array.isArray(link)) {
|
|
889
|
+
return `${where} must be a link object. ${LINK_SHAPE_HELP}`;
|
|
890
|
+
}
|
|
891
|
+
const l = link;
|
|
892
|
+
if ("href" in l && !("linkType" in l)) {
|
|
893
|
+
return `${where} uses a legacy { href } shape without "linkType". Use "linkType" + "externalLink"/"pageType" instead. ${LINK_SHAPE_HELP}`;
|
|
894
|
+
}
|
|
895
|
+
if (typeof l.linkType !== "string" ||
|
|
896
|
+
!VALID_LINK_TYPES.includes(l.linkType)) {
|
|
897
|
+
return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
|
|
898
|
+
}
|
|
899
|
+
// Require the destination field for the declared linkType, so a link that points nowhere never
|
|
900
|
+
// reaches a published theme. A PAGE default carries a pageType (pageId is per-store, filled on
|
|
901
|
+
// materialization); pageId alone is also accepted for a specific page.
|
|
902
|
+
if (l.linkType === "PAGE" && !nonEmpty(l.pageType) && !nonEmpty(l.pageId)) {
|
|
903
|
+
return `${where}: a "PAGE" link needs a "pageType" (e.g. "INDEX") — or a "pageId" for a specific page. ${LINK_SHAPE_HELP}`;
|
|
904
|
+
}
|
|
905
|
+
if (l.linkType === "EXTERNAL" && !nonEmpty(l.externalLink)) {
|
|
906
|
+
return `${where}: an "EXTERNAL" link needs a non-empty "externalLink" URL. ${LINK_SHAPE_HELP}`;
|
|
907
|
+
}
|
|
908
|
+
if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
|
|
909
|
+
return `${where}: "subLinks" must be an array (use [] when there are none).`;
|
|
910
|
+
}
|
|
911
|
+
if (Array.isArray(l.subLinks)) {
|
|
912
|
+
return firstError(l.subLinks, (sl, i) => validateSingleLink(sl, propName, `${path}.subLinks[${i}]`));
|
|
913
|
+
}
|
|
914
|
+
return null;
|
|
915
|
+
};
|
|
916
|
+
// Validates a LINK / LIST_OF_LINK defaultValue. Returns an error message, or null if valid.
|
|
917
|
+
// These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
|
|
918
|
+
// legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
|
|
919
|
+
// out of published themes.
|
|
920
|
+
export function validateLinkDefaultValue(propType, value, propName) {
|
|
921
|
+
if (value === undefined || value === null)
|
|
922
|
+
return null;
|
|
923
|
+
if (propType === "LINK") {
|
|
924
|
+
return validateSingleLink(value, propName, "");
|
|
925
|
+
}
|
|
926
|
+
// LIST_OF_LINK
|
|
927
|
+
if (typeof value === "string") {
|
|
928
|
+
return `defaultValue for prop "${propName}" is a JSON string but must be an object { "links": [...] }. ${LINK_SHAPE_HELP}`;
|
|
929
|
+
}
|
|
930
|
+
if (!value ||
|
|
931
|
+
typeof value !== "object" ||
|
|
932
|
+
Array.isArray(value) ||
|
|
933
|
+
!Array.isArray(value.links)) {
|
|
934
|
+
return `defaultValue for prop "${propName}" must be an object with a "links" array: { "links": [ <link>, ... ] }. ${LINK_SHAPE_HELP}`;
|
|
935
|
+
}
|
|
936
|
+
const links = value.links;
|
|
937
|
+
return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
|
|
938
|
+
}
|
|
939
|
+
const COMPONENT_SHAPE_HELP = "A COMPONENT / COMPONENT_LIST default must be an object listing child code components: " +
|
|
940
|
+
'{ "components": [ { "codeComponentId": "<id>" } ] }. Only code components are allowed; each entry ' +
|
|
941
|
+
'needs a string "codeComponentId" (no componentId, no inline propValues).';
|
|
942
|
+
/**
|
|
943
|
+
* Validate a COMPONENT / COMPONENT_LIST defaultValue. Reference-only: each entry must name a code
|
|
944
|
+
* component via `codeComponentId`. Rejects non-CC (`componentId`) children, missing ids, more than
|
|
945
|
+
* one entry for a single COMPONENT, and ids outside the prop's filteredComponentIds allowlist.
|
|
946
|
+
* Returns an error message, or null if valid.
|
|
947
|
+
*/
|
|
948
|
+
export function validateComponentDefaultValue(propType, value, propName, filteredComponentIds, existingComponentIds) {
|
|
949
|
+
if (value === undefined || value === null)
|
|
950
|
+
return null;
|
|
951
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
952
|
+
if (typeof value === "string") {
|
|
953
|
+
return `${where} is a JSON string but must be an object. ${COMPONENT_SHAPE_HELP}`;
|
|
954
|
+
}
|
|
955
|
+
if (!value ||
|
|
956
|
+
typeof value !== "object" ||
|
|
957
|
+
Array.isArray(value) ||
|
|
958
|
+
!Array.isArray(value.components)) {
|
|
959
|
+
return `${where} must be an object with a "components" array. ${COMPONENT_SHAPE_HELP}`;
|
|
960
|
+
}
|
|
961
|
+
const components = value.components;
|
|
962
|
+
if (propType === "COMPONENT" && components.length > 1) {
|
|
963
|
+
return `${where}: a COMPONENT prop holds a single child — provide at most one entry (use COMPONENT_LIST for many).`;
|
|
964
|
+
}
|
|
965
|
+
const allow = filteredComponentIds && filteredComponentIds.length
|
|
966
|
+
? new Set(filteredComponentIds)
|
|
967
|
+
: null;
|
|
968
|
+
return firstError(components, (entry, i) => {
|
|
969
|
+
const at = `${where}.components[${i}]`;
|
|
970
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
971
|
+
return `${at} must be an object { "codeComponentId": "<id>" }. ${COMPONENT_SHAPE_HELP}`;
|
|
972
|
+
}
|
|
973
|
+
const e = entry;
|
|
974
|
+
if ("componentId" in e && !("codeComponentId" in e)) {
|
|
975
|
+
return `${at} references a non-code component (componentId). Only code components are allowed as defaults. ${COMPONENT_SHAPE_HELP}`;
|
|
976
|
+
}
|
|
977
|
+
if (typeof e.codeComponentId !== "string" ||
|
|
978
|
+
e.codeComponentId.trim().length === 0) {
|
|
979
|
+
return `${at} must have a non-empty string "codeComponentId". ${COMPONENT_SHAPE_HELP}`;
|
|
980
|
+
}
|
|
981
|
+
if (allow && !allow.has(e.codeComponentId)) {
|
|
982
|
+
return `${at}: "${e.codeComponentId}" is not in this prop's filteredComponentIds allowlist.`;
|
|
983
|
+
}
|
|
984
|
+
// Reject a reference to a component that does not exist in the project (deleted, renamed, or
|
|
985
|
+
// never created). A dangling default would seed a ghost entry on every placement that renders
|
|
986
|
+
// nothing. Only enforced when the caller supplies the known-component set (the config-backed
|
|
987
|
+
// add-prop / update-prop / batch sites); pure-shape callers pass no set and skip this check.
|
|
988
|
+
if (existingComponentIds && !existingComponentIds.has(e.codeComponentId)) {
|
|
989
|
+
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).`;
|
|
990
|
+
}
|
|
991
|
+
return null;
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
const SVG_DEFAULT_HELP = "An SVG default must be a single well-formed <svg>…</svg> string ≤ 64 KB with no embedded base64 raster images. " +
|
|
995
|
+
'Tip: the editor uses each svg\'s `class` attribute as its display name — set class="my-icon" to name it; ' +
|
|
996
|
+
"SVG_LIST items with no class show as svg-1, svg-2, ….";
|
|
997
|
+
// Keep the cap and base64 pattern in sync with @ikas/editor-models validate-svg.ts (the
|
|
998
|
+
// authoritative validator). Duplicated here on purpose — see validateSvgShape below.
|
|
999
|
+
const SVG_MAX_SIZE_KB = 64;
|
|
1000
|
+
const SVG_BASE64_RASTER_RE = /data:image\/[a-z0-9.+-]+;base64/i;
|
|
1001
|
+
const SVG_SHAPE_RE = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>][\s\S]*<\/svg>\s*$/i;
|
|
1002
|
+
// Count TOP-LEVEL <svg> roots without a DOM: scan svg open/close tags in order and track
|
|
1003
|
+
// nesting depth. A root is any <svg> opened at depth 0. This matches the editor's DOM-based
|
|
1004
|
+
// single-root check — it rejects siblings (`<svg/><svg/>`) while allowing a nested <svg>.
|
|
1005
|
+
// Self-closing <svg .../> counts as a root but opens no nesting level.
|
|
1006
|
+
function svgRootCount(input) {
|
|
1007
|
+
const tags = input.match(/<svg\b[^>]*>|<\/svg\s*>/gi) || [];
|
|
1008
|
+
return tags.reduce((acc, tag) => {
|
|
1009
|
+
if (tag.startsWith("</"))
|
|
1010
|
+
return { depth: Math.max(0, acc.depth - 1), roots: acc.roots };
|
|
1011
|
+
const roots = acc.depth === 0 ? acc.roots + 1 : acc.roots;
|
|
1012
|
+
const depth = /\/>\s*$/.test(tag) ? acc.depth : acc.depth + 1;
|
|
1013
|
+
return { depth, roots };
|
|
1014
|
+
}, { depth: 0, roots: 0 }).roots;
|
|
1015
|
+
}
|
|
1016
|
+
// Lightweight, dependency-free SVG structure check. This standalone CLI is publishable and
|
|
1017
|
+
// must not depend on the private @ikas/editor-models package (nor pull a DOM/DOMPurify into a
|
|
1018
|
+
// CLI), so it mirrors the RULES of the editor's validateSvg (size, base64, single <svg> root)
|
|
1019
|
+
// for fast authoring feedback. The authoritative sanitize runs in the editor/upload path.
|
|
1020
|
+
// Returns a rejection reason, or null if the shape is acceptable.
|
|
1021
|
+
function validateSvgShape(input) {
|
|
1022
|
+
if (typeof input !== "string" || input.trim() === "")
|
|
1023
|
+
return "empty or not a string";
|
|
1024
|
+
if (Buffer.byteLength(input, "utf8") > SVG_MAX_SIZE_KB * 1024)
|
|
1025
|
+
return `exceeds ${SVG_MAX_SIZE_KB} KB`;
|
|
1026
|
+
if (SVG_BASE64_RASTER_RE.test(input))
|
|
1027
|
+
return "embedded base64 raster image not allowed";
|
|
1028
|
+
if (!SVG_SHAPE_RE.test(input))
|
|
1029
|
+
return "not a single-root <svg> document";
|
|
1030
|
+
if (svgRootCount(input) !== 1)
|
|
1031
|
+
return "must be exactly one <svg> root";
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
// Validates an SVG / SVG_LIST defaultValue. Returns an error message, or null if valid.
|
|
1035
|
+
// Authoring an SVG default is a common mistake source for agents; rejecting here (with an
|
|
1036
|
+
// instructive message) keeps malformed/oversized SVGs out of published components.
|
|
1037
|
+
export function validateSvgDefaultValue(propType, value, propName) {
|
|
1038
|
+
if (value === undefined || value === null)
|
|
1039
|
+
return null;
|
|
1040
|
+
if (propType === "SVG") {
|
|
1041
|
+
const reason = validateSvgShape(value);
|
|
1042
|
+
return reason
|
|
1043
|
+
? `defaultValue for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
|
|
1044
|
+
: null;
|
|
1045
|
+
}
|
|
1046
|
+
// SVG_LIST
|
|
1047
|
+
if (!Array.isArray(value)) {
|
|
1048
|
+
return `defaultValue for prop "${propName}" must be an array of SVG strings: [ "<svg>…</svg>", ... ]. ${SVG_DEFAULT_HELP}`;
|
|
1049
|
+
}
|
|
1050
|
+
return firstError(value, (svg, i) => {
|
|
1051
|
+
const reason = validateSvgShape(svg);
|
|
1052
|
+
return reason
|
|
1053
|
+
? `defaultValue[${i}] for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
|
|
1054
|
+
: null;
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
const NUMBER_RANGE_DEFAULT_HELP = 'A NUMBER_RANGE default must be an object { "value": <number>, "unit": "px" | null }. ' +
|
|
1058
|
+
"When the prop defines numberRangeData, value must be within [min, max] and on the interval grid (min + k·interval).";
|
|
1059
|
+
// Validates a NUMBER_RANGE defaultValue. Returns an error message, or null if valid.
|
|
1060
|
+
// Checks the { value, unit } shape and — when the prop carries numberRangeData — that the
|
|
1061
|
+
// value is within [min, max] and lands on the interval grid, mirroring the editor's slider
|
|
1062
|
+
// so an agent can't author an out-of-range or off-step default.
|
|
1063
|
+
export function validateNumberRangeDefaultValue(value, numberRangeData, propName) {
|
|
1064
|
+
if (value === undefined || value === null)
|
|
1065
|
+
return null;
|
|
1066
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
1067
|
+
if (typeof value === "string") {
|
|
1068
|
+
return `${where} is a JSON string but must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1069
|
+
}
|
|
1070
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
1071
|
+
return `${where} must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1072
|
+
}
|
|
1073
|
+
const v = value;
|
|
1074
|
+
if (typeof v.value !== "number" || !Number.isFinite(v.value)) {
|
|
1075
|
+
return `${where}: "value" must be a finite number. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1076
|
+
}
|
|
1077
|
+
if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
|
|
1078
|
+
return `${where}: "unit" must be a string or null. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1079
|
+
}
|
|
1080
|
+
if (numberRangeData) {
|
|
1081
|
+
const { min, max, interval } = numberRangeData;
|
|
1082
|
+
if (min !== undefined && v.value < min) {
|
|
1083
|
+
return `${where}: value ${v.value} is below the configured min (${min}).`;
|
|
1084
|
+
}
|
|
1085
|
+
if (max !== undefined && v.value > max) {
|
|
1086
|
+
return `${where}: value ${v.value} is above the configured max (${max}).`;
|
|
1087
|
+
}
|
|
1088
|
+
if (interval !== undefined &&
|
|
1089
|
+
interval > 0 &&
|
|
1090
|
+
min !== undefined &&
|
|
1091
|
+
(v.value - min) % interval !== 0) {
|
|
1092
|
+
return `${where}: value ${v.value} is not on the interval grid (min ${min} + k·${interval}).`;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
const NUMBER_DEFAULT_HELP = "A NUMBER default must be a finite number, e.g. --defaultValue 42.";
|
|
1098
|
+
// Validates a NUMBER defaultValue. Returns an error message, or null if valid. Guards two footguns:
|
|
1099
|
+
// an empty/whitespace flag string (Number("") === 0 would silently store 0) and a non-numeric or
|
|
1100
|
+
// non-finite value (e.g. "42px" → NaN, "Infinity"). The flag path passes the raw --defaultValue
|
|
1101
|
+
// string; the --props path passes the already-parsed JSON value — both are handled here.
|
|
1102
|
+
export function validateNumberDefaultValue(value, propName) {
|
|
1103
|
+
if (value === undefined || value === null)
|
|
1104
|
+
return null;
|
|
1105
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
1106
|
+
if (typeof value === "string") {
|
|
1107
|
+
if (value.trim() === "") {
|
|
1108
|
+
return `${where} is empty, but NUMBER requires a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1109
|
+
}
|
|
1110
|
+
if (!Number.isFinite(Number(value))) {
|
|
1111
|
+
return `${where} ("${value}") is not a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1112
|
+
}
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1116
|
+
return `${where} must be a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1117
|
+
}
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
const STYLE_TYPE_DEFAULT_HELP = "A size/style TYPE default must be an object matching the type's shape: " +
|
|
1121
|
+
'{ "value": <number>, "unit": "px" | "rem" | "%" | "vh" | "vw" } (or { "css": "<raw css>" }). ' +
|
|
1122
|
+
"Author it via add-component --props '[…]' or --defaultValue '<json>'.";
|
|
1123
|
+
const BP_STOREFRONT_PREFIX = "@ikas/bp-storefront-models-";
|
|
1124
|
+
// The size/style types that share the { css?, value?, unit? } shape. This is EXACTLY the set of
|
|
1125
|
+
// TYPE typeIds whose default the generator emits (mirror of DYNAMIC_STYLE_TYPE_IDS in
|
|
1126
|
+
// @ikas/blueprint-modules design-generator/generate-helpers/styles.ts — the standalone CLI must not
|
|
1127
|
+
// depend on that package, so it is hand-maintained here and kept in sync by a parity test). Every
|
|
1128
|
+
// OTHER TYPE typeId (BorderStyleType, BoxShadowStyleType, GridTemplateColumnsStyleType, string-union
|
|
1129
|
+
// styles like TextAlignStyleType, IkasProduct, custom types) has a DIFFERENT value shape, so their
|
|
1130
|
+
// defaults are not shape-checked (and are dead code in the generator anyway).
|
|
1131
|
+
const SIZE_STYLE_TYPE_NAMES = [
|
|
1132
|
+
"SizeStyleType",
|
|
1133
|
+
"PaddingStyleType",
|
|
1134
|
+
"PaddingTopStyleType",
|
|
1135
|
+
"PaddingRightStyleType",
|
|
1136
|
+
"PaddingBottomStyleType",
|
|
1137
|
+
"PaddingLeftStyleType",
|
|
1138
|
+
"MarginStyleType",
|
|
1139
|
+
"MarginTopStyleType",
|
|
1140
|
+
"MarginRightStyleType",
|
|
1141
|
+
"MarginBottomStyleType",
|
|
1142
|
+
"MarginLeftStyleType",
|
|
1143
|
+
"BorderRadiusStyleType",
|
|
1144
|
+
"BorderRadiusTopLeftStyleType",
|
|
1145
|
+
"BorderRadiusTopRightStyleType",
|
|
1146
|
+
"BorderRadiusBottomRightStyleType",
|
|
1147
|
+
"BorderRadiusBottomLeftStyleType",
|
|
1148
|
+
"FontSizeStyleType",
|
|
1149
|
+
"LineHeightStyleType",
|
|
1150
|
+
"LetterSpacingStyleType",
|
|
1151
|
+
"HeightStyleType",
|
|
1152
|
+
"MinHeightStyleType",
|
|
1153
|
+
"MaxHeightStyleType",
|
|
1154
|
+
"WidthStyleType",
|
|
1155
|
+
"MinWidthStyleType",
|
|
1156
|
+
"MaxWidthStyleType",
|
|
1157
|
+
"GapStyleType",
|
|
1158
|
+
"BorderWidthStyleType",
|
|
1159
|
+
"TopStyleType",
|
|
1160
|
+
"RightStyleType",
|
|
1161
|
+
"BottomStyleType",
|
|
1162
|
+
"LeftStyleType",
|
|
1163
|
+
];
|
|
1164
|
+
// Validates a single style-type default object. Every size style type shares the optional
|
|
1165
|
+
// { css?, value?, unit? } shape, so inner fields are checked loosely — the key rejection is a
|
|
1166
|
+
// non-object (e.g. a bare JSON string, the common failure when a --defaultValue flag is not parsed).
|
|
1167
|
+
function styleDefaultObjectError(value, where) {
|
|
1168
|
+
if (typeof value === "string") {
|
|
1169
|
+
return `${where} is a JSON string but must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1170
|
+
}
|
|
1171
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1172
|
+
return `${where} must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1173
|
+
}
|
|
1174
|
+
const v = value;
|
|
1175
|
+
// css/value/unit are all optional, and the editor treats an explicit null as "unset" (it accepts
|
|
1176
|
+
// { value: null }). So only type-check a field that is present AND non-null — otherwise the CLI
|
|
1177
|
+
// would be stricter than the authoritative editor and reject a value it would happily accept.
|
|
1178
|
+
// Unknown fields ARE rejected below: every size-style type shares exactly this { css?, value?, unit? }
|
|
1179
|
+
// shape, so an extra key is always a mistake (this matches the editor). The per-type value-DOMAIN
|
|
1180
|
+
// (which units a given type allows — BorderWidth also permits em/vmin/vmax while the rest are
|
|
1181
|
+
// px/rem/%/vh/vw) stays editor-side: it is per-type and can't be mirrored offline without drift.
|
|
1182
|
+
if (v.css !== undefined && v.css !== null && typeof v.css !== "string") {
|
|
1183
|
+
return `${where}: "css" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1184
|
+
}
|
|
1185
|
+
if (v.value !== undefined &&
|
|
1186
|
+
v.value !== null &&
|
|
1187
|
+
(typeof v.value !== "number" || !Number.isFinite(v.value))) {
|
|
1188
|
+
return `${where}: "value" must be a finite number. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1189
|
+
}
|
|
1190
|
+
if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
|
|
1191
|
+
return `${where}: "unit" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1192
|
+
}
|
|
1193
|
+
const unknownKey = Object.keys(v).find((k) => k !== "css" && k !== "value" && k !== "unit");
|
|
1194
|
+
if (unknownKey) {
|
|
1195
|
+
return `${where}: unknown field "${unknownKey}". A size/style default allows only "css", "value", "unit". ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1196
|
+
}
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
// Validates a TYPE prop defaultValue — but ONLY for the size/style types whose shape we know
|
|
1200
|
+
// ({ css?, value?, unit? }). Any other TYPE typeId (color/border/box-shadow/grid/string-union
|
|
1201
|
+
// styles, IkasProduct, custom types) has a different value shape we can't verify offline, so its
|
|
1202
|
+
// default is passed through unchecked. The generator only emits a default for these size/style
|
|
1203
|
+
// types anyway. Returns an error message, or null if valid / not a size-style type.
|
|
1204
|
+
export function validateTypeDefaultValue(value, propName, typeId) {
|
|
1205
|
+
if (value === undefined || value === null)
|
|
1206
|
+
return null;
|
|
1207
|
+
if (typeof typeId !== "string" || !typeId.startsWith(BP_STOREFRONT_PREFIX)) {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
// Strip the prefix and an optional _array suffix to get the base type name.
|
|
1211
|
+
const withoutPrefix = typeId.slice(BP_STOREFRONT_PREFIX.length);
|
|
1212
|
+
const isArray = withoutPrefix.endsWith("_array");
|
|
1213
|
+
const baseName = isArray
|
|
1214
|
+
? withoutPrefix.slice(0, -"_array".length)
|
|
1215
|
+
: withoutPrefix;
|
|
1216
|
+
if (!SIZE_STYLE_TYPE_NAMES.includes(baseName))
|
|
1217
|
+
return null;
|
|
1218
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
1219
|
+
if (isArray) {
|
|
1220
|
+
if (!Array.isArray(value)) {
|
|
1221
|
+
return `${where} must be an array for an _array TYPE prop: [ { "value": …, "unit": … }, … ]. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1222
|
+
}
|
|
1223
|
+
return firstError(value, (el, i) => styleDefaultObjectError(el, `${where}[${i}]`));
|
|
1224
|
+
}
|
|
1225
|
+
return styleDefaultObjectError(value, where);
|
|
1226
|
+
}
|
|
1227
|
+
async function updateProp(ref, options) {
|
|
538
1228
|
const { config, configPath } = loadConfig();
|
|
539
1229
|
const component = resolveComponent(config, ref);
|
|
540
1230
|
const propIndex = component.props.findIndex((p) => p.name === options.prop);
|
|
@@ -566,8 +1256,87 @@ function updateProp(ref, options) {
|
|
|
566
1256
|
if (options.description !== undefined) {
|
|
567
1257
|
prop.description = options.description || undefined;
|
|
568
1258
|
}
|
|
1259
|
+
// Resolve numberRangeData before defaultValue so a NUMBER_RANGE default can be validated
|
|
1260
|
+
// against the (possibly just-updated) slider config in the same call.
|
|
1261
|
+
if (options.numberRangeData !== undefined) {
|
|
1262
|
+
if (options.numberRangeData === "" || options.numberRangeData === "none") {
|
|
1263
|
+
delete prop.numberRangeData;
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
prop.numberRangeData = parseNumberRangeData(options.numberRangeData, prop.name);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
// A NUMBER_RANGE prop must keep a valid slider config (min + max). Enforce the invariant only when
|
|
1270
|
+
// this update touches the type or numberRangeData — a benign update (e.g. displayName) on an
|
|
1271
|
+
// existing prop must still pass. This catches switching a prop TO NUMBER_RANGE without a config, or
|
|
1272
|
+
// clearing/blanking numberRangeData on a NUMBER_RANGE prop.
|
|
1273
|
+
if ((options.type !== undefined || options.numberRangeData !== undefined) &&
|
|
1274
|
+
prop.type === "NUMBER_RANGE" &&
|
|
1275
|
+
(!prop.numberRangeData || !isNumberRangeComplete(prop.numberRangeData))) {
|
|
1276
|
+
console.log(JSON.stringify({
|
|
1277
|
+
success: false,
|
|
1278
|
+
error: numberRangeDataRequiredError(prop.name),
|
|
1279
|
+
}));
|
|
1280
|
+
process.exit(1);
|
|
1281
|
+
}
|
|
1282
|
+
// Resolve filteredComponentIds before defaultValue so a COMPONENT / COMPONENT_LIST default can be
|
|
1283
|
+
// validated against the (possibly just-updated) allowlist in the same call.
|
|
1284
|
+
if (options.filteredComponentIds !== undefined) {
|
|
1285
|
+
if (options.filteredComponentIds === "" ||
|
|
1286
|
+
options.filteredComponentIds === "none") {
|
|
1287
|
+
delete prop.filteredComponentIds;
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
try {
|
|
1291
|
+
const parsed = JSON.parse(options.filteredComponentIds);
|
|
1292
|
+
if (!Array.isArray(parsed)) {
|
|
1293
|
+
console.log(JSON.stringify({
|
|
1294
|
+
success: false,
|
|
1295
|
+
error: "--filteredComponentIds must be a JSON array of strings.",
|
|
1296
|
+
}));
|
|
1297
|
+
process.exit(1);
|
|
1298
|
+
}
|
|
1299
|
+
assertKnownComponentIds(parsed, config);
|
|
1300
|
+
prop.filteredComponentIds = parsed;
|
|
1301
|
+
}
|
|
1302
|
+
catch (err) {
|
|
1303
|
+
if (err instanceof SyntaxError) {
|
|
1304
|
+
console.log(JSON.stringify({
|
|
1305
|
+
success: false,
|
|
1306
|
+
error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
|
|
1307
|
+
}));
|
|
1308
|
+
process.exit(1);
|
|
1309
|
+
}
|
|
1310
|
+
throw err;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
569
1314
|
if (options.defaultValue !== undefined) {
|
|
570
|
-
prop.
|
|
1315
|
+
rejectDynamicDefault(prop.name, prop.type, options.defaultValue);
|
|
1316
|
+
const parsed = parseDefaultValue(options.defaultValue, prop.type);
|
|
1317
|
+
// Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
|
|
1318
|
+
if (prop.type === "NUMBER") {
|
|
1319
|
+
const numError = validateNumberDefaultValue(options.defaultValue, prop.name);
|
|
1320
|
+
failIf(numError);
|
|
1321
|
+
}
|
|
1322
|
+
if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
|
|
1323
|
+
const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
|
|
1324
|
+
failIf(linkError);
|
|
1325
|
+
}
|
|
1326
|
+
if ((prop.type === "COMPONENT" || prop.type === "COMPONENT_LIST") &&
|
|
1327
|
+
parsed !== undefined) {
|
|
1328
|
+
failIf(validateComponentDefaultValue(prop.type, parsed, prop.name, prop.filteredComponentIds, existingComponentIdSet(config)));
|
|
1329
|
+
}
|
|
1330
|
+
if (prop.type === "NUMBER_RANGE") {
|
|
1331
|
+
const nrError = validateNumberRangeDefaultValue(parsed, prop.numberRangeData, prop.name);
|
|
1332
|
+
failIf(nrError);
|
|
1333
|
+
}
|
|
1334
|
+
if (prop.type === "TYPE") {
|
|
1335
|
+
// Prefer a typeId supplied in this same update over the stored one.
|
|
1336
|
+
const typeError = validateTypeDefaultValue(parsed, prop.name, options.typeId ?? prop.typeId);
|
|
1337
|
+
failIf(typeError);
|
|
1338
|
+
}
|
|
1339
|
+
prop.defaultValue = parsed;
|
|
571
1340
|
}
|
|
572
1341
|
if (options.group !== undefined) {
|
|
573
1342
|
if (options.group === "" || options.group === "none") {
|
|
@@ -600,6 +1369,13 @@ function updateProp(ref, options) {
|
|
|
600
1369
|
prop.typeId = options.typeId;
|
|
601
1370
|
}
|
|
602
1371
|
}
|
|
1372
|
+
// Reject section-disallowed types when the caller changes type/typeId;
|
|
1373
|
+
// untouched legacy props stay editable (displayName, group, ...).
|
|
1374
|
+
if ((options.type !== undefined || options.typeId !== undefined) &&
|
|
1375
|
+
prop.type === "TYPE" &&
|
|
1376
|
+
prop.typeId) {
|
|
1377
|
+
await assertTypeIdsUsable(component.type, [prop.typeId]);
|
|
1378
|
+
}
|
|
603
1379
|
if (options.enumTypeId !== undefined) {
|
|
604
1380
|
if (options.enumTypeId === "" || options.enumTypeId === "none") {
|
|
605
1381
|
delete prop.enumTypeId;
|
|
@@ -608,29 +1384,6 @@ function updateProp(ref, options) {
|
|
|
608
1384
|
prop.enumTypeId = options.enumTypeId;
|
|
609
1385
|
}
|
|
610
1386
|
}
|
|
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
1387
|
if (options.privateVarMap !== undefined) {
|
|
635
1388
|
if (options.privateVarMap === "" || options.privateVarMap === "none") {
|
|
636
1389
|
delete prop.privateVarMap;
|
|
@@ -639,17 +1392,50 @@ function updateProp(ref, options) {
|
|
|
639
1392
|
try {
|
|
640
1393
|
const parsed = JSON.parse(options.privateVarMap);
|
|
641
1394
|
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
642
|
-
console.log(JSON.stringify({
|
|
1395
|
+
console.log(JSON.stringify({
|
|
1396
|
+
success: false,
|
|
1397
|
+
error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
|
|
1398
|
+
}));
|
|
643
1399
|
process.exit(1);
|
|
644
1400
|
}
|
|
645
1401
|
prop.privateVarMap = parsed;
|
|
646
1402
|
}
|
|
647
1403
|
catch {
|
|
648
|
-
console.log(JSON.stringify({
|
|
1404
|
+
console.log(JSON.stringify({
|
|
1405
|
+
success: false,
|
|
1406
|
+
error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
|
|
1407
|
+
}));
|
|
649
1408
|
process.exit(1);
|
|
650
1409
|
}
|
|
651
1410
|
}
|
|
652
1411
|
}
|
|
1412
|
+
if (options.requiredProp !== undefined) {
|
|
1413
|
+
if (options.requiredProp === "" || options.requiredProp === "none") {
|
|
1414
|
+
// Every field goes together — see `IProp.requiredPropId`.
|
|
1415
|
+
delete prop.requiredProp;
|
|
1416
|
+
delete prop.requiredPropValue;
|
|
1417
|
+
delete prop.requiredEnumOptionId;
|
|
1418
|
+
delete prop.requiredEnumOptionValue;
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
prop.requiredProp = options.requiredProp;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (options.requiredEnumOptionValue !== undefined && prop.requiredProp) {
|
|
1425
|
+
prop.requiredEnumOptionId = options.requiredEnumOptionValue;
|
|
1426
|
+
prop.requiredEnumOptionValue = options.requiredEnumOptionValue;
|
|
1427
|
+
delete prop.requiredPropValue;
|
|
1428
|
+
}
|
|
1429
|
+
if (options.requiredPropValue !== undefined && prop.requiredProp) {
|
|
1430
|
+
delete prop.requiredEnumOptionId;
|
|
1431
|
+
delete prop.requiredEnumOptionValue;
|
|
1432
|
+
prop.requiredPropValue = parseRequiredPropValue(options.requiredPropValue, `prop "${prop.name}"`);
|
|
1433
|
+
}
|
|
1434
|
+
// Re-checked on every update, not only when a visibility flag is passed: flipping `required`, or
|
|
1435
|
+
// renaming the prop this one hangs on, can invalidate a gate that was sound when it was written.
|
|
1436
|
+
failIf(visibilityGateError(component, prop, `prop "${prop.name}"`, {
|
|
1437
|
+
selfPropName: prop.name,
|
|
1438
|
+
}));
|
|
653
1439
|
saveConfig(configPath, config);
|
|
654
1440
|
// Regenerate types.ts and global types
|
|
655
1441
|
regenerateTypes(component, undefined, config);
|
|
@@ -661,12 +1447,24 @@ function updateProp(ref, options) {
|
|
|
661
1447
|
name: prop.name,
|
|
662
1448
|
displayName: prop.displayName,
|
|
663
1449
|
type: prop.type,
|
|
664
|
-
required: prop.required,
|
|
665
1450
|
...(prop.groupId ? { groupId: prop.groupId } : {}),
|
|
666
1451
|
...(prop.typeId ? { typeId: prop.typeId } : {}),
|
|
667
1452
|
...(prop.enumTypeId ? { enumTypeId: prop.enumTypeId } : {}),
|
|
668
|
-
...(prop.filteredComponentIds
|
|
1453
|
+
...(prop.filteredComponentIds
|
|
1454
|
+
? { filteredComponentIds: prop.filteredComponentIds }
|
|
1455
|
+
: {}),
|
|
669
1456
|
...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
|
|
1457
|
+
...(prop.numberRangeData
|
|
1458
|
+
? { numberRangeData: prop.numberRangeData }
|
|
1459
|
+
: {}),
|
|
1460
|
+
...(prop.requiredProp
|
|
1461
|
+
? {
|
|
1462
|
+
requiredProp: prop.requiredProp,
|
|
1463
|
+
...(prop.requiredEnumOptionValue !== undefined
|
|
1464
|
+
? { requiredEnumOptionValue: prop.requiredEnumOptionValue }
|
|
1465
|
+
: { requiredPropValue: prop.requiredPropValue ?? true }),
|
|
1466
|
+
}
|
|
1467
|
+
: {}),
|
|
670
1468
|
},
|
|
671
1469
|
}));
|
|
672
1470
|
}
|
|
@@ -729,7 +1527,10 @@ function removeComponent(ref) {
|
|
|
729
1527
|
removedComponentId: component.id,
|
|
730
1528
|
removedComponentName: component.name,
|
|
731
1529
|
removedDirectory: path.relative(process.cwd(), componentDir),
|
|
732
|
-
remainingComponents: config.components.map((c) => ({
|
|
1530
|
+
remainingComponents: config.components.map((c) => ({
|
|
1531
|
+
id: c.id,
|
|
1532
|
+
name: c.name,
|
|
1533
|
+
})),
|
|
733
1534
|
}));
|
|
734
1535
|
}
|
|
735
1536
|
function addPropGroup(ref, options) {
|
|
@@ -750,7 +1551,24 @@ function addPropGroup(ref, options) {
|
|
|
750
1551
|
id: options.id,
|
|
751
1552
|
name: options.name,
|
|
752
1553
|
...(options.description ? { description: options.description } : {}),
|
|
1554
|
+
...(options.requiredProp ? { requiredProp: options.requiredProp } : {}),
|
|
1555
|
+
...(options.requiredProp && options.requiredEnumOptionValue !== undefined
|
|
1556
|
+
? {
|
|
1557
|
+
requiredEnumOptionId: options.requiredEnumOptionValue,
|
|
1558
|
+
requiredEnumOptionValue: options.requiredEnumOptionValue,
|
|
1559
|
+
}
|
|
1560
|
+
: {}),
|
|
1561
|
+
...(options.requiredProp &&
|
|
1562
|
+
options.requiredEnumOptionValue === undefined &&
|
|
1563
|
+
options.requiredPropValue !== undefined
|
|
1564
|
+
? {
|
|
1565
|
+
requiredPropValue: parseRequiredPropValue(options.requiredPropValue, `group "${options.id}"`),
|
|
1566
|
+
}
|
|
1567
|
+
: {}),
|
|
753
1568
|
};
|
|
1569
|
+
failIf(visibilityGateError(component, newGroup, `group "${newGroup.id}"`, {
|
|
1570
|
+
group: newGroup,
|
|
1571
|
+
}));
|
|
754
1572
|
if (options.parent) {
|
|
755
1573
|
const found = findPropGroup(component.propGroups, options.parent);
|
|
756
1574
|
if (!found) {
|
|
@@ -782,6 +1600,14 @@ function addPropGroup(ref, options) {
|
|
|
782
1600
|
id: newGroup.id,
|
|
783
1601
|
name: newGroup.name,
|
|
784
1602
|
...(options.parent ? { parentId: options.parent } : {}),
|
|
1603
|
+
...(newGroup.requiredProp
|
|
1604
|
+
? {
|
|
1605
|
+
requiredProp: newGroup.requiredProp,
|
|
1606
|
+
...(newGroup.requiredEnumOptionValue !== undefined
|
|
1607
|
+
? { requiredEnumOptionValue: newGroup.requiredEnumOptionValue }
|
|
1608
|
+
: { requiredPropValue: newGroup.requiredPropValue ?? true }),
|
|
1609
|
+
}
|
|
1610
|
+
: {}),
|
|
785
1611
|
},
|
|
786
1612
|
}));
|
|
787
1613
|
}
|
|
@@ -807,6 +1633,34 @@ function updatePropGroup(ref, options) {
|
|
|
807
1633
|
found.group.name = options.name;
|
|
808
1634
|
if (options.description !== undefined)
|
|
809
1635
|
found.group.description = options.description || undefined;
|
|
1636
|
+
if (options.requiredProp !== undefined) {
|
|
1637
|
+
if (options.requiredProp === "" || options.requiredProp === "none") {
|
|
1638
|
+
// Every field goes together — see `IThemePropGroup.requiredPropId`.
|
|
1639
|
+
delete found.group.requiredProp;
|
|
1640
|
+
delete found.group.requiredPropValue;
|
|
1641
|
+
delete found.group.requiredEnumOptionId;
|
|
1642
|
+
delete found.group.requiredEnumOptionValue;
|
|
1643
|
+
}
|
|
1644
|
+
else {
|
|
1645
|
+
found.group.requiredProp = options.requiredProp;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
if (options.requiredEnumOptionValue !== undefined &&
|
|
1649
|
+
found.group.requiredProp) {
|
|
1650
|
+
found.group.requiredEnumOptionId = options.requiredEnumOptionValue;
|
|
1651
|
+
found.group.requiredEnumOptionValue = options.requiredEnumOptionValue;
|
|
1652
|
+
delete found.group.requiredPropValue;
|
|
1653
|
+
}
|
|
1654
|
+
if (options.requiredPropValue !== undefined && found.group.requiredProp) {
|
|
1655
|
+
delete found.group.requiredEnumOptionId;
|
|
1656
|
+
delete found.group.requiredEnumOptionValue;
|
|
1657
|
+
found.group.requiredPropValue = parseRequiredPropValue(options.requiredPropValue, `group "${found.group.id}"`);
|
|
1658
|
+
}
|
|
1659
|
+
// Re-checked on every update: moving a prop into this group can invalidate a gate that was sound
|
|
1660
|
+
// when it was written.
|
|
1661
|
+
failIf(visibilityGateError(component, found.group, `group "${found.group.id}"`, {
|
|
1662
|
+
group: found.group,
|
|
1663
|
+
}));
|
|
810
1664
|
saveConfig(configPath, config);
|
|
811
1665
|
console.log(JSON.stringify({
|
|
812
1666
|
success: true,
|
|
@@ -814,6 +1668,16 @@ function updatePropGroup(ref, options) {
|
|
|
814
1668
|
updatedPropGroup: {
|
|
815
1669
|
id: found.group.id,
|
|
816
1670
|
name: found.group.name,
|
|
1671
|
+
...(found.group.requiredProp
|
|
1672
|
+
? {
|
|
1673
|
+
requiredProp: found.group.requiredProp,
|
|
1674
|
+
...(found.group.requiredEnumOptionValue !== undefined
|
|
1675
|
+
? {
|
|
1676
|
+
requiredEnumOptionValue: found.group.requiredEnumOptionValue,
|
|
1677
|
+
}
|
|
1678
|
+
: { requiredPropValue: found.group.requiredPropValue ?? true }),
|
|
1679
|
+
}
|
|
1680
|
+
: {}),
|
|
817
1681
|
},
|
|
818
1682
|
}));
|
|
819
1683
|
}
|
|
@@ -836,10 +1700,10 @@ function removePropGroup(ref, options) {
|
|
|
836
1700
|
process.exit(1);
|
|
837
1701
|
}
|
|
838
1702
|
if (found.parent) {
|
|
839
|
-
found.parent.children = found.parent.children.filter(c => c.id !== options.id);
|
|
1703
|
+
found.parent.children = found.parent.children.filter((c) => c.id !== options.id);
|
|
840
1704
|
}
|
|
841
1705
|
else {
|
|
842
|
-
component.propGroups = component.propGroups.filter(g => g.id !== options.id);
|
|
1706
|
+
component.propGroups = component.propGroups.filter((g) => g.id !== options.id);
|
|
843
1707
|
}
|
|
844
1708
|
// Clear groupId from props referencing the deleted group
|
|
845
1709
|
for (const prop of component.props) {
|
|
@@ -852,7 +1716,7 @@ function removePropGroup(ref, options) {
|
|
|
852
1716
|
success: true,
|
|
853
1717
|
componentName: component.name,
|
|
854
1718
|
removedPropGroup: options.id,
|
|
855
|
-
remainingPropGroups: component.propGroups.map(g => g.id),
|
|
1719
|
+
remainingPropGroups: component.propGroups.map((g) => g.id),
|
|
856
1720
|
}));
|
|
857
1721
|
}
|
|
858
1722
|
function movePropGroup(ref, options) {
|
|
@@ -869,7 +1733,10 @@ function movePropGroup(ref, options) {
|
|
|
869
1733
|
component.propGroups = movePropGroupInTree(component.propGroups, options.id, options.parent || null, options.index);
|
|
870
1734
|
}
|
|
871
1735
|
catch (e) {
|
|
872
|
-
console.log(JSON.stringify({
|
|
1736
|
+
console.log(JSON.stringify({
|
|
1737
|
+
success: false,
|
|
1738
|
+
error: e.message || "Failed to move prop group",
|
|
1739
|
+
}));
|
|
873
1740
|
process.exit(1);
|
|
874
1741
|
}
|
|
875
1742
|
saveConfig(configPath, config);
|
|
@@ -925,7 +1792,9 @@ function listComponents() {
|
|
|
925
1792
|
required: p.required,
|
|
926
1793
|
...(p.groupId ? { groupId: p.groupId } : {}),
|
|
927
1794
|
})),
|
|
928
|
-
...(c.propGroups && c.propGroups.length > 0
|
|
1795
|
+
...(c.propGroups && c.propGroups.length > 0
|
|
1796
|
+
? { propGroups: c.propGroups }
|
|
1797
|
+
: {}),
|
|
929
1798
|
}));
|
|
930
1799
|
console.log(JSON.stringify({
|
|
931
1800
|
success: true,
|
|
@@ -939,7 +1808,9 @@ function listComponents() {
|
|
|
939
1808
|
async function addEnum(options) {
|
|
940
1809
|
const { config, configPath } = loadConfig();
|
|
941
1810
|
// Validate name
|
|
942
|
-
if (!options.name ||
|
|
1811
|
+
if (!options.name ||
|
|
1812
|
+
typeof options.name !== "string" ||
|
|
1813
|
+
!options.name.trim()) {
|
|
943
1814
|
console.log(JSON.stringify({ success: false, error: "Enum name is required." }));
|
|
944
1815
|
process.exit(1);
|
|
945
1816
|
}
|
|
@@ -1053,7 +1924,7 @@ function updateEnum(options) {
|
|
|
1053
1924
|
Object.keys(enumOptions).length === 0) {
|
|
1054
1925
|
console.log(JSON.stringify({
|
|
1055
1926
|
success: false,
|
|
1056
|
-
error:
|
|
1927
|
+
error: "Options must be a non-empty JSON object.",
|
|
1057
1928
|
}));
|
|
1058
1929
|
process.exit(1);
|
|
1059
1930
|
}
|
|
@@ -1126,8 +1997,7 @@ function listEnums() {
|
|
|
1126
1997
|
}
|
|
1127
1998
|
// --- Command registration ---
|
|
1128
1999
|
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)");
|
|
2000
|
+
const config = new Command("config").description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
|
|
1131
2001
|
config
|
|
1132
2002
|
.command("add-component")
|
|
1133
2003
|
.description("Add a new component to the project")
|
|
@@ -1136,7 +2006,7 @@ export function createConfigCommand() {
|
|
|
1136
2006
|
.option("--isHeader", "Mark this section as the store header (only for type: section)")
|
|
1137
2007
|
.option("--isFooter", "Mark this section as the store footer (only for type: section)")
|
|
1138
2008
|
.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
|
-
|
|
2009
|
+
'Example: \'[{"name":"title","type":"TEXT","required":true,"defaultValue":"Hello","groupId":"basic"}]\'')
|
|
1140
2010
|
.action((options) => {
|
|
1141
2011
|
addComponent(options.name, options);
|
|
1142
2012
|
});
|
|
@@ -1155,7 +2025,11 @@ export function createConfigCommand() {
|
|
|
1155
2025
|
.option("--typeId <typeId>", "Type ID for TYPE props (required when type is TYPE)")
|
|
1156
2026
|
.option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
|
|
1157
2027
|
.option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
|
|
1158
|
-
.option("--privateVarMap <json>",
|
|
2028
|
+
.option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)")
|
|
2029
|
+
.option("--numberRangeData <json>", 'JSON slider config for NUMBER_RANGE props, e.g. \'{"min":0,"max":100,"interval":5,"unit":"px"}\'')
|
|
2030
|
+
.option("--requiredProp <propName>", 'Show this only while the named BOOLEAN or ENUM prop satisfies the condition ("Koşullu Prop")')
|
|
2031
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2032
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1159
2033
|
.action((options) => {
|
|
1160
2034
|
addProp({ id: options.componentId, name: options.component }, options);
|
|
1161
2035
|
});
|
|
@@ -1175,6 +2049,10 @@ export function createConfigCommand() {
|
|
|
1175
2049
|
.option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (use 'none' to clear)")
|
|
1176
2050
|
.option("--filteredComponentIds <json>", "JSON array of component IDs (use 'none' to clear)")
|
|
1177
2051
|
.option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (use 'none' to clear)")
|
|
2052
|
+
.option("--numberRangeData <json>", "JSON slider config for NUMBER_RANGE props (use 'none' to clear)")
|
|
2053
|
+
.option("--requiredProp <propName>", "BOOLEAN or ENUM prop that gates this prop in the editor panel (use 'none' to clear)")
|
|
2054
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2055
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1178
2056
|
.action((options) => {
|
|
1179
2057
|
updateProp({ id: options.componentId, name: options.component }, options);
|
|
1180
2058
|
});
|
|
@@ -1216,6 +2094,9 @@ export function createConfigCommand() {
|
|
|
1216
2094
|
.requiredOption("--name <name>", "Display name in editor")
|
|
1217
2095
|
.option("--description <description>", "Group description")
|
|
1218
2096
|
.option("--parent <parentId>", "Parent group ID (for nesting, max 1 level)")
|
|
2097
|
+
.option("--requiredProp <propName>", 'Show this only while the named BOOLEAN or ENUM prop satisfies the condition ("Koşullu Prop")')
|
|
2098
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2099
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1219
2100
|
.action((options) => {
|
|
1220
2101
|
addPropGroup({ id: options.componentId, name: options.component }, options);
|
|
1221
2102
|
});
|
|
@@ -1227,6 +2108,9 @@ export function createConfigCommand() {
|
|
|
1227
2108
|
.requiredOption("--id <id>", "Group ID to update")
|
|
1228
2109
|
.option("--name <name>", "New display name")
|
|
1229
2110
|
.option("--description <description>", "New description")
|
|
2111
|
+
.option("--requiredProp <propName>", "BOOLEAN or ENUM prop that gates this group in the editor panel (use 'none' to clear)")
|
|
2112
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2113
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1230
2114
|
.action((options) => {
|
|
1231
2115
|
updatePropGroup({ id: options.componentId, name: options.component }, options);
|
|
1232
2116
|
});
|
|
@@ -1246,7 +2130,7 @@ export function createConfigCommand() {
|
|
|
1246
2130
|
.option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1247
2131
|
.requiredOption("--id <id>", "Group ID to move")
|
|
1248
2132
|
.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))
|
|
2133
|
+
.option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", (v) => parseInt(v, 10))
|
|
1250
2134
|
.action((options) => {
|
|
1251
2135
|
movePropGroup({ id: options.componentId, name: options.component }, options);
|
|
1252
2136
|
});
|
|
@@ -1306,54 +2190,15 @@ export function createConfigCommand() {
|
|
|
1306
2190
|
* Returns the matching type (with enumOptions) if found, null otherwise.
|
|
1307
2191
|
*/
|
|
1308
2192
|
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;
|
|
2193
|
+
const types = await fetchLiveEditorTypes();
|
|
2194
|
+
const match = types?.find((t) => t.category === "enum" && t.id === enumId);
|
|
2195
|
+
return match
|
|
2196
|
+
? {
|
|
2197
|
+
id: match.id,
|
|
2198
|
+
name: match.name,
|
|
2199
|
+
...(match.enumOptions ? { enumOptions: match.enumOptions } : {}),
|
|
1333
2200
|
}
|
|
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
|
-
});
|
|
2201
|
+
: null;
|
|
1357
2202
|
}
|
|
1358
2203
|
/**
|
|
1359
2204
|
* Best-effort lookup of an enum by PascalCase name in the live editor session.
|
|
@@ -1363,8 +2208,20 @@ async function fetchLiveEditorEnumById(enumId) {
|
|
|
1363
2208
|
* must treat null as "could not verify, fall back to on-disk check only".
|
|
1364
2209
|
*/
|
|
1365
2210
|
async function fetchLiveEditorEnumByName(pascalName) {
|
|
2211
|
+
const types = await fetchLiveEditorTypes();
|
|
2212
|
+
const match = types?.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
|
|
2213
|
+
return match ? { id: match.id, name: match.name } : null;
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Best-effort fetch of the editor's synced type list (with category /
|
|
2217
|
+
* sectionAllowed / enumOptions metadata). Returns null on any failure (no dev
|
|
2218
|
+
* server, timeout) — callers must treat null as "could not verify, allow".
|
|
2219
|
+
*/
|
|
2220
|
+
async function fetchLiveEditorTypes() {
|
|
1366
2221
|
const { WebSocket } = await import("ws");
|
|
1367
|
-
|
|
2222
|
+
// `client=control` keeps this short-lived socket out of the dev server's
|
|
2223
|
+
// `editor-action` target pool — it only reads types, it can't apply commands.
|
|
2224
|
+
const WS_URL = "ws://localhost:5201/?client=control";
|
|
1368
2225
|
const TIMEOUT_MS = 1500;
|
|
1369
2226
|
return new Promise((resolve) => {
|
|
1370
2227
|
let ws;
|
|
@@ -1397,9 +2254,7 @@ async function fetchLiveEditorEnumByName(pascalName) {
|
|
|
1397
2254
|
const message = JSON.parse(data.toString());
|
|
1398
2255
|
if (message.type === "types-list") {
|
|
1399
2256
|
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);
|
|
2257
|
+
finish(message.payload?.types || []);
|
|
1403
2258
|
}
|
|
1404
2259
|
}
|
|
1405
2260
|
catch {
|
|
@@ -1412,9 +2267,52 @@ async function fetchLiveEditorEnumByName(pascalName) {
|
|
|
1412
2267
|
});
|
|
1413
2268
|
});
|
|
1414
2269
|
}
|
|
2270
|
+
/**
|
|
2271
|
+
* Definition-time guard for TYPE-prop typeIds, verified against the live
|
|
2272
|
+
* editor's type list (skipped when the dev server is unreachable or the type
|
|
2273
|
+
* is unknown to the editor). Two rules:
|
|
2274
|
+
* - deprecated types are rejected on ANY component: they are legacy
|
|
2275
|
+
* render-only types — old themes may still carry values, but new props must
|
|
2276
|
+
* not be defined with them.
|
|
2277
|
+
* - on section components, only section-allowed (style) types pass — any other
|
|
2278
|
+
* type renders an empty placeholder merchants cannot fill, and section-level
|
|
2279
|
+
* bindings cannot supply a value either, so the prop would be dead weight.
|
|
2280
|
+
* On violation prints the standard { success: false } envelope and exits.
|
|
2281
|
+
*/
|
|
2282
|
+
async function assertTypeIdsUsable(componentType, typeIds) {
|
|
2283
|
+
if (typeIds.length === 0)
|
|
2284
|
+
return;
|
|
2285
|
+
const types = await fetchLiveEditorTypes();
|
|
2286
|
+
if (!types)
|
|
2287
|
+
return;
|
|
2288
|
+
const matches = typeIds.flatMap((typeId) => {
|
|
2289
|
+
const match = types.find((t) => t.id === typeId);
|
|
2290
|
+
return match ? [match] : [];
|
|
2291
|
+
});
|
|
2292
|
+
const legacy = matches.find((t) => t.deprecated === true);
|
|
2293
|
+
if (legacy) {
|
|
2294
|
+
console.log(JSON.stringify({
|
|
2295
|
+
success: false,
|
|
2296
|
+
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).`,
|
|
2297
|
+
}));
|
|
2298
|
+
process.exit(1);
|
|
2299
|
+
}
|
|
2300
|
+
if (componentType !== "section")
|
|
2301
|
+
return;
|
|
2302
|
+
const disallowed = matches.find((t) => t.sectionAllowed === false);
|
|
2303
|
+
if (!disallowed)
|
|
2304
|
+
return;
|
|
2305
|
+
console.log(JSON.stringify({
|
|
2306
|
+
success: false,
|
|
2307
|
+
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.`,
|
|
2308
|
+
}));
|
|
2309
|
+
process.exit(1);
|
|
2310
|
+
}
|
|
1415
2311
|
async function listTypes(componentType) {
|
|
1416
2312
|
const { WebSocket } = await import("ws");
|
|
1417
|
-
|
|
2313
|
+
// `client=control` keeps this short-lived socket out of the dev server's
|
|
2314
|
+
// `editor-action` target pool — it only reads types, it can't apply commands.
|
|
2315
|
+
const WS_URL = "ws://localhost:5201/?client=control";
|
|
1418
2316
|
const TIMEOUT_MS = 5000;
|
|
1419
2317
|
await new Promise((resolve) => {
|
|
1420
2318
|
let ws;
|
|
@@ -1424,7 +2322,7 @@ async function listTypes(componentType) {
|
|
|
1424
2322
|
catch {
|
|
1425
2323
|
console.log(JSON.stringify({
|
|
1426
2324
|
success: false,
|
|
1427
|
-
error: "Failed to connect to dev server. Make sure 'ikas
|
|
2325
|
+
error: "Failed to connect to dev server. Make sure 'ikas theme dev' is running.",
|
|
1428
2326
|
}));
|
|
1429
2327
|
process.exit(1);
|
|
1430
2328
|
return;
|
|
@@ -1445,7 +2343,8 @@ async function listTypes(componentType) {
|
|
|
1445
2343
|
const message = JSON.parse(data.toString());
|
|
1446
2344
|
if (message.type === "types-list") {
|
|
1447
2345
|
clearTimeout(timeout);
|
|
1448
|
-
|
|
2346
|
+
// Legacy render-only types are never offered for new props.
|
|
2347
|
+
let types = message.payload.types.filter((t) => !t.deprecated);
|
|
1449
2348
|
if (componentType === "section") {
|
|
1450
2349
|
types = types.filter((t) => t.sectionAllowed);
|
|
1451
2350
|
}
|
|
@@ -1465,7 +2364,7 @@ async function listTypes(componentType) {
|
|
|
1465
2364
|
clearTimeout(timeout);
|
|
1466
2365
|
console.log(JSON.stringify({
|
|
1467
2366
|
success: false,
|
|
1468
|
-
error: "Failed to connect to dev server. Make sure 'ikas
|
|
2367
|
+
error: "Failed to connect to dev server. Make sure 'ikas theme dev' is running.",
|
|
1469
2368
|
}));
|
|
1470
2369
|
process.exit(1);
|
|
1471
2370
|
});
|