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