@ikas/component-cli 1.4.0-beta.2 → 1.4.0-beta.200
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-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 +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 +1235 -228
- 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 +73 -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 +398 -53
- 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/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-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-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 +31 -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/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 +70 -0
- package/dist/commands/update-global-variable.js.map +1 -0
- package/dist/commands/update-page-sections.d.ts +3 -0
- package/dist/commands/update-page-sections.d.ts.map +1 -0
- package/dist/commands/update-page-sections.js +39 -0
- package/dist/commands/update-page-sections.js.map +1 -0
- package/dist/commands/update-section-prop.d.ts +3 -0
- package/dist/commands/update-section-prop.d.ts.map +1 -0
- package/dist/commands/update-section-prop.js +76 -0
- package/dist/commands/update-section-prop.js.map +1 -0
- package/dist/commands/upload-image.d.ts +3 -0
- package/dist/commands/upload-image.d.ts.map +1 -0
- package/dist/commands/upload-image.js +38 -0
- package/dist/commands/upload-image.js.map +1 -0
- package/dist/commands/upload-images.d.ts +3 -0
- package/dist/commands/upload-images.d.ts.map +1 -0
- package/dist/commands/upload-images.js +48 -0
- package/dist/commands/upload-images.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +52 -1
- 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 +750 -117
- package/dist/utils/compile.js.map +1 -1
- package/dist/utils/component-helpers.d.ts +148 -3
- package/dist/utils/component-helpers.d.ts.map +1 -1
- package/dist/utils/component-helpers.js +376 -29
- 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 +29 -0
- package/dist/utils/editor-action-client.d.ts.map +1 -0
- package/dist/utils/editor-action-client.js +137 -0
- package/dist/utils/editor-action-client.js.map +1 -0
- 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 +134 -3
- package/dist/utils/websocket-server.d.ts.map +1 -1
- package/dist/utils/websocket-server.js +279 -24
- 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 -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,8 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { runEditorAction } from "../utils/editor-action-client.js";
|
|
5
|
+
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
6
|
function loadConfig() {
|
|
6
7
|
const configPath = path.resolve(process.cwd(), "ikas.config.json");
|
|
7
8
|
if (!fs.existsSync(configPath)) {
|
|
@@ -17,13 +18,90 @@ function loadConfig() {
|
|
|
17
18
|
function saveConfig(configPath, config) {
|
|
18
19
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a component by canonical id (preferred) or exact PascalCase name.
|
|
23
|
+
* No silent normalization: pass --id from `config list-components`, or pass --name
|
|
24
|
+
* exactly as stored in ikas.config.json. Exits with a structured JSON error
|
|
25
|
+
* (listing valid {id, name} pairs) if zero/both/missing.
|
|
26
|
+
*/
|
|
27
|
+
function resolveComponent(config, ref) {
|
|
28
|
+
const id = ref.id?.trim() || undefined;
|
|
29
|
+
const name = ref.name?.trim() || undefined;
|
|
30
|
+
if (!id && !name) {
|
|
31
|
+
console.log(JSON.stringify({
|
|
32
|
+
success: false,
|
|
33
|
+
error: "Component reference required: pass --id (preferred) or --name (exact PascalCase). " +
|
|
34
|
+
"Run `ikas-component config list-components` to see canonical ids.",
|
|
35
|
+
validComponents: config.components.map((c) => ({
|
|
36
|
+
id: c.id,
|
|
37
|
+
name: c.name,
|
|
38
|
+
})),
|
|
39
|
+
}));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
if (id && name) {
|
|
43
|
+
console.log(JSON.stringify({
|
|
44
|
+
success: false,
|
|
45
|
+
error: "Specify only one of --id or --name, not both.",
|
|
46
|
+
}));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const found = id
|
|
50
|
+
? config.components.find((c) => c.id === id)
|
|
51
|
+
: config.components.find((c) => c.name === name);
|
|
52
|
+
if (!found) {
|
|
53
|
+
console.log(JSON.stringify({
|
|
54
|
+
success: false,
|
|
55
|
+
error: id
|
|
56
|
+
? `Component not found: id="${id}".`
|
|
57
|
+
: `Component not found: name="${name}". Names are matched exactly (PascalCase, no normalization).`,
|
|
58
|
+
lookup: id ? { id } : { name },
|
|
59
|
+
validComponents: config.components.map((c) => ({
|
|
60
|
+
id: c.id,
|
|
61
|
+
name: c.name,
|
|
62
|
+
})),
|
|
63
|
+
hint: "Use --id (preferred) or exact PascalCase --name. " +
|
|
64
|
+
"Run `ikas-component config list-components` to see canonical ids.",
|
|
65
|
+
}));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
return found;
|
|
23
69
|
}
|
|
24
70
|
function getComponentNames(config) {
|
|
25
71
|
return config.components.map((c) => c.name);
|
|
26
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Emit a structured JSON error for unknown filteredComponentIds entries and exit non-zero.
|
|
75
|
+
* Lists valid `{ id, name }` pairs so the LLM/caller can recover.
|
|
76
|
+
*/
|
|
77
|
+
function emitFilteredComponentIdsError(unknown, components) {
|
|
78
|
+
console.log(JSON.stringify({
|
|
79
|
+
success: false,
|
|
80
|
+
error: `Unknown filteredComponentIds: ${unknown.join(", ")}. ` +
|
|
81
|
+
`Component ids are opaque and cannot be derived from names — they must come from the ` +
|
|
82
|
+
`componentId returned by 'config add-component' or from 'config list'.`,
|
|
83
|
+
validComponents: components.map((c) => ({ id: c.id, name: c.name })),
|
|
84
|
+
}));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate `input` ids against `config.components` and return the same array on success.
|
|
89
|
+
* Exits with a JSON error if any id is unknown. Use at every write site that accepts
|
|
90
|
+
* filteredComponentIds from user/LLM input.
|
|
91
|
+
*/
|
|
92
|
+
function assertKnownComponentIds(input, config) {
|
|
93
|
+
const components = config?.components ?? [];
|
|
94
|
+
const { unknown } = validateFilteredComponentIds(input, components);
|
|
95
|
+
if (unknown.length > 0) {
|
|
96
|
+
emitFilteredComponentIdsError(unknown, components);
|
|
97
|
+
}
|
|
98
|
+
return input;
|
|
99
|
+
}
|
|
100
|
+
// The set of component ids that currently exist in the project, for validateComponentDefaultValue's
|
|
101
|
+
// existence check. Undefined when no config is loaded, which makes the check a no-op (back-compat).
|
|
102
|
+
function existingComponentIdSet(config) {
|
|
103
|
+
return config ? new Set(config.components.map((c) => c.id)) : undefined;
|
|
104
|
+
}
|
|
27
105
|
function regenerateTypes(component, componentType, config) {
|
|
28
106
|
const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
|
|
29
107
|
const typesPath = path.join(componentDir, "types.ts");
|
|
@@ -78,6 +156,125 @@ function camelCaseToDisplayName(name) {
|
|
|
78
156
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
79
157
|
.replace(/^./, (c) => c.toUpperCase());
|
|
80
158
|
}
|
|
159
|
+
const ALLOWED_PROP_FIELDS = new Set([
|
|
160
|
+
"name",
|
|
161
|
+
"displayName",
|
|
162
|
+
"type",
|
|
163
|
+
"required",
|
|
164
|
+
"description",
|
|
165
|
+
"defaultValue",
|
|
166
|
+
"groupId",
|
|
167
|
+
"typeId",
|
|
168
|
+
"enumTypeId",
|
|
169
|
+
"filteredComponentIds",
|
|
170
|
+
"privateVarMap",
|
|
171
|
+
"numberRangeData",
|
|
172
|
+
"requiredProp",
|
|
173
|
+
"requiredPropValue",
|
|
174
|
+
"requiredEnumOptionId",
|
|
175
|
+
"requiredEnumOptionValue",
|
|
176
|
+
]);
|
|
177
|
+
// Emit the standard CLI error envelope and exit(1) when a validator returned a message; a no-op
|
|
178
|
+
// when the message is null. Collapses the repeated `if (err) { console.log(...); process.exit(1) }`
|
|
179
|
+
// tail that follows every default-value validator call.
|
|
180
|
+
function failIf(error) {
|
|
181
|
+
if (error) {
|
|
182
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A `--requiredPropValue` flag as a boolean. Commander hands every flag over as a string, and this is
|
|
188
|
+
* the one place that turns it back; anything other than true/false is refused rather than coerced,
|
|
189
|
+
* since "0" or "off" silently becoming `true` is exactly the kind of gate nobody would think to
|
|
190
|
+
* check.
|
|
191
|
+
*/
|
|
192
|
+
function parseRequiredPropValue(raw, where) {
|
|
193
|
+
if (raw === undefined)
|
|
194
|
+
return undefined;
|
|
195
|
+
if (typeof raw === "boolean")
|
|
196
|
+
return raw;
|
|
197
|
+
if (raw === "true")
|
|
198
|
+
return true;
|
|
199
|
+
if (raw === "false")
|
|
200
|
+
return false;
|
|
201
|
+
failIf(`--requiredPropValue for ${where} must be "true" or "false" (got "${raw}").`);
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Validate/sanitize a numberRangeData object for a NUMBER_RANGE prop. Accepts an object
|
|
206
|
+
* or a JSON string. min/max/interval are truncated to integers; only a positive interval
|
|
207
|
+
* is kept (0/negative would yield an invalid slider step). Exits with a structured error
|
|
208
|
+
* on malformed input.
|
|
209
|
+
*/
|
|
210
|
+
function parseNumberRangeData(raw, propName) {
|
|
211
|
+
const fail = (error) => {
|
|
212
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
};
|
|
215
|
+
let obj = raw;
|
|
216
|
+
if (typeof raw === "string") {
|
|
217
|
+
try {
|
|
218
|
+
obj = JSON.parse(raw);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
fail(`Invalid --numberRangeData JSON for prop "${propName}": ${raw}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
225
|
+
fail(`numberRangeData for prop "${propName}" must be an object { min, max, interval?, unit? }.`);
|
|
226
|
+
}
|
|
227
|
+
const o = obj;
|
|
228
|
+
const asInt = (v, field) => {
|
|
229
|
+
if (v === undefined || v === null)
|
|
230
|
+
return undefined;
|
|
231
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
232
|
+
fail(`numberRangeData.${field} for prop "${propName}" must be a finite number.`);
|
|
233
|
+
}
|
|
234
|
+
return Math.trunc(v);
|
|
235
|
+
};
|
|
236
|
+
const min = asInt(o.min, "min");
|
|
237
|
+
const max = asInt(o.max, "max");
|
|
238
|
+
const interval = asInt(o.interval, "interval");
|
|
239
|
+
const unit = typeof o.unit === "string" && o.unit.trim() ? o.unit.trim() : undefined;
|
|
240
|
+
if (min !== undefined && max !== undefined && min >= max) {
|
|
241
|
+
fail(`numberRangeData for prop "${propName}": min (${min}) must be less than max (${max}).`);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
...(min !== undefined ? { min } : {}),
|
|
245
|
+
...(max !== undefined ? { max } : {}),
|
|
246
|
+
...(interval !== undefined && interval > 0 ? { interval } : {}),
|
|
247
|
+
...(unit ? { unit } : {}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const numberRangeDataRequiredError = (propName) => `numberRangeData with min and max is required for NUMBER_RANGE prop "${propName}". ` +
|
|
251
|
+
`Provide it, e.g. --numberRangeData '{"min":0,"max":100,"interval":5,"unit":"px"}' ` +
|
|
252
|
+
`(or a "numberRangeData" field inside --props). interval and unit are optional.`;
|
|
253
|
+
// A NUMBER_RANGE slider config is complete only with both bounds; interval/unit stay optional.
|
|
254
|
+
export const isNumberRangeComplete = (data) => data.min !== undefined && data.max !== undefined;
|
|
255
|
+
/**
|
|
256
|
+
* A NUMBER_RANGE prop must carry a slider config with at least min and max — the editor UI marks
|
|
257
|
+
* Minimum/Maksimum Değer as required, so a prop authored without them yields a half-configured
|
|
258
|
+
* slider. Enforce it at creation time: reject when numberRangeData is absent or missing min/max.
|
|
259
|
+
*/
|
|
260
|
+
export function requireNumberRangeData(raw, propName) {
|
|
261
|
+
if (raw === undefined) {
|
|
262
|
+
console.log(JSON.stringify({
|
|
263
|
+
success: false,
|
|
264
|
+
error: numberRangeDataRequiredError(propName),
|
|
265
|
+
}));
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
const parsed = parseNumberRangeData(raw, propName);
|
|
269
|
+
if (!isNumberRangeComplete(parsed)) {
|
|
270
|
+
console.log(JSON.stringify({
|
|
271
|
+
success: false,
|
|
272
|
+
error: numberRangeDataRequiredError(propName),
|
|
273
|
+
}));
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
return parsed;
|
|
277
|
+
}
|
|
81
278
|
/**
|
|
82
279
|
* Parse and validate the --props JSON flag for add-component.
|
|
83
280
|
* Returns validated ComponentProp[] or exits with error.
|
|
@@ -103,8 +300,20 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
103
300
|
}
|
|
104
301
|
const seenNames = new Set();
|
|
105
302
|
const props = [];
|
|
303
|
+
// Loop-invariant: build the known-id set once for the whole batch, not per prop.
|
|
304
|
+
const existingComponentIds = existingComponentIdSet(config);
|
|
106
305
|
for (const raw of rawProps) {
|
|
107
306
|
const r = raw;
|
|
307
|
+
const unknownFields = Object.keys(r).filter((k) => !ALLOWED_PROP_FIELDS.has(k));
|
|
308
|
+
if (unknownFields.length > 0) {
|
|
309
|
+
console.log(JSON.stringify({
|
|
310
|
+
success: false,
|
|
311
|
+
error: `Unknown field(s) in --props entry: ${unknownFields.join(", ")}. ` +
|
|
312
|
+
`Allowed: ${[...ALLOWED_PROP_FIELDS].join(", ")}. ` +
|
|
313
|
+
`Note: use "groupId" (not "group") and "defaultValue" (not "default") in --props JSON.`,
|
|
314
|
+
}));
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
108
317
|
if (!r.name || typeof r.name !== "string") {
|
|
109
318
|
console.log(JSON.stringify({
|
|
110
319
|
success: false,
|
|
@@ -145,6 +354,36 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
145
354
|
}));
|
|
146
355
|
process.exit(1);
|
|
147
356
|
}
|
|
357
|
+
// Dynamic (merchant-data reference) props can't carry a static default.
|
|
358
|
+
rejectDynamicDefault(r.name, propType, r.defaultValue);
|
|
359
|
+
// Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
|
|
360
|
+
if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
|
|
361
|
+
r.defaultValue !== undefined) {
|
|
362
|
+
const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
|
|
363
|
+
failIf(linkError);
|
|
364
|
+
}
|
|
365
|
+
// Validate SVG / SVG_LIST defaultValue (reject malformed/oversized/unsafe SVG)
|
|
366
|
+
if ((propType === "SVG" || propType === "SVG_LIST") &&
|
|
367
|
+
r.defaultValue !== undefined) {
|
|
368
|
+
const svgError = validateSvgDefaultValue(propType, r.defaultValue, r.name);
|
|
369
|
+
failIf(svgError);
|
|
370
|
+
}
|
|
371
|
+
// Validate COMPONENT / COMPONENT_LIST defaultValue (reference-only child code components)
|
|
372
|
+
if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
373
|
+
r.defaultValue !== undefined) {
|
|
374
|
+
failIf(validateComponentDefaultValue(propType, r.defaultValue, r.name, r.filteredComponentIds, existingComponentIds));
|
|
375
|
+
}
|
|
376
|
+
// Validate TYPE (structured/style) defaultValue shape — must be an object (or an array for an
|
|
377
|
+
// _array typeId), never a bare string that would be emitted as a wrong-shaped default.
|
|
378
|
+
if (propType === "TYPE" && r.defaultValue !== undefined) {
|
|
379
|
+
const typeError = validateTypeDefaultValue(r.defaultValue, r.name, r.typeId);
|
|
380
|
+
failIf(typeError);
|
|
381
|
+
}
|
|
382
|
+
// Validate NUMBER defaultValue — reject empty/non-finite (Number("") silently becomes 0).
|
|
383
|
+
if (propType === "NUMBER" && r.defaultValue !== undefined) {
|
|
384
|
+
const numError = validateNumberDefaultValue(r.defaultValue, r.name);
|
|
385
|
+
failIf(numError);
|
|
386
|
+
}
|
|
148
387
|
// Auto-generate displayName if omitted
|
|
149
388
|
const displayName = typeof r.displayName === "string" && r.displayName
|
|
150
389
|
? r.displayName
|
|
@@ -169,7 +408,10 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
169
408
|
process.exit(1);
|
|
170
409
|
}
|
|
171
410
|
// Validate enum type exists (checks config then live dev server)
|
|
172
|
-
if (config &&
|
|
411
|
+
if (config &&
|
|
412
|
+
configPath &&
|
|
413
|
+
typeof r.enumTypeId === "string" &&
|
|
414
|
+
!isBuiltInEnumTypeId(r.enumTypeId)) {
|
|
173
415
|
const resolved = await resolveEnumType(r.enumTypeId, config, configPath);
|
|
174
416
|
if (!resolved) {
|
|
175
417
|
console.log(JSON.stringify({
|
|
@@ -188,9 +430,7 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
188
430
|
...(typeof r.description === "string" && r.description
|
|
189
431
|
? { description: r.description }
|
|
190
432
|
: {}),
|
|
191
|
-
...(r.defaultValue !== undefined
|
|
192
|
-
? { defaultValue: r.defaultValue }
|
|
193
|
-
: {}),
|
|
433
|
+
...(r.defaultValue !== undefined ? { defaultValue: r.defaultValue } : {}),
|
|
194
434
|
...(typeof r.groupId === "string" && r.groupId
|
|
195
435
|
? { groupId: r.groupId }
|
|
196
436
|
: {}),
|
|
@@ -200,15 +440,47 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
200
440
|
...(propType === "ENUM" && typeof r.enumTypeId === "string"
|
|
201
441
|
? { enumTypeId: r.enumTypeId }
|
|
202
442
|
: {}),
|
|
203
|
-
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
204
|
-
|
|
443
|
+
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
444
|
+
r.filteredComponentIds &&
|
|
445
|
+
Array.isArray(r.filteredComponentIds)
|
|
446
|
+
? {
|
|
447
|
+
filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config),
|
|
448
|
+
}
|
|
449
|
+
: {}),
|
|
450
|
+
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
451
|
+
r.privateVarMap &&
|
|
452
|
+
typeof r.privateVarMap === "object"
|
|
453
|
+
? {
|
|
454
|
+
privateVarMap: r.privateVarMap,
|
|
455
|
+
}
|
|
456
|
+
: {}),
|
|
457
|
+
...(propType === "NUMBER_RANGE"
|
|
458
|
+
? { numberRangeData: requireNumberRangeData(r.numberRangeData, r.name) }
|
|
459
|
+
: {}),
|
|
460
|
+
...(r.requiredProp !== undefined
|
|
461
|
+
? { requiredProp: r.requiredProp }
|
|
462
|
+
: {}),
|
|
463
|
+
...(r.requiredPropValue !== undefined
|
|
464
|
+
? { requiredPropValue: r.requiredPropValue }
|
|
205
465
|
: {}),
|
|
206
|
-
...(
|
|
207
|
-
? {
|
|
466
|
+
...(r.requiredEnumOptionId !== undefined
|
|
467
|
+
? { requiredEnumOptionId: r.requiredEnumOptionId }
|
|
468
|
+
: {}),
|
|
469
|
+
...(r.requiredEnumOptionValue !== undefined
|
|
470
|
+
? { requiredEnumOptionValue: r.requiredEnumOptionValue }
|
|
208
471
|
: {}),
|
|
209
472
|
};
|
|
210
473
|
props.push(prop);
|
|
211
474
|
}
|
|
475
|
+
// Gates are checked once the whole batch is parsed: a prop may name one declared after it, and
|
|
476
|
+
// cycle detection needs every prop in hand. The batch IS the component's prop list here — the
|
|
477
|
+
// component itself does not exist yet — so it stands in as one.
|
|
478
|
+
const batch = { name: "the new component", props };
|
|
479
|
+
for (const prop of props) {
|
|
480
|
+
failIf(visibilityGateError(batch, prop, `prop "${prop.name}"`, {
|
|
481
|
+
selfPropName: prop.name,
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
212
484
|
return props;
|
|
213
485
|
}
|
|
214
486
|
// --- Subcommands ---
|
|
@@ -236,14 +508,29 @@ async function addComponent(name, options) {
|
|
|
236
508
|
if (!config.projectId) {
|
|
237
509
|
config.projectId = generateProjectId();
|
|
238
510
|
}
|
|
511
|
+
else if (!isValidProjectId(config.projectId)) {
|
|
512
|
+
// A malformed projectId (hand-edited config) breaks the editor's republish
|
|
513
|
+
// stability: published/local id prefixes stop matching and every publish
|
|
514
|
+
// INSERTs duplicate design elements. Fail fast instead of corrupting data.
|
|
515
|
+
console.log(JSON.stringify({
|
|
516
|
+
success: false,
|
|
517
|
+
error: `Invalid projectId "${config.projectId}" in ikas.config.json. ` +
|
|
518
|
+
"projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
|
|
519
|
+
"Restore the original generated value — do not hand-edit it.",
|
|
520
|
+
}));
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
// Parse --props (and validate filteredComponentIds) BEFORE any filesystem side-effects
|
|
524
|
+
// so a bad payload doesn't leave an empty component directory behind.
|
|
525
|
+
const props = options.props
|
|
526
|
+
? await parsePropsFlag(options.props, config, configPath)
|
|
527
|
+
: [];
|
|
528
|
+
// Reject section-disallowed TYPE props at definition time (best-effort, needs dev server)
|
|
529
|
+
await assertTypeIdsUsable(componentType, props.flatMap((p) => (p.type === "TYPE" && p.typeId ? [p.typeId] : [])));
|
|
239
530
|
const componentId = generateComponentId(config.projectId);
|
|
240
531
|
const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
|
|
241
532
|
// Create directory
|
|
242
533
|
fs.mkdirSync(componentDir, { recursive: true });
|
|
243
|
-
// Parse --props if provided, otherwise empty
|
|
244
|
-
const props = options.props
|
|
245
|
-
? await parsePropsFlag(options.props, config, configPath)
|
|
246
|
-
: [];
|
|
247
534
|
// Auto-create prop groups from groupId references in props
|
|
248
535
|
const referencedGroupIds = new Set();
|
|
249
536
|
for (const prop of props) {
|
|
@@ -294,16 +581,9 @@ async function addComponent(name, options) {
|
|
|
294
581
|
],
|
|
295
582
|
}));
|
|
296
583
|
}
|
|
297
|
-
async function addProp(
|
|
584
|
+
async function addProp(ref, options) {
|
|
298
585
|
const { config, configPath } = loadConfig();
|
|
299
|
-
const component =
|
|
300
|
-
if (!component) {
|
|
301
|
-
console.log(JSON.stringify({
|
|
302
|
-
success: false,
|
|
303
|
-
error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
|
|
304
|
-
}));
|
|
305
|
-
process.exit(1);
|
|
306
|
-
}
|
|
586
|
+
const component = resolveComponent(config, ref);
|
|
307
587
|
// Validate prop type
|
|
308
588
|
const propType = options.type.toUpperCase();
|
|
309
589
|
if (!PROP_TYPES.includes(propType)) {
|
|
@@ -337,6 +617,10 @@ async function addProp(componentName, options) {
|
|
|
337
617
|
}));
|
|
338
618
|
process.exit(1);
|
|
339
619
|
}
|
|
620
|
+
// Reject section-disallowed types at definition time (best-effort, needs dev server)
|
|
621
|
+
if (propType === "TYPE" && options.typeId) {
|
|
622
|
+
await assertTypeIdsUsable(component.type, [options.typeId]);
|
|
623
|
+
}
|
|
340
624
|
// Validate enumTypeId for ENUM props
|
|
341
625
|
if (propType === "ENUM" && !options.enumTypeId) {
|
|
342
626
|
console.log(JSON.stringify({
|
|
@@ -347,7 +631,9 @@ async function addProp(componentName, options) {
|
|
|
347
631
|
}
|
|
348
632
|
// Validate that enumTypeId references an existing enum type
|
|
349
633
|
// (checks config.customTypes first, then falls back to the live dev server)
|
|
350
|
-
if (propType === "ENUM" &&
|
|
634
|
+
if (propType === "ENUM" &&
|
|
635
|
+
options.enumTypeId &&
|
|
636
|
+
!isBuiltInEnumTypeId(options.enumTypeId)) {
|
|
351
637
|
const resolved = await resolveEnumType(options.enumTypeId, config, configPath);
|
|
352
638
|
if (!resolved) {
|
|
353
639
|
console.log(JSON.stringify({
|
|
@@ -377,49 +663,117 @@ async function addProp(componentName, options) {
|
|
|
377
663
|
}
|
|
378
664
|
// Parse filteredComponentIds JSON for COMPONENT/COMPONENT_LIST
|
|
379
665
|
let parsedFilteredIds;
|
|
380
|
-
if (options.filteredComponentIds &&
|
|
666
|
+
if (options.filteredComponentIds &&
|
|
667
|
+
(propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
|
|
381
668
|
try {
|
|
382
669
|
parsedFilteredIds = JSON.parse(options.filteredComponentIds);
|
|
383
670
|
if (!Array.isArray(parsedFilteredIds)) {
|
|
384
|
-
console.log(JSON.stringify({
|
|
671
|
+
console.log(JSON.stringify({
|
|
672
|
+
success: false,
|
|
673
|
+
error: "--filteredComponentIds must be a JSON array of strings.",
|
|
674
|
+
}));
|
|
385
675
|
process.exit(1);
|
|
386
676
|
}
|
|
387
677
|
}
|
|
388
678
|
catch {
|
|
389
|
-
console.log(JSON.stringify({
|
|
679
|
+
console.log(JSON.stringify({
|
|
680
|
+
success: false,
|
|
681
|
+
error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
|
|
682
|
+
}));
|
|
390
683
|
process.exit(1);
|
|
391
684
|
}
|
|
685
|
+
assertKnownComponentIds(parsedFilteredIds, config);
|
|
392
686
|
}
|
|
393
687
|
// Parse privateVarMap JSON for COMPONENT/COMPONENT_LIST
|
|
394
688
|
let parsedPrivateVarMap;
|
|
395
|
-
if (options.privateVarMap &&
|
|
689
|
+
if (options.privateVarMap &&
|
|
690
|
+
(propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
|
|
396
691
|
try {
|
|
397
692
|
parsedPrivateVarMap = JSON.parse(options.privateVarMap);
|
|
398
|
-
if (typeof parsedPrivateVarMap !== "object" ||
|
|
399
|
-
|
|
693
|
+
if (typeof parsedPrivateVarMap !== "object" ||
|
|
694
|
+
Array.isArray(parsedPrivateVarMap)) {
|
|
695
|
+
console.log(JSON.stringify({
|
|
696
|
+
success: false,
|
|
697
|
+
error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
|
|
698
|
+
}));
|
|
400
699
|
process.exit(1);
|
|
401
700
|
}
|
|
402
701
|
}
|
|
403
702
|
catch {
|
|
404
|
-
console.log(JSON.stringify({
|
|
703
|
+
console.log(JSON.stringify({
|
|
704
|
+
success: false,
|
|
705
|
+
error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
|
|
706
|
+
}));
|
|
405
707
|
process.exit(1);
|
|
406
708
|
}
|
|
407
709
|
}
|
|
710
|
+
rejectDynamicDefault(options.name, propType, options.defaultValue);
|
|
711
|
+
const parsedDefaultValue = options.defaultValue !== undefined
|
|
712
|
+
? parseDefaultValue(options.defaultValue, propType)
|
|
713
|
+
: undefined;
|
|
714
|
+
if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
|
|
715
|
+
parsedDefaultValue !== undefined) {
|
|
716
|
+
const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
|
|
717
|
+
failIf(linkError);
|
|
718
|
+
}
|
|
719
|
+
if ((propType === "SVG" || propType === "SVG_LIST") &&
|
|
720
|
+
parsedDefaultValue !== undefined) {
|
|
721
|
+
const svgError = validateSvgDefaultValue(propType, parsedDefaultValue, options.name);
|
|
722
|
+
failIf(svgError);
|
|
723
|
+
}
|
|
724
|
+
if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
725
|
+
parsedDefaultValue !== undefined) {
|
|
726
|
+
failIf(validateComponentDefaultValue(propType, parsedDefaultValue, options.name, parsedFilteredIds, existingComponentIdSet(config)));
|
|
727
|
+
}
|
|
728
|
+
if (propType === "TYPE" && parsedDefaultValue !== undefined) {
|
|
729
|
+
const typeError = validateTypeDefaultValue(parsedDefaultValue, options.name, options.typeId);
|
|
730
|
+
failIf(typeError);
|
|
731
|
+
}
|
|
732
|
+
// Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
|
|
733
|
+
if (propType === "NUMBER" && options.defaultValue !== undefined) {
|
|
734
|
+
const numError = validateNumberDefaultValue(options.defaultValue, options.name);
|
|
735
|
+
failIf(numError);
|
|
736
|
+
}
|
|
737
|
+
const parsedVisibilityValue = parseRequiredPropValue(options.requiredPropValue, `prop "${options.name}"`);
|
|
738
|
+
const parsedNumberRangeData = propType === "NUMBER_RANGE"
|
|
739
|
+
? requireNumberRangeData(options.numberRangeData, options.name)
|
|
740
|
+
: undefined;
|
|
741
|
+
if (propType === "NUMBER_RANGE" && parsedDefaultValue !== undefined) {
|
|
742
|
+
const nrError = validateNumberRangeDefaultValue(parsedDefaultValue, parsedNumberRangeData, options.name);
|
|
743
|
+
failIf(nrError);
|
|
744
|
+
}
|
|
408
745
|
const newProp = {
|
|
409
746
|
name: options.name,
|
|
410
747
|
displayName: options.displayName,
|
|
411
748
|
type: propType,
|
|
412
749
|
required: options.required ?? false,
|
|
413
750
|
...(options.description ? { description: options.description } : {}),
|
|
414
|
-
...(
|
|
415
|
-
? { defaultValue:
|
|
751
|
+
...(parsedDefaultValue !== undefined
|
|
752
|
+
? { defaultValue: parsedDefaultValue }
|
|
416
753
|
: {}),
|
|
417
754
|
...(options.group ? { groupId: options.group } : {}),
|
|
418
755
|
...(options.typeId ? { typeId: options.typeId } : {}),
|
|
419
756
|
...(options.enumTypeId ? { enumTypeId: options.enumTypeId } : {}),
|
|
420
757
|
...(parsedFilteredIds ? { filteredComponentIds: parsedFilteredIds } : {}),
|
|
421
758
|
...(parsedPrivateVarMap ? { privateVarMap: parsedPrivateVarMap } : {}),
|
|
759
|
+
...(parsedNumberRangeData
|
|
760
|
+
? { numberRangeData: parsedNumberRangeData }
|
|
761
|
+
: {}),
|
|
762
|
+
...(options.requiredProp ? { requiredProp: options.requiredProp } : {}),
|
|
763
|
+
...(parsedVisibilityValue !== undefined
|
|
764
|
+
? { requiredPropValue: parsedVisibilityValue }
|
|
765
|
+
: {}),
|
|
766
|
+
...(options.requiredProp && options.requiredEnumOptionValue !== undefined
|
|
767
|
+
? {
|
|
768
|
+
// The editor has no separate option id; the value doubles as one.
|
|
769
|
+
requiredEnumOptionId: options.requiredEnumOptionValue,
|
|
770
|
+
requiredEnumOptionValue: options.requiredEnumOptionValue,
|
|
771
|
+
}
|
|
772
|
+
: {}),
|
|
422
773
|
};
|
|
774
|
+
failIf(visibilityGateError(component, newProp, `prop "${newProp.name}"`, {
|
|
775
|
+
selfPropName: newProp.name,
|
|
776
|
+
}));
|
|
423
777
|
component.props.push(newProp);
|
|
424
778
|
saveConfig(configPath, config);
|
|
425
779
|
// Regenerate types.ts and global types
|
|
@@ -432,34 +786,448 @@ async function addProp(componentName, options) {
|
|
|
432
786
|
name: newProp.name,
|
|
433
787
|
displayName: newProp.displayName,
|
|
434
788
|
type: newProp.type,
|
|
435
|
-
required: newProp.required,
|
|
436
789
|
...(newProp.groupId ? { groupId: newProp.groupId } : {}),
|
|
437
790
|
...(newProp.enumTypeId ? { enumTypeId: newProp.enumTypeId } : {}),
|
|
438
|
-
...(newProp.filteredComponentIds
|
|
439
|
-
|
|
791
|
+
...(newProp.filteredComponentIds
|
|
792
|
+
? { filteredComponentIds: newProp.filteredComponentIds }
|
|
793
|
+
: {}),
|
|
794
|
+
...(newProp.privateVarMap
|
|
795
|
+
? { privateVarMap: newProp.privateVarMap }
|
|
796
|
+
: {}),
|
|
797
|
+
...(newProp.numberRangeData
|
|
798
|
+
? { numberRangeData: newProp.numberRangeData }
|
|
799
|
+
: {}),
|
|
800
|
+
...(newProp.requiredProp
|
|
801
|
+
? {
|
|
802
|
+
requiredProp: newProp.requiredProp,
|
|
803
|
+
...(newProp.requiredEnumOptionValue !== undefined
|
|
804
|
+
? { requiredEnumOptionValue: newProp.requiredEnumOptionValue }
|
|
805
|
+
: { requiredPropValue: newProp.requiredPropValue ?? true }),
|
|
806
|
+
}
|
|
807
|
+
: {}),
|
|
440
808
|
},
|
|
441
809
|
}));
|
|
442
810
|
}
|
|
443
|
-
function parseDefaultValue(value, propType) {
|
|
811
|
+
export function parseDefaultValue(value, propType) {
|
|
444
812
|
switch (propType) {
|
|
445
813
|
case "NUMBER":
|
|
446
814
|
return Number(value);
|
|
447
815
|
case "BOOLEAN":
|
|
448
816
|
return value === "true";
|
|
817
|
+
case "COMPONENT":
|
|
818
|
+
case "COMPONENT_LIST":
|
|
819
|
+
case "LINK":
|
|
820
|
+
case "LIST_OF_LINK":
|
|
821
|
+
case "SVG_LIST":
|
|
822
|
+
case "NUMBER_RANGE":
|
|
823
|
+
case "TYPE":
|
|
824
|
+
// These all carry a typed value (object/array), not a scalar. The --defaultValue flag arrives
|
|
825
|
+
// as a string, so parse it here; if it is not valid JSON, return the raw string so the type's
|
|
826
|
+
// validator (validateLinkDefaultValue / validateSvgDefaultValue / validateNumberRangeDefaultValue
|
|
827
|
+
// / validateTypeDefaultValue / validateComponentDefaultValue) can report a precise error instead
|
|
828
|
+
// of silently storing a bad shape.
|
|
829
|
+
try {
|
|
830
|
+
return JSON.parse(value);
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
return value;
|
|
834
|
+
}
|
|
449
835
|
default:
|
|
450
836
|
return value;
|
|
451
837
|
}
|
|
452
838
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
839
|
+
// Prop types whose value is a dynamic reference to merchant storefront data (products, media,
|
|
840
|
+
// categories, brands, blogs, raffles). They resolve at storefront render time, so a static config
|
|
841
|
+
// default is invalid and is rejected on add/update-prop. Keep in sync with @ikas/editor-models
|
|
842
|
+
// DYNAMIC_PROP_TYPES (the CLI must not depend on that private package).
|
|
843
|
+
export const DYNAMIC_PROP_TYPES = [
|
|
844
|
+
"IMAGE",
|
|
845
|
+
"IMAGE_LIST",
|
|
846
|
+
"VIDEO",
|
|
847
|
+
"PRODUCT",
|
|
848
|
+
"PRODUCT_LIST",
|
|
849
|
+
"PRODUCT_ATTRIBUTE",
|
|
850
|
+
"PRODUCT_ATTRIBUTE_LIST",
|
|
851
|
+
"BRAND",
|
|
852
|
+
"BRAND_LIST",
|
|
853
|
+
"CATEGORY",
|
|
854
|
+
"CATEGORY_LIST",
|
|
855
|
+
"BLOG",
|
|
856
|
+
"BLOG_LIST",
|
|
857
|
+
"BLOG_CATEGORY",
|
|
858
|
+
"BLOG_CATEGORY_LIST",
|
|
859
|
+
"RAFFLE",
|
|
860
|
+
"RAFFLE_LIST",
|
|
861
|
+
];
|
|
862
|
+
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.`;
|
|
863
|
+
// Reject a static default on a dynamic (merchant-data reference) prop type: print the standard
|
|
864
|
+
// error and exit. No-op when the type isn't dynamic or no default was supplied.
|
|
865
|
+
export const rejectDynamicDefault = (propName, propType, defaultValue) => {
|
|
866
|
+
if (DYNAMIC_PROP_TYPES.includes(propType) && defaultValue !== undefined) {
|
|
457
867
|
console.log(JSON.stringify({
|
|
458
868
|
success: false,
|
|
459
|
-
error:
|
|
869
|
+
error: dynamicPropDefaultError(propName, propType),
|
|
460
870
|
}));
|
|
461
871
|
process.exit(1);
|
|
462
872
|
}
|
|
873
|
+
};
|
|
874
|
+
const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
|
|
875
|
+
// FILE is intentionally excluded from CLI-authored defaults: a code-component / AI-authored default
|
|
876
|
+
// can't reference a store-specific uploaded file, so a "FILE" link default is always rejected. Store
|
|
877
|
+
// owners can still create FILE links manually in the editor (editor-core keeps FILE in its own set).
|
|
878
|
+
const VALID_LINK_TYPES = ["PAGE", "EXTERNAL"];
|
|
879
|
+
const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
|
|
880
|
+
'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
|
|
881
|
+
"Do NOT pass a JSON string or a { label, href } shape.";
|
|
882
|
+
// Returns the index of the first array element that fails `fn`, as an error string, or null.
|
|
883
|
+
const firstError = (arr, fn) => arr.reduce((acc, item, index) => acc ?? fn(item, index), null);
|
|
884
|
+
const validateSingleLink = (link, propName, path) => {
|
|
885
|
+
const where = `defaultValue for prop "${propName}"${path}`;
|
|
886
|
+
if (typeof link === "string") {
|
|
887
|
+
return `${where} is a JSON string but must be an object. ${LINK_SHAPE_HELP}`;
|
|
888
|
+
}
|
|
889
|
+
if (!link || typeof link !== "object" || Array.isArray(link)) {
|
|
890
|
+
return `${where} must be a link object. ${LINK_SHAPE_HELP}`;
|
|
891
|
+
}
|
|
892
|
+
const l = link;
|
|
893
|
+
if ("href" in l && !("linkType" in l)) {
|
|
894
|
+
return `${where} uses a legacy { href } shape without "linkType". Use "linkType" + "externalLink"/"pageType" instead. ${LINK_SHAPE_HELP}`;
|
|
895
|
+
}
|
|
896
|
+
if (typeof l.linkType !== "string" ||
|
|
897
|
+
!VALID_LINK_TYPES.includes(l.linkType)) {
|
|
898
|
+
return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
|
|
899
|
+
}
|
|
900
|
+
// Require the destination field for the declared linkType, so a link that points nowhere never
|
|
901
|
+
// reaches a published theme. A PAGE default carries a pageType (pageId is per-store, filled on
|
|
902
|
+
// materialization); pageId alone is also accepted for a specific page.
|
|
903
|
+
if (l.linkType === "PAGE" && !nonEmpty(l.pageType) && !nonEmpty(l.pageId)) {
|
|
904
|
+
return `${where}: a "PAGE" link needs a "pageType" (e.g. "INDEX") — or a "pageId" for a specific page. ${LINK_SHAPE_HELP}`;
|
|
905
|
+
}
|
|
906
|
+
if (l.linkType === "EXTERNAL" && !nonEmpty(l.externalLink)) {
|
|
907
|
+
return `${where}: an "EXTERNAL" link needs a non-empty "externalLink" URL. ${LINK_SHAPE_HELP}`;
|
|
908
|
+
}
|
|
909
|
+
if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
|
|
910
|
+
return `${where}: "subLinks" must be an array (use [] when there are none).`;
|
|
911
|
+
}
|
|
912
|
+
if (Array.isArray(l.subLinks)) {
|
|
913
|
+
return firstError(l.subLinks, (sl, i) => validateSingleLink(sl, propName, `${path}.subLinks[${i}]`));
|
|
914
|
+
}
|
|
915
|
+
return null;
|
|
916
|
+
};
|
|
917
|
+
// Validates a LINK / LIST_OF_LINK defaultValue. Returns an error message, or null if valid.
|
|
918
|
+
// These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
|
|
919
|
+
// legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
|
|
920
|
+
// out of published themes.
|
|
921
|
+
export function validateLinkDefaultValue(propType, value, propName) {
|
|
922
|
+
if (value === undefined || value === null)
|
|
923
|
+
return null;
|
|
924
|
+
if (propType === "LINK") {
|
|
925
|
+
return validateSingleLink(value, propName, "");
|
|
926
|
+
}
|
|
927
|
+
// LIST_OF_LINK
|
|
928
|
+
if (typeof value === "string") {
|
|
929
|
+
return `defaultValue for prop "${propName}" is a JSON string but must be an object { "links": [...] }. ${LINK_SHAPE_HELP}`;
|
|
930
|
+
}
|
|
931
|
+
if (!value ||
|
|
932
|
+
typeof value !== "object" ||
|
|
933
|
+
Array.isArray(value) ||
|
|
934
|
+
!Array.isArray(value.links)) {
|
|
935
|
+
return `defaultValue for prop "${propName}" must be an object with a "links" array: { "links": [ <link>, ... ] }. ${LINK_SHAPE_HELP}`;
|
|
936
|
+
}
|
|
937
|
+
const links = value.links;
|
|
938
|
+
return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
|
|
939
|
+
}
|
|
940
|
+
const COMPONENT_SHAPE_HELP = "A COMPONENT / COMPONENT_LIST default must be an object listing child code components: " +
|
|
941
|
+
'{ "components": [ { "codeComponentId": "<id>" } ] }. Only code components are allowed; each entry ' +
|
|
942
|
+
'needs a string "codeComponentId" (no componentId, no inline propValues).';
|
|
943
|
+
/**
|
|
944
|
+
* Validate a COMPONENT / COMPONENT_LIST defaultValue. Reference-only: each entry must name a code
|
|
945
|
+
* component via `codeComponentId`. Rejects non-CC (`componentId`) children, missing ids, more than
|
|
946
|
+
* one entry for a single COMPONENT, and ids outside the prop's filteredComponentIds allowlist.
|
|
947
|
+
* Returns an error message, or null if valid.
|
|
948
|
+
*/
|
|
949
|
+
export function validateComponentDefaultValue(propType, value, propName, filteredComponentIds, existingComponentIds) {
|
|
950
|
+
if (value === undefined || value === null)
|
|
951
|
+
return null;
|
|
952
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
953
|
+
if (typeof value === "string") {
|
|
954
|
+
return `${where} is a JSON string but must be an object. ${COMPONENT_SHAPE_HELP}`;
|
|
955
|
+
}
|
|
956
|
+
if (!value ||
|
|
957
|
+
typeof value !== "object" ||
|
|
958
|
+
Array.isArray(value) ||
|
|
959
|
+
!Array.isArray(value.components)) {
|
|
960
|
+
return `${where} must be an object with a "components" array. ${COMPONENT_SHAPE_HELP}`;
|
|
961
|
+
}
|
|
962
|
+
const components = value.components;
|
|
963
|
+
if (propType === "COMPONENT" && components.length > 1) {
|
|
964
|
+
return `${where}: a COMPONENT prop holds a single child — provide at most one entry (use COMPONENT_LIST for many).`;
|
|
965
|
+
}
|
|
966
|
+
const allow = filteredComponentIds && filteredComponentIds.length
|
|
967
|
+
? new Set(filteredComponentIds)
|
|
968
|
+
: null;
|
|
969
|
+
return firstError(components, (entry, i) => {
|
|
970
|
+
const at = `${where}.components[${i}]`;
|
|
971
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
972
|
+
return `${at} must be an object { "codeComponentId": "<id>" }. ${COMPONENT_SHAPE_HELP}`;
|
|
973
|
+
}
|
|
974
|
+
const e = entry;
|
|
975
|
+
if ("componentId" in e && !("codeComponentId" in e)) {
|
|
976
|
+
return `${at} references a non-code component (componentId). Only code components are allowed as defaults. ${COMPONENT_SHAPE_HELP}`;
|
|
977
|
+
}
|
|
978
|
+
if (typeof e.codeComponentId !== "string" ||
|
|
979
|
+
e.codeComponentId.trim().length === 0) {
|
|
980
|
+
return `${at} must have a non-empty string "codeComponentId". ${COMPONENT_SHAPE_HELP}`;
|
|
981
|
+
}
|
|
982
|
+
if (allow && !allow.has(e.codeComponentId)) {
|
|
983
|
+
return `${at}: "${e.codeComponentId}" is not in this prop's filteredComponentIds allowlist.`;
|
|
984
|
+
}
|
|
985
|
+
// Reject a reference to a component that does not exist in the project (deleted, renamed, or
|
|
986
|
+
// never created). A dangling default would seed a ghost entry on every placement that renders
|
|
987
|
+
// nothing. Only enforced when the caller supplies the known-component set (the config-backed
|
|
988
|
+
// add-prop / update-prop / batch sites); pure-shape callers pass no set and skip this check.
|
|
989
|
+
if (existingComponentIds && !existingComponentIds.has(e.codeComponentId)) {
|
|
990
|
+
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).`;
|
|
991
|
+
}
|
|
992
|
+
return null;
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
const SVG_DEFAULT_HELP = "An SVG default must be a single well-formed <svg>…</svg> string ≤ 64 KB with no embedded base64 raster images. " +
|
|
996
|
+
'Tip: the editor uses each svg\'s `class` attribute as its display name — set class="my-icon" to name it; ' +
|
|
997
|
+
"SVG_LIST items with no class show as svg-1, svg-2, ….";
|
|
998
|
+
// Keep the cap and base64 pattern in sync with @ikas/editor-models validate-svg.ts (the
|
|
999
|
+
// authoritative validator). Duplicated here on purpose — see validateSvgShape below.
|
|
1000
|
+
const SVG_MAX_SIZE_KB = 64;
|
|
1001
|
+
const SVG_BASE64_RASTER_RE = /data:image\/[a-z0-9.+-]+;base64/i;
|
|
1002
|
+
const SVG_SHAPE_RE = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>][\s\S]*<\/svg>\s*$/i;
|
|
1003
|
+
// Count TOP-LEVEL <svg> roots without a DOM: scan svg open/close tags in order and track
|
|
1004
|
+
// nesting depth. A root is any <svg> opened at depth 0. This matches the editor's DOM-based
|
|
1005
|
+
// single-root check — it rejects siblings (`<svg/><svg/>`) while allowing a nested <svg>.
|
|
1006
|
+
// Self-closing <svg .../> counts as a root but opens no nesting level.
|
|
1007
|
+
function svgRootCount(input) {
|
|
1008
|
+
const tags = input.match(/<svg\b[^>]*>|<\/svg\s*>/gi) || [];
|
|
1009
|
+
return tags.reduce((acc, tag) => {
|
|
1010
|
+
if (tag.startsWith("</"))
|
|
1011
|
+
return { depth: Math.max(0, acc.depth - 1), roots: acc.roots };
|
|
1012
|
+
const roots = acc.depth === 0 ? acc.roots + 1 : acc.roots;
|
|
1013
|
+
const depth = /\/>\s*$/.test(tag) ? acc.depth : acc.depth + 1;
|
|
1014
|
+
return { depth, roots };
|
|
1015
|
+
}, { depth: 0, roots: 0 }).roots;
|
|
1016
|
+
}
|
|
1017
|
+
// Lightweight, dependency-free SVG structure check. This standalone CLI is publishable and
|
|
1018
|
+
// must not depend on the private @ikas/editor-models package (nor pull a DOM/DOMPurify into a
|
|
1019
|
+
// CLI), so it mirrors the RULES of the editor's validateSvg (size, base64, single <svg> root)
|
|
1020
|
+
// for fast authoring feedback. The authoritative sanitize runs in the editor/upload path.
|
|
1021
|
+
// Returns a rejection reason, or null if the shape is acceptable.
|
|
1022
|
+
function validateSvgShape(input) {
|
|
1023
|
+
if (typeof input !== "string" || input.trim() === "")
|
|
1024
|
+
return "empty or not a string";
|
|
1025
|
+
if (Buffer.byteLength(input, "utf8") > SVG_MAX_SIZE_KB * 1024)
|
|
1026
|
+
return `exceeds ${SVG_MAX_SIZE_KB} KB`;
|
|
1027
|
+
if (SVG_BASE64_RASTER_RE.test(input))
|
|
1028
|
+
return "embedded base64 raster image not allowed";
|
|
1029
|
+
if (!SVG_SHAPE_RE.test(input))
|
|
1030
|
+
return "not a single-root <svg> document";
|
|
1031
|
+
if (svgRootCount(input) !== 1)
|
|
1032
|
+
return "must be exactly one <svg> root";
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
// Validates an SVG / SVG_LIST defaultValue. Returns an error message, or null if valid.
|
|
1036
|
+
// Authoring an SVG default is a common mistake source for agents; rejecting here (with an
|
|
1037
|
+
// instructive message) keeps malformed/oversized SVGs out of published components.
|
|
1038
|
+
export function validateSvgDefaultValue(propType, value, propName) {
|
|
1039
|
+
if (value === undefined || value === null)
|
|
1040
|
+
return null;
|
|
1041
|
+
if (propType === "SVG") {
|
|
1042
|
+
const reason = validateSvgShape(value);
|
|
1043
|
+
return reason
|
|
1044
|
+
? `defaultValue for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
|
|
1045
|
+
: null;
|
|
1046
|
+
}
|
|
1047
|
+
// SVG_LIST
|
|
1048
|
+
if (!Array.isArray(value)) {
|
|
1049
|
+
return `defaultValue for prop "${propName}" must be an array of SVG strings: [ "<svg>…</svg>", ... ]. ${SVG_DEFAULT_HELP}`;
|
|
1050
|
+
}
|
|
1051
|
+
return firstError(value, (svg, i) => {
|
|
1052
|
+
const reason = validateSvgShape(svg);
|
|
1053
|
+
return reason
|
|
1054
|
+
? `defaultValue[${i}] for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
|
|
1055
|
+
: null;
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
const NUMBER_RANGE_DEFAULT_HELP = 'A NUMBER_RANGE default must be an object { "value": <number>, "unit": "px" | null }. ' +
|
|
1059
|
+
"When the prop defines numberRangeData, value must be within [min, max] and on the interval grid (min + k·interval).";
|
|
1060
|
+
// Validates a NUMBER_RANGE defaultValue. Returns an error message, or null if valid.
|
|
1061
|
+
// Checks the { value, unit } shape and — when the prop carries numberRangeData — that the
|
|
1062
|
+
// value is within [min, max] and lands on the interval grid, mirroring the editor's slider
|
|
1063
|
+
// so an agent can't author an out-of-range or off-step default.
|
|
1064
|
+
export function validateNumberRangeDefaultValue(value, numberRangeData, propName) {
|
|
1065
|
+
if (value === undefined || value === null)
|
|
1066
|
+
return null;
|
|
1067
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
1068
|
+
if (typeof value === "string") {
|
|
1069
|
+
return `${where} is a JSON string but must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1070
|
+
}
|
|
1071
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
1072
|
+
return `${where} must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1073
|
+
}
|
|
1074
|
+
const v = value;
|
|
1075
|
+
if (typeof v.value !== "number" || !Number.isFinite(v.value)) {
|
|
1076
|
+
return `${where}: "value" must be a finite number. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1077
|
+
}
|
|
1078
|
+
if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
|
|
1079
|
+
return `${where}: "unit" must be a string or null. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1080
|
+
}
|
|
1081
|
+
if (numberRangeData) {
|
|
1082
|
+
const { min, max, interval } = numberRangeData;
|
|
1083
|
+
if (min !== undefined && v.value < min) {
|
|
1084
|
+
return `${where}: value ${v.value} is below the configured min (${min}).`;
|
|
1085
|
+
}
|
|
1086
|
+
if (max !== undefined && v.value > max) {
|
|
1087
|
+
return `${where}: value ${v.value} is above the configured max (${max}).`;
|
|
1088
|
+
}
|
|
1089
|
+
if (interval !== undefined &&
|
|
1090
|
+
interval > 0 &&
|
|
1091
|
+
min !== undefined &&
|
|
1092
|
+
(v.value - min) % interval !== 0) {
|
|
1093
|
+
return `${where}: value ${v.value} is not on the interval grid (min ${min} + k·${interval}).`;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
const NUMBER_DEFAULT_HELP = "A NUMBER default must be a finite number, e.g. --defaultValue 42.";
|
|
1099
|
+
// Validates a NUMBER defaultValue. Returns an error message, or null if valid. Guards two footguns:
|
|
1100
|
+
// an empty/whitespace flag string (Number("") === 0 would silently store 0) and a non-numeric or
|
|
1101
|
+
// non-finite value (e.g. "42px" → NaN, "Infinity"). The flag path passes the raw --defaultValue
|
|
1102
|
+
// string; the --props path passes the already-parsed JSON value — both are handled here.
|
|
1103
|
+
export function validateNumberDefaultValue(value, propName) {
|
|
1104
|
+
if (value === undefined || value === null)
|
|
1105
|
+
return null;
|
|
1106
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
1107
|
+
if (typeof value === "string") {
|
|
1108
|
+
if (value.trim() === "") {
|
|
1109
|
+
return `${where} is empty, but NUMBER requires a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1110
|
+
}
|
|
1111
|
+
if (!Number.isFinite(Number(value))) {
|
|
1112
|
+
return `${where} ("${value}") is not a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1113
|
+
}
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1117
|
+
return `${where} must be a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1118
|
+
}
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
const STYLE_TYPE_DEFAULT_HELP = "A size/style TYPE default must be an object matching the type's shape: " +
|
|
1122
|
+
'{ "value": <number>, "unit": "px" | "rem" | "%" | "vh" | "vw" } (or { "css": "<raw css>" }). ' +
|
|
1123
|
+
"Author it via add-component --props '[…]' or --defaultValue '<json>'.";
|
|
1124
|
+
const BP_STOREFRONT_PREFIX = "@ikas/bp-storefront-models-";
|
|
1125
|
+
// The size/style types that share the { css?, value?, unit? } shape. This is EXACTLY the set of
|
|
1126
|
+
// TYPE typeIds whose default the generator emits (mirror of DYNAMIC_STYLE_TYPE_IDS in
|
|
1127
|
+
// @ikas/blueprint-modules design-generator/generate-helpers/styles.ts — the standalone CLI must not
|
|
1128
|
+
// depend on that package, so it is hand-maintained here and kept in sync by a parity test). Every
|
|
1129
|
+
// OTHER TYPE typeId (BorderStyleType, BoxShadowStyleType, GridTemplateColumnsStyleType, string-union
|
|
1130
|
+
// styles like TextAlignStyleType, IkasProduct, custom types) has a DIFFERENT value shape, so their
|
|
1131
|
+
// defaults are not shape-checked (and are dead code in the generator anyway).
|
|
1132
|
+
const SIZE_STYLE_TYPE_NAMES = [
|
|
1133
|
+
"SizeStyleType",
|
|
1134
|
+
"PaddingStyleType",
|
|
1135
|
+
"PaddingTopStyleType",
|
|
1136
|
+
"PaddingRightStyleType",
|
|
1137
|
+
"PaddingBottomStyleType",
|
|
1138
|
+
"PaddingLeftStyleType",
|
|
1139
|
+
"MarginStyleType",
|
|
1140
|
+
"MarginTopStyleType",
|
|
1141
|
+
"MarginRightStyleType",
|
|
1142
|
+
"MarginBottomStyleType",
|
|
1143
|
+
"MarginLeftStyleType",
|
|
1144
|
+
"BorderRadiusStyleType",
|
|
1145
|
+
"BorderRadiusTopLeftStyleType",
|
|
1146
|
+
"BorderRadiusTopRightStyleType",
|
|
1147
|
+
"BorderRadiusBottomRightStyleType",
|
|
1148
|
+
"BorderRadiusBottomLeftStyleType",
|
|
1149
|
+
"FontSizeStyleType",
|
|
1150
|
+
"LineHeightStyleType",
|
|
1151
|
+
"LetterSpacingStyleType",
|
|
1152
|
+
"HeightStyleType",
|
|
1153
|
+
"MinHeightStyleType",
|
|
1154
|
+
"MaxHeightStyleType",
|
|
1155
|
+
"WidthStyleType",
|
|
1156
|
+
"MinWidthStyleType",
|
|
1157
|
+
"MaxWidthStyleType",
|
|
1158
|
+
"GapStyleType",
|
|
1159
|
+
"BorderWidthStyleType",
|
|
1160
|
+
"TopStyleType",
|
|
1161
|
+
"RightStyleType",
|
|
1162
|
+
"BottomStyleType",
|
|
1163
|
+
"LeftStyleType",
|
|
1164
|
+
];
|
|
1165
|
+
// Validates a single style-type default object. Every size style type shares the optional
|
|
1166
|
+
// { css?, value?, unit? } shape, so inner fields are checked loosely — the key rejection is a
|
|
1167
|
+
// non-object (e.g. a bare JSON string, the common failure when a --defaultValue flag is not parsed).
|
|
1168
|
+
function styleDefaultObjectError(value, where) {
|
|
1169
|
+
if (typeof value === "string") {
|
|
1170
|
+
return `${where} is a JSON string but must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1171
|
+
}
|
|
1172
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1173
|
+
return `${where} must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1174
|
+
}
|
|
1175
|
+
const v = value;
|
|
1176
|
+
// css/value/unit are all optional, and the editor treats an explicit null as "unset" (it accepts
|
|
1177
|
+
// { value: null }). So only type-check a field that is present AND non-null — otherwise the CLI
|
|
1178
|
+
// would be stricter than the authoritative editor and reject a value it would happily accept.
|
|
1179
|
+
// Unknown fields ARE rejected below: every size-style type shares exactly this { css?, value?, unit? }
|
|
1180
|
+
// shape, so an extra key is always a mistake (this matches the editor). The per-type value-DOMAIN
|
|
1181
|
+
// (which units a given type allows — BorderWidth also permits em/vmin/vmax while the rest are
|
|
1182
|
+
// px/rem/%/vh/vw) stays editor-side: it is per-type and can't be mirrored offline without drift.
|
|
1183
|
+
if (v.css !== undefined && v.css !== null && typeof v.css !== "string") {
|
|
1184
|
+
return `${where}: "css" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1185
|
+
}
|
|
1186
|
+
if (v.value !== undefined &&
|
|
1187
|
+
v.value !== null &&
|
|
1188
|
+
(typeof v.value !== "number" || !Number.isFinite(v.value))) {
|
|
1189
|
+
return `${where}: "value" must be a finite number. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1190
|
+
}
|
|
1191
|
+
if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
|
|
1192
|
+
return `${where}: "unit" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1193
|
+
}
|
|
1194
|
+
const unknownKey = Object.keys(v).find((k) => k !== "css" && k !== "value" && k !== "unit");
|
|
1195
|
+
if (unknownKey) {
|
|
1196
|
+
return `${where}: unknown field "${unknownKey}". A size/style default allows only "css", "value", "unit". ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1197
|
+
}
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
// Validates a TYPE prop defaultValue — but ONLY for the size/style types whose shape we know
|
|
1201
|
+
// ({ css?, value?, unit? }). Any other TYPE typeId (color/border/box-shadow/grid/string-union
|
|
1202
|
+
// styles, IkasProduct, custom types) has a different value shape we can't verify offline, so its
|
|
1203
|
+
// default is passed through unchecked. The generator only emits a default for these size/style
|
|
1204
|
+
// types anyway. Returns an error message, or null if valid / not a size-style type.
|
|
1205
|
+
export function validateTypeDefaultValue(value, propName, typeId) {
|
|
1206
|
+
if (value === undefined || value === null)
|
|
1207
|
+
return null;
|
|
1208
|
+
if (typeof typeId !== "string" || !typeId.startsWith(BP_STOREFRONT_PREFIX)) {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
// Strip the prefix and an optional _array suffix to get the base type name.
|
|
1212
|
+
const withoutPrefix = typeId.slice(BP_STOREFRONT_PREFIX.length);
|
|
1213
|
+
const isArray = withoutPrefix.endsWith("_array");
|
|
1214
|
+
const baseName = isArray
|
|
1215
|
+
? withoutPrefix.slice(0, -"_array".length)
|
|
1216
|
+
: withoutPrefix;
|
|
1217
|
+
if (!SIZE_STYLE_TYPE_NAMES.includes(baseName))
|
|
1218
|
+
return null;
|
|
1219
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
1220
|
+
if (isArray) {
|
|
1221
|
+
if (!Array.isArray(value)) {
|
|
1222
|
+
return `${where} must be an array for an _array TYPE prop: [ { "value": …, "unit": … }, … ]. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1223
|
+
}
|
|
1224
|
+
return firstError(value, (el, i) => styleDefaultObjectError(el, `${where}[${i}]`));
|
|
1225
|
+
}
|
|
1226
|
+
return styleDefaultObjectError(value, where);
|
|
1227
|
+
}
|
|
1228
|
+
async function updateProp(ref, options) {
|
|
1229
|
+
const { config, configPath } = loadConfig();
|
|
1230
|
+
const component = resolveComponent(config, ref);
|
|
463
1231
|
const propIndex = component.props.findIndex((p) => p.name === options.prop);
|
|
464
1232
|
if (propIndex === -1) {
|
|
465
1233
|
console.log(JSON.stringify({
|
|
@@ -489,8 +1257,87 @@ function updateProp(componentName, options) {
|
|
|
489
1257
|
if (options.description !== undefined) {
|
|
490
1258
|
prop.description = options.description || undefined;
|
|
491
1259
|
}
|
|
1260
|
+
// Resolve numberRangeData before defaultValue so a NUMBER_RANGE default can be validated
|
|
1261
|
+
// against the (possibly just-updated) slider config in the same call.
|
|
1262
|
+
if (options.numberRangeData !== undefined) {
|
|
1263
|
+
if (options.numberRangeData === "" || options.numberRangeData === "none") {
|
|
1264
|
+
delete prop.numberRangeData;
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
prop.numberRangeData = parseNumberRangeData(options.numberRangeData, prop.name);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
// A NUMBER_RANGE prop must keep a valid slider config (min + max). Enforce the invariant only when
|
|
1271
|
+
// this update touches the type or numberRangeData — a benign update (e.g. displayName) on an
|
|
1272
|
+
// existing prop must still pass. This catches switching a prop TO NUMBER_RANGE without a config, or
|
|
1273
|
+
// clearing/blanking numberRangeData on a NUMBER_RANGE prop.
|
|
1274
|
+
if ((options.type !== undefined || options.numberRangeData !== undefined) &&
|
|
1275
|
+
prop.type === "NUMBER_RANGE" &&
|
|
1276
|
+
(!prop.numberRangeData || !isNumberRangeComplete(prop.numberRangeData))) {
|
|
1277
|
+
console.log(JSON.stringify({
|
|
1278
|
+
success: false,
|
|
1279
|
+
error: numberRangeDataRequiredError(prop.name),
|
|
1280
|
+
}));
|
|
1281
|
+
process.exit(1);
|
|
1282
|
+
}
|
|
1283
|
+
// Resolve filteredComponentIds before defaultValue so a COMPONENT / COMPONENT_LIST default can be
|
|
1284
|
+
// validated against the (possibly just-updated) allowlist in the same call.
|
|
1285
|
+
if (options.filteredComponentIds !== undefined) {
|
|
1286
|
+
if (options.filteredComponentIds === "" ||
|
|
1287
|
+
options.filteredComponentIds === "none") {
|
|
1288
|
+
delete prop.filteredComponentIds;
|
|
1289
|
+
}
|
|
1290
|
+
else {
|
|
1291
|
+
try {
|
|
1292
|
+
const parsed = JSON.parse(options.filteredComponentIds);
|
|
1293
|
+
if (!Array.isArray(parsed)) {
|
|
1294
|
+
console.log(JSON.stringify({
|
|
1295
|
+
success: false,
|
|
1296
|
+
error: "--filteredComponentIds must be a JSON array of strings.",
|
|
1297
|
+
}));
|
|
1298
|
+
process.exit(1);
|
|
1299
|
+
}
|
|
1300
|
+
assertKnownComponentIds(parsed, config);
|
|
1301
|
+
prop.filteredComponentIds = parsed;
|
|
1302
|
+
}
|
|
1303
|
+
catch (err) {
|
|
1304
|
+
if (err instanceof SyntaxError) {
|
|
1305
|
+
console.log(JSON.stringify({
|
|
1306
|
+
success: false,
|
|
1307
|
+
error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
|
|
1308
|
+
}));
|
|
1309
|
+
process.exit(1);
|
|
1310
|
+
}
|
|
1311
|
+
throw err;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
492
1315
|
if (options.defaultValue !== undefined) {
|
|
493
|
-
prop.
|
|
1316
|
+
rejectDynamicDefault(prop.name, prop.type, options.defaultValue);
|
|
1317
|
+
const parsed = parseDefaultValue(options.defaultValue, prop.type);
|
|
1318
|
+
// Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
|
|
1319
|
+
if (prop.type === "NUMBER") {
|
|
1320
|
+
const numError = validateNumberDefaultValue(options.defaultValue, prop.name);
|
|
1321
|
+
failIf(numError);
|
|
1322
|
+
}
|
|
1323
|
+
if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
|
|
1324
|
+
const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
|
|
1325
|
+
failIf(linkError);
|
|
1326
|
+
}
|
|
1327
|
+
if ((prop.type === "COMPONENT" || prop.type === "COMPONENT_LIST") &&
|
|
1328
|
+
parsed !== undefined) {
|
|
1329
|
+
failIf(validateComponentDefaultValue(prop.type, parsed, prop.name, prop.filteredComponentIds, existingComponentIdSet(config)));
|
|
1330
|
+
}
|
|
1331
|
+
if (prop.type === "NUMBER_RANGE") {
|
|
1332
|
+
const nrError = validateNumberRangeDefaultValue(parsed, prop.numberRangeData, prop.name);
|
|
1333
|
+
failIf(nrError);
|
|
1334
|
+
}
|
|
1335
|
+
if (prop.type === "TYPE") {
|
|
1336
|
+
// Prefer a typeId supplied in this same update over the stored one.
|
|
1337
|
+
const typeError = validateTypeDefaultValue(parsed, prop.name, options.typeId ?? prop.typeId);
|
|
1338
|
+
failIf(typeError);
|
|
1339
|
+
}
|
|
1340
|
+
prop.defaultValue = parsed;
|
|
494
1341
|
}
|
|
495
1342
|
if (options.group !== undefined) {
|
|
496
1343
|
if (options.group === "" || options.group === "none") {
|
|
@@ -523,6 +1370,13 @@ function updateProp(componentName, options) {
|
|
|
523
1370
|
prop.typeId = options.typeId;
|
|
524
1371
|
}
|
|
525
1372
|
}
|
|
1373
|
+
// Reject section-disallowed types when the caller changes type/typeId;
|
|
1374
|
+
// untouched legacy props stay editable (displayName, group, ...).
|
|
1375
|
+
if ((options.type !== undefined || options.typeId !== undefined) &&
|
|
1376
|
+
prop.type === "TYPE" &&
|
|
1377
|
+
prop.typeId) {
|
|
1378
|
+
await assertTypeIdsUsable(component.type, [prop.typeId]);
|
|
1379
|
+
}
|
|
526
1380
|
if (options.enumTypeId !== undefined) {
|
|
527
1381
|
if (options.enumTypeId === "" || options.enumTypeId === "none") {
|
|
528
1382
|
delete prop.enumTypeId;
|
|
@@ -531,25 +1385,6 @@ function updateProp(componentName, options) {
|
|
|
531
1385
|
prop.enumTypeId = options.enumTypeId;
|
|
532
1386
|
}
|
|
533
1387
|
}
|
|
534
|
-
if (options.filteredComponentIds !== undefined) {
|
|
535
|
-
if (options.filteredComponentIds === "" || options.filteredComponentIds === "none") {
|
|
536
|
-
delete prop.filteredComponentIds;
|
|
537
|
-
}
|
|
538
|
-
else {
|
|
539
|
-
try {
|
|
540
|
-
const parsed = JSON.parse(options.filteredComponentIds);
|
|
541
|
-
if (!Array.isArray(parsed)) {
|
|
542
|
-
console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
|
|
543
|
-
process.exit(1);
|
|
544
|
-
}
|
|
545
|
-
prop.filteredComponentIds = parsed;
|
|
546
|
-
}
|
|
547
|
-
catch {
|
|
548
|
-
console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
|
|
549
|
-
process.exit(1);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
1388
|
if (options.privateVarMap !== undefined) {
|
|
554
1389
|
if (options.privateVarMap === "" || options.privateVarMap === "none") {
|
|
555
1390
|
delete prop.privateVarMap;
|
|
@@ -558,17 +1393,50 @@ function updateProp(componentName, options) {
|
|
|
558
1393
|
try {
|
|
559
1394
|
const parsed = JSON.parse(options.privateVarMap);
|
|
560
1395
|
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
561
|
-
console.log(JSON.stringify({
|
|
1396
|
+
console.log(JSON.stringify({
|
|
1397
|
+
success: false,
|
|
1398
|
+
error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
|
|
1399
|
+
}));
|
|
562
1400
|
process.exit(1);
|
|
563
1401
|
}
|
|
564
1402
|
prop.privateVarMap = parsed;
|
|
565
1403
|
}
|
|
566
1404
|
catch {
|
|
567
|
-
console.log(JSON.stringify({
|
|
1405
|
+
console.log(JSON.stringify({
|
|
1406
|
+
success: false,
|
|
1407
|
+
error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
|
|
1408
|
+
}));
|
|
568
1409
|
process.exit(1);
|
|
569
1410
|
}
|
|
570
1411
|
}
|
|
571
1412
|
}
|
|
1413
|
+
if (options.requiredProp !== undefined) {
|
|
1414
|
+
if (options.requiredProp === "" || options.requiredProp === "none") {
|
|
1415
|
+
// Every field goes together — see `IProp.requiredPropId`.
|
|
1416
|
+
delete prop.requiredProp;
|
|
1417
|
+
delete prop.requiredPropValue;
|
|
1418
|
+
delete prop.requiredEnumOptionId;
|
|
1419
|
+
delete prop.requiredEnumOptionValue;
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
prop.requiredProp = options.requiredProp;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (options.requiredEnumOptionValue !== undefined && prop.requiredProp) {
|
|
1426
|
+
prop.requiredEnumOptionId = options.requiredEnumOptionValue;
|
|
1427
|
+
prop.requiredEnumOptionValue = options.requiredEnumOptionValue;
|
|
1428
|
+
delete prop.requiredPropValue;
|
|
1429
|
+
}
|
|
1430
|
+
if (options.requiredPropValue !== undefined && prop.requiredProp) {
|
|
1431
|
+
delete prop.requiredEnumOptionId;
|
|
1432
|
+
delete prop.requiredEnumOptionValue;
|
|
1433
|
+
prop.requiredPropValue = parseRequiredPropValue(options.requiredPropValue, `prop "${prop.name}"`);
|
|
1434
|
+
}
|
|
1435
|
+
// Re-checked on every update, not only when a visibility flag is passed: flipping `required`, or
|
|
1436
|
+
// renaming the prop this one hangs on, can invalidate a gate that was sound when it was written.
|
|
1437
|
+
failIf(visibilityGateError(component, prop, `prop "${prop.name}"`, {
|
|
1438
|
+
selfPropName: prop.name,
|
|
1439
|
+
}));
|
|
572
1440
|
saveConfig(configPath, config);
|
|
573
1441
|
// Regenerate types.ts and global types
|
|
574
1442
|
regenerateTypes(component, undefined, config);
|
|
@@ -580,25 +1448,30 @@ function updateProp(componentName, options) {
|
|
|
580
1448
|
name: prop.name,
|
|
581
1449
|
displayName: prop.displayName,
|
|
582
1450
|
type: prop.type,
|
|
583
|
-
required: prop.required,
|
|
584
1451
|
...(prop.groupId ? { groupId: prop.groupId } : {}),
|
|
585
1452
|
...(prop.typeId ? { typeId: prop.typeId } : {}),
|
|
586
1453
|
...(prop.enumTypeId ? { enumTypeId: prop.enumTypeId } : {}),
|
|
587
|
-
...(prop.filteredComponentIds
|
|
1454
|
+
...(prop.filteredComponentIds
|
|
1455
|
+
? { filteredComponentIds: prop.filteredComponentIds }
|
|
1456
|
+
: {}),
|
|
588
1457
|
...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
|
|
1458
|
+
...(prop.numberRangeData
|
|
1459
|
+
? { numberRangeData: prop.numberRangeData }
|
|
1460
|
+
: {}),
|
|
1461
|
+
...(prop.requiredProp
|
|
1462
|
+
? {
|
|
1463
|
+
requiredProp: prop.requiredProp,
|
|
1464
|
+
...(prop.requiredEnumOptionValue !== undefined
|
|
1465
|
+
? { requiredEnumOptionValue: prop.requiredEnumOptionValue }
|
|
1466
|
+
: { requiredPropValue: prop.requiredPropValue ?? true }),
|
|
1467
|
+
}
|
|
1468
|
+
: {}),
|
|
589
1469
|
},
|
|
590
1470
|
}));
|
|
591
1471
|
}
|
|
592
|
-
function removeProp(
|
|
1472
|
+
function removeProp(ref, options) {
|
|
593
1473
|
const { config, configPath } = loadConfig();
|
|
594
|
-
const component =
|
|
595
|
-
if (!component) {
|
|
596
|
-
console.log(JSON.stringify({
|
|
597
|
-
success: false,
|
|
598
|
-
error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
|
|
599
|
-
}));
|
|
600
|
-
process.exit(1);
|
|
601
|
-
}
|
|
1474
|
+
const component = resolveComponent(config, ref);
|
|
602
1475
|
const propIndex = component.props.findIndex((p) => p.name === options.prop);
|
|
603
1476
|
if (propIndex === -1) {
|
|
604
1477
|
console.log(JSON.stringify({
|
|
@@ -619,21 +1492,30 @@ function removeProp(componentName, options) {
|
|
|
619
1492
|
remainingProps: component.props.map((p) => p.name),
|
|
620
1493
|
}));
|
|
621
1494
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1495
|
+
/**
|
|
1496
|
+
* Strip a deleted component's ID from every other component's COMPONENT/COMPONENT_LIST
|
|
1497
|
+
* `filteredComponentIds` allowlist. Empty arrays are deleted to keep the field optional
|
|
1498
|
+
* (mirrors the editor-side convention).
|
|
1499
|
+
*/
|
|
1500
|
+
function pruneFilteredComponentIdRefs(config, removedId) {
|
|
1501
|
+
for (const component of config.components) {
|
|
1502
|
+
for (const prop of component.props || []) {
|
|
1503
|
+
if (!prop.filteredComponentIds)
|
|
1504
|
+
continue;
|
|
1505
|
+
prop.filteredComponentIds = prop.filteredComponentIds.filter((id) => id !== removedId);
|
|
1506
|
+
if (prop.filteredComponentIds.length === 0)
|
|
1507
|
+
delete prop.filteredComponentIds;
|
|
1508
|
+
}
|
|
632
1509
|
}
|
|
633
|
-
|
|
1510
|
+
}
|
|
1511
|
+
function removeComponent(ref) {
|
|
1512
|
+
const { config, configPath } = loadConfig();
|
|
1513
|
+
const component = resolveComponent(config, ref);
|
|
1514
|
+
const componentIndex = config.components.indexOf(component);
|
|
634
1515
|
const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
|
|
635
|
-
// Remove component from config
|
|
1516
|
+
// Remove component from config and strip orphaned references from remaining components
|
|
636
1517
|
config.components.splice(componentIndex, 1);
|
|
1518
|
+
pruneFilteredComponentIdRefs(config, component.id);
|
|
637
1519
|
saveConfig(configPath, config);
|
|
638
1520
|
// Remove component directory
|
|
639
1521
|
if (fs.existsSync(componentDir)) {
|
|
@@ -643,21 +1525,18 @@ function removeComponent(options) {
|
|
|
643
1525
|
updateBarrelExport(process.cwd(), getComponentNames(config));
|
|
644
1526
|
console.log(JSON.stringify({
|
|
645
1527
|
success: true,
|
|
646
|
-
|
|
1528
|
+
removedComponentId: component.id,
|
|
1529
|
+
removedComponentName: component.name,
|
|
647
1530
|
removedDirectory: path.relative(process.cwd(), componentDir),
|
|
648
|
-
remainingComponents:
|
|
1531
|
+
remainingComponents: config.components.map((c) => ({
|
|
1532
|
+
id: c.id,
|
|
1533
|
+
name: c.name,
|
|
1534
|
+
})),
|
|
649
1535
|
}));
|
|
650
1536
|
}
|
|
651
|
-
function addPropGroup(
|
|
1537
|
+
function addPropGroup(ref, options) {
|
|
652
1538
|
const { config, configPath } = loadConfig();
|
|
653
|
-
const component =
|
|
654
|
-
if (!component) {
|
|
655
|
-
console.log(JSON.stringify({
|
|
656
|
-
success: false,
|
|
657
|
-
error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
|
|
658
|
-
}));
|
|
659
|
-
process.exit(1);
|
|
660
|
-
}
|
|
1539
|
+
const component = resolveComponent(config, ref);
|
|
661
1540
|
if (!component.propGroups)
|
|
662
1541
|
component.propGroups = [];
|
|
663
1542
|
// Check uniqueness
|
|
@@ -673,7 +1552,24 @@ function addPropGroup(componentName, options) {
|
|
|
673
1552
|
id: options.id,
|
|
674
1553
|
name: options.name,
|
|
675
1554
|
...(options.description ? { description: options.description } : {}),
|
|
1555
|
+
...(options.requiredProp ? { requiredProp: options.requiredProp } : {}),
|
|
1556
|
+
...(options.requiredProp && options.requiredEnumOptionValue !== undefined
|
|
1557
|
+
? {
|
|
1558
|
+
requiredEnumOptionId: options.requiredEnumOptionValue,
|
|
1559
|
+
requiredEnumOptionValue: options.requiredEnumOptionValue,
|
|
1560
|
+
}
|
|
1561
|
+
: {}),
|
|
1562
|
+
...(options.requiredProp &&
|
|
1563
|
+
options.requiredEnumOptionValue === undefined &&
|
|
1564
|
+
options.requiredPropValue !== undefined
|
|
1565
|
+
? {
|
|
1566
|
+
requiredPropValue: parseRequiredPropValue(options.requiredPropValue, `group "${options.id}"`),
|
|
1567
|
+
}
|
|
1568
|
+
: {}),
|
|
676
1569
|
};
|
|
1570
|
+
failIf(visibilityGateError(component, newGroup, `group "${newGroup.id}"`, {
|
|
1571
|
+
group: newGroup,
|
|
1572
|
+
}));
|
|
677
1573
|
if (options.parent) {
|
|
678
1574
|
const found = findPropGroup(component.propGroups, options.parent);
|
|
679
1575
|
if (!found) {
|
|
@@ -705,19 +1601,20 @@ function addPropGroup(componentName, options) {
|
|
|
705
1601
|
id: newGroup.id,
|
|
706
1602
|
name: newGroup.name,
|
|
707
1603
|
...(options.parent ? { parentId: options.parent } : {}),
|
|
1604
|
+
...(newGroup.requiredProp
|
|
1605
|
+
? {
|
|
1606
|
+
requiredProp: newGroup.requiredProp,
|
|
1607
|
+
...(newGroup.requiredEnumOptionValue !== undefined
|
|
1608
|
+
? { requiredEnumOptionValue: newGroup.requiredEnumOptionValue }
|
|
1609
|
+
: { requiredPropValue: newGroup.requiredPropValue ?? true }),
|
|
1610
|
+
}
|
|
1611
|
+
: {}),
|
|
708
1612
|
},
|
|
709
1613
|
}));
|
|
710
1614
|
}
|
|
711
|
-
function updatePropGroup(
|
|
1615
|
+
function updatePropGroup(ref, options) {
|
|
712
1616
|
const { config, configPath } = loadConfig();
|
|
713
|
-
const component =
|
|
714
|
-
if (!component) {
|
|
715
|
-
console.log(JSON.stringify({
|
|
716
|
-
success: false,
|
|
717
|
-
error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
|
|
718
|
-
}));
|
|
719
|
-
process.exit(1);
|
|
720
|
-
}
|
|
1617
|
+
const component = resolveComponent(config, ref);
|
|
721
1618
|
if (!component.propGroups || component.propGroups.length === 0) {
|
|
722
1619
|
console.log(JSON.stringify({
|
|
723
1620
|
success: false,
|
|
@@ -737,6 +1634,34 @@ function updatePropGroup(componentName, options) {
|
|
|
737
1634
|
found.group.name = options.name;
|
|
738
1635
|
if (options.description !== undefined)
|
|
739
1636
|
found.group.description = options.description || undefined;
|
|
1637
|
+
if (options.requiredProp !== undefined) {
|
|
1638
|
+
if (options.requiredProp === "" || options.requiredProp === "none") {
|
|
1639
|
+
// Every field goes together — see `IThemePropGroup.requiredPropId`.
|
|
1640
|
+
delete found.group.requiredProp;
|
|
1641
|
+
delete found.group.requiredPropValue;
|
|
1642
|
+
delete found.group.requiredEnumOptionId;
|
|
1643
|
+
delete found.group.requiredEnumOptionValue;
|
|
1644
|
+
}
|
|
1645
|
+
else {
|
|
1646
|
+
found.group.requiredProp = options.requiredProp;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (options.requiredEnumOptionValue !== undefined &&
|
|
1650
|
+
found.group.requiredProp) {
|
|
1651
|
+
found.group.requiredEnumOptionId = options.requiredEnumOptionValue;
|
|
1652
|
+
found.group.requiredEnumOptionValue = options.requiredEnumOptionValue;
|
|
1653
|
+
delete found.group.requiredPropValue;
|
|
1654
|
+
}
|
|
1655
|
+
if (options.requiredPropValue !== undefined && found.group.requiredProp) {
|
|
1656
|
+
delete found.group.requiredEnumOptionId;
|
|
1657
|
+
delete found.group.requiredEnumOptionValue;
|
|
1658
|
+
found.group.requiredPropValue = parseRequiredPropValue(options.requiredPropValue, `group "${found.group.id}"`);
|
|
1659
|
+
}
|
|
1660
|
+
// Re-checked on every update: moving a prop into this group can invalidate a gate that was sound
|
|
1661
|
+
// when it was written.
|
|
1662
|
+
failIf(visibilityGateError(component, found.group, `group "${found.group.id}"`, {
|
|
1663
|
+
group: found.group,
|
|
1664
|
+
}));
|
|
740
1665
|
saveConfig(configPath, config);
|
|
741
1666
|
console.log(JSON.stringify({
|
|
742
1667
|
success: true,
|
|
@@ -744,19 +1669,22 @@ function updatePropGroup(componentName, options) {
|
|
|
744
1669
|
updatedPropGroup: {
|
|
745
1670
|
id: found.group.id,
|
|
746
1671
|
name: found.group.name,
|
|
1672
|
+
...(found.group.requiredProp
|
|
1673
|
+
? {
|
|
1674
|
+
requiredProp: found.group.requiredProp,
|
|
1675
|
+
...(found.group.requiredEnumOptionValue !== undefined
|
|
1676
|
+
? {
|
|
1677
|
+
requiredEnumOptionValue: found.group.requiredEnumOptionValue,
|
|
1678
|
+
}
|
|
1679
|
+
: { requiredPropValue: found.group.requiredPropValue ?? true }),
|
|
1680
|
+
}
|
|
1681
|
+
: {}),
|
|
747
1682
|
},
|
|
748
1683
|
}));
|
|
749
1684
|
}
|
|
750
|
-
function removePropGroup(
|
|
1685
|
+
function removePropGroup(ref, options) {
|
|
751
1686
|
const { config, configPath } = loadConfig();
|
|
752
|
-
const component =
|
|
753
|
-
if (!component) {
|
|
754
|
-
console.log(JSON.stringify({
|
|
755
|
-
success: false,
|
|
756
|
-
error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
|
|
757
|
-
}));
|
|
758
|
-
process.exit(1);
|
|
759
|
-
}
|
|
1687
|
+
const component = resolveComponent(config, ref);
|
|
760
1688
|
if (!component.propGroups || component.propGroups.length === 0) {
|
|
761
1689
|
console.log(JSON.stringify({
|
|
762
1690
|
success: false,
|
|
@@ -773,10 +1701,10 @@ function removePropGroup(componentName, options) {
|
|
|
773
1701
|
process.exit(1);
|
|
774
1702
|
}
|
|
775
1703
|
if (found.parent) {
|
|
776
|
-
found.parent.children = found.parent.children.filter(c => c.id !== options.id);
|
|
1704
|
+
found.parent.children = found.parent.children.filter((c) => c.id !== options.id);
|
|
777
1705
|
}
|
|
778
1706
|
else {
|
|
779
|
-
component.propGroups = component.propGroups.filter(g => g.id !== options.id);
|
|
1707
|
+
component.propGroups = component.propGroups.filter((g) => g.id !== options.id);
|
|
780
1708
|
}
|
|
781
1709
|
// Clear groupId from props referencing the deleted group
|
|
782
1710
|
for (const prop of component.props) {
|
|
@@ -789,19 +1717,12 @@ function removePropGroup(componentName, options) {
|
|
|
789
1717
|
success: true,
|
|
790
1718
|
componentName: component.name,
|
|
791
1719
|
removedPropGroup: options.id,
|
|
792
|
-
remainingPropGroups: component.propGroups.map(g => g.id),
|
|
1720
|
+
remainingPropGroups: component.propGroups.map((g) => g.id),
|
|
793
1721
|
}));
|
|
794
1722
|
}
|
|
795
|
-
function movePropGroup(
|
|
1723
|
+
function movePropGroup(ref, options) {
|
|
796
1724
|
const { config, configPath } = loadConfig();
|
|
797
|
-
const component =
|
|
798
|
-
if (!component) {
|
|
799
|
-
console.log(JSON.stringify({
|
|
800
|
-
success: false,
|
|
801
|
-
error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
|
|
802
|
-
}));
|
|
803
|
-
process.exit(1);
|
|
804
|
-
}
|
|
1725
|
+
const component = resolveComponent(config, ref);
|
|
805
1726
|
if (!component.propGroups || component.propGroups.length === 0) {
|
|
806
1727
|
console.log(JSON.stringify({
|
|
807
1728
|
success: false,
|
|
@@ -813,7 +1734,10 @@ function movePropGroup(componentName, options) {
|
|
|
813
1734
|
component.propGroups = movePropGroupInTree(component.propGroups, options.id, options.parent || null, options.index);
|
|
814
1735
|
}
|
|
815
1736
|
catch (e) {
|
|
816
|
-
console.log(JSON.stringify({
|
|
1737
|
+
console.log(JSON.stringify({
|
|
1738
|
+
success: false,
|
|
1739
|
+
error: e.message || "Failed to move prop group",
|
|
1740
|
+
}));
|
|
817
1741
|
process.exit(1);
|
|
818
1742
|
}
|
|
819
1743
|
saveConfig(configPath, config);
|
|
@@ -824,16 +1748,9 @@ function movePropGroup(componentName, options) {
|
|
|
824
1748
|
parentGroupId: options.parent || null,
|
|
825
1749
|
}));
|
|
826
1750
|
}
|
|
827
|
-
function updateComponent(options) {
|
|
1751
|
+
function updateComponent(ref, options) {
|
|
828
1752
|
const { config, configPath } = loadConfig();
|
|
829
|
-
const component =
|
|
830
|
-
if (!component) {
|
|
831
|
-
console.log(JSON.stringify({
|
|
832
|
-
success: false,
|
|
833
|
-
error: `Component "${options.name}" not found. Available: ${getComponentNames(config).join(", ")}`,
|
|
834
|
-
}));
|
|
835
|
-
process.exit(1);
|
|
836
|
-
}
|
|
1753
|
+
const component = resolveComponent(config, ref);
|
|
837
1754
|
if (options.isHeader !== undefined) {
|
|
838
1755
|
if (options.isHeader) {
|
|
839
1756
|
component.isHeader = true;
|
|
@@ -876,7 +1793,9 @@ function listComponents() {
|
|
|
876
1793
|
required: p.required,
|
|
877
1794
|
...(p.groupId ? { groupId: p.groupId } : {}),
|
|
878
1795
|
})),
|
|
879
|
-
...(c.propGroups && c.propGroups.length > 0
|
|
1796
|
+
...(c.propGroups && c.propGroups.length > 0
|
|
1797
|
+
? { propGroups: c.propGroups }
|
|
1798
|
+
: {}),
|
|
880
1799
|
}));
|
|
881
1800
|
console.log(JSON.stringify({
|
|
882
1801
|
success: true,
|
|
@@ -890,7 +1809,9 @@ function listComponents() {
|
|
|
890
1809
|
async function addEnum(options) {
|
|
891
1810
|
const { config, configPath } = loadConfig();
|
|
892
1811
|
// Validate name
|
|
893
|
-
if (!options.name ||
|
|
1812
|
+
if (!options.name ||
|
|
1813
|
+
typeof options.name !== "string" ||
|
|
1814
|
+
!options.name.trim()) {
|
|
894
1815
|
console.log(JSON.stringify({ success: false, error: "Enum name is required." }));
|
|
895
1816
|
process.exit(1);
|
|
896
1817
|
}
|
|
@@ -1004,7 +1925,7 @@ function updateEnum(options) {
|
|
|
1004
1925
|
Object.keys(enumOptions).length === 0) {
|
|
1005
1926
|
console.log(JSON.stringify({
|
|
1006
1927
|
success: false,
|
|
1007
|
-
error:
|
|
1928
|
+
error: "Options must be a non-empty JSON object.",
|
|
1008
1929
|
}));
|
|
1009
1930
|
process.exit(1);
|
|
1010
1931
|
}
|
|
@@ -1023,7 +1944,7 @@ function updateEnum(options) {
|
|
|
1023
1944
|
enumOptions: enumType.enumOptions,
|
|
1024
1945
|
}));
|
|
1025
1946
|
}
|
|
1026
|
-
function removeEnum(options) {
|
|
1947
|
+
async function removeEnum(options) {
|
|
1027
1948
|
const { config, configPath } = loadConfig();
|
|
1028
1949
|
if (!config.customTypes)
|
|
1029
1950
|
config.customTypes = [];
|
|
@@ -1047,6 +1968,26 @@ function removeEnum(options) {
|
|
|
1047
1968
|
}
|
|
1048
1969
|
}
|
|
1049
1970
|
}
|
|
1971
|
+
// A theme ENUM global variable can select from this enum too. Those live in the editor's
|
|
1972
|
+
// project, not in ikas.config.json, so ask the connected editor (best effort: with no dev
|
|
1973
|
+
// server / editor the removal proceeds and the result carries a warning). The editor itself
|
|
1974
|
+
// refuses to delete a type its globals reference, so this mirrors that guard for the CLI path.
|
|
1975
|
+
const liveGlobals = await fetchLiveThemeGlobals();
|
|
1976
|
+
const referencingGlobals = liveGlobals
|
|
1977
|
+
? Object.values(liveGlobals).filter((s) => s.type === "ENUM" && s.enumTypeId === options.id)
|
|
1978
|
+
: [];
|
|
1979
|
+
if (referencingGlobals.length > 0) {
|
|
1980
|
+
const names = referencingGlobals
|
|
1981
|
+
.map((s) => `"${s.displayName}" (${s.name})`)
|
|
1982
|
+
.join(", ");
|
|
1983
|
+
console.log(JSON.stringify({
|
|
1984
|
+
success: false,
|
|
1985
|
+
error: `Cannot remove enum "${config.customTypes[enumIndex].name}": theme global variable(s) ${names} ` +
|
|
1986
|
+
`in the connected editor select from it. Change or delete those globals first ` +
|
|
1987
|
+
`(update-global-variable --enum-type-id / delete-global-variable).`,
|
|
1988
|
+
}));
|
|
1989
|
+
process.exit(1);
|
|
1990
|
+
}
|
|
1050
1991
|
const removed = config.customTypes.splice(enumIndex, 1)[0];
|
|
1051
1992
|
saveConfig(configPath, config);
|
|
1052
1993
|
// Regenerate global types and all component types
|
|
@@ -1058,6 +1999,11 @@ function removeEnum(options) {
|
|
|
1058
1999
|
success: true,
|
|
1059
2000
|
removedEnumId: removed.id,
|
|
1060
2001
|
removedEnumName: removed.name,
|
|
2002
|
+
...(liveGlobals === null
|
|
2003
|
+
? {
|
|
2004
|
+
warning: "Editor not connected — could not verify that no theme global variable selects from this enum.",
|
|
2005
|
+
}
|
|
2006
|
+
: {}),
|
|
1061
2007
|
}));
|
|
1062
2008
|
}
|
|
1063
2009
|
function listEnums() {
|
|
@@ -1077,8 +2023,7 @@ function listEnums() {
|
|
|
1077
2023
|
}
|
|
1078
2024
|
// --- Command registration ---
|
|
1079
2025
|
export function createConfigCommand() {
|
|
1080
|
-
const config = new Command("config")
|
|
1081
|
-
.description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
|
|
2026
|
+
const config = new Command("config").description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
|
|
1082
2027
|
config
|
|
1083
2028
|
.command("add-component")
|
|
1084
2029
|
.description("Add a new component to the project")
|
|
@@ -1086,14 +2031,16 @@ export function createConfigCommand() {
|
|
|
1086
2031
|
.option("--type <type>", "Component type: section or component", "component")
|
|
1087
2032
|
.option("--isHeader", "Mark this section as the store header (only for type: section)")
|
|
1088
2033
|
.option("--isFooter", "Mark this section as the store footer (only for type: section)")
|
|
1089
|
-
.option("--props <json>", "JSON array of props
|
|
2034
|
+
.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. " +
|
|
2035
|
+
'Example: \'[{"name":"title","type":"TEXT","required":true,"defaultValue":"Hello","groupId":"basic"}]\'')
|
|
1090
2036
|
.action((options) => {
|
|
1091
2037
|
addComponent(options.name, options);
|
|
1092
2038
|
});
|
|
1093
2039
|
config
|
|
1094
2040
|
.command("add-prop")
|
|
1095
2041
|
.description("Add a prop to a component")
|
|
1096
|
-
.
|
|
2042
|
+
.option("--component-id <id>", "Component id (preferred; from `config list-components`)")
|
|
2043
|
+
.option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1097
2044
|
.requiredOption("--name <name>", "Prop name (camelCase)")
|
|
1098
2045
|
.requiredOption("--displayName <displayName>", "Display name in editor")
|
|
1099
2046
|
.requiredOption("--type <type>", `Prop type: ${PROP_TYPES.join(", ")}`)
|
|
@@ -1104,14 +2051,19 @@ export function createConfigCommand() {
|
|
|
1104
2051
|
.option("--typeId <typeId>", "Type ID for TYPE props (required when type is TYPE)")
|
|
1105
2052
|
.option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
|
|
1106
2053
|
.option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
|
|
1107
|
-
.option("--privateVarMap <json>",
|
|
2054
|
+
.option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)")
|
|
2055
|
+
.option("--numberRangeData <json>", 'JSON slider config for NUMBER_RANGE props, e.g. \'{"min":0,"max":100,"interval":5,"unit":"px"}\'')
|
|
2056
|
+
.option("--requiredProp <propName>", 'Show this only while the named BOOLEAN or ENUM prop satisfies the condition ("Koşullu Prop")')
|
|
2057
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2058
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1108
2059
|
.action((options) => {
|
|
1109
|
-
addProp(options.component, options);
|
|
2060
|
+
addProp({ id: options.componentId, name: options.component }, options);
|
|
1110
2061
|
});
|
|
1111
2062
|
config
|
|
1112
2063
|
.command("update-prop")
|
|
1113
2064
|
.description("Update a prop on a component")
|
|
1114
|
-
.
|
|
2065
|
+
.option("--component-id <id>", "Component id (preferred; from `config list-components`)")
|
|
2066
|
+
.option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1115
2067
|
.requiredOption("--prop <propName>", "Prop name to update")
|
|
1116
2068
|
.option("--displayName <displayName>", "New display name")
|
|
1117
2069
|
.option("--type <type>", "New prop type")
|
|
@@ -1123,77 +2075,100 @@ export function createConfigCommand() {
|
|
|
1123
2075
|
.option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (use 'none' to clear)")
|
|
1124
2076
|
.option("--filteredComponentIds <json>", "JSON array of component IDs (use 'none' to clear)")
|
|
1125
2077
|
.option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (use 'none' to clear)")
|
|
2078
|
+
.option("--numberRangeData <json>", "JSON slider config for NUMBER_RANGE props (use 'none' to clear)")
|
|
2079
|
+
.option("--requiredProp <propName>", "BOOLEAN or ENUM prop that gates this prop in the editor panel (use 'none' to clear)")
|
|
2080
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2081
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1126
2082
|
.action((options) => {
|
|
1127
|
-
updateProp(options.component, options);
|
|
2083
|
+
updateProp({ id: options.componentId, name: options.component }, options);
|
|
1128
2084
|
});
|
|
1129
2085
|
config
|
|
1130
2086
|
.command("remove-prop")
|
|
1131
2087
|
.description("Remove a prop from a component")
|
|
1132
|
-
.
|
|
2088
|
+
.option("--component-id <id>", "Component id (preferred; from `config list-components`)")
|
|
2089
|
+
.option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1133
2090
|
.requiredOption("--prop <propName>", "Prop name to remove")
|
|
1134
2091
|
.action((options) => {
|
|
1135
|
-
removeProp(options.component, options);
|
|
2092
|
+
removeProp({ id: options.componentId, name: options.component }, options);
|
|
1136
2093
|
});
|
|
1137
2094
|
config
|
|
1138
2095
|
.command("update-component")
|
|
1139
|
-
.description("Update a component's metadata (isHeader, isFooter)")
|
|
1140
|
-
.
|
|
2096
|
+
.description("Update a component's metadata (isHeader, isFooter). Identify by --id (preferred) or exact --name.")
|
|
2097
|
+
.option("--id <id>", "Component id (preferred; from `config list-components`)")
|
|
2098
|
+
.option("--name <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1141
2099
|
.option("--isHeader", "Mark as store header")
|
|
1142
2100
|
.option("--no-isHeader", "Unmark as store header")
|
|
1143
2101
|
.option("--isFooter", "Mark as store footer")
|
|
1144
2102
|
.option("--no-isFooter", "Unmark as store footer")
|
|
1145
2103
|
.action((options) => {
|
|
1146
|
-
updateComponent(options);
|
|
2104
|
+
updateComponent({ id: options.id, name: options.name }, options);
|
|
1147
2105
|
});
|
|
1148
2106
|
config
|
|
1149
2107
|
.command("remove-component")
|
|
1150
|
-
.description("Remove a component from the project (deletes files)")
|
|
1151
|
-
.
|
|
2108
|
+
.description("Remove a component from the project (deletes files). Identify by --id (preferred) or exact --name.")
|
|
2109
|
+
.option("--id <id>", "Component id (preferred; from `config list-components`)")
|
|
2110
|
+
.option("--name <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1152
2111
|
.action((options) => {
|
|
1153
|
-
removeComponent(options);
|
|
2112
|
+
removeComponent({ id: options.id, name: options.name });
|
|
1154
2113
|
});
|
|
1155
2114
|
config
|
|
1156
2115
|
.command("add-prop-group")
|
|
1157
2116
|
.description("Add a prop group to a component")
|
|
1158
|
-
.
|
|
2117
|
+
.option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
|
|
2118
|
+
.option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1159
2119
|
.requiredOption("--id <id>", "Group ID (unique within component, kebab-case)")
|
|
1160
2120
|
.requiredOption("--name <name>", "Display name in editor")
|
|
1161
2121
|
.option("--description <description>", "Group description")
|
|
1162
2122
|
.option("--parent <parentId>", "Parent group ID (for nesting, max 1 level)")
|
|
2123
|
+
.option("--requiredProp <propName>", 'Show this only while the named BOOLEAN or ENUM prop satisfies the condition ("Koşullu Prop")')
|
|
2124
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2125
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1163
2126
|
.action((options) => {
|
|
1164
|
-
addPropGroup(options.component, options);
|
|
2127
|
+
addPropGroup({ id: options.componentId, name: options.component }, options);
|
|
1165
2128
|
});
|
|
1166
2129
|
config
|
|
1167
2130
|
.command("update-prop-group")
|
|
1168
2131
|
.description("Update a prop group on a component")
|
|
1169
|
-
.
|
|
2132
|
+
.option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
|
|
2133
|
+
.option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1170
2134
|
.requiredOption("--id <id>", "Group ID to update")
|
|
1171
2135
|
.option("--name <name>", "New display name")
|
|
1172
2136
|
.option("--description <description>", "New description")
|
|
2137
|
+
.option("--requiredProp <propName>", "BOOLEAN or ENUM prop that gates this group in the editor panel (use 'none' to clear)")
|
|
2138
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2139
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1173
2140
|
.action((options) => {
|
|
1174
|
-
updatePropGroup(options.component, options);
|
|
2141
|
+
updatePropGroup({ id: options.componentId, name: options.component }, options);
|
|
1175
2142
|
});
|
|
1176
2143
|
config
|
|
1177
2144
|
.command("remove-prop-group")
|
|
1178
2145
|
.description("Remove a prop group from a component (props become ungrouped)")
|
|
1179
|
-
.
|
|
2146
|
+
.option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
|
|
2147
|
+
.option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1180
2148
|
.requiredOption("--id <id>", "Group ID to remove")
|
|
1181
2149
|
.action((options) => {
|
|
1182
|
-
removePropGroup(options.component, options);
|
|
2150
|
+
removePropGroup({ id: options.componentId, name: options.component }, options);
|
|
1183
2151
|
});
|
|
1184
2152
|
config
|
|
1185
2153
|
.command("move-prop-group")
|
|
1186
2154
|
.description("Move a prop group to a different parent or position (for drag-and-drop reordering)")
|
|
1187
|
-
.
|
|
2155
|
+
.option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
|
|
2156
|
+
.option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1188
2157
|
.requiredOption("--id <id>", "Group ID to move")
|
|
1189
2158
|
.option("--parent <parentId>", "Target parent group ID (omit to move to root)")
|
|
1190
|
-
.option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", v => parseInt(v, 10))
|
|
2159
|
+
.option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", (v) => parseInt(v, 10))
|
|
1191
2160
|
.action((options) => {
|
|
1192
|
-
movePropGroup(options.component, options);
|
|
2161
|
+
movePropGroup({ id: options.componentId, name: options.component }, options);
|
|
2162
|
+
});
|
|
2163
|
+
config
|
|
2164
|
+
.command("list-components")
|
|
2165
|
+
.description("List all components and their props (with canonical ids for use with --id flags)")
|
|
2166
|
+
.action(() => {
|
|
2167
|
+
listComponents();
|
|
1193
2168
|
});
|
|
1194
2169
|
config
|
|
1195
2170
|
.command("list")
|
|
1196
|
-
.description("
|
|
2171
|
+
.description("Alias for `list-components` (kept for backwards compatibility)")
|
|
1197
2172
|
.action(() => {
|
|
1198
2173
|
listComponents();
|
|
1199
2174
|
});
|
|
@@ -1223,10 +2198,10 @@ export function createConfigCommand() {
|
|
|
1223
2198
|
});
|
|
1224
2199
|
config
|
|
1225
2200
|
.command("remove-enum")
|
|
1226
|
-
.description("Remove a custom enum type (fails if any prop references it)")
|
|
2201
|
+
.description("Remove a custom enum type (fails if any prop references it, or — while the editor is connected — if a theme ENUM global variable selects from it)")
|
|
1227
2202
|
.requiredOption("--id <id>", "Enum ID to remove")
|
|
1228
|
-
.action((options) => {
|
|
1229
|
-
removeEnum(options);
|
|
2203
|
+
.action(async (options) => {
|
|
2204
|
+
await removeEnum(options);
|
|
1230
2205
|
});
|
|
1231
2206
|
config
|
|
1232
2207
|
.command("list-enums")
|
|
@@ -1241,54 +2216,15 @@ export function createConfigCommand() {
|
|
|
1241
2216
|
* Returns the matching type (with enumOptions) if found, null otherwise.
|
|
1242
2217
|
*/
|
|
1243
2218
|
async function fetchLiveEditorEnumById(enumId) {
|
|
1244
|
-
const
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
if (settled)
|
|
1252
|
-
return;
|
|
1253
|
-
settled = true;
|
|
1254
|
-
try {
|
|
1255
|
-
ws?.close();
|
|
1256
|
-
}
|
|
1257
|
-
catch {
|
|
1258
|
-
/* ignore */
|
|
1259
|
-
}
|
|
1260
|
-
resolve(value);
|
|
1261
|
-
};
|
|
1262
|
-
try {
|
|
1263
|
-
ws = new WebSocket(WS_URL);
|
|
2219
|
+
const types = await fetchLiveEditorTypes();
|
|
2220
|
+
const match = types?.find((t) => t.category === "enum" && t.id === enumId);
|
|
2221
|
+
return match
|
|
2222
|
+
? {
|
|
2223
|
+
id: match.id,
|
|
2224
|
+
name: match.name,
|
|
2225
|
+
...(match.enumOptions ? { enumOptions: match.enumOptions } : {}),
|
|
1264
2226
|
}
|
|
1265
|
-
|
|
1266
|
-
resolve(null);
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
const timeout = setTimeout(() => finish(null), TIMEOUT_MS);
|
|
1270
|
-
ws.on("open", () => {
|
|
1271
|
-
ws.send(JSON.stringify({ type: "request-types" }));
|
|
1272
|
-
});
|
|
1273
|
-
ws.on("message", (data) => {
|
|
1274
|
-
try {
|
|
1275
|
-
const message = JSON.parse(data.toString());
|
|
1276
|
-
if (message.type === "types-list") {
|
|
1277
|
-
clearTimeout(timeout);
|
|
1278
|
-
const types = message.payload?.types || [];
|
|
1279
|
-
const match = types.find((t) => t.category === "enum" && t.id === enumId);
|
|
1280
|
-
finish(match ? { id: match.id, name: match.name, ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}) } : null);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
catch {
|
|
1284
|
-
// Ignore non-JSON messages
|
|
1285
|
-
}
|
|
1286
|
-
});
|
|
1287
|
-
ws.on("error", () => {
|
|
1288
|
-
clearTimeout(timeout);
|
|
1289
|
-
finish(null);
|
|
1290
|
-
});
|
|
1291
|
-
});
|
|
2227
|
+
: null;
|
|
1292
2228
|
}
|
|
1293
2229
|
/**
|
|
1294
2230
|
* Best-effort lookup of an enum by PascalCase name in the live editor session.
|
|
@@ -1298,8 +2234,37 @@ async function fetchLiveEditorEnumById(enumId) {
|
|
|
1298
2234
|
* must treat null as "could not verify, fall back to on-disk check only".
|
|
1299
2235
|
*/
|
|
1300
2236
|
async function fetchLiveEditorEnumByName(pascalName) {
|
|
2237
|
+
const types = await fetchLiveEditorTypes();
|
|
2238
|
+
const match = types?.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
|
|
2239
|
+
return match ? { id: match.id, name: match.name } : null;
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Best-effort read of the connected editor's theme global variables (the same editor action
|
|
2243
|
+
* `list-theme-globals` runs). Returns null on any failure — no dev server, no editor connected,
|
|
2244
|
+
* timeout — and callers must treat null as "could not verify, allow".
|
|
2245
|
+
*/
|
|
2246
|
+
async function fetchLiveThemeGlobals() {
|
|
2247
|
+
try {
|
|
2248
|
+
const result = await runEditorAction("list-theme-globals", {}, { timeoutMs: 1500 });
|
|
2249
|
+
if (!result.success)
|
|
2250
|
+
return null;
|
|
2251
|
+
const settings = result.payload?.settings;
|
|
2252
|
+
return settings && typeof settings === "object" ? settings : null;
|
|
2253
|
+
}
|
|
2254
|
+
catch {
|
|
2255
|
+
return null;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* Best-effort fetch of the editor's synced type list (with category /
|
|
2260
|
+
* sectionAllowed / enumOptions metadata). Returns null on any failure (no dev
|
|
2261
|
+
* server, timeout) — callers must treat null as "could not verify, allow".
|
|
2262
|
+
*/
|
|
2263
|
+
async function fetchLiveEditorTypes() {
|
|
1301
2264
|
const { WebSocket } = await import("ws");
|
|
1302
|
-
|
|
2265
|
+
// `client=control` keeps this short-lived socket out of the dev server's
|
|
2266
|
+
// `editor-action` target pool — it only reads types, it can't apply commands.
|
|
2267
|
+
const WS_URL = "ws://localhost:5201/?client=control";
|
|
1303
2268
|
const TIMEOUT_MS = 1500;
|
|
1304
2269
|
return new Promise((resolve) => {
|
|
1305
2270
|
let ws;
|
|
@@ -1332,9 +2297,7 @@ async function fetchLiveEditorEnumByName(pascalName) {
|
|
|
1332
2297
|
const message = JSON.parse(data.toString());
|
|
1333
2298
|
if (message.type === "types-list") {
|
|
1334
2299
|
clearTimeout(timeout);
|
|
1335
|
-
|
|
1336
|
-
const match = types.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
|
|
1337
|
-
finish(match ? { id: match.id, name: match.name } : null);
|
|
2300
|
+
finish(message.payload?.types || []);
|
|
1338
2301
|
}
|
|
1339
2302
|
}
|
|
1340
2303
|
catch {
|
|
@@ -1347,9 +2310,52 @@ async function fetchLiveEditorEnumByName(pascalName) {
|
|
|
1347
2310
|
});
|
|
1348
2311
|
});
|
|
1349
2312
|
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Definition-time guard for TYPE-prop typeIds, verified against the live
|
|
2315
|
+
* editor's type list (skipped when the dev server is unreachable or the type
|
|
2316
|
+
* is unknown to the editor). Two rules:
|
|
2317
|
+
* - deprecated types are rejected on ANY component: they are legacy
|
|
2318
|
+
* render-only types — old themes may still carry values, but new props must
|
|
2319
|
+
* not be defined with them.
|
|
2320
|
+
* - on section components, only section-allowed (style) types pass — any other
|
|
2321
|
+
* type renders an empty placeholder merchants cannot fill, and section-level
|
|
2322
|
+
* bindings cannot supply a value either, so the prop would be dead weight.
|
|
2323
|
+
* On violation prints the standard { success: false } envelope and exits.
|
|
2324
|
+
*/
|
|
2325
|
+
async function assertTypeIdsUsable(componentType, typeIds) {
|
|
2326
|
+
if (typeIds.length === 0)
|
|
2327
|
+
return;
|
|
2328
|
+
const types = await fetchLiveEditorTypes();
|
|
2329
|
+
if (!types)
|
|
2330
|
+
return;
|
|
2331
|
+
const matches = typeIds.flatMap((typeId) => {
|
|
2332
|
+
const match = types.find((t) => t.id === typeId);
|
|
2333
|
+
return match ? [match] : [];
|
|
2334
|
+
});
|
|
2335
|
+
const legacy = matches.find((t) => t.deprecated === true);
|
|
2336
|
+
if (legacy) {
|
|
2337
|
+
console.log(JSON.stringify({
|
|
2338
|
+
success: false,
|
|
2339
|
+
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).`,
|
|
2340
|
+
}));
|
|
2341
|
+
process.exit(1);
|
|
2342
|
+
}
|
|
2343
|
+
if (componentType !== "section")
|
|
2344
|
+
return;
|
|
2345
|
+
const disallowed = matches.find((t) => t.sectionAllowed === false);
|
|
2346
|
+
if (!disallowed)
|
|
2347
|
+
return;
|
|
2348
|
+
console.log(JSON.stringify({
|
|
2349
|
+
success: false,
|
|
2350
|
+
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.`,
|
|
2351
|
+
}));
|
|
2352
|
+
process.exit(1);
|
|
2353
|
+
}
|
|
1350
2354
|
async function listTypes(componentType) {
|
|
1351
2355
|
const { WebSocket } = await import("ws");
|
|
1352
|
-
|
|
2356
|
+
// `client=control` keeps this short-lived socket out of the dev server's
|
|
2357
|
+
// `editor-action` target pool — it only reads types, it can't apply commands.
|
|
2358
|
+
const WS_URL = "ws://localhost:5201/?client=control";
|
|
1353
2359
|
const TIMEOUT_MS = 5000;
|
|
1354
2360
|
await new Promise((resolve) => {
|
|
1355
2361
|
let ws;
|
|
@@ -1359,7 +2365,7 @@ async function listTypes(componentType) {
|
|
|
1359
2365
|
catch {
|
|
1360
2366
|
console.log(JSON.stringify({
|
|
1361
2367
|
success: false,
|
|
1362
|
-
error: "Failed to connect to dev server. Make sure 'ikas
|
|
2368
|
+
error: "Failed to connect to dev server. Make sure 'ikas theme dev' is running.",
|
|
1363
2369
|
}));
|
|
1364
2370
|
process.exit(1);
|
|
1365
2371
|
return;
|
|
@@ -1380,7 +2386,8 @@ async function listTypes(componentType) {
|
|
|
1380
2386
|
const message = JSON.parse(data.toString());
|
|
1381
2387
|
if (message.type === "types-list") {
|
|
1382
2388
|
clearTimeout(timeout);
|
|
1383
|
-
|
|
2389
|
+
// Legacy render-only types are never offered for new props.
|
|
2390
|
+
let types = message.payload.types.filter((t) => !t.deprecated);
|
|
1384
2391
|
if (componentType === "section") {
|
|
1385
2392
|
types = types.filter((t) => t.sectionAllowed);
|
|
1386
2393
|
}
|
|
@@ -1400,7 +2407,7 @@ async function listTypes(componentType) {
|
|
|
1400
2407
|
clearTimeout(timeout);
|
|
1401
2408
|
console.log(JSON.stringify({
|
|
1402
2409
|
success: false,
|
|
1403
|
-
error: "Failed to connect to dev server. Make sure 'ikas
|
|
2410
|
+
error: "Failed to connect to dev server. Make sure 'ikas theme dev' is running.",
|
|
1404
2411
|
}));
|
|
1405
2412
|
process.exit(1);
|
|
1406
2413
|
});
|