@ikas/component-cli 1.4.0-beta.23 → 1.4.0-beta.230
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.js +1 -1
- package/dist/commands/add-to-page.js.map +1 -1
- 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 +23 -170
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/config-texts.d.ts +39 -0
- package/dist/commands/config-texts.d.ts.map +1 -0
- package/dist/commands/config-texts.js +192 -0
- package/dist/commands/config-texts.js.map +1 -0
- package/dist/commands/config.d.ts +45 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +1205 -154
- 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 +388 -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 +50 -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 +328 -39
- 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 +28 -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 +43 -0
- package/dist/commands/get-section-values.js.map +1 -0
- package/dist/commands/import.js +1 -1
- package/dist/commands/import.js.map +1 -1
- 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.js +1 -1
- package/dist/commands/list-imported.js.map +1 -1
- 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.js +1 -1
- package/dist/commands/list-pages.js.map +1 -1
- package/dist/commands/list-theme-globals.d.ts +3 -0
- package/dist/commands/list-theme-globals.d.ts.map +1 -0
- package/dist/commands/list-theme-globals.js +22 -0
- package/dist/commands/list-theme-globals.js.map +1 -0
- package/dist/commands/list-translations.d.ts +3 -0
- package/dist/commands/list-translations.d.ts.map +1 -0
- package/dist/commands/list-translations.js +33 -0
- package/dist/commands/list-translations.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 +72 -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/translation-languages.d.ts +3 -0
- package/dist/commands/translation-languages.d.ts.map +1 -0
- package/dist/commands/translation-languages.js +43 -0
- package/dist/commands/translation-languages.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 +42 -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 +81 -0
- package/dist/commands/update-section-prop.js.map +1 -0
- package/dist/commands/update-translations.d.ts +3 -0
- package/dist/commands/update-translations.d.ts.map +1 -0
- package/dist/commands/update-translations.js +45 -0
- package/dist/commands/update-translations.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 +66 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/archive-project.d.ts +50 -0
- package/dist/utils/archive-project.d.ts.map +1 -0
- package/dist/utils/archive-project.js +178 -0
- package/dist/utils/archive-project.js.map +1 -0
- package/dist/utils/compile.d.ts +17 -2
- package/dist/utils/compile.d.ts.map +1 -1
- package/dist/utils/compile.js +742 -109
- package/dist/utils/compile.js.map +1 -1
- package/dist/utils/component-helpers.d.ts +236 -4
- package/dist/utils/component-helpers.d.ts.map +1 -1
- package/dist/utils/component-helpers.js +656 -37
- 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/dev-server-auth.d.ts +28 -0
- package/dist/utils/dev-server-auth.d.ts.map +1 -0
- package/dist/utils/dev-server-auth.js +79 -0
- package/dist/utils/dev-server-auth.js.map +1 -0
- package/dist/utils/editor-action-client.d.ts +20 -3
- package/dist/utils/editor-action-client.d.ts.map +1 -1
- package/dist/utils/editor-action-client.js +81 -19
- package/dist/utils/editor-action-client.js.map +1 -1
- package/dist/utils/esm-transform.d.ts +30 -0
- package/dist/utils/esm-transform.d.ts.map +1 -1
- package/dist/utils/esm-transform.js +169 -2
- package/dist/utils/esm-transform.js.map +1 -1
- package/dist/utils/find-component-for-file.d.ts +12 -0
- package/dist/utils/find-component-for-file.d.ts.map +1 -0
- package/dist/utils/find-component-for-file.js +24 -0
- package/dist/utils/find-component-for-file.js.map +1 -0
- package/dist/utils/forbid-deep-imports.d.ts +3 -0
- package/dist/utils/forbid-deep-imports.d.ts.map +1 -0
- package/dist/utils/forbid-deep-imports.js +54 -0
- package/dist/utils/forbid-deep-imports.js.map +1 -0
- package/dist/utils/forbid-external-packages.d.ts +26 -0
- package/dist/utils/forbid-external-packages.d.ts.map +1 -0
- package/dist/utils/forbid-external-packages.js +99 -0
- package/dist/utils/forbid-external-packages.js.map +1 -0
- package/dist/utils/i18n.d.ts +98 -0
- package/dist/utils/i18n.d.ts.map +1 -0
- package/dist/utils/i18n.js +408 -0
- package/dist/utils/i18n.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/storefront-t.d.ts +17 -0
- package/dist/utils/storefront-t.d.ts.map +1 -0
- package/dist/utils/storefront-t.js +187 -0
- package/dist/utils/storefront-t.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 +165 -7
- package/dist/utils/websocket-server.d.ts.map +1 -1
- package/dist/utils/websocket-server.js +326 -45
- package/dist/utils/websocket-server.js.map +1 -1
- package/package.json +19 -2
- package/templates/add/section.css +11 -0
- package/dist/commands/create.d.ts +0 -4
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js +0 -176
- package/dist/commands/create.js.map +0 -1
package/dist/commands/config.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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 { listTexts, removeText, renameText, setText } from "./config-texts.js";
|
|
6
|
+
import { PROP_TYPES, toPascalCase, generateTypesFile, isBuiltInEnumTypeId, normalizeEnumOptionsInput, generateGlobalTypesFile, collectUsedEnumIds, generateComponentFile, generateStylesFile, generateProjectId, isValidProjectId, generateComponentId, generateUniqueId, updateBarrelExport, findPropGroup, collectPropGroupIds, movePropGroupInTree, validateFilteredComponentIds, visibilityGateError, checkConfigTranslations, } from "../utils/component-helpers.js";
|
|
5
7
|
function loadConfig() {
|
|
6
8
|
const configPath = path.resolve(process.cwd(), "ikas.config.json");
|
|
7
9
|
if (!fs.existsSync(configPath)) {
|
|
@@ -31,7 +33,10 @@ function resolveComponent(config, ref) {
|
|
|
31
33
|
success: false,
|
|
32
34
|
error: "Component reference required: pass --id (preferred) or --name (exact PascalCase). " +
|
|
33
35
|
"Run `ikas-component config list-components` to see canonical ids.",
|
|
34
|
-
validComponents: config.components.map((c) => ({
|
|
36
|
+
validComponents: config.components.map((c) => ({
|
|
37
|
+
id: c.id,
|
|
38
|
+
name: c.name,
|
|
39
|
+
})),
|
|
35
40
|
}));
|
|
36
41
|
process.exit(1);
|
|
37
42
|
}
|
|
@@ -52,7 +57,10 @@ function resolveComponent(config, ref) {
|
|
|
52
57
|
? `Component not found: id="${id}".`
|
|
53
58
|
: `Component not found: name="${name}". Names are matched exactly (PascalCase, no normalization).`,
|
|
54
59
|
lookup: id ? { id } : { name },
|
|
55
|
-
validComponents: config.components.map((c) => ({
|
|
60
|
+
validComponents: config.components.map((c) => ({
|
|
61
|
+
id: c.id,
|
|
62
|
+
name: c.name,
|
|
63
|
+
})),
|
|
56
64
|
hint: "Use --id (preferred) or exact PascalCase --name. " +
|
|
57
65
|
"Run `ikas-component config list-components` to see canonical ids.",
|
|
58
66
|
}));
|
|
@@ -60,9 +68,6 @@ function resolveComponent(config, ref) {
|
|
|
60
68
|
}
|
|
61
69
|
return found;
|
|
62
70
|
}
|
|
63
|
-
function getComponentNames(config) {
|
|
64
|
-
return config.components.map((c) => c.name);
|
|
65
|
-
}
|
|
66
71
|
/**
|
|
67
72
|
* Emit a structured JSON error for unknown filteredComponentIds entries and exit non-zero.
|
|
68
73
|
* Lists valid `{ id, name }` pairs so the LLM/caller can recover.
|
|
@@ -90,6 +95,11 @@ function assertKnownComponentIds(input, config) {
|
|
|
90
95
|
}
|
|
91
96
|
return input;
|
|
92
97
|
}
|
|
98
|
+
// The set of component ids that currently exist in the project, for validateComponentDefaultValue's
|
|
99
|
+
// existence check. Undefined when no config is loaded, which makes the check a no-op (back-compat).
|
|
100
|
+
function existingComponentIdSet(config) {
|
|
101
|
+
return config ? new Set(config.components.map((c) => c.id)) : undefined;
|
|
102
|
+
}
|
|
93
103
|
function regenerateTypes(component, componentType, config) {
|
|
94
104
|
const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
|
|
95
105
|
const typesPath = path.join(componentDir, "types.ts");
|
|
@@ -156,7 +166,118 @@ const ALLOWED_PROP_FIELDS = new Set([
|
|
|
156
166
|
"enumTypeId",
|
|
157
167
|
"filteredComponentIds",
|
|
158
168
|
"privateVarMap",
|
|
169
|
+
"numberRangeData",
|
|
170
|
+
"requiredProp",
|
|
171
|
+
"requiredPropValue",
|
|
172
|
+
"requiredEnumOptionId",
|
|
173
|
+
"requiredEnumOptionValue",
|
|
174
|
+
"translations",
|
|
159
175
|
]);
|
|
176
|
+
// Emit the standard CLI error envelope and exit(1) when a validator returned a message; a no-op
|
|
177
|
+
// when the message is null. Collapses the repeated `if (err) { console.log(...); process.exit(1) }`
|
|
178
|
+
// tail that follows every default-value validator call.
|
|
179
|
+
function failIf(error) {
|
|
180
|
+
if (error) {
|
|
181
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Refusal of a removal run without `--yes`: the user's consent, never the agent's own call. */
|
|
186
|
+
const unconfirmedRemoval = (what, yes) => yes
|
|
187
|
+
? null
|
|
188
|
+
: `Refused: this would ${what}. Ask the user first and re-run with --yes once they have agreed. Do NOT add it on your own.`;
|
|
189
|
+
/**
|
|
190
|
+
* A `--requiredPropValue` flag as a boolean. Commander hands every flag over as a string, and this is
|
|
191
|
+
* the one place that turns it back; anything other than true/false is refused rather than coerced,
|
|
192
|
+
* since "0" or "off" silently becoming `true` is exactly the kind of gate nobody would think to
|
|
193
|
+
* check.
|
|
194
|
+
*/
|
|
195
|
+
function parseRequiredPropValue(raw, where) {
|
|
196
|
+
if (raw === undefined)
|
|
197
|
+
return undefined;
|
|
198
|
+
if (typeof raw === "boolean")
|
|
199
|
+
return raw;
|
|
200
|
+
if (raw === "true")
|
|
201
|
+
return true;
|
|
202
|
+
if (raw === "false")
|
|
203
|
+
return false;
|
|
204
|
+
failIf(`--requiredPropValue for ${where} must be "true" or "false" (got "${raw}").`);
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Validate/sanitize a numberRangeData object for a NUMBER_RANGE prop. Accepts an object
|
|
209
|
+
* or a JSON string. min/max/interval are truncated to integers; only a positive interval
|
|
210
|
+
* is kept (0/negative would yield an invalid slider step). Exits with a structured error
|
|
211
|
+
* on malformed input.
|
|
212
|
+
*/
|
|
213
|
+
function parseNumberRangeData(raw, propName) {
|
|
214
|
+
const fail = (error) => {
|
|
215
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
216
|
+
process.exit(1);
|
|
217
|
+
};
|
|
218
|
+
let obj = raw;
|
|
219
|
+
if (typeof raw === "string") {
|
|
220
|
+
try {
|
|
221
|
+
obj = JSON.parse(raw);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
fail(`Invalid --numberRangeData JSON for prop "${propName}": ${raw}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
228
|
+
fail(`numberRangeData for prop "${propName}" must be an object { min, max, interval?, unit? }.`);
|
|
229
|
+
}
|
|
230
|
+
const o = obj;
|
|
231
|
+
const asInt = (v, field) => {
|
|
232
|
+
if (v === undefined || v === null)
|
|
233
|
+
return undefined;
|
|
234
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
235
|
+
fail(`numberRangeData.${field} for prop "${propName}" must be a finite number.`);
|
|
236
|
+
}
|
|
237
|
+
return Math.trunc(v);
|
|
238
|
+
};
|
|
239
|
+
const min = asInt(o.min, "min");
|
|
240
|
+
const max = asInt(o.max, "max");
|
|
241
|
+
const interval = asInt(o.interval, "interval");
|
|
242
|
+
const unit = typeof o.unit === "string" && o.unit.trim() ? o.unit.trim() : undefined;
|
|
243
|
+
if (min !== undefined && max !== undefined && min >= max) {
|
|
244
|
+
fail(`numberRangeData for prop "${propName}": min (${min}) must be less than max (${max}).`);
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
...(min !== undefined ? { min } : {}),
|
|
248
|
+
...(max !== undefined ? { max } : {}),
|
|
249
|
+
...(interval !== undefined && interval > 0 ? { interval } : {}),
|
|
250
|
+
...(unit ? { unit } : {}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const numberRangeDataRequiredError = (propName) => `numberRangeData with min and max is required for NUMBER_RANGE prop "${propName}". ` +
|
|
254
|
+
`Provide it, e.g. --numberRangeData '{"min":0,"max":100,"interval":5,"unit":"px"}' ` +
|
|
255
|
+
`(or a "numberRangeData" field inside --props). interval and unit are optional.`;
|
|
256
|
+
// A NUMBER_RANGE slider config is complete only with both bounds; interval/unit stay optional.
|
|
257
|
+
export const isNumberRangeComplete = (data) => data.min !== undefined && data.max !== undefined;
|
|
258
|
+
/**
|
|
259
|
+
* A NUMBER_RANGE prop must carry a slider config with at least min and max — the editor UI marks
|
|
260
|
+
* Minimum/Maksimum Değer as required, so a prop authored without them yields a half-configured
|
|
261
|
+
* slider. Enforce it at creation time: reject when numberRangeData is absent or missing min/max.
|
|
262
|
+
*/
|
|
263
|
+
export function requireNumberRangeData(raw, propName) {
|
|
264
|
+
if (raw === undefined) {
|
|
265
|
+
console.log(JSON.stringify({
|
|
266
|
+
success: false,
|
|
267
|
+
error: numberRangeDataRequiredError(propName),
|
|
268
|
+
}));
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
const parsed = parseNumberRangeData(raw, propName);
|
|
272
|
+
if (!isNumberRangeComplete(parsed)) {
|
|
273
|
+
console.log(JSON.stringify({
|
|
274
|
+
success: false,
|
|
275
|
+
error: numberRangeDataRequiredError(propName),
|
|
276
|
+
}));
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
return parsed;
|
|
280
|
+
}
|
|
160
281
|
/**
|
|
161
282
|
* Parse and validate the --props JSON flag for add-component.
|
|
162
283
|
* Returns validated ComponentProp[] or exits with error.
|
|
@@ -182,6 +303,8 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
182
303
|
}
|
|
183
304
|
const seenNames = new Set();
|
|
184
305
|
const props = [];
|
|
306
|
+
// Loop-invariant: build the known-id set once for the whole batch, not per prop.
|
|
307
|
+
const existingComponentIds = existingComponentIdSet(config);
|
|
185
308
|
for (const raw of rawProps) {
|
|
186
309
|
const r = raw;
|
|
187
310
|
const unknownFields = Object.keys(r).filter((k) => !ALLOWED_PROP_FIELDS.has(k));
|
|
@@ -234,6 +357,36 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
234
357
|
}));
|
|
235
358
|
process.exit(1);
|
|
236
359
|
}
|
|
360
|
+
// Dynamic (merchant-data reference) props can't carry a static default.
|
|
361
|
+
rejectDynamicDefault(r.name, propType, r.defaultValue);
|
|
362
|
+
// Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
|
|
363
|
+
if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
|
|
364
|
+
r.defaultValue !== undefined) {
|
|
365
|
+
const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
|
|
366
|
+
failIf(linkError);
|
|
367
|
+
}
|
|
368
|
+
// Validate SVG / SVG_LIST defaultValue (reject malformed/oversized/unsafe SVG)
|
|
369
|
+
if ((propType === "SVG" || propType === "SVG_LIST") &&
|
|
370
|
+
r.defaultValue !== undefined) {
|
|
371
|
+
const svgError = validateSvgDefaultValue(propType, r.defaultValue, r.name);
|
|
372
|
+
failIf(svgError);
|
|
373
|
+
}
|
|
374
|
+
// Validate COMPONENT / COMPONENT_LIST defaultValue (reference-only child code components)
|
|
375
|
+
if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
376
|
+
r.defaultValue !== undefined) {
|
|
377
|
+
failIf(validateComponentDefaultValue(propType, r.defaultValue, r.name, r.filteredComponentIds, existingComponentIds));
|
|
378
|
+
}
|
|
379
|
+
// Validate TYPE (structured/style) defaultValue shape — must be an object (or an array for an
|
|
380
|
+
// _array typeId), never a bare string that would be emitted as a wrong-shaped default.
|
|
381
|
+
if (propType === "TYPE" && r.defaultValue !== undefined) {
|
|
382
|
+
const typeError = validateTypeDefaultValue(r.defaultValue, r.name, r.typeId);
|
|
383
|
+
failIf(typeError);
|
|
384
|
+
}
|
|
385
|
+
// Validate NUMBER defaultValue — reject empty/non-finite (Number("") silently becomes 0).
|
|
386
|
+
if (propType === "NUMBER" && r.defaultValue !== undefined) {
|
|
387
|
+
const numError = validateNumberDefaultValue(r.defaultValue, r.name);
|
|
388
|
+
failIf(numError);
|
|
389
|
+
}
|
|
237
390
|
// Auto-generate displayName if omitted
|
|
238
391
|
const displayName = typeof r.displayName === "string" && r.displayName
|
|
239
392
|
? r.displayName
|
|
@@ -258,7 +411,10 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
258
411
|
process.exit(1);
|
|
259
412
|
}
|
|
260
413
|
// Validate enum type exists (checks config then live dev server)
|
|
261
|
-
if (config &&
|
|
414
|
+
if (config &&
|
|
415
|
+
configPath &&
|
|
416
|
+
typeof r.enumTypeId === "string" &&
|
|
417
|
+
!isBuiltInEnumTypeId(r.enumTypeId)) {
|
|
262
418
|
const resolved = await resolveEnumType(r.enumTypeId, config, configPath);
|
|
263
419
|
if (!resolved) {
|
|
264
420
|
console.log(JSON.stringify({
|
|
@@ -277,9 +433,7 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
277
433
|
...(typeof r.description === "string" && r.description
|
|
278
434
|
? { description: r.description }
|
|
279
435
|
: {}),
|
|
280
|
-
...(r.defaultValue !== undefined
|
|
281
|
-
? { defaultValue: r.defaultValue }
|
|
282
|
-
: {}),
|
|
436
|
+
...(r.defaultValue !== undefined ? { defaultValue: r.defaultValue } : {}),
|
|
283
437
|
...(typeof r.groupId === "string" && r.groupId
|
|
284
438
|
? { groupId: r.groupId }
|
|
285
439
|
: {}),
|
|
@@ -289,15 +443,53 @@ async function parsePropsFlag(propsJson, config, configPath) {
|
|
|
289
443
|
...(propType === "ENUM" && typeof r.enumTypeId === "string"
|
|
290
444
|
? { enumTypeId: r.enumTypeId }
|
|
291
445
|
: {}),
|
|
292
|
-
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
293
|
-
|
|
446
|
+
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
447
|
+
r.filteredComponentIds &&
|
|
448
|
+
Array.isArray(r.filteredComponentIds)
|
|
449
|
+
? {
|
|
450
|
+
filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config),
|
|
451
|
+
}
|
|
452
|
+
: {}),
|
|
453
|
+
...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
454
|
+
r.privateVarMap &&
|
|
455
|
+
typeof r.privateVarMap === "object"
|
|
456
|
+
? {
|
|
457
|
+
privateVarMap: r.privateVarMap,
|
|
458
|
+
}
|
|
459
|
+
: {}),
|
|
460
|
+
...(propType === "NUMBER_RANGE"
|
|
461
|
+
? { numberRangeData: requireNumberRangeData(r.numberRangeData, r.name) }
|
|
462
|
+
: {}),
|
|
463
|
+
...(r.requiredProp !== undefined
|
|
464
|
+
? { requiredProp: r.requiredProp }
|
|
465
|
+
: {}),
|
|
466
|
+
...(r.requiredPropValue !== undefined
|
|
467
|
+
? { requiredPropValue: r.requiredPropValue }
|
|
468
|
+
: {}),
|
|
469
|
+
...(r.requiredEnumOptionId !== undefined
|
|
470
|
+
? { requiredEnumOptionId: r.requiredEnumOptionId }
|
|
294
471
|
: {}),
|
|
295
|
-
...(
|
|
296
|
-
? {
|
|
472
|
+
...(r.requiredEnumOptionValue !== undefined
|
|
473
|
+
? { requiredEnumOptionValue: r.requiredEnumOptionValue }
|
|
474
|
+
: {}),
|
|
475
|
+
...(r.translations !== undefined
|
|
476
|
+
? { translations: r.translations }
|
|
297
477
|
: {}),
|
|
298
478
|
};
|
|
299
479
|
props.push(prop);
|
|
300
480
|
}
|
|
481
|
+
// Gates are checked once the whole batch is parsed: a prop may name one declared after it, and
|
|
482
|
+
// cycle detection needs every prop in hand. The batch IS the component's prop list here — the
|
|
483
|
+
// component itself does not exist yet — so it stands in as one.
|
|
484
|
+
const batch = { name: "the new component", props };
|
|
485
|
+
// The props' labels in the editor languages (`translations`), as a config's are checked
|
|
486
|
+
const translations = checkConfigTranslations([batch]);
|
|
487
|
+
failIf([...translations.errors, ...translations.warnings].join("; ") || null);
|
|
488
|
+
for (const prop of props) {
|
|
489
|
+
failIf(visibilityGateError(batch, prop, `prop "${prop.name}"`, {
|
|
490
|
+
selfPropName: prop.name,
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
301
493
|
return props;
|
|
302
494
|
}
|
|
303
495
|
// --- Subcommands ---
|
|
@@ -325,11 +517,25 @@ async function addComponent(name, options) {
|
|
|
325
517
|
if (!config.projectId) {
|
|
326
518
|
config.projectId = generateProjectId();
|
|
327
519
|
}
|
|
520
|
+
else if (!isValidProjectId(config.projectId)) {
|
|
521
|
+
// A malformed projectId (hand-edited config) breaks the editor's republish
|
|
522
|
+
// stability: published/local id prefixes stop matching and every publish
|
|
523
|
+
// INSERTs duplicate design elements. Fail fast instead of corrupting data.
|
|
524
|
+
console.log(JSON.stringify({
|
|
525
|
+
success: false,
|
|
526
|
+
error: `Invalid projectId "${config.projectId}" in ikas.config.json. ` +
|
|
527
|
+
"projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
|
|
528
|
+
"Restore the original generated value — do not hand-edit it.",
|
|
529
|
+
}));
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
328
532
|
// Parse --props (and validate filteredComponentIds) BEFORE any filesystem side-effects
|
|
329
533
|
// so a bad payload doesn't leave an empty component directory behind.
|
|
330
534
|
const props = options.props
|
|
331
535
|
? await parsePropsFlag(options.props, config, configPath)
|
|
332
536
|
: [];
|
|
537
|
+
// Reject section-disallowed TYPE props at definition time (best-effort, needs dev server)
|
|
538
|
+
await assertTypeIdsUsable(componentType, props.flatMap((p) => (p.type === "TYPE" && p.typeId ? [p.typeId] : [])));
|
|
333
539
|
const componentId = generateComponentId(config.projectId);
|
|
334
540
|
const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
|
|
335
541
|
// Create directory
|
|
@@ -366,7 +572,7 @@ async function addComponent(name, options) {
|
|
|
366
572
|
config.components.push(newComponent);
|
|
367
573
|
saveConfig(configPath, config);
|
|
368
574
|
// Update barrel export
|
|
369
|
-
updateBarrelExport(process.cwd(),
|
|
575
|
+
updateBarrelExport(process.cwd(), config.components);
|
|
370
576
|
// Regenerate global types
|
|
371
577
|
regenerateGlobalTypes(config);
|
|
372
578
|
console.log(JSON.stringify({
|
|
@@ -420,6 +626,10 @@ async function addProp(ref, options) {
|
|
|
420
626
|
}));
|
|
421
627
|
process.exit(1);
|
|
422
628
|
}
|
|
629
|
+
// Reject section-disallowed types at definition time (best-effort, needs dev server)
|
|
630
|
+
if (propType === "TYPE" && options.typeId) {
|
|
631
|
+
await assertTypeIdsUsable(component.type, [options.typeId]);
|
|
632
|
+
}
|
|
423
633
|
// Validate enumTypeId for ENUM props
|
|
424
634
|
if (propType === "ENUM" && !options.enumTypeId) {
|
|
425
635
|
console.log(JSON.stringify({
|
|
@@ -430,7 +640,9 @@ async function addProp(ref, options) {
|
|
|
430
640
|
}
|
|
431
641
|
// Validate that enumTypeId references an existing enum type
|
|
432
642
|
// (checks config.customTypes first, then falls back to the live dev server)
|
|
433
|
-
if (propType === "ENUM" &&
|
|
643
|
+
if (propType === "ENUM" &&
|
|
644
|
+
options.enumTypeId &&
|
|
645
|
+
!isBuiltInEnumTypeId(options.enumTypeId)) {
|
|
434
646
|
const resolved = await resolveEnumType(options.enumTypeId, config, configPath);
|
|
435
647
|
if (!resolved) {
|
|
436
648
|
console.log(JSON.stringify({
|
|
@@ -460,50 +672,117 @@ async function addProp(ref, options) {
|
|
|
460
672
|
}
|
|
461
673
|
// Parse filteredComponentIds JSON for COMPONENT/COMPONENT_LIST
|
|
462
674
|
let parsedFilteredIds;
|
|
463
|
-
if (options.filteredComponentIds &&
|
|
675
|
+
if (options.filteredComponentIds &&
|
|
676
|
+
(propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
|
|
464
677
|
try {
|
|
465
678
|
parsedFilteredIds = JSON.parse(options.filteredComponentIds);
|
|
466
679
|
if (!Array.isArray(parsedFilteredIds)) {
|
|
467
|
-
console.log(JSON.stringify({
|
|
680
|
+
console.log(JSON.stringify({
|
|
681
|
+
success: false,
|
|
682
|
+
error: "--filteredComponentIds must be a JSON array of strings.",
|
|
683
|
+
}));
|
|
468
684
|
process.exit(1);
|
|
469
685
|
}
|
|
470
686
|
}
|
|
471
687
|
catch {
|
|
472
|
-
console.log(JSON.stringify({
|
|
688
|
+
console.log(JSON.stringify({
|
|
689
|
+
success: false,
|
|
690
|
+
error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
|
|
691
|
+
}));
|
|
473
692
|
process.exit(1);
|
|
474
693
|
}
|
|
475
694
|
assertKnownComponentIds(parsedFilteredIds, config);
|
|
476
695
|
}
|
|
477
696
|
// Parse privateVarMap JSON for COMPONENT/COMPONENT_LIST
|
|
478
697
|
let parsedPrivateVarMap;
|
|
479
|
-
if (options.privateVarMap &&
|
|
698
|
+
if (options.privateVarMap &&
|
|
699
|
+
(propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
|
|
480
700
|
try {
|
|
481
701
|
parsedPrivateVarMap = JSON.parse(options.privateVarMap);
|
|
482
|
-
if (typeof parsedPrivateVarMap !== "object" ||
|
|
483
|
-
|
|
702
|
+
if (typeof parsedPrivateVarMap !== "object" ||
|
|
703
|
+
Array.isArray(parsedPrivateVarMap)) {
|
|
704
|
+
console.log(JSON.stringify({
|
|
705
|
+
success: false,
|
|
706
|
+
error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
|
|
707
|
+
}));
|
|
484
708
|
process.exit(1);
|
|
485
709
|
}
|
|
486
710
|
}
|
|
487
711
|
catch {
|
|
488
|
-
console.log(JSON.stringify({
|
|
712
|
+
console.log(JSON.stringify({
|
|
713
|
+
success: false,
|
|
714
|
+
error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
|
|
715
|
+
}));
|
|
489
716
|
process.exit(1);
|
|
490
717
|
}
|
|
491
718
|
}
|
|
719
|
+
rejectDynamicDefault(options.name, propType, options.defaultValue);
|
|
720
|
+
const parsedDefaultValue = options.defaultValue !== undefined
|
|
721
|
+
? parseDefaultValue(options.defaultValue, propType)
|
|
722
|
+
: undefined;
|
|
723
|
+
if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
|
|
724
|
+
parsedDefaultValue !== undefined) {
|
|
725
|
+
const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
|
|
726
|
+
failIf(linkError);
|
|
727
|
+
}
|
|
728
|
+
if ((propType === "SVG" || propType === "SVG_LIST") &&
|
|
729
|
+
parsedDefaultValue !== undefined) {
|
|
730
|
+
const svgError = validateSvgDefaultValue(propType, parsedDefaultValue, options.name);
|
|
731
|
+
failIf(svgError);
|
|
732
|
+
}
|
|
733
|
+
if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
|
|
734
|
+
parsedDefaultValue !== undefined) {
|
|
735
|
+
failIf(validateComponentDefaultValue(propType, parsedDefaultValue, options.name, parsedFilteredIds, existingComponentIdSet(config)));
|
|
736
|
+
}
|
|
737
|
+
if (propType === "TYPE" && parsedDefaultValue !== undefined) {
|
|
738
|
+
const typeError = validateTypeDefaultValue(parsedDefaultValue, options.name, options.typeId);
|
|
739
|
+
failIf(typeError);
|
|
740
|
+
}
|
|
741
|
+
// Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
|
|
742
|
+
if (propType === "NUMBER" && options.defaultValue !== undefined) {
|
|
743
|
+
const numError = validateNumberDefaultValue(options.defaultValue, options.name);
|
|
744
|
+
failIf(numError);
|
|
745
|
+
}
|
|
746
|
+
const parsedVisibilityValue = parseRequiredPropValue(options.requiredPropValue, `prop "${options.name}"`);
|
|
747
|
+
const parsedNumberRangeData = propType === "NUMBER_RANGE"
|
|
748
|
+
? requireNumberRangeData(options.numberRangeData, options.name)
|
|
749
|
+
: undefined;
|
|
750
|
+
if (propType === "NUMBER_RANGE" && parsedDefaultValue !== undefined) {
|
|
751
|
+
const nrError = validateNumberRangeDefaultValue(parsedDefaultValue, parsedNumberRangeData, options.name);
|
|
752
|
+
failIf(nrError);
|
|
753
|
+
}
|
|
492
754
|
const newProp = {
|
|
493
755
|
name: options.name,
|
|
494
756
|
displayName: options.displayName,
|
|
495
757
|
type: propType,
|
|
496
758
|
required: options.required ?? false,
|
|
497
759
|
...(options.description ? { description: options.description } : {}),
|
|
498
|
-
...(
|
|
499
|
-
? { defaultValue:
|
|
760
|
+
...(parsedDefaultValue !== undefined
|
|
761
|
+
? { defaultValue: parsedDefaultValue }
|
|
500
762
|
: {}),
|
|
501
763
|
...(options.group ? { groupId: options.group } : {}),
|
|
502
764
|
...(options.typeId ? { typeId: options.typeId } : {}),
|
|
503
765
|
...(options.enumTypeId ? { enumTypeId: options.enumTypeId } : {}),
|
|
504
766
|
...(parsedFilteredIds ? { filteredComponentIds: parsedFilteredIds } : {}),
|
|
505
767
|
...(parsedPrivateVarMap ? { privateVarMap: parsedPrivateVarMap } : {}),
|
|
768
|
+
...(parsedNumberRangeData
|
|
769
|
+
? { numberRangeData: parsedNumberRangeData }
|
|
770
|
+
: {}),
|
|
771
|
+
...(options.requiredProp ? { requiredProp: options.requiredProp } : {}),
|
|
772
|
+
...(parsedVisibilityValue !== undefined
|
|
773
|
+
? { requiredPropValue: parsedVisibilityValue }
|
|
774
|
+
: {}),
|
|
775
|
+
...(options.requiredProp && options.requiredEnumOptionValue !== undefined
|
|
776
|
+
? {
|
|
777
|
+
// The editor has no separate option id; the value doubles as one.
|
|
778
|
+
requiredEnumOptionId: options.requiredEnumOptionValue,
|
|
779
|
+
requiredEnumOptionValue: options.requiredEnumOptionValue,
|
|
780
|
+
}
|
|
781
|
+
: {}),
|
|
506
782
|
};
|
|
783
|
+
failIf(visibilityGateError(component, newProp, `prop "${newProp.name}"`, {
|
|
784
|
+
selfPropName: newProp.name,
|
|
785
|
+
}));
|
|
507
786
|
component.props.push(newProp);
|
|
508
787
|
saveConfig(configPath, config);
|
|
509
788
|
// Regenerate types.ts and global types
|
|
@@ -516,25 +795,467 @@ async function addProp(ref, options) {
|
|
|
516
795
|
name: newProp.name,
|
|
517
796
|
displayName: newProp.displayName,
|
|
518
797
|
type: newProp.type,
|
|
519
|
-
required: newProp.required,
|
|
520
798
|
...(newProp.groupId ? { groupId: newProp.groupId } : {}),
|
|
521
799
|
...(newProp.enumTypeId ? { enumTypeId: newProp.enumTypeId } : {}),
|
|
522
|
-
...(newProp.filteredComponentIds
|
|
523
|
-
|
|
800
|
+
...(newProp.filteredComponentIds
|
|
801
|
+
? { filteredComponentIds: newProp.filteredComponentIds }
|
|
802
|
+
: {}),
|
|
803
|
+
...(newProp.privateVarMap
|
|
804
|
+
? { privateVarMap: newProp.privateVarMap }
|
|
805
|
+
: {}),
|
|
806
|
+
...(newProp.numberRangeData
|
|
807
|
+
? { numberRangeData: newProp.numberRangeData }
|
|
808
|
+
: {}),
|
|
809
|
+
...(newProp.requiredProp
|
|
810
|
+
? {
|
|
811
|
+
requiredProp: newProp.requiredProp,
|
|
812
|
+
...(newProp.requiredEnumOptionValue !== undefined
|
|
813
|
+
? { requiredEnumOptionValue: newProp.requiredEnumOptionValue }
|
|
814
|
+
: { requiredPropValue: newProp.requiredPropValue ?? true }),
|
|
815
|
+
}
|
|
816
|
+
: {}),
|
|
524
817
|
},
|
|
525
818
|
}));
|
|
526
819
|
}
|
|
527
|
-
function parseDefaultValue(value, propType) {
|
|
820
|
+
export function parseDefaultValue(value, propType) {
|
|
528
821
|
switch (propType) {
|
|
529
822
|
case "NUMBER":
|
|
530
823
|
return Number(value);
|
|
531
824
|
case "BOOLEAN":
|
|
532
825
|
return value === "true";
|
|
826
|
+
case "COMPONENT":
|
|
827
|
+
case "COMPONENT_LIST":
|
|
828
|
+
case "LINK":
|
|
829
|
+
case "LIST_OF_LINK":
|
|
830
|
+
case "SVG_LIST":
|
|
831
|
+
case "NUMBER_RANGE":
|
|
832
|
+
case "TYPE":
|
|
833
|
+
// These all carry a typed value (object/array), not a scalar. The --defaultValue flag arrives
|
|
834
|
+
// as a string, so parse it here; if it is not valid JSON, return the raw string so the type's
|
|
835
|
+
// validator (validateLinkDefaultValue / validateSvgDefaultValue / validateNumberRangeDefaultValue
|
|
836
|
+
// / validateTypeDefaultValue / validateComponentDefaultValue) can report a precise error instead
|
|
837
|
+
// of silently storing a bad shape.
|
|
838
|
+
try {
|
|
839
|
+
return JSON.parse(value);
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
return value;
|
|
843
|
+
}
|
|
533
844
|
default:
|
|
534
845
|
return value;
|
|
535
846
|
}
|
|
536
847
|
}
|
|
537
|
-
|
|
848
|
+
// Prop types whose value is a dynamic reference to merchant storefront data (products, media,
|
|
849
|
+
// categories, brands, blogs, raffles). They resolve at storefront render time, so a static config
|
|
850
|
+
// default is invalid and is rejected on add/update-prop. Keep in sync with @ikas/editor-models
|
|
851
|
+
// DYNAMIC_PROP_TYPES (the CLI must not depend on that private package).
|
|
852
|
+
export const DYNAMIC_PROP_TYPES = [
|
|
853
|
+
"IMAGE",
|
|
854
|
+
"IMAGE_LIST",
|
|
855
|
+
"VIDEO",
|
|
856
|
+
"PRODUCT",
|
|
857
|
+
"PRODUCT_LIST",
|
|
858
|
+
"PRODUCT_ATTRIBUTE",
|
|
859
|
+
"PRODUCT_ATTRIBUTE_LIST",
|
|
860
|
+
"BRAND",
|
|
861
|
+
"BRAND_LIST",
|
|
862
|
+
"CATEGORY",
|
|
863
|
+
"CATEGORY_LIST",
|
|
864
|
+
"BLOG",
|
|
865
|
+
"BLOG_LIST",
|
|
866
|
+
"BLOG_CATEGORY",
|
|
867
|
+
"BLOG_CATEGORY_LIST",
|
|
868
|
+
"RAFFLE",
|
|
869
|
+
"RAFFLE_LIST",
|
|
870
|
+
];
|
|
871
|
+
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.`;
|
|
872
|
+
// Reject a static default on a dynamic (merchant-data reference) prop type: print the standard
|
|
873
|
+
// error and exit. No-op when the type isn't dynamic or no default was supplied.
|
|
874
|
+
export const rejectDynamicDefault = (propName, propType, defaultValue) => {
|
|
875
|
+
if (DYNAMIC_PROP_TYPES.includes(propType) && defaultValue !== undefined) {
|
|
876
|
+
console.log(JSON.stringify({
|
|
877
|
+
success: false,
|
|
878
|
+
error: dynamicPropDefaultError(propName, propType),
|
|
879
|
+
}));
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
|
|
884
|
+
// FILE is intentionally excluded from CLI-authored defaults: a code-component / AI-authored default
|
|
885
|
+
// can't reference a store-specific uploaded file, so a "FILE" link default is always rejected. Store
|
|
886
|
+
// owners can still create FILE links manually in the editor (editor-core keeps FILE in its own set).
|
|
887
|
+
const VALID_LINK_TYPES = ["PAGE", "EXTERNAL"];
|
|
888
|
+
const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
|
|
889
|
+
'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
|
|
890
|
+
"Do NOT pass a JSON string or a { label, href } shape. " +
|
|
891
|
+
'Optional "translations": [{ "locale": "en", "label": "Home" }] = the label in each other language, one per language ' +
|
|
892
|
+
"(a store routing's own label is entered in the editor, not here).";
|
|
893
|
+
// Returns the index of the first array element that fails `fn`, as an error string, or null.
|
|
894
|
+
const firstError = (arr, fn) => arr.reduce((acc, item, index) => acc ?? fn(item, index), null);
|
|
895
|
+
const validateSingleLink = (link, propName, path) => {
|
|
896
|
+
const where = `defaultValue for prop "${propName}"${path}`;
|
|
897
|
+
if (typeof link === "string") {
|
|
898
|
+
return `${where} is a JSON string but must be an object. ${LINK_SHAPE_HELP}`;
|
|
899
|
+
}
|
|
900
|
+
if (!link || typeof link !== "object" || Array.isArray(link)) {
|
|
901
|
+
return `${where} must be a link object. ${LINK_SHAPE_HELP}`;
|
|
902
|
+
}
|
|
903
|
+
const l = link;
|
|
904
|
+
if ("href" in l && !("linkType" in l)) {
|
|
905
|
+
return `${where} uses a legacy { href } shape without "linkType". Use "linkType" + "externalLink"/"pageType" instead. ${LINK_SHAPE_HELP}`;
|
|
906
|
+
}
|
|
907
|
+
if (typeof l.linkType !== "string" ||
|
|
908
|
+
!VALID_LINK_TYPES.includes(l.linkType)) {
|
|
909
|
+
return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
|
|
910
|
+
}
|
|
911
|
+
// Require the destination field for the declared linkType, so a link that points nowhere never
|
|
912
|
+
// reaches a published theme. A PAGE default carries a pageType (pageId is per-store, filled on
|
|
913
|
+
// materialization); pageId alone is also accepted for a specific page.
|
|
914
|
+
if (l.linkType === "PAGE" && !nonEmpty(l.pageType) && !nonEmpty(l.pageId)) {
|
|
915
|
+
return `${where}: a "PAGE" link needs a "pageType" (e.g. "INDEX") — or a "pageId" for a specific page. ${LINK_SHAPE_HELP}`;
|
|
916
|
+
}
|
|
917
|
+
if (l.linkType === "EXTERNAL" && !nonEmpty(l.externalLink)) {
|
|
918
|
+
return `${where}: an "EXTERNAL" link needs a non-empty "externalLink" URL. ${LINK_SHAPE_HELP}`;
|
|
919
|
+
}
|
|
920
|
+
// The editor's checks for a section's link (dev-server `checkLink`), a routing's own label left out: a config
|
|
921
|
+
// knows no store's routings
|
|
922
|
+
if (l.translations != null &&
|
|
923
|
+
(!Array.isArray(l.translations) ||
|
|
924
|
+
l.translations.some((t) => !t ||
|
|
925
|
+
!nonEmpty(t.locale) ||
|
|
926
|
+
typeof t.label !== "string" ||
|
|
927
|
+
t.sfRoutingId != null))) {
|
|
928
|
+
return `${where}: "translations" must be a list of { "locale": "<language>", "label": "<text>" } (the label in each other language; a routing's own label is entered in the editor).`;
|
|
929
|
+
}
|
|
930
|
+
const locales = (l.translations ?? []).map((t) => t.locale);
|
|
931
|
+
const repeated = locales.find((locale, i) => locales.indexOf(locale) !== i);
|
|
932
|
+
if (repeated) {
|
|
933
|
+
return `${where}: "translations" holds two labels for "${repeated}": keep one.`;
|
|
934
|
+
}
|
|
935
|
+
if (l.useItemNameTranslations != null &&
|
|
936
|
+
typeof l.useItemNameTranslations !== "boolean") {
|
|
937
|
+
return `${where}: "useItemNameTranslations" must be true or false.`;
|
|
938
|
+
}
|
|
939
|
+
if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
|
|
940
|
+
return `${where}: "subLinks" must be an array (use [] when there are none).`;
|
|
941
|
+
}
|
|
942
|
+
if (Array.isArray(l.subLinks)) {
|
|
943
|
+
return firstError(l.subLinks, (sl, i) => validateSingleLink(sl, propName, `${path}.subLinks[${i}]`));
|
|
944
|
+
}
|
|
945
|
+
return null;
|
|
946
|
+
};
|
|
947
|
+
// Validates a LINK / LIST_OF_LINK defaultValue. Returns an error message, or null if valid.
|
|
948
|
+
// These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
|
|
949
|
+
// legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
|
|
950
|
+
// out of published themes.
|
|
951
|
+
export function validateLinkDefaultValue(propType, value, propName) {
|
|
952
|
+
if (value === undefined || value === null)
|
|
953
|
+
return null;
|
|
954
|
+
if (propType === "LINK") {
|
|
955
|
+
return validateSingleLink(value, propName, "");
|
|
956
|
+
}
|
|
957
|
+
// LIST_OF_LINK
|
|
958
|
+
if (typeof value === "string") {
|
|
959
|
+
return `defaultValue for prop "${propName}" is a JSON string but must be an object { "links": [...] }. ${LINK_SHAPE_HELP}`;
|
|
960
|
+
}
|
|
961
|
+
if (!value ||
|
|
962
|
+
typeof value !== "object" ||
|
|
963
|
+
Array.isArray(value) ||
|
|
964
|
+
!Array.isArray(value.links)) {
|
|
965
|
+
return `defaultValue for prop "${propName}" must be an object with a "links" array: { "links": [ <link>, ... ] }. ${LINK_SHAPE_HELP}`;
|
|
966
|
+
}
|
|
967
|
+
const links = value.links;
|
|
968
|
+
return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
|
|
969
|
+
}
|
|
970
|
+
const COMPONENT_SHAPE_HELP = "A COMPONENT / COMPONENT_LIST default must be an object listing child code components: " +
|
|
971
|
+
'{ "components": [ { "codeComponentId": "<id>" } ] }. Only code components are allowed; each entry ' +
|
|
972
|
+
'needs a string "codeComponentId" (no componentId, no inline propValues).';
|
|
973
|
+
/**
|
|
974
|
+
* Validate a COMPONENT / COMPONENT_LIST defaultValue. Reference-only: each entry must name a code
|
|
975
|
+
* component via `codeComponentId`. Rejects non-CC (`componentId`) children, missing ids, more than
|
|
976
|
+
* one entry for a single COMPONENT, and ids outside the prop's filteredComponentIds allowlist.
|
|
977
|
+
* Returns an error message, or null if valid.
|
|
978
|
+
*/
|
|
979
|
+
export function validateComponentDefaultValue(propType, value, propName, filteredComponentIds, existingComponentIds) {
|
|
980
|
+
if (value === undefined || value === null)
|
|
981
|
+
return null;
|
|
982
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
983
|
+
if (typeof value === "string") {
|
|
984
|
+
return `${where} is a JSON string but must be an object. ${COMPONENT_SHAPE_HELP}`;
|
|
985
|
+
}
|
|
986
|
+
if (!value ||
|
|
987
|
+
typeof value !== "object" ||
|
|
988
|
+
Array.isArray(value) ||
|
|
989
|
+
!Array.isArray(value.components)) {
|
|
990
|
+
return `${where} must be an object with a "components" array. ${COMPONENT_SHAPE_HELP}`;
|
|
991
|
+
}
|
|
992
|
+
const components = value.components;
|
|
993
|
+
if (propType === "COMPONENT" && components.length > 1) {
|
|
994
|
+
return `${where}: a COMPONENT prop holds a single child — provide at most one entry (use COMPONENT_LIST for many).`;
|
|
995
|
+
}
|
|
996
|
+
const allow = filteredComponentIds && filteredComponentIds.length
|
|
997
|
+
? new Set(filteredComponentIds)
|
|
998
|
+
: null;
|
|
999
|
+
return firstError(components, (entry, i) => {
|
|
1000
|
+
const at = `${where}.components[${i}]`;
|
|
1001
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1002
|
+
return `${at} must be an object { "codeComponentId": "<id>" }. ${COMPONENT_SHAPE_HELP}`;
|
|
1003
|
+
}
|
|
1004
|
+
const e = entry;
|
|
1005
|
+
if ("componentId" in e && !("codeComponentId" in e)) {
|
|
1006
|
+
return `${at} references a non-code component (componentId). Only code components are allowed as defaults. ${COMPONENT_SHAPE_HELP}`;
|
|
1007
|
+
}
|
|
1008
|
+
if (typeof e.codeComponentId !== "string" ||
|
|
1009
|
+
e.codeComponentId.trim().length === 0) {
|
|
1010
|
+
return `${at} must have a non-empty string "codeComponentId". ${COMPONENT_SHAPE_HELP}`;
|
|
1011
|
+
}
|
|
1012
|
+
if (allow && !allow.has(e.codeComponentId)) {
|
|
1013
|
+
return `${at}: "${e.codeComponentId}" is not in this prop's filteredComponentIds allowlist.`;
|
|
1014
|
+
}
|
|
1015
|
+
// Reject a reference to a component that does not exist in the project (deleted, renamed, or
|
|
1016
|
+
// never created). A dangling default would seed a ghost entry on every placement that renders
|
|
1017
|
+
// nothing. Only enforced when the caller supplies the known-component set (the config-backed
|
|
1018
|
+
// add-prop / update-prop / batch sites); pure-shape callers pass no set and skip this check.
|
|
1019
|
+
if (existingComponentIds && !existingComponentIds.has(e.codeComponentId)) {
|
|
1020
|
+
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).`;
|
|
1021
|
+
}
|
|
1022
|
+
return null;
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
const SVG_DEFAULT_HELP = "An SVG default must be a single well-formed <svg>…</svg> string ≤ 64 KB with no embedded base64 raster images. " +
|
|
1026
|
+
'Tip: the editor uses each svg\'s `class` attribute as its display name — set class="my-icon" to name it; ' +
|
|
1027
|
+
"SVG_LIST items with no class show as svg-1, svg-2, ….";
|
|
1028
|
+
// Keep the cap and base64 pattern in sync with @ikas/editor-models validate-svg.ts (the
|
|
1029
|
+
// authoritative validator). Duplicated here on purpose — see validateSvgShape below.
|
|
1030
|
+
const SVG_MAX_SIZE_KB = 64;
|
|
1031
|
+
const SVG_BASE64_RASTER_RE = /data:image\/[a-z0-9.+-]+;base64/i;
|
|
1032
|
+
const SVG_SHAPE_RE = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>][\s\S]*<\/svg>\s*$/i;
|
|
1033
|
+
// Count TOP-LEVEL <svg> roots without a DOM: scan svg open/close tags in order and track
|
|
1034
|
+
// nesting depth. A root is any <svg> opened at depth 0. This matches the editor's DOM-based
|
|
1035
|
+
// single-root check — it rejects siblings (`<svg/><svg/>`) while allowing a nested <svg>.
|
|
1036
|
+
// Self-closing <svg .../> counts as a root but opens no nesting level.
|
|
1037
|
+
function svgRootCount(input) {
|
|
1038
|
+
const tags = input.match(/<svg\b[^>]*>|<\/svg\s*>/gi) || [];
|
|
1039
|
+
return tags.reduce((acc, tag) => {
|
|
1040
|
+
if (tag.startsWith("</"))
|
|
1041
|
+
return { depth: Math.max(0, acc.depth - 1), roots: acc.roots };
|
|
1042
|
+
const roots = acc.depth === 0 ? acc.roots + 1 : acc.roots;
|
|
1043
|
+
const depth = /\/>\s*$/.test(tag) ? acc.depth : acc.depth + 1;
|
|
1044
|
+
return { depth, roots };
|
|
1045
|
+
}, { depth: 0, roots: 0 }).roots;
|
|
1046
|
+
}
|
|
1047
|
+
// Lightweight, dependency-free SVG structure check. This standalone CLI is publishable and
|
|
1048
|
+
// must not depend on the private @ikas/editor-models package (nor pull a DOM/DOMPurify into a
|
|
1049
|
+
// CLI), so it mirrors the RULES of the editor's validateSvg (size, base64, single <svg> root)
|
|
1050
|
+
// for fast authoring feedback. The authoritative sanitize runs in the editor/upload path.
|
|
1051
|
+
// Returns a rejection reason, or null if the shape is acceptable.
|
|
1052
|
+
function validateSvgShape(input) {
|
|
1053
|
+
if (typeof input !== "string" || input.trim() === "")
|
|
1054
|
+
return "empty or not a string";
|
|
1055
|
+
if (Buffer.byteLength(input, "utf8") > SVG_MAX_SIZE_KB * 1024)
|
|
1056
|
+
return `exceeds ${SVG_MAX_SIZE_KB} KB`;
|
|
1057
|
+
if (SVG_BASE64_RASTER_RE.test(input))
|
|
1058
|
+
return "embedded base64 raster image not allowed";
|
|
1059
|
+
if (!SVG_SHAPE_RE.test(input))
|
|
1060
|
+
return "not a single-root <svg> document";
|
|
1061
|
+
if (svgRootCount(input) !== 1)
|
|
1062
|
+
return "must be exactly one <svg> root";
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
// Validates an SVG / SVG_LIST defaultValue. Returns an error message, or null if valid.
|
|
1066
|
+
// Authoring an SVG default is a common mistake source for agents; rejecting here (with an
|
|
1067
|
+
// instructive message) keeps malformed/oversized SVGs out of published components.
|
|
1068
|
+
export function validateSvgDefaultValue(propType, value, propName) {
|
|
1069
|
+
if (value === undefined || value === null)
|
|
1070
|
+
return null;
|
|
1071
|
+
if (propType === "SVG") {
|
|
1072
|
+
const reason = validateSvgShape(value);
|
|
1073
|
+
return reason
|
|
1074
|
+
? `defaultValue for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
|
|
1075
|
+
: null;
|
|
1076
|
+
}
|
|
1077
|
+
// SVG_LIST
|
|
1078
|
+
if (!Array.isArray(value)) {
|
|
1079
|
+
return `defaultValue for prop "${propName}" must be an array of SVG strings: [ "<svg>…</svg>", ... ]. ${SVG_DEFAULT_HELP}`;
|
|
1080
|
+
}
|
|
1081
|
+
return firstError(value, (svg, i) => {
|
|
1082
|
+
const reason = validateSvgShape(svg);
|
|
1083
|
+
return reason
|
|
1084
|
+
? `defaultValue[${i}] for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
|
|
1085
|
+
: null;
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
const NUMBER_RANGE_DEFAULT_HELP = 'A NUMBER_RANGE default must be an object { "value": <number>, "unit": "px" | null }. ' +
|
|
1089
|
+
"When the prop defines numberRangeData, value must be within [min, max] and on the interval grid (min + k·interval).";
|
|
1090
|
+
// Validates a NUMBER_RANGE defaultValue. Returns an error message, or null if valid.
|
|
1091
|
+
// Checks the { value, unit } shape and — when the prop carries numberRangeData — that the
|
|
1092
|
+
// value is within [min, max] and lands on the interval grid, mirroring the editor's slider
|
|
1093
|
+
// so an agent can't author an out-of-range or off-step default.
|
|
1094
|
+
export function validateNumberRangeDefaultValue(value, numberRangeData, propName) {
|
|
1095
|
+
if (value === undefined || value === null)
|
|
1096
|
+
return null;
|
|
1097
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
1098
|
+
if (typeof value === "string") {
|
|
1099
|
+
return `${where} is a JSON string but must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1100
|
+
}
|
|
1101
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
1102
|
+
return `${where} must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1103
|
+
}
|
|
1104
|
+
const v = value;
|
|
1105
|
+
if (typeof v.value !== "number" || !Number.isFinite(v.value)) {
|
|
1106
|
+
return `${where}: "value" must be a finite number. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1107
|
+
}
|
|
1108
|
+
if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
|
|
1109
|
+
return `${where}: "unit" must be a string or null. ${NUMBER_RANGE_DEFAULT_HELP}`;
|
|
1110
|
+
}
|
|
1111
|
+
if (numberRangeData) {
|
|
1112
|
+
const { min, max, interval } = numberRangeData;
|
|
1113
|
+
if (min !== undefined && v.value < min) {
|
|
1114
|
+
return `${where}: value ${v.value} is below the configured min (${min}).`;
|
|
1115
|
+
}
|
|
1116
|
+
if (max !== undefined && v.value > max) {
|
|
1117
|
+
return `${where}: value ${v.value} is above the configured max (${max}).`;
|
|
1118
|
+
}
|
|
1119
|
+
if (interval !== undefined &&
|
|
1120
|
+
interval > 0 &&
|
|
1121
|
+
min !== undefined &&
|
|
1122
|
+
(v.value - min) % interval !== 0) {
|
|
1123
|
+
return `${where}: value ${v.value} is not on the interval grid (min ${min} + k·${interval}).`;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
const NUMBER_DEFAULT_HELP = "A NUMBER default must be a finite number, e.g. --defaultValue 42.";
|
|
1129
|
+
// Validates a NUMBER defaultValue. Returns an error message, or null if valid. Guards two footguns:
|
|
1130
|
+
// an empty/whitespace flag string (Number("") === 0 would silently store 0) and a non-numeric or
|
|
1131
|
+
// non-finite value (e.g. "42px" → NaN, "Infinity"). The flag path passes the raw --defaultValue
|
|
1132
|
+
// string; the --props path passes the already-parsed JSON value — both are handled here.
|
|
1133
|
+
export function validateNumberDefaultValue(value, propName) {
|
|
1134
|
+
if (value === undefined || value === null)
|
|
1135
|
+
return null;
|
|
1136
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
1137
|
+
if (typeof value === "string") {
|
|
1138
|
+
if (value.trim() === "") {
|
|
1139
|
+
return `${where} is empty, but NUMBER requires a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1140
|
+
}
|
|
1141
|
+
if (!Number.isFinite(Number(value))) {
|
|
1142
|
+
return `${where} ("${value}") is not a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1143
|
+
}
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1147
|
+
return `${where} must be a finite number. ${NUMBER_DEFAULT_HELP}`;
|
|
1148
|
+
}
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
const STYLE_TYPE_DEFAULT_HELP = "A size/style TYPE default must be an object matching the type's shape: " +
|
|
1152
|
+
'{ "value": <number>, "unit": "px" | "rem" | "%" | "vh" | "vw" } (or { "css": "<raw css>" }). ' +
|
|
1153
|
+
"Author it via add-component --props '[…]' or --defaultValue '<json>'.";
|
|
1154
|
+
const BP_STOREFRONT_PREFIX = "@ikas/bp-storefront-models-";
|
|
1155
|
+
// The size/style types that share the { css?, value?, unit? } shape. This is EXACTLY the set of
|
|
1156
|
+
// TYPE typeIds whose default the generator emits (mirror of DYNAMIC_STYLE_TYPE_IDS in
|
|
1157
|
+
// @ikas/blueprint-modules design-generator/generate-helpers/styles.ts — the standalone CLI must not
|
|
1158
|
+
// depend on that package, so it is hand-maintained here and kept in sync by a parity test). Every
|
|
1159
|
+
// OTHER TYPE typeId (BorderStyleType, BoxShadowStyleType, GridTemplateColumnsStyleType, string-union
|
|
1160
|
+
// styles like TextAlignStyleType, IkasProduct, custom types) has a DIFFERENT value shape, so their
|
|
1161
|
+
// defaults are not shape-checked (and are dead code in the generator anyway).
|
|
1162
|
+
const SIZE_STYLE_TYPE_NAMES = [
|
|
1163
|
+
"SizeStyleType",
|
|
1164
|
+
"PaddingStyleType",
|
|
1165
|
+
"PaddingTopStyleType",
|
|
1166
|
+
"PaddingRightStyleType",
|
|
1167
|
+
"PaddingBottomStyleType",
|
|
1168
|
+
"PaddingLeftStyleType",
|
|
1169
|
+
"MarginStyleType",
|
|
1170
|
+
"MarginTopStyleType",
|
|
1171
|
+
"MarginRightStyleType",
|
|
1172
|
+
"MarginBottomStyleType",
|
|
1173
|
+
"MarginLeftStyleType",
|
|
1174
|
+
"BorderRadiusStyleType",
|
|
1175
|
+
"BorderRadiusTopLeftStyleType",
|
|
1176
|
+
"BorderRadiusTopRightStyleType",
|
|
1177
|
+
"BorderRadiusBottomRightStyleType",
|
|
1178
|
+
"BorderRadiusBottomLeftStyleType",
|
|
1179
|
+
"FontSizeStyleType",
|
|
1180
|
+
"LineHeightStyleType",
|
|
1181
|
+
"LetterSpacingStyleType",
|
|
1182
|
+
"HeightStyleType",
|
|
1183
|
+
"MinHeightStyleType",
|
|
1184
|
+
"MaxHeightStyleType",
|
|
1185
|
+
"WidthStyleType",
|
|
1186
|
+
"MinWidthStyleType",
|
|
1187
|
+
"MaxWidthStyleType",
|
|
1188
|
+
"GapStyleType",
|
|
1189
|
+
"BorderWidthStyleType",
|
|
1190
|
+
"TopStyleType",
|
|
1191
|
+
"RightStyleType",
|
|
1192
|
+
"BottomStyleType",
|
|
1193
|
+
"LeftStyleType",
|
|
1194
|
+
];
|
|
1195
|
+
// Validates a single style-type default object. Every size style type shares the optional
|
|
1196
|
+
// { css?, value?, unit? } shape, so inner fields are checked loosely — the key rejection is a
|
|
1197
|
+
// non-object (e.g. a bare JSON string, the common failure when a --defaultValue flag is not parsed).
|
|
1198
|
+
function styleDefaultObjectError(value, where) {
|
|
1199
|
+
if (typeof value === "string") {
|
|
1200
|
+
return `${where} is a JSON string but must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1201
|
+
}
|
|
1202
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1203
|
+
return `${where} must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1204
|
+
}
|
|
1205
|
+
const v = value;
|
|
1206
|
+
// css/value/unit are all optional, and the editor treats an explicit null as "unset" (it accepts
|
|
1207
|
+
// { value: null }). So only type-check a field that is present AND non-null — otherwise the CLI
|
|
1208
|
+
// would be stricter than the authoritative editor and reject a value it would happily accept.
|
|
1209
|
+
// Unknown fields ARE rejected below: every size-style type shares exactly this { css?, value?, unit? }
|
|
1210
|
+
// shape, so an extra key is always a mistake (this matches the editor). The per-type value-DOMAIN
|
|
1211
|
+
// (which units a given type allows — BorderWidth also permits em/vmin/vmax while the rest are
|
|
1212
|
+
// px/rem/%/vh/vw) stays editor-side: it is per-type and can't be mirrored offline without drift.
|
|
1213
|
+
if (v.css !== undefined && v.css !== null && typeof v.css !== "string") {
|
|
1214
|
+
return `${where}: "css" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1215
|
+
}
|
|
1216
|
+
if (v.value !== undefined &&
|
|
1217
|
+
v.value !== null &&
|
|
1218
|
+
(typeof v.value !== "number" || !Number.isFinite(v.value))) {
|
|
1219
|
+
return `${where}: "value" must be a finite number. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1220
|
+
}
|
|
1221
|
+
if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
|
|
1222
|
+
return `${where}: "unit" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1223
|
+
}
|
|
1224
|
+
const unknownKey = Object.keys(v).find((k) => k !== "css" && k !== "value" && k !== "unit");
|
|
1225
|
+
if (unknownKey) {
|
|
1226
|
+
return `${where}: unknown field "${unknownKey}". A size/style default allows only "css", "value", "unit". ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1227
|
+
}
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
// Validates a TYPE prop defaultValue — but ONLY for the size/style types whose shape we know
|
|
1231
|
+
// ({ css?, value?, unit? }). Any other TYPE typeId (color/border/box-shadow/grid/string-union
|
|
1232
|
+
// styles, IkasProduct, custom types) has a different value shape we can't verify offline, so its
|
|
1233
|
+
// default is passed through unchecked. The generator only emits a default for these size/style
|
|
1234
|
+
// types anyway. Returns an error message, or null if valid / not a size-style type.
|
|
1235
|
+
export function validateTypeDefaultValue(value, propName, typeId) {
|
|
1236
|
+
if (value === undefined || value === null)
|
|
1237
|
+
return null;
|
|
1238
|
+
if (typeof typeId !== "string" || !typeId.startsWith(BP_STOREFRONT_PREFIX)) {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
// Strip the prefix and an optional _array suffix to get the base type name.
|
|
1242
|
+
const withoutPrefix = typeId.slice(BP_STOREFRONT_PREFIX.length);
|
|
1243
|
+
const isArray = withoutPrefix.endsWith("_array");
|
|
1244
|
+
const baseName = isArray
|
|
1245
|
+
? withoutPrefix.slice(0, -"_array".length)
|
|
1246
|
+
: withoutPrefix;
|
|
1247
|
+
if (!SIZE_STYLE_TYPE_NAMES.includes(baseName))
|
|
1248
|
+
return null;
|
|
1249
|
+
const where = `defaultValue for prop "${propName}"`;
|
|
1250
|
+
if (isArray) {
|
|
1251
|
+
if (!Array.isArray(value)) {
|
|
1252
|
+
return `${where} must be an array for an _array TYPE prop: [ { "value": …, "unit": … }, … ]. ${STYLE_TYPE_DEFAULT_HELP}`;
|
|
1253
|
+
}
|
|
1254
|
+
return firstError(value, (el, i) => styleDefaultObjectError(el, `${where}[${i}]`));
|
|
1255
|
+
}
|
|
1256
|
+
return styleDefaultObjectError(value, where);
|
|
1257
|
+
}
|
|
1258
|
+
async function updateProp(ref, options) {
|
|
538
1259
|
const { config, configPath } = loadConfig();
|
|
539
1260
|
const component = resolveComponent(config, ref);
|
|
540
1261
|
const propIndex = component.props.findIndex((p) => p.name === options.prop);
|
|
@@ -566,8 +1287,87 @@ function updateProp(ref, options) {
|
|
|
566
1287
|
if (options.description !== undefined) {
|
|
567
1288
|
prop.description = options.description || undefined;
|
|
568
1289
|
}
|
|
1290
|
+
// Resolve numberRangeData before defaultValue so a NUMBER_RANGE default can be validated
|
|
1291
|
+
// against the (possibly just-updated) slider config in the same call.
|
|
1292
|
+
if (options.numberRangeData !== undefined) {
|
|
1293
|
+
if (options.numberRangeData === "" || options.numberRangeData === "none") {
|
|
1294
|
+
delete prop.numberRangeData;
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
prop.numberRangeData = parseNumberRangeData(options.numberRangeData, prop.name);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
// A NUMBER_RANGE prop must keep a valid slider config (min + max). Enforce the invariant only when
|
|
1301
|
+
// this update touches the type or numberRangeData — a benign update (e.g. displayName) on an
|
|
1302
|
+
// existing prop must still pass. This catches switching a prop TO NUMBER_RANGE without a config, or
|
|
1303
|
+
// clearing/blanking numberRangeData on a NUMBER_RANGE prop.
|
|
1304
|
+
if ((options.type !== undefined || options.numberRangeData !== undefined) &&
|
|
1305
|
+
prop.type === "NUMBER_RANGE" &&
|
|
1306
|
+
(!prop.numberRangeData || !isNumberRangeComplete(prop.numberRangeData))) {
|
|
1307
|
+
console.log(JSON.stringify({
|
|
1308
|
+
success: false,
|
|
1309
|
+
error: numberRangeDataRequiredError(prop.name),
|
|
1310
|
+
}));
|
|
1311
|
+
process.exit(1);
|
|
1312
|
+
}
|
|
1313
|
+
// Resolve filteredComponentIds before defaultValue so a COMPONENT / COMPONENT_LIST default can be
|
|
1314
|
+
// validated against the (possibly just-updated) allowlist in the same call.
|
|
1315
|
+
if (options.filteredComponentIds !== undefined) {
|
|
1316
|
+
if (options.filteredComponentIds === "" ||
|
|
1317
|
+
options.filteredComponentIds === "none") {
|
|
1318
|
+
delete prop.filteredComponentIds;
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
try {
|
|
1322
|
+
const parsed = JSON.parse(options.filteredComponentIds);
|
|
1323
|
+
if (!Array.isArray(parsed)) {
|
|
1324
|
+
console.log(JSON.stringify({
|
|
1325
|
+
success: false,
|
|
1326
|
+
error: "--filteredComponentIds must be a JSON array of strings.",
|
|
1327
|
+
}));
|
|
1328
|
+
process.exit(1);
|
|
1329
|
+
}
|
|
1330
|
+
assertKnownComponentIds(parsed, config);
|
|
1331
|
+
prop.filteredComponentIds = parsed;
|
|
1332
|
+
}
|
|
1333
|
+
catch (err) {
|
|
1334
|
+
if (err instanceof SyntaxError) {
|
|
1335
|
+
console.log(JSON.stringify({
|
|
1336
|
+
success: false,
|
|
1337
|
+
error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
|
|
1338
|
+
}));
|
|
1339
|
+
process.exit(1);
|
|
1340
|
+
}
|
|
1341
|
+
throw err;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
569
1345
|
if (options.defaultValue !== undefined) {
|
|
570
|
-
prop.
|
|
1346
|
+
rejectDynamicDefault(prop.name, prop.type, options.defaultValue);
|
|
1347
|
+
const parsed = parseDefaultValue(options.defaultValue, prop.type);
|
|
1348
|
+
// Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
|
|
1349
|
+
if (prop.type === "NUMBER") {
|
|
1350
|
+
const numError = validateNumberDefaultValue(options.defaultValue, prop.name);
|
|
1351
|
+
failIf(numError);
|
|
1352
|
+
}
|
|
1353
|
+
if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
|
|
1354
|
+
const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
|
|
1355
|
+
failIf(linkError);
|
|
1356
|
+
}
|
|
1357
|
+
if ((prop.type === "COMPONENT" || prop.type === "COMPONENT_LIST") &&
|
|
1358
|
+
parsed !== undefined) {
|
|
1359
|
+
failIf(validateComponentDefaultValue(prop.type, parsed, prop.name, prop.filteredComponentIds, existingComponentIdSet(config)));
|
|
1360
|
+
}
|
|
1361
|
+
if (prop.type === "NUMBER_RANGE") {
|
|
1362
|
+
const nrError = validateNumberRangeDefaultValue(parsed, prop.numberRangeData, prop.name);
|
|
1363
|
+
failIf(nrError);
|
|
1364
|
+
}
|
|
1365
|
+
if (prop.type === "TYPE") {
|
|
1366
|
+
// Prefer a typeId supplied in this same update over the stored one.
|
|
1367
|
+
const typeError = validateTypeDefaultValue(parsed, prop.name, options.typeId ?? prop.typeId);
|
|
1368
|
+
failIf(typeError);
|
|
1369
|
+
}
|
|
1370
|
+
prop.defaultValue = parsed;
|
|
571
1371
|
}
|
|
572
1372
|
if (options.group !== undefined) {
|
|
573
1373
|
if (options.group === "" || options.group === "none") {
|
|
@@ -600,6 +1400,13 @@ function updateProp(ref, options) {
|
|
|
600
1400
|
prop.typeId = options.typeId;
|
|
601
1401
|
}
|
|
602
1402
|
}
|
|
1403
|
+
// Reject section-disallowed types when the caller changes type/typeId;
|
|
1404
|
+
// untouched legacy props stay editable (displayName, group, ...).
|
|
1405
|
+
if ((options.type !== undefined || options.typeId !== undefined) &&
|
|
1406
|
+
prop.type === "TYPE" &&
|
|
1407
|
+
prop.typeId) {
|
|
1408
|
+
await assertTypeIdsUsable(component.type, [prop.typeId]);
|
|
1409
|
+
}
|
|
603
1410
|
if (options.enumTypeId !== undefined) {
|
|
604
1411
|
if (options.enumTypeId === "" || options.enumTypeId === "none") {
|
|
605
1412
|
delete prop.enumTypeId;
|
|
@@ -608,29 +1415,6 @@ function updateProp(ref, options) {
|
|
|
608
1415
|
prop.enumTypeId = options.enumTypeId;
|
|
609
1416
|
}
|
|
610
1417
|
}
|
|
611
|
-
if (options.filteredComponentIds !== undefined) {
|
|
612
|
-
if (options.filteredComponentIds === "" || options.filteredComponentIds === "none") {
|
|
613
|
-
delete prop.filteredComponentIds;
|
|
614
|
-
}
|
|
615
|
-
else {
|
|
616
|
-
try {
|
|
617
|
-
const parsed = JSON.parse(options.filteredComponentIds);
|
|
618
|
-
if (!Array.isArray(parsed)) {
|
|
619
|
-
console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
|
|
620
|
-
process.exit(1);
|
|
621
|
-
}
|
|
622
|
-
assertKnownComponentIds(parsed, config);
|
|
623
|
-
prop.filteredComponentIds = parsed;
|
|
624
|
-
}
|
|
625
|
-
catch (err) {
|
|
626
|
-
if (err instanceof SyntaxError) {
|
|
627
|
-
console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
|
|
628
|
-
process.exit(1);
|
|
629
|
-
}
|
|
630
|
-
throw err;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
1418
|
if (options.privateVarMap !== undefined) {
|
|
635
1419
|
if (options.privateVarMap === "" || options.privateVarMap === "none") {
|
|
636
1420
|
delete prop.privateVarMap;
|
|
@@ -639,17 +1423,50 @@ function updateProp(ref, options) {
|
|
|
639
1423
|
try {
|
|
640
1424
|
const parsed = JSON.parse(options.privateVarMap);
|
|
641
1425
|
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
642
|
-
console.log(JSON.stringify({
|
|
1426
|
+
console.log(JSON.stringify({
|
|
1427
|
+
success: false,
|
|
1428
|
+
error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
|
|
1429
|
+
}));
|
|
643
1430
|
process.exit(1);
|
|
644
1431
|
}
|
|
645
1432
|
prop.privateVarMap = parsed;
|
|
646
1433
|
}
|
|
647
1434
|
catch {
|
|
648
|
-
console.log(JSON.stringify({
|
|
1435
|
+
console.log(JSON.stringify({
|
|
1436
|
+
success: false,
|
|
1437
|
+
error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
|
|
1438
|
+
}));
|
|
649
1439
|
process.exit(1);
|
|
650
1440
|
}
|
|
651
1441
|
}
|
|
652
1442
|
}
|
|
1443
|
+
if (options.requiredProp !== undefined) {
|
|
1444
|
+
if (options.requiredProp === "" || options.requiredProp === "none") {
|
|
1445
|
+
// Every field goes together — see `IProp.requiredPropId`.
|
|
1446
|
+
delete prop.requiredProp;
|
|
1447
|
+
delete prop.requiredPropValue;
|
|
1448
|
+
delete prop.requiredEnumOptionId;
|
|
1449
|
+
delete prop.requiredEnumOptionValue;
|
|
1450
|
+
}
|
|
1451
|
+
else {
|
|
1452
|
+
prop.requiredProp = options.requiredProp;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (options.requiredEnumOptionValue !== undefined && prop.requiredProp) {
|
|
1456
|
+
prop.requiredEnumOptionId = options.requiredEnumOptionValue;
|
|
1457
|
+
prop.requiredEnumOptionValue = options.requiredEnumOptionValue;
|
|
1458
|
+
delete prop.requiredPropValue;
|
|
1459
|
+
}
|
|
1460
|
+
if (options.requiredPropValue !== undefined && prop.requiredProp) {
|
|
1461
|
+
delete prop.requiredEnumOptionId;
|
|
1462
|
+
delete prop.requiredEnumOptionValue;
|
|
1463
|
+
prop.requiredPropValue = parseRequiredPropValue(options.requiredPropValue, `prop "${prop.name}"`);
|
|
1464
|
+
}
|
|
1465
|
+
// Re-checked on every update, not only when a visibility flag is passed: flipping `required`, or
|
|
1466
|
+
// renaming the prop this one hangs on, can invalidate a gate that was sound when it was written.
|
|
1467
|
+
failIf(visibilityGateError(component, prop, `prop "${prop.name}"`, {
|
|
1468
|
+
selfPropName: prop.name,
|
|
1469
|
+
}));
|
|
653
1470
|
saveConfig(configPath, config);
|
|
654
1471
|
// Regenerate types.ts and global types
|
|
655
1472
|
regenerateTypes(component, undefined, config);
|
|
@@ -661,16 +1478,29 @@ function updateProp(ref, options) {
|
|
|
661
1478
|
name: prop.name,
|
|
662
1479
|
displayName: prop.displayName,
|
|
663
1480
|
type: prop.type,
|
|
664
|
-
required: prop.required,
|
|
665
1481
|
...(prop.groupId ? { groupId: prop.groupId } : {}),
|
|
666
1482
|
...(prop.typeId ? { typeId: prop.typeId } : {}),
|
|
667
1483
|
...(prop.enumTypeId ? { enumTypeId: prop.enumTypeId } : {}),
|
|
668
|
-
...(prop.filteredComponentIds
|
|
1484
|
+
...(prop.filteredComponentIds
|
|
1485
|
+
? { filteredComponentIds: prop.filteredComponentIds }
|
|
1486
|
+
: {}),
|
|
669
1487
|
...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
|
|
1488
|
+
...(prop.numberRangeData
|
|
1489
|
+
? { numberRangeData: prop.numberRangeData }
|
|
1490
|
+
: {}),
|
|
1491
|
+
...(prop.requiredProp
|
|
1492
|
+
? {
|
|
1493
|
+
requiredProp: prop.requiredProp,
|
|
1494
|
+
...(prop.requiredEnumOptionValue !== undefined
|
|
1495
|
+
? { requiredEnumOptionValue: prop.requiredEnumOptionValue }
|
|
1496
|
+
: { requiredPropValue: prop.requiredPropValue ?? true }),
|
|
1497
|
+
}
|
|
1498
|
+
: {}),
|
|
670
1499
|
},
|
|
671
1500
|
}));
|
|
672
1501
|
}
|
|
673
1502
|
function removeProp(ref, options) {
|
|
1503
|
+
failIf(unconfirmedRemoval(`remove the prop "${options.prop}" and the values set for it on every page`, options.yes));
|
|
674
1504
|
const { config, configPath } = loadConfig();
|
|
675
1505
|
const component = resolveComponent(config, ref);
|
|
676
1506
|
const propIndex = component.props.findIndex((p) => p.name === options.prop);
|
|
@@ -709,11 +1539,30 @@ function pruneFilteredComponentIdRefs(config, removedId) {
|
|
|
709
1539
|
}
|
|
710
1540
|
}
|
|
711
1541
|
}
|
|
712
|
-
|
|
1542
|
+
/**
|
|
1543
|
+
* Why `component`'s folder may not be deleted, or null: only its own folder directly under src/components
|
|
1544
|
+
* (where components are created), never one holding another component — so a hand-edited entry can not
|
|
1545
|
+
* point the removal at the project, a folder outside it, or any other project folder.
|
|
1546
|
+
*/
|
|
1547
|
+
export function componentDirRemovalError(components, component, root) {
|
|
1548
|
+
const dir = path.resolve(root, path.dirname(component.entry));
|
|
1549
|
+
const holdsOther = components.some((other) => {
|
|
1550
|
+
if (other === component)
|
|
1551
|
+
return false;
|
|
1552
|
+
const otherDir = path.resolve(root, path.dirname(other.entry));
|
|
1553
|
+
return otherDir === dir || otherDir.startsWith(dir + path.sep);
|
|
1554
|
+
});
|
|
1555
|
+
return path.dirname(dir) !== path.resolve(root, "src/components") || holdsOther
|
|
1556
|
+
? `Refused: "${path.relative(root, dir) || "."}" (from the component's entry) is not a folder of its own under src/components; nothing was removed.`
|
|
1557
|
+
: null;
|
|
1558
|
+
}
|
|
1559
|
+
function removeComponent(ref, yes) {
|
|
713
1560
|
const { config, configPath } = loadConfig();
|
|
714
1561
|
const component = resolveComponent(config, ref);
|
|
1562
|
+
failIf(unconfirmedRemoval(`delete the component "${component.name}" and its folder`, yes));
|
|
715
1563
|
const componentIndex = config.components.indexOf(component);
|
|
716
1564
|
const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
|
|
1565
|
+
failIf(componentDirRemovalError(config.components, component, process.cwd()));
|
|
717
1566
|
// Remove component from config and strip orphaned references from remaining components
|
|
718
1567
|
config.components.splice(componentIndex, 1);
|
|
719
1568
|
pruneFilteredComponentIdRefs(config, component.id);
|
|
@@ -723,13 +1572,16 @@ function removeComponent(ref) {
|
|
|
723
1572
|
fs.rmSync(componentDir, { recursive: true });
|
|
724
1573
|
}
|
|
725
1574
|
// Update barrel export
|
|
726
|
-
updateBarrelExport(process.cwd(),
|
|
1575
|
+
updateBarrelExport(process.cwd(), config.components);
|
|
727
1576
|
console.log(JSON.stringify({
|
|
728
1577
|
success: true,
|
|
729
1578
|
removedComponentId: component.id,
|
|
730
1579
|
removedComponentName: component.name,
|
|
731
1580
|
removedDirectory: path.relative(process.cwd(), componentDir),
|
|
732
|
-
remainingComponents: config.components.map((c) => ({
|
|
1581
|
+
remainingComponents: config.components.map((c) => ({
|
|
1582
|
+
id: c.id,
|
|
1583
|
+
name: c.name,
|
|
1584
|
+
})),
|
|
733
1585
|
}));
|
|
734
1586
|
}
|
|
735
1587
|
function addPropGroup(ref, options) {
|
|
@@ -750,7 +1602,24 @@ function addPropGroup(ref, options) {
|
|
|
750
1602
|
id: options.id,
|
|
751
1603
|
name: options.name,
|
|
752
1604
|
...(options.description ? { description: options.description } : {}),
|
|
1605
|
+
...(options.requiredProp ? { requiredProp: options.requiredProp } : {}),
|
|
1606
|
+
...(options.requiredProp && options.requiredEnumOptionValue !== undefined
|
|
1607
|
+
? {
|
|
1608
|
+
requiredEnumOptionId: options.requiredEnumOptionValue,
|
|
1609
|
+
requiredEnumOptionValue: options.requiredEnumOptionValue,
|
|
1610
|
+
}
|
|
1611
|
+
: {}),
|
|
1612
|
+
...(options.requiredProp &&
|
|
1613
|
+
options.requiredEnumOptionValue === undefined &&
|
|
1614
|
+
options.requiredPropValue !== undefined
|
|
1615
|
+
? {
|
|
1616
|
+
requiredPropValue: parseRequiredPropValue(options.requiredPropValue, `group "${options.id}"`),
|
|
1617
|
+
}
|
|
1618
|
+
: {}),
|
|
753
1619
|
};
|
|
1620
|
+
failIf(visibilityGateError(component, newGroup, `group "${newGroup.id}"`, {
|
|
1621
|
+
group: newGroup,
|
|
1622
|
+
}));
|
|
754
1623
|
if (options.parent) {
|
|
755
1624
|
const found = findPropGroup(component.propGroups, options.parent);
|
|
756
1625
|
if (!found) {
|
|
@@ -782,6 +1651,14 @@ function addPropGroup(ref, options) {
|
|
|
782
1651
|
id: newGroup.id,
|
|
783
1652
|
name: newGroup.name,
|
|
784
1653
|
...(options.parent ? { parentId: options.parent } : {}),
|
|
1654
|
+
...(newGroup.requiredProp
|
|
1655
|
+
? {
|
|
1656
|
+
requiredProp: newGroup.requiredProp,
|
|
1657
|
+
...(newGroup.requiredEnumOptionValue !== undefined
|
|
1658
|
+
? { requiredEnumOptionValue: newGroup.requiredEnumOptionValue }
|
|
1659
|
+
: { requiredPropValue: newGroup.requiredPropValue ?? true }),
|
|
1660
|
+
}
|
|
1661
|
+
: {}),
|
|
785
1662
|
},
|
|
786
1663
|
}));
|
|
787
1664
|
}
|
|
@@ -807,6 +1684,34 @@ function updatePropGroup(ref, options) {
|
|
|
807
1684
|
found.group.name = options.name;
|
|
808
1685
|
if (options.description !== undefined)
|
|
809
1686
|
found.group.description = options.description || undefined;
|
|
1687
|
+
if (options.requiredProp !== undefined) {
|
|
1688
|
+
if (options.requiredProp === "" || options.requiredProp === "none") {
|
|
1689
|
+
// Every field goes together — see `IThemePropGroup.requiredPropId`.
|
|
1690
|
+
delete found.group.requiredProp;
|
|
1691
|
+
delete found.group.requiredPropValue;
|
|
1692
|
+
delete found.group.requiredEnumOptionId;
|
|
1693
|
+
delete found.group.requiredEnumOptionValue;
|
|
1694
|
+
}
|
|
1695
|
+
else {
|
|
1696
|
+
found.group.requiredProp = options.requiredProp;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
if (options.requiredEnumOptionValue !== undefined &&
|
|
1700
|
+
found.group.requiredProp) {
|
|
1701
|
+
found.group.requiredEnumOptionId = options.requiredEnumOptionValue;
|
|
1702
|
+
found.group.requiredEnumOptionValue = options.requiredEnumOptionValue;
|
|
1703
|
+
delete found.group.requiredPropValue;
|
|
1704
|
+
}
|
|
1705
|
+
if (options.requiredPropValue !== undefined && found.group.requiredProp) {
|
|
1706
|
+
delete found.group.requiredEnumOptionId;
|
|
1707
|
+
delete found.group.requiredEnumOptionValue;
|
|
1708
|
+
found.group.requiredPropValue = parseRequiredPropValue(options.requiredPropValue, `group "${found.group.id}"`);
|
|
1709
|
+
}
|
|
1710
|
+
// Re-checked on every update: moving a prop into this group can invalidate a gate that was sound
|
|
1711
|
+
// when it was written.
|
|
1712
|
+
failIf(visibilityGateError(component, found.group, `group "${found.group.id}"`, {
|
|
1713
|
+
group: found.group,
|
|
1714
|
+
}));
|
|
810
1715
|
saveConfig(configPath, config);
|
|
811
1716
|
console.log(JSON.stringify({
|
|
812
1717
|
success: true,
|
|
@@ -814,6 +1719,16 @@ function updatePropGroup(ref, options) {
|
|
|
814
1719
|
updatedPropGroup: {
|
|
815
1720
|
id: found.group.id,
|
|
816
1721
|
name: found.group.name,
|
|
1722
|
+
...(found.group.requiredProp
|
|
1723
|
+
? {
|
|
1724
|
+
requiredProp: found.group.requiredProp,
|
|
1725
|
+
...(found.group.requiredEnumOptionValue !== undefined
|
|
1726
|
+
? {
|
|
1727
|
+
requiredEnumOptionValue: found.group.requiredEnumOptionValue,
|
|
1728
|
+
}
|
|
1729
|
+
: { requiredPropValue: found.group.requiredPropValue ?? true }),
|
|
1730
|
+
}
|
|
1731
|
+
: {}),
|
|
817
1732
|
},
|
|
818
1733
|
}));
|
|
819
1734
|
}
|
|
@@ -836,10 +1751,10 @@ function removePropGroup(ref, options) {
|
|
|
836
1751
|
process.exit(1);
|
|
837
1752
|
}
|
|
838
1753
|
if (found.parent) {
|
|
839
|
-
found.parent.children = found.parent.children.filter(c => c.id !== options.id);
|
|
1754
|
+
found.parent.children = found.parent.children.filter((c) => c.id !== options.id);
|
|
840
1755
|
}
|
|
841
1756
|
else {
|
|
842
|
-
component.propGroups = component.propGroups.filter(g => g.id !== options.id);
|
|
1757
|
+
component.propGroups = component.propGroups.filter((g) => g.id !== options.id);
|
|
843
1758
|
}
|
|
844
1759
|
// Clear groupId from props referencing the deleted group
|
|
845
1760
|
for (const prop of component.props) {
|
|
@@ -852,7 +1767,7 @@ function removePropGroup(ref, options) {
|
|
|
852
1767
|
success: true,
|
|
853
1768
|
componentName: component.name,
|
|
854
1769
|
removedPropGroup: options.id,
|
|
855
|
-
remainingPropGroups: component.propGroups.map(g => g.id),
|
|
1770
|
+
remainingPropGroups: component.propGroups.map((g) => g.id),
|
|
856
1771
|
}));
|
|
857
1772
|
}
|
|
858
1773
|
function movePropGroup(ref, options) {
|
|
@@ -869,7 +1784,10 @@ function movePropGroup(ref, options) {
|
|
|
869
1784
|
component.propGroups = movePropGroupInTree(component.propGroups, options.id, options.parent || null, options.index);
|
|
870
1785
|
}
|
|
871
1786
|
catch (e) {
|
|
872
|
-
console.log(JSON.stringify({
|
|
1787
|
+
console.log(JSON.stringify({
|
|
1788
|
+
success: false,
|
|
1789
|
+
error: e.message || "Failed to move prop group",
|
|
1790
|
+
}));
|
|
873
1791
|
process.exit(1);
|
|
874
1792
|
}
|
|
875
1793
|
saveConfig(configPath, config);
|
|
@@ -925,7 +1843,9 @@ function listComponents() {
|
|
|
925
1843
|
required: p.required,
|
|
926
1844
|
...(p.groupId ? { groupId: p.groupId } : {}),
|
|
927
1845
|
})),
|
|
928
|
-
...(c.propGroups && c.propGroups.length > 0
|
|
1846
|
+
...(c.propGroups && c.propGroups.length > 0
|
|
1847
|
+
? { propGroups: c.propGroups }
|
|
1848
|
+
: {}),
|
|
929
1849
|
}));
|
|
930
1850
|
console.log(JSON.stringify({
|
|
931
1851
|
success: true,
|
|
@@ -939,7 +1859,9 @@ function listComponents() {
|
|
|
939
1859
|
async function addEnum(options) {
|
|
940
1860
|
const { config, configPath } = loadConfig();
|
|
941
1861
|
// Validate name
|
|
942
|
-
if (!options.name ||
|
|
1862
|
+
if (!options.name ||
|
|
1863
|
+
typeof options.name !== "string" ||
|
|
1864
|
+
!options.name.trim()) {
|
|
943
1865
|
console.log(JSON.stringify({ success: false, error: "Enum name is required." }));
|
|
944
1866
|
process.exit(1);
|
|
945
1867
|
}
|
|
@@ -967,10 +1889,10 @@ async function addEnum(options) {
|
|
|
967
1889
|
}));
|
|
968
1890
|
process.exit(1);
|
|
969
1891
|
}
|
|
970
|
-
// Parse and validate options JSON
|
|
971
|
-
let
|
|
1892
|
+
// Parse and validate options JSON (object form, or the `[{ name, value }]` array form)
|
|
1893
|
+
let parsedOptions;
|
|
972
1894
|
try {
|
|
973
|
-
|
|
1895
|
+
parsedOptions = JSON.parse(options.options);
|
|
974
1896
|
}
|
|
975
1897
|
catch {
|
|
976
1898
|
console.log(JSON.stringify({
|
|
@@ -979,15 +1901,12 @@ async function addEnum(options) {
|
|
|
979
1901
|
}));
|
|
980
1902
|
process.exit(1);
|
|
981
1903
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
console.log(JSON.stringify({
|
|
986
|
-
success: false,
|
|
987
|
-
error: 'Options must be a non-empty JSON object, e.g. \'{"Small":"sm","Medium":"md","Large":"lg"}\'.',
|
|
988
|
-
}));
|
|
1904
|
+
const normalizedOptions = normalizeEnumOptionsInput(parsedOptions);
|
|
1905
|
+
if ("error" in normalizedOptions) {
|
|
1906
|
+
console.log(JSON.stringify({ success: false, error: normalizedOptions.error }));
|
|
989
1907
|
process.exit(1);
|
|
990
1908
|
}
|
|
1909
|
+
const enumOptions = normalizedOptions.options;
|
|
991
1910
|
const enumId = generateUniqueId();
|
|
992
1911
|
config.customTypes.push({
|
|
993
1912
|
id: enumId,
|
|
@@ -1037,9 +1956,9 @@ function updateEnum(options) {
|
|
|
1037
1956
|
enumType.name = options.name;
|
|
1038
1957
|
}
|
|
1039
1958
|
if (options.options !== undefined) {
|
|
1040
|
-
let
|
|
1959
|
+
let parsedOptions;
|
|
1041
1960
|
try {
|
|
1042
|
-
|
|
1961
|
+
parsedOptions = JSON.parse(options.options);
|
|
1043
1962
|
}
|
|
1044
1963
|
catch {
|
|
1045
1964
|
console.log(JSON.stringify({
|
|
@@ -1048,16 +1967,12 @@ function updateEnum(options) {
|
|
|
1048
1967
|
}));
|
|
1049
1968
|
process.exit(1);
|
|
1050
1969
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
console.log(JSON.stringify({
|
|
1055
|
-
success: false,
|
|
1056
|
-
error: 'Options must be a non-empty JSON object.',
|
|
1057
|
-
}));
|
|
1970
|
+
const normalizedOptions = normalizeEnumOptionsInput(parsedOptions);
|
|
1971
|
+
if ("error" in normalizedOptions) {
|
|
1972
|
+
console.log(JSON.stringify({ success: false, error: normalizedOptions.error }));
|
|
1058
1973
|
process.exit(1);
|
|
1059
1974
|
}
|
|
1060
|
-
enumType.enumOptions =
|
|
1975
|
+
enumType.enumOptions = normalizedOptions.options;
|
|
1061
1976
|
}
|
|
1062
1977
|
saveConfig(configPath, config);
|
|
1063
1978
|
// Regenerate global types and all component types
|
|
@@ -1072,7 +1987,7 @@ function updateEnum(options) {
|
|
|
1072
1987
|
enumOptions: enumType.enumOptions,
|
|
1073
1988
|
}));
|
|
1074
1989
|
}
|
|
1075
|
-
function removeEnum(options) {
|
|
1990
|
+
async function removeEnum(options) {
|
|
1076
1991
|
const { config, configPath } = loadConfig();
|
|
1077
1992
|
if (!config.customTypes)
|
|
1078
1993
|
config.customTypes = [];
|
|
@@ -1096,6 +2011,26 @@ function removeEnum(options) {
|
|
|
1096
2011
|
}
|
|
1097
2012
|
}
|
|
1098
2013
|
}
|
|
2014
|
+
// A theme ENUM global variable can select from this enum too. Those live in the editor's
|
|
2015
|
+
// project, not in ikas.config.json, so ask the connected editor (best effort: with no dev
|
|
2016
|
+
// server / editor the removal proceeds and the result carries a warning). The editor itself
|
|
2017
|
+
// refuses to delete a type its globals reference, so this mirrors that guard for the CLI path.
|
|
2018
|
+
const liveGlobals = await fetchLiveThemeGlobals();
|
|
2019
|
+
const referencingGlobals = liveGlobals
|
|
2020
|
+
? Object.values(liveGlobals).filter((s) => s.type === "ENUM" && s.enumTypeId === options.id)
|
|
2021
|
+
: [];
|
|
2022
|
+
if (referencingGlobals.length > 0) {
|
|
2023
|
+
const names = referencingGlobals
|
|
2024
|
+
.map((s) => `"${s.displayName}" (${s.name})`)
|
|
2025
|
+
.join(", ");
|
|
2026
|
+
console.log(JSON.stringify({
|
|
2027
|
+
success: false,
|
|
2028
|
+
error: `Cannot remove enum "${config.customTypes[enumIndex].name}": theme global variable(s) ${names} ` +
|
|
2029
|
+
`in the connected editor select from it. Change or delete those globals first ` +
|
|
2030
|
+
`(update-global-variable --enum-type-id / delete-global-variable).`,
|
|
2031
|
+
}));
|
|
2032
|
+
process.exit(1);
|
|
2033
|
+
}
|
|
1099
2034
|
const removed = config.customTypes.splice(enumIndex, 1)[0];
|
|
1100
2035
|
saveConfig(configPath, config);
|
|
1101
2036
|
// Regenerate global types and all component types
|
|
@@ -1107,6 +2042,11 @@ function removeEnum(options) {
|
|
|
1107
2042
|
success: true,
|
|
1108
2043
|
removedEnumId: removed.id,
|
|
1109
2044
|
removedEnumName: removed.name,
|
|
2045
|
+
...(liveGlobals === null
|
|
2046
|
+
? {
|
|
2047
|
+
warning: "Editor not connected — could not verify that no theme global variable selects from this enum.",
|
|
2048
|
+
}
|
|
2049
|
+
: {}),
|
|
1110
2050
|
}));
|
|
1111
2051
|
}
|
|
1112
2052
|
function listEnums() {
|
|
@@ -1126,8 +2066,7 @@ function listEnums() {
|
|
|
1126
2066
|
}
|
|
1127
2067
|
// --- Command registration ---
|
|
1128
2068
|
export function createConfigCommand() {
|
|
1129
|
-
const config = new Command("config")
|
|
1130
|
-
.description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
|
|
2069
|
+
const config = new Command("config").description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
|
|
1131
2070
|
config
|
|
1132
2071
|
.command("add-component")
|
|
1133
2072
|
.description("Add a new component to the project")
|
|
@@ -1136,7 +2075,7 @@ export function createConfigCommand() {
|
|
|
1136
2075
|
.option("--isHeader", "Mark this section as the store header (only for type: section)")
|
|
1137
2076
|
.option("--isFooter", "Mark this section as the store footer (only for type: section)")
|
|
1138
2077
|
.option("--props <json>", "JSON array of props. Required per entry: name, type. Optional: displayName (auto from name), required, description, defaultValue, groupId, typeId (TYPE props), enumTypeId (ENUM props), filteredComponentIds, privateVarMap. " +
|
|
1139
|
-
|
|
2078
|
+
'Example: \'[{"name":"title","type":"TEXT","required":true,"defaultValue":"Hello","groupId":"basic"}]\'')
|
|
1140
2079
|
.action((options) => {
|
|
1141
2080
|
addComponent(options.name, options);
|
|
1142
2081
|
});
|
|
@@ -1155,7 +2094,11 @@ export function createConfigCommand() {
|
|
|
1155
2094
|
.option("--typeId <typeId>", "Type ID for TYPE props (required when type is TYPE)")
|
|
1156
2095
|
.option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
|
|
1157
2096
|
.option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
|
|
1158
|
-
.option("--privateVarMap <json>",
|
|
2097
|
+
.option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)")
|
|
2098
|
+
.option("--numberRangeData <json>", 'JSON slider config for NUMBER_RANGE props, e.g. \'{"min":0,"max":100,"interval":5,"unit":"px"}\'')
|
|
2099
|
+
.option("--requiredProp <propName>", 'Show this only while the named BOOLEAN or ENUM prop satisfies the condition ("Koşullu Prop")')
|
|
2100
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2101
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1159
2102
|
.action((options) => {
|
|
1160
2103
|
addProp({ id: options.componentId, name: options.component }, options);
|
|
1161
2104
|
});
|
|
@@ -1175,6 +2118,10 @@ export function createConfigCommand() {
|
|
|
1175
2118
|
.option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (use 'none' to clear)")
|
|
1176
2119
|
.option("--filteredComponentIds <json>", "JSON array of component IDs (use 'none' to clear)")
|
|
1177
2120
|
.option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (use 'none' to clear)")
|
|
2121
|
+
.option("--numberRangeData <json>", "JSON slider config for NUMBER_RANGE props (use 'none' to clear)")
|
|
2122
|
+
.option("--requiredProp <propName>", "BOOLEAN or ENUM prop that gates this prop in the editor panel (use 'none' to clear)")
|
|
2123
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2124
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1178
2125
|
.action((options) => {
|
|
1179
2126
|
updateProp({ id: options.componentId, name: options.component }, options);
|
|
1180
2127
|
});
|
|
@@ -1184,6 +2131,7 @@ export function createConfigCommand() {
|
|
|
1184
2131
|
.option("--component-id <id>", "Component id (preferred; from `config list-components`)")
|
|
1185
2132
|
.option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1186
2133
|
.requiredOption("--prop <propName>", "Prop name to remove")
|
|
2134
|
+
.option("--yes", "Confirm the removal. Required: the command refuses to run without it.")
|
|
1187
2135
|
.action((options) => {
|
|
1188
2136
|
removeProp({ id: options.componentId, name: options.component }, options);
|
|
1189
2137
|
});
|
|
@@ -1199,13 +2147,78 @@ export function createConfigCommand() {
|
|
|
1199
2147
|
.action((options) => {
|
|
1200
2148
|
updateComponent({ id: options.id, name: options.name }, options);
|
|
1201
2149
|
});
|
|
2150
|
+
/**
|
|
2151
|
+
* Whether the store shows a language's UI texts: asked of the editor connected to the dev server on the default
|
|
2152
|
+
* port. Only said, never refused: that server may be another project's, its store not this text's.
|
|
2153
|
+
*/
|
|
2154
|
+
const storeLanguageCheck = async (locale) => {
|
|
2155
|
+
try {
|
|
2156
|
+
const result = await runEditorAction("list-translations", { mode: "store" }, { timeoutMs: 5000 });
|
|
2157
|
+
const languages = result.payload?.languages;
|
|
2158
|
+
if (!result.success || !languages)
|
|
2159
|
+
return { warning: `Not checked that the store has "${locale}": ${result.error ?? "no languages"}.` };
|
|
2160
|
+
return languages.includes(locale)
|
|
2161
|
+
? {}
|
|
2162
|
+
: {
|
|
2163
|
+
warning: `The store of the editor connected through the dev server has no "${locale}" language (${languages.join(", ")}): its texts show only once it is added (ikas-component translation-languages --mode store --add ${locale}).`,
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
catch (e) {
|
|
2167
|
+
return {
|
|
2168
|
+
warning: `Not checked that the store has "${locale}" (no editor connected through \`ikas theme dev\`): its texts show only once the store adds the language.`,
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
// The project's UI texts (`src/i18n/default.json` + a `<locale>.json` per language)
|
|
2173
|
+
const textsCommand = (run) => {
|
|
2174
|
+
const { config: projectConfig, configPath } = loadConfig();
|
|
2175
|
+
const result = run(projectConfig, path.dirname(configPath));
|
|
2176
|
+
if (result.configChanged)
|
|
2177
|
+
saveConfig(configPath, projectConfig);
|
|
2178
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2179
|
+
if (!result.success)
|
|
2180
|
+
process.exit(1);
|
|
2181
|
+
};
|
|
2182
|
+
config
|
|
2183
|
+
.command("set-text")
|
|
2184
|
+
.description("Add or update a UI text of the project (a fixed text of the components' UI, read with t(key, params) imported from @ikas/bp-storefront). " +
|
|
2185
|
+
"Keys are dotted groups: common.addToCart. Without --locale the base text (default.json); with --locale a language's. {{param}} placeholders must match the base text's.")
|
|
2186
|
+
.requiredOption("--key <key>", "Dotted key, e.g. common.addToCart")
|
|
2187
|
+
.requiredOption("--text <text>", 'The text, e.g. "{{count}} adet kaldı"')
|
|
2188
|
+
.option("--locale <code>", "A language code (en, de, ...): set that language's text instead of the base text")
|
|
2189
|
+
.action(async (options) => {
|
|
2190
|
+
const store = options.locale
|
|
2191
|
+
? await storeLanguageCheck(options.locale)
|
|
2192
|
+
: {};
|
|
2193
|
+
textsCommand((projectConfig, root) => ({
|
|
2194
|
+
...setText(projectConfig, options, root),
|
|
2195
|
+
...(store.warning ? { warning: store.warning } : {}),
|
|
2196
|
+
}));
|
|
2197
|
+
});
|
|
2198
|
+
config
|
|
2199
|
+
.command("remove-text")
|
|
2200
|
+
.description("Remove a UI text: from the base and every language, or with --locale only that language's text")
|
|
2201
|
+
.requiredOption("--key <key>", "Dotted key")
|
|
2202
|
+
.option("--locale <code>", "Remove only this language's text")
|
|
2203
|
+
.action((options) => textsCommand((projectConfig, root) => removeText(projectConfig, options, root)));
|
|
2204
|
+
config
|
|
2205
|
+
.command("rename-text")
|
|
2206
|
+
.description("Rename a UI text, or a group with every text under it (product → item), in the base and every language. Change the t() calls after: the type check reports the old keys.")
|
|
2207
|
+
.requiredOption("--key <key>", "Dotted key or group, e.g. common.addToCart")
|
|
2208
|
+
.requiredOption("--to <key>", "The new dotted key or group")
|
|
2209
|
+
.action((options) => textsCommand((projectConfig, root) => renameText(projectConfig, options, root)));
|
|
2210
|
+
config
|
|
2211
|
+
.command("list-texts")
|
|
2212
|
+
.description("List the project's UI texts: the base texts, each language's, and the keys a language lacks")
|
|
2213
|
+
.action(() => textsCommand((projectConfig, root) => listTexts(projectConfig, root)));
|
|
1202
2214
|
config
|
|
1203
2215
|
.command("remove-component")
|
|
1204
2216
|
.description("Remove a component from the project (deletes files). Identify by --id (preferred) or exact --name.")
|
|
1205
2217
|
.option("--id <id>", "Component id (preferred; from `config list-components`)")
|
|
1206
2218
|
.option("--name <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
2219
|
+
.option("--yes", "Confirm the removal. Required: the command refuses to run without it.")
|
|
1207
2220
|
.action((options) => {
|
|
1208
|
-
removeComponent({ id: options.id, name: options.name });
|
|
2221
|
+
removeComponent({ id: options.id, name: options.name }, options.yes);
|
|
1209
2222
|
});
|
|
1210
2223
|
config
|
|
1211
2224
|
.command("add-prop-group")
|
|
@@ -1216,6 +2229,9 @@ export function createConfigCommand() {
|
|
|
1216
2229
|
.requiredOption("--name <name>", "Display name in editor")
|
|
1217
2230
|
.option("--description <description>", "Group description")
|
|
1218
2231
|
.option("--parent <parentId>", "Parent group ID (for nesting, max 1 level)")
|
|
2232
|
+
.option("--requiredProp <propName>", 'Show this only while the named BOOLEAN or ENUM prop satisfies the condition ("Koşullu Prop")')
|
|
2233
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2234
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1219
2235
|
.action((options) => {
|
|
1220
2236
|
addPropGroup({ id: options.componentId, name: options.component }, options);
|
|
1221
2237
|
});
|
|
@@ -1227,6 +2243,9 @@ export function createConfigCommand() {
|
|
|
1227
2243
|
.requiredOption("--id <id>", "Group ID to update")
|
|
1228
2244
|
.option("--name <name>", "New display name")
|
|
1229
2245
|
.option("--description <description>", "New description")
|
|
2246
|
+
.option("--requiredProp <propName>", "BOOLEAN or ENUM prop that gates this group in the editor panel (use 'none' to clear)")
|
|
2247
|
+
.option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
|
|
2248
|
+
.option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
|
|
1230
2249
|
.action((options) => {
|
|
1231
2250
|
updatePropGroup({ id: options.componentId, name: options.component }, options);
|
|
1232
2251
|
});
|
|
@@ -1246,7 +2265,7 @@ export function createConfigCommand() {
|
|
|
1246
2265
|
.option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
|
|
1247
2266
|
.requiredOption("--id <id>", "Group ID to move")
|
|
1248
2267
|
.option("--parent <parentId>", "Target parent group ID (omit to move to root)")
|
|
1249
|
-
.option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", v => parseInt(v, 10))
|
|
2268
|
+
.option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", (v) => parseInt(v, 10))
|
|
1250
2269
|
.action((options) => {
|
|
1251
2270
|
movePropGroup({ id: options.componentId, name: options.component }, options);
|
|
1252
2271
|
});
|
|
@@ -1273,7 +2292,7 @@ export function createConfigCommand() {
|
|
|
1273
2292
|
.command("add-enum")
|
|
1274
2293
|
.description("Create a custom enum type (offline, no editor needed)")
|
|
1275
2294
|
.requiredOption("--name <name>", "Enum name (e.g. Size, AspectRatio)")
|
|
1276
|
-
.requiredOption("--options <json>", 'JSON object of label→value pairs, e.g. \'{"Small":"sm","Medium":"md","Large":"lg"}\'')
|
|
2295
|
+
.requiredOption("--options <json>", 'JSON object of label→value pairs, e.g. \'{"Small":"sm","Medium":"md","Large":"lg"}\' (an array \'[{"name":"Small","value":"sm"}]\' is also accepted)')
|
|
1277
2296
|
.action(async (options) => {
|
|
1278
2297
|
await addEnum(options);
|
|
1279
2298
|
});
|
|
@@ -1282,16 +2301,16 @@ export function createConfigCommand() {
|
|
|
1282
2301
|
.description("Update a custom enum type")
|
|
1283
2302
|
.requiredOption("--id <id>", "Enum ID to update")
|
|
1284
2303
|
.option("--name <name>", "New enum name")
|
|
1285
|
-
.option("--options <json>", "New JSON object of label→value pairs")
|
|
2304
|
+
.option("--options <json>", "New JSON object of label→value pairs (an array of { name, value } is also accepted)")
|
|
1286
2305
|
.action((options) => {
|
|
1287
2306
|
updateEnum(options);
|
|
1288
2307
|
});
|
|
1289
2308
|
config
|
|
1290
2309
|
.command("remove-enum")
|
|
1291
|
-
.description("Remove a custom enum type (fails if any prop references it)")
|
|
2310
|
+
.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)")
|
|
1292
2311
|
.requiredOption("--id <id>", "Enum ID to remove")
|
|
1293
|
-
.action((options) => {
|
|
1294
|
-
removeEnum(options);
|
|
2312
|
+
.action(async (options) => {
|
|
2313
|
+
await removeEnum(options);
|
|
1295
2314
|
});
|
|
1296
2315
|
config
|
|
1297
2316
|
.command("list-enums")
|
|
@@ -1306,54 +2325,15 @@ export function createConfigCommand() {
|
|
|
1306
2325
|
* Returns the matching type (with enumOptions) if found, null otherwise.
|
|
1307
2326
|
*/
|
|
1308
2327
|
async function fetchLiveEditorEnumById(enumId) {
|
|
1309
|
-
const
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
if (settled)
|
|
1317
|
-
return;
|
|
1318
|
-
settled = true;
|
|
1319
|
-
try {
|
|
1320
|
-
ws?.close();
|
|
1321
|
-
}
|
|
1322
|
-
catch {
|
|
1323
|
-
/* ignore */
|
|
1324
|
-
}
|
|
1325
|
-
resolve(value);
|
|
1326
|
-
};
|
|
1327
|
-
try {
|
|
1328
|
-
ws = new WebSocket(WS_URL);
|
|
1329
|
-
}
|
|
1330
|
-
catch {
|
|
1331
|
-
resolve(null);
|
|
1332
|
-
return;
|
|
2328
|
+
const types = await fetchLiveEditorTypes();
|
|
2329
|
+
const match = types?.find((t) => t.category === "enum" && t.id === enumId);
|
|
2330
|
+
return match
|
|
2331
|
+
? {
|
|
2332
|
+
id: match.id,
|
|
2333
|
+
name: match.name,
|
|
2334
|
+
...(match.enumOptions ? { enumOptions: match.enumOptions } : {}),
|
|
1333
2335
|
}
|
|
1334
|
-
|
|
1335
|
-
ws.on("open", () => {
|
|
1336
|
-
ws.send(JSON.stringify({ type: "request-types" }));
|
|
1337
|
-
});
|
|
1338
|
-
ws.on("message", (data) => {
|
|
1339
|
-
try {
|
|
1340
|
-
const message = JSON.parse(data.toString());
|
|
1341
|
-
if (message.type === "types-list") {
|
|
1342
|
-
clearTimeout(timeout);
|
|
1343
|
-
const types = message.payload?.types || [];
|
|
1344
|
-
const match = types.find((t) => t.category === "enum" && t.id === enumId);
|
|
1345
|
-
finish(match ? { id: match.id, name: match.name, ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}) } : null);
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
catch {
|
|
1349
|
-
// Ignore non-JSON messages
|
|
1350
|
-
}
|
|
1351
|
-
});
|
|
1352
|
-
ws.on("error", () => {
|
|
1353
|
-
clearTimeout(timeout);
|
|
1354
|
-
finish(null);
|
|
1355
|
-
});
|
|
1356
|
-
});
|
|
2336
|
+
: null;
|
|
1357
2337
|
}
|
|
1358
2338
|
/**
|
|
1359
2339
|
* Best-effort lookup of an enum by PascalCase name in the live editor session.
|
|
@@ -1363,8 +2343,37 @@ async function fetchLiveEditorEnumById(enumId) {
|
|
|
1363
2343
|
* must treat null as "could not verify, fall back to on-disk check only".
|
|
1364
2344
|
*/
|
|
1365
2345
|
async function fetchLiveEditorEnumByName(pascalName) {
|
|
2346
|
+
const types = await fetchLiveEditorTypes();
|
|
2347
|
+
const match = types?.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
|
|
2348
|
+
return match ? { id: match.id, name: match.name } : null;
|
|
2349
|
+
}
|
|
2350
|
+
/**
|
|
2351
|
+
* Best-effort read of the connected editor's theme global variables (the same editor action
|
|
2352
|
+
* `list-theme-globals` runs). Returns null on any failure — no dev server, no editor connected,
|
|
2353
|
+
* timeout — and callers must treat null as "could not verify, allow".
|
|
2354
|
+
*/
|
|
2355
|
+
async function fetchLiveThemeGlobals() {
|
|
2356
|
+
try {
|
|
2357
|
+
const result = await runEditorAction("list-theme-globals", {}, { timeoutMs: 1500 });
|
|
2358
|
+
if (!result.success)
|
|
2359
|
+
return null;
|
|
2360
|
+
const settings = result.payload?.settings;
|
|
2361
|
+
return settings && typeof settings === "object" ? settings : null;
|
|
2362
|
+
}
|
|
2363
|
+
catch {
|
|
2364
|
+
return null;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Best-effort fetch of the editor's synced type list (with category /
|
|
2369
|
+
* sectionAllowed / enumOptions metadata). Returns null on any failure (no dev
|
|
2370
|
+
* server, timeout) — callers must treat null as "could not verify, allow".
|
|
2371
|
+
*/
|
|
2372
|
+
async function fetchLiveEditorTypes() {
|
|
1366
2373
|
const { WebSocket } = await import("ws");
|
|
1367
|
-
|
|
2374
|
+
// `client=control` keeps this short-lived socket out of the dev server's
|
|
2375
|
+
// `editor-action` target pool — it only reads types, it can't apply commands.
|
|
2376
|
+
const WS_URL = "ws://localhost:5201/?client=control";
|
|
1368
2377
|
const TIMEOUT_MS = 1500;
|
|
1369
2378
|
return new Promise((resolve) => {
|
|
1370
2379
|
let ws;
|
|
@@ -1397,9 +2406,7 @@ async function fetchLiveEditorEnumByName(pascalName) {
|
|
|
1397
2406
|
const message = JSON.parse(data.toString());
|
|
1398
2407
|
if (message.type === "types-list") {
|
|
1399
2408
|
clearTimeout(timeout);
|
|
1400
|
-
|
|
1401
|
-
const match = types.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
|
|
1402
|
-
finish(match ? { id: match.id, name: match.name } : null);
|
|
2409
|
+
finish(message.payload?.types || []);
|
|
1403
2410
|
}
|
|
1404
2411
|
}
|
|
1405
2412
|
catch {
|
|
@@ -1412,9 +2419,52 @@ async function fetchLiveEditorEnumByName(pascalName) {
|
|
|
1412
2419
|
});
|
|
1413
2420
|
});
|
|
1414
2421
|
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Definition-time guard for TYPE-prop typeIds, verified against the live
|
|
2424
|
+
* editor's type list (skipped when the dev server is unreachable or the type
|
|
2425
|
+
* is unknown to the editor). Two rules:
|
|
2426
|
+
* - deprecated types are rejected on ANY component: they are legacy
|
|
2427
|
+
* render-only types — old themes may still carry values, but new props must
|
|
2428
|
+
* not be defined with them.
|
|
2429
|
+
* - on section components, only section-allowed (style) types pass — any other
|
|
2430
|
+
* type renders an empty placeholder merchants cannot fill, and section-level
|
|
2431
|
+
* bindings cannot supply a value either, so the prop would be dead weight.
|
|
2432
|
+
* On violation prints the standard { success: false } envelope and exits.
|
|
2433
|
+
*/
|
|
2434
|
+
async function assertTypeIdsUsable(componentType, typeIds) {
|
|
2435
|
+
if (typeIds.length === 0)
|
|
2436
|
+
return;
|
|
2437
|
+
const types = await fetchLiveEditorTypes();
|
|
2438
|
+
if (!types)
|
|
2439
|
+
return;
|
|
2440
|
+
const matches = typeIds.flatMap((typeId) => {
|
|
2441
|
+
const match = types.find((t) => t.id === typeId);
|
|
2442
|
+
return match ? [match] : [];
|
|
2443
|
+
});
|
|
2444
|
+
const legacy = matches.find((t) => t.deprecated === true);
|
|
2445
|
+
if (legacy) {
|
|
2446
|
+
console.log(JSON.stringify({
|
|
2447
|
+
success: false,
|
|
2448
|
+
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).`,
|
|
2449
|
+
}));
|
|
2450
|
+
process.exit(1);
|
|
2451
|
+
}
|
|
2452
|
+
if (componentType !== "section")
|
|
2453
|
+
return;
|
|
2454
|
+
const disallowed = matches.find((t) => t.sectionAllowed === false);
|
|
2455
|
+
if (!disallowed)
|
|
2456
|
+
return;
|
|
2457
|
+
console.log(JSON.stringify({
|
|
2458
|
+
success: false,
|
|
2459
|
+
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.`,
|
|
2460
|
+
}));
|
|
2461
|
+
process.exit(1);
|
|
2462
|
+
}
|
|
1415
2463
|
async function listTypes(componentType) {
|
|
1416
2464
|
const { WebSocket } = await import("ws");
|
|
1417
|
-
|
|
2465
|
+
// `client=control` keeps this short-lived socket out of the dev server's
|
|
2466
|
+
// `editor-action` target pool — it only reads types, it can't apply commands.
|
|
2467
|
+
const WS_URL = "ws://localhost:5201/?client=control";
|
|
1418
2468
|
const TIMEOUT_MS = 5000;
|
|
1419
2469
|
await new Promise((resolve) => {
|
|
1420
2470
|
let ws;
|
|
@@ -1424,7 +2474,7 @@ async function listTypes(componentType) {
|
|
|
1424
2474
|
catch {
|
|
1425
2475
|
console.log(JSON.stringify({
|
|
1426
2476
|
success: false,
|
|
1427
|
-
error: "Failed to connect to dev server. Make sure 'ikas
|
|
2477
|
+
error: "Failed to connect to dev server. Make sure 'ikas theme dev' is running.",
|
|
1428
2478
|
}));
|
|
1429
2479
|
process.exit(1);
|
|
1430
2480
|
return;
|
|
@@ -1445,7 +2495,8 @@ async function listTypes(componentType) {
|
|
|
1445
2495
|
const message = JSON.parse(data.toString());
|
|
1446
2496
|
if (message.type === "types-list") {
|
|
1447
2497
|
clearTimeout(timeout);
|
|
1448
|
-
|
|
2498
|
+
// Legacy render-only types are never offered for new props.
|
|
2499
|
+
let types = message.payload.types.filter((t) => !t.deprecated);
|
|
1449
2500
|
if (componentType === "section") {
|
|
1450
2501
|
types = types.filter((t) => t.sectionAllowed);
|
|
1451
2502
|
}
|
|
@@ -1465,7 +2516,7 @@ async function listTypes(componentType) {
|
|
|
1465
2516
|
clearTimeout(timeout);
|
|
1466
2517
|
console.log(JSON.stringify({
|
|
1467
2518
|
success: false,
|
|
1468
|
-
error: "Failed to connect to dev server. Make sure 'ikas
|
|
2519
|
+
error: "Failed to connect to dev server. Make sure 'ikas theme dev' is running.",
|
|
1469
2520
|
}));
|
|
1470
2521
|
process.exit(1);
|
|
1471
2522
|
});
|