@routevn/creator-model 1.10.3 → 1.11.0

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.
Files changed (3) hide show
  1. package/README.md +67 -0
  2. package/package.json +1 -1
  3. package/src/model.js +1425 -88
package/src/model.js CHANGED
@@ -42,6 +42,7 @@ const LINE_UPDATE_ACTIONS_PRESERVE_PATHS_SET = new Set(
42
42
  LINE_UPDATE_ACTIONS_PRESERVE_PATHS,
43
43
  );
44
44
  const CURRENT_LAYOUT_SCHEMA_VERSION = 2;
45
+ const FONT_WEIGHT_KEYS = ["minWeight", "defaultWeight", "maxWeight"];
45
46
  const isPositiveFiniteNumber = (value) => isFiniteNumber(value) && value > 0;
46
47
  const normalizeLayoutSchemaVersion = (value) =>
47
48
  Number.isInteger(value) && value >= 1 ? value : 1;
@@ -340,7 +341,7 @@ const ANIMATION_EASING_KEYS = [
340
341
  "easeInOutElastic",
341
342
  ];
342
343
  const VARIABLE_SCOPE_KEYS = ["context", "device", "account"];
343
- const VARIABLE_TYPE_KEYS = ["string", "number", "boolean"];
344
+ const VARIABLE_TYPE_KEYS = ["string", "number", "boolean", "object"];
344
345
  const LAYOUT_TYPE_KEYS = [
345
346
  "general",
346
347
  "save-load",
@@ -402,7 +403,7 @@ const SAVE_LOAD_DATE_FORMATS = new Set([
402
403
  "DD MMM YYYY",
403
404
  "YYYY年MM月DD日",
404
405
  ]);
405
- export const SCHEMA_VERSION = 10;
406
+ export const SCHEMA_VERSION = 11;
406
407
  const LAYOUT_CONTAINER_ELEMENT_TYPES = [
407
408
  "folder",
408
409
  "container",
@@ -2388,6 +2389,41 @@ const validateAnimationItems = ({ items, path, errorFactory }) => {
2388
2389
  }
2389
2390
  };
2390
2391
 
2392
+ const validateFontWeightFields = ({ value, path, errorFactory }) => {
2393
+ const presentKeys = FONT_WEIGHT_KEYS.filter((key) =>
2394
+ Object.hasOwn(value, key),
2395
+ );
2396
+ if (presentKeys.length === 0) {
2397
+ return;
2398
+ }
2399
+
2400
+ if (presentKeys.length !== FONT_WEIGHT_KEYS.length) {
2401
+ return invalidFromErrorFactory(
2402
+ errorFactory,
2403
+ `${path} must include minWeight, defaultWeight, and maxWeight together`,
2404
+ );
2405
+ }
2406
+
2407
+ for (const key of FONT_WEIGHT_KEYS) {
2408
+ if (!isFiniteNumber(value[key]) || value[key] < 1 || value[key] > 1000) {
2409
+ return invalidFromErrorFactory(
2410
+ errorFactory,
2411
+ `${path}.${key} must be a finite number between 1 and 1000`,
2412
+ );
2413
+ }
2414
+ }
2415
+
2416
+ if (
2417
+ value.minWeight > value.defaultWeight ||
2418
+ value.defaultWeight > value.maxWeight
2419
+ ) {
2420
+ return invalidFromErrorFactory(
2421
+ errorFactory,
2422
+ `${path} must satisfy minWeight <= defaultWeight <= maxWeight`,
2423
+ );
2424
+ }
2425
+ };
2426
+
2391
2427
  const validateFontItems = ({ items, path, errorFactory }) => {
2392
2428
  for (const [itemId, item] of Object.entries(items)) {
2393
2429
  const itemPath = `${path}.${itemId}`;
@@ -2413,6 +2449,7 @@ const validateFontItems = ({ items, path, errorFactory }) => {
2413
2449
  "tagIds",
2414
2450
  "fileId",
2415
2451
  "fontFamily",
2452
+ ...FONT_WEIGHT_KEYS,
2416
2453
  ],
2417
2454
  path: itemPath,
2418
2455
  errorFactory,
@@ -2476,6 +2513,17 @@ const validateFontItems = ({ items, path, errorFactory }) => {
2476
2513
  `${itemPath}.fontFamily must be a non-empty string`,
2477
2514
  );
2478
2515
  }
2516
+
2517
+ {
2518
+ const result = validateFontWeightFields({
2519
+ value: item,
2520
+ path: itemPath,
2521
+ errorFactory,
2522
+ });
2523
+ if (result?.valid === false) {
2524
+ return result;
2525
+ }
2526
+ }
2479
2527
  }
2480
2528
  }
2481
2529
  };
@@ -2817,49 +2865,1122 @@ const validateParticleItems = ({ items, path, errorFactory }) => {
2817
2865
  );
2818
2866
  }
2819
2867
 
2820
- if (
2821
- item.thumbnailFileId !== undefined &&
2822
- !isNonEmptyString(item.thumbnailFileId)
2823
- ) {
2824
- return invalidFromErrorFactory(
2825
- errorFactory,
2826
- `${itemPath}.thumbnailFileId must be a non-empty string when provided`,
2827
- );
2868
+ if (
2869
+ item.thumbnailFileId !== undefined &&
2870
+ !isNonEmptyString(item.thumbnailFileId)
2871
+ ) {
2872
+ return invalidFromErrorFactory(
2873
+ errorFactory,
2874
+ `${itemPath}.thumbnailFileId must be a non-empty string when provided`,
2875
+ );
2876
+ }
2877
+
2878
+ {
2879
+ const result = validateParticleModules({
2880
+ modules: item.modules,
2881
+ path: `${itemPath}.modules`,
2882
+ errorFactory,
2883
+ });
2884
+ if (result?.valid === false) {
2885
+ return result;
2886
+ }
2887
+ }
2888
+ }
2889
+ };
2890
+
2891
+ const validateVariableTypedValue = ({
2892
+ value,
2893
+ variableType,
2894
+ path,
2895
+ errorFactory,
2896
+ }) => {
2897
+ if (variableType === "string" && !isString(value)) {
2898
+ return invalidFromErrorFactory(errorFactory, `${path} must be a string`);
2899
+ }
2900
+
2901
+ if (variableType === "number" && !isFiniteNumber(value)) {
2902
+ return invalidFromErrorFactory(
2903
+ errorFactory,
2904
+ `${path} must be a finite number`,
2905
+ );
2906
+ }
2907
+
2908
+ if (variableType === "boolean" && typeof value !== "boolean") {
2909
+ return invalidFromErrorFactory(errorFactory, `${path} must be a boolean`);
2910
+ }
2911
+
2912
+ if (
2913
+ variableType === "object" &&
2914
+ (value === null || typeof value !== "object")
2915
+ ) {
2916
+ return invalidFromErrorFactory(
2917
+ errorFactory,
2918
+ `${path} must be a non-null object or array`,
2919
+ );
2920
+ }
2921
+
2922
+ if (variableType === "object") {
2923
+ return validateComputedDataValue({
2924
+ value,
2925
+ path,
2926
+ errorFactory,
2927
+ });
2928
+ }
2929
+ };
2930
+
2931
+ const COMPUTED_EXPRESSION_FIXED_OPERAND_COUNTS = Object.freeze({
2932
+ add: 2,
2933
+ sub: 2,
2934
+ mul: 2,
2935
+ div: 2,
2936
+ mod: 2,
2937
+ neg: 1,
2938
+ round: 1,
2939
+ floor: 1,
2940
+ ceil: 1,
2941
+ min: 2,
2942
+ max: 2,
2943
+ clamp: 3,
2944
+ eq: 2,
2945
+ neq: 2,
2946
+ gt: 2,
2947
+ gte: 2,
2948
+ lt: 2,
2949
+ lte: 2,
2950
+ in: 2,
2951
+ not: 1,
2952
+ length: 1,
2953
+ includes: 2,
2954
+ });
2955
+ const COMPUTED_EXPRESSION_VARIADIC_OPERATORS = new Set([
2956
+ "and",
2957
+ "or",
2958
+ "all",
2959
+ "any",
2960
+ ]);
2961
+ const COMPUTED_EXPRESSION_NUMERIC_OPERAND_OPERATORS = new Set([
2962
+ "add",
2963
+ "sub",
2964
+ "mul",
2965
+ "div",
2966
+ "mod",
2967
+ "neg",
2968
+ "round",
2969
+ "floor",
2970
+ "ceil",
2971
+ "min",
2972
+ "max",
2973
+ "clamp",
2974
+ ]);
2975
+ const COMPUTED_EXPRESSION_NUMERIC_RESULT_OPERATORS = new Set([
2976
+ ...COMPUTED_EXPRESSION_NUMERIC_OPERAND_OPERATORS,
2977
+ "length",
2978
+ ]);
2979
+ const COMPUTED_EXPRESSION_BOOLEAN_RESULT_OPERATORS = new Set([
2980
+ "eq",
2981
+ "neq",
2982
+ "gt",
2983
+ "gte",
2984
+ "lt",
2985
+ "lte",
2986
+ "in",
2987
+ "and",
2988
+ "or",
2989
+ "all",
2990
+ "any",
2991
+ "not",
2992
+ "includes",
2993
+ ]);
2994
+ const COMPUTED_CONDITION_FIXED_OPERAND_COUNTS = Object.freeze({
2995
+ eq: 2,
2996
+ neq: 2,
2997
+ gt: 2,
2998
+ gte: 2,
2999
+ lt: 2,
3000
+ lte: 2,
3001
+ in: 2,
3002
+ add: 2,
3003
+ sub: 2,
3004
+ });
3005
+ const COMPUTED_CONDITION_VARIADIC_OPERATORS = new Set(["all", "any"]);
3006
+
3007
+ const getComputedValueType = (value) => {
3008
+ if (value === null) {
3009
+ return "null";
3010
+ }
3011
+ if (typeof value === "object") {
3012
+ return "object";
3013
+ }
3014
+ return typeof value;
3015
+ };
3016
+
3017
+ const validComputedResult = (valueType) => ({
3018
+ valid: true,
3019
+ valueType,
3020
+ });
3021
+
3022
+ const validateComputedDataValue = ({
3023
+ value,
3024
+ path,
3025
+ errorFactory,
3026
+ ancestors = new Set(),
3027
+ }) => {
3028
+ if (
3029
+ value === null ||
3030
+ typeof value === "string" ||
3031
+ typeof value === "boolean"
3032
+ ) {
3033
+ return VALID_RESULT;
3034
+ }
3035
+
3036
+ if (typeof value === "number") {
3037
+ if (Number.isFinite(value)) {
3038
+ return VALID_RESULT;
3039
+ }
3040
+ return invalidFromErrorFactory(
3041
+ errorFactory,
3042
+ `${path} must use finite numeric values`,
3043
+ );
3044
+ }
3045
+
3046
+ if (Array.isArray(value)) {
3047
+ if (ancestors.has(value)) {
3048
+ return invalidFromErrorFactory(
3049
+ errorFactory,
3050
+ `${path} must not contain cyclic data`,
3051
+ );
3052
+ }
3053
+ ancestors.add(value);
3054
+
3055
+ for (const [index, item] of value.entries()) {
3056
+ const result = validateComputedDataValue({
3057
+ value: item,
3058
+ path: `${path}[${index}]`,
3059
+ errorFactory,
3060
+ ancestors,
3061
+ });
3062
+ if (result?.valid === false) {
3063
+ return result;
3064
+ }
3065
+ }
3066
+ ancestors.delete(value);
3067
+ return VALID_RESULT;
3068
+ }
3069
+
3070
+ if (
3071
+ !isPlainObject(value) ||
3072
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value))
3073
+ ) {
3074
+ return invalidFromErrorFactory(
3075
+ errorFactory,
3076
+ `${path} must contain JSON-compatible data`,
3077
+ );
3078
+ }
3079
+
3080
+ if (ancestors.has(value)) {
3081
+ return invalidFromErrorFactory(
3082
+ errorFactory,
3083
+ `${path} must not contain cyclic data`,
3084
+ );
3085
+ }
3086
+ ancestors.add(value);
3087
+
3088
+ for (const [key, item] of Object.entries(value)) {
3089
+ const result = validateComputedDataValue({
3090
+ value: item,
3091
+ path: `${path}.${key}`,
3092
+ errorFactory,
3093
+ ancestors,
3094
+ });
3095
+ if (result?.valid === false) {
3096
+ return result;
3097
+ }
3098
+ }
3099
+
3100
+ ancestors.delete(value);
3101
+ return VALID_RESULT;
3102
+ };
3103
+
3104
+ const invalidComputedReferenceParseResult = Object.freeze({
3105
+ valid: false,
3106
+ });
3107
+
3108
+ const decodeComputedReferenceQuotedPart = (rawValue) => {
3109
+ if (rawValue[0] === '"') {
3110
+ try {
3111
+ return {
3112
+ valid: true,
3113
+ value: JSON.parse(rawValue),
3114
+ };
3115
+ } catch {
3116
+ return invalidComputedReferenceParseResult;
3117
+ }
3118
+ }
3119
+
3120
+ let value = "";
3121
+ const escapedValues = {
3122
+ "\\": "\\",
3123
+ '"': '"',
3124
+ "'": "'",
3125
+ "/": "/",
3126
+ n: "\n",
3127
+ r: "\r",
3128
+ t: "\t",
3129
+ b: "\b",
3130
+ f: "\f",
3131
+ };
3132
+
3133
+ for (let index = 1; index < rawValue.length - 1; index += 1) {
3134
+ const character = rawValue[index];
3135
+ if (character !== "\\") {
3136
+ if (character.charCodeAt(0) < 0x20) {
3137
+ return invalidComputedReferenceParseResult;
3138
+ }
3139
+ value += character;
3140
+ continue;
3141
+ }
3142
+
3143
+ index += 1;
3144
+ if (index >= rawValue.length - 1) {
3145
+ return invalidComputedReferenceParseResult;
3146
+ }
3147
+
3148
+ const escapedCharacter = rawValue[index];
3149
+ if (escapedCharacter === "u") {
3150
+ const hexValue = rawValue.slice(index + 1, index + 5);
3151
+ if (hexValue.length !== 4 || !/^[0-9a-fA-F]{4}$/.test(hexValue)) {
3152
+ return invalidComputedReferenceParseResult;
3153
+ }
3154
+ value += String.fromCharCode(Number.parseInt(hexValue, 16));
3155
+ index += 4;
3156
+ continue;
3157
+ }
3158
+
3159
+ if (!Object.hasOwn(escapedValues, escapedCharacter)) {
3160
+ return invalidComputedReferenceParseResult;
3161
+ }
3162
+ value += escapedValues[escapedCharacter];
3163
+ }
3164
+
3165
+ return {
3166
+ valid: true,
3167
+ value,
3168
+ };
3169
+ };
3170
+
3171
+ const parseComputedReferencePath = (value) => {
3172
+ if (
3173
+ typeof value !== "string" ||
3174
+ value.length === 0 ||
3175
+ value.trim() !== value
3176
+ ) {
3177
+ return invalidComputedReferenceParseResult;
3178
+ }
3179
+
3180
+ const parts = [];
3181
+ let index = 0;
3182
+
3183
+ const readBarePart = () => {
3184
+ const startIndex = index;
3185
+ while (
3186
+ index < value.length &&
3187
+ value[index] !== "." &&
3188
+ value[index] !== "[" &&
3189
+ value[index] !== "]"
3190
+ ) {
3191
+ index += 1;
3192
+ }
3193
+
3194
+ const part = value.slice(startIndex, index);
3195
+ if (part.length === 0 || part.trim() !== part || /\s/.test(part)) {
3196
+ return false;
3197
+ }
3198
+ parts.push(part);
3199
+ return true;
3200
+ };
3201
+
3202
+ const readBracketPart = () => {
3203
+ index += 1;
3204
+ while (index < value.length && /\s/.test(value[index])) {
3205
+ index += 1;
3206
+ }
3207
+
3208
+ const firstCharacter = value[index];
3209
+ if (/\d/.test(firstCharacter ?? "")) {
3210
+ const startIndex = index;
3211
+ while (index < value.length && /\d/.test(value[index])) {
3212
+ index += 1;
3213
+ }
3214
+ const rawPart = value.slice(startIndex, index);
3215
+ while (index < value.length && /\s/.test(value[index])) {
3216
+ index += 1;
3217
+ }
3218
+ if (
3219
+ value[index] !== "]" ||
3220
+ (rawPart.length > 1 && rawPart.startsWith("0"))
3221
+ ) {
3222
+ return false;
3223
+ }
3224
+ index += 1;
3225
+ parts.push(rawPart);
3226
+ return true;
3227
+ }
3228
+
3229
+ if (firstCharacter !== '"' && firstCharacter !== "'") {
3230
+ return false;
3231
+ }
3232
+
3233
+ const quote = firstCharacter;
3234
+ const startIndex = index;
3235
+ index += 1;
3236
+ let escaped = false;
3237
+ while (index < value.length) {
3238
+ const character = value[index];
3239
+ if (escaped) {
3240
+ escaped = false;
3241
+ } else if (character === "\\") {
3242
+ escaped = true;
3243
+ } else if (character === quote) {
3244
+ const decodedPart = decodeComputedReferenceQuotedPart(
3245
+ value.slice(startIndex, index + 1),
3246
+ );
3247
+ if (!decodedPart.valid) {
3248
+ return false;
3249
+ }
3250
+
3251
+ index += 1;
3252
+ while (index < value.length && /\s/.test(value[index])) {
3253
+ index += 1;
3254
+ }
3255
+ if (value[index] !== "]") {
3256
+ return false;
3257
+ }
3258
+ index += 1;
3259
+ parts.push(decodedPart.value);
3260
+ return true;
3261
+ }
3262
+ index += 1;
3263
+ }
3264
+
3265
+ return false;
3266
+ };
3267
+
3268
+ if (!readBarePart()) {
3269
+ return invalidComputedReferenceParseResult;
3270
+ }
3271
+
3272
+ while (index < value.length) {
3273
+ if (value[index] === ".") {
3274
+ index += 1;
3275
+ if (!readBarePart()) {
3276
+ return invalidComputedReferenceParseResult;
3277
+ }
3278
+ continue;
3279
+ }
3280
+
3281
+ if (value[index] === "[") {
3282
+ if (!readBracketPart()) {
3283
+ return invalidComputedReferenceParseResult;
3284
+ }
3285
+ continue;
3286
+ }
3287
+
3288
+ return invalidComputedReferenceParseResult;
3289
+ }
3290
+
3291
+ return {
3292
+ valid: true,
3293
+ parts,
3294
+ };
3295
+ };
3296
+
3297
+ const validateComputedReferencePath = ({
3298
+ value,
3299
+ path,
3300
+ errorFactory,
3301
+ variables,
3302
+ dependencies,
3303
+ }) => {
3304
+ if (!isNonEmptyString(value)) {
3305
+ return invalidFromErrorFactory(
3306
+ errorFactory,
3307
+ `${path} must be a non-empty string path`,
3308
+ );
3309
+ }
3310
+
3311
+ const parsedPath = parseComputedReferencePath(value);
3312
+ if (!parsedPath.valid) {
3313
+ return invalidFromErrorFactory(
3314
+ errorFactory,
3315
+ `${path} has an invalid reference path`,
3316
+ );
3317
+ }
3318
+
3319
+ const [root, referencedId, ...nestedPath] = parsedPath.parts;
3320
+ if (root !== "variables" && root !== "runtime") {
3321
+ return invalidFromErrorFactory(
3322
+ errorFactory,
3323
+ `${path} must reference a concrete variables.* or runtime.* path`,
3324
+ );
3325
+ }
3326
+
3327
+ if (!isNonEmptyString(referencedId)) {
3328
+ return invalidFromErrorFactory(
3329
+ errorFactory,
3330
+ `${path} must reference a concrete ${root} member`,
3331
+ );
3332
+ }
3333
+
3334
+ if (root === "runtime" || variables === undefined) {
3335
+ return validComputedResult(undefined);
3336
+ }
3337
+
3338
+ const referencedVariable = Object.hasOwn(variables, referencedId)
3339
+ ? variables[referencedId]
3340
+ : undefined;
3341
+ if (
3342
+ !isPlainObject(referencedVariable) ||
3343
+ referencedVariable.type !== "variable"
3344
+ ) {
3345
+ return invalidFromErrorFactory(
3346
+ errorFactory,
3347
+ `${path} references unknown variable '${referencedId}'`,
3348
+ );
3349
+ }
3350
+
3351
+ if (Object.hasOwn(referencedVariable, "computed")) {
3352
+ dependencies?.add(referencedId);
3353
+ }
3354
+
3355
+ return validComputedResult(
3356
+ nestedPath.length === 0 ? referencedVariable.variableType : undefined,
3357
+ );
3358
+ };
3359
+
3360
+ const validateComputedExpression = ({
3361
+ expression,
3362
+ path,
3363
+ errorFactory,
3364
+ variables,
3365
+ dependencies,
3366
+ ancestors = new Set(),
3367
+ }) => {
3368
+ if (expression === null) {
3369
+ return validComputedResult("null");
3370
+ }
3371
+
3372
+ if (typeof expression !== "object") {
3373
+ if (typeof expression === "number") {
3374
+ if (Number.isFinite(expression)) {
3375
+ return validComputedResult("number");
3376
+ }
3377
+ return invalidFromErrorFactory(
3378
+ errorFactory,
3379
+ `${path} must use finite numeric literals`,
3380
+ );
3381
+ }
3382
+
3383
+ if (
3384
+ typeof expression === "string" ||
3385
+ typeof expression === "boolean"
3386
+ ) {
3387
+ return validComputedResult(typeof expression);
3388
+ }
3389
+
3390
+ return invalidFromErrorFactory(
3391
+ errorFactory,
3392
+ `${path} must use JSON-compatible primitive literals`,
3393
+ );
3394
+ }
3395
+
3396
+ if (Array.isArray(expression)) {
3397
+ return invalidFromErrorFactory(
3398
+ errorFactory,
3399
+ `${path} arrays must be wrapped in a literal operator or authored as value`,
3400
+ );
3401
+ }
3402
+
3403
+ if (ancestors.has(expression)) {
3404
+ return invalidFromErrorFactory(
3405
+ errorFactory,
3406
+ `${path} must not contain cyclic expression data`,
3407
+ );
3408
+ }
3409
+ const nextAncestors = new Set(ancestors);
3410
+ nextAncestors.add(expression);
3411
+
3412
+ const entries = Object.entries(expression);
3413
+ if (entries.length !== 1) {
3414
+ return invalidFromErrorFactory(
3415
+ errorFactory,
3416
+ `${path} must contain exactly one expression operator`,
3417
+ );
3418
+ }
3419
+
3420
+ const [[operator, operands]] = entries;
3421
+ if (operator === "var") {
3422
+ const result = validateComputedReferencePath({
3423
+ value: operands,
3424
+ path: `${path}.var`,
3425
+ errorFactory,
3426
+ variables,
3427
+ dependencies,
3428
+ });
3429
+ return result;
3430
+ }
3431
+
3432
+ if (operator === "literal") {
3433
+ const result = validateComputedDataValue({
3434
+ value: operands,
3435
+ path: `${path}.literal`,
3436
+ errorFactory,
3437
+ });
3438
+ return result?.valid === false
3439
+ ? result
3440
+ : validComputedResult(getComputedValueType(operands));
3441
+ }
3442
+
3443
+ const fixedOperandCount = COMPUTED_EXPRESSION_FIXED_OPERAND_COUNTS[operator];
3444
+ const isVariadic = COMPUTED_EXPRESSION_VARIADIC_OPERATORS.has(operator);
3445
+ if (fixedOperandCount === undefined && !isVariadic) {
3446
+ return invalidFromErrorFactory(
3447
+ errorFactory,
3448
+ `${path} contains unsupported expression operator '${operator}'`,
3449
+ );
3450
+ }
3451
+
3452
+ if (!Array.isArray(operands)) {
3453
+ return invalidFromErrorFactory(
3454
+ errorFactory,
3455
+ `${path}.${operator} must be an operand array`,
3456
+ );
3457
+ }
3458
+
3459
+ if (
3460
+ (fixedOperandCount !== undefined &&
3461
+ operands.length !== fixedOperandCount) ||
3462
+ (isVariadic && operands.length === 0)
3463
+ ) {
3464
+ const operandRequirement = isVariadic
3465
+ ? "at least one operand"
3466
+ : `exactly ${fixedOperandCount} operands`;
3467
+ return invalidFromErrorFactory(
3468
+ errorFactory,
3469
+ `${path}.${operator} requires ${operandRequirement}`,
3470
+ );
3471
+ }
3472
+
3473
+ const operandTypes = [];
3474
+ for (const [index, operand] of operands.entries()) {
3475
+ const result = validateComputedExpression({
3476
+ expression: operand,
3477
+ path: `${path}.${operator}[${index}]`,
3478
+ errorFactory,
3479
+ variables,
3480
+ dependencies,
3481
+ ancestors: nextAncestors,
3482
+ });
3483
+ if (result?.valid === false) {
3484
+ return result;
3485
+ }
3486
+ operandTypes.push(result.valueType);
3487
+ }
3488
+
3489
+ if (
3490
+ COMPUTED_EXPRESSION_NUMERIC_OPERAND_OPERATORS.has(operator) &&
3491
+ operandTypes.some(
3492
+ (operandType) => operandType !== undefined && operandType !== "number",
3493
+ )
3494
+ ) {
3495
+ return invalidFromErrorFactory(
3496
+ errorFactory,
3497
+ `${path}.${operator} requires numeric operands`,
3498
+ );
3499
+ }
3500
+
3501
+ if (COMPUTED_EXPRESSION_NUMERIC_RESULT_OPERATORS.has(operator)) {
3502
+ return validComputedResult("number");
3503
+ }
3504
+ if (COMPUTED_EXPRESSION_BOOLEAN_RESULT_OPERATORS.has(operator)) {
3505
+ return validComputedResult("boolean");
3506
+ }
3507
+ return validComputedResult(undefined);
3508
+ };
3509
+
3510
+ const validateComputedCondition = ({
3511
+ condition,
3512
+ path,
3513
+ errorFactory,
3514
+ variables,
3515
+ dependencies,
3516
+ isRoot = true,
3517
+ ancestors = new Set(),
3518
+ }) => {
3519
+ if (isRoot && typeof condition === "string") {
3520
+ return invalidFromErrorFactory(
3521
+ errorFactory,
3522
+ `${path} string conditions are not supported`,
3523
+ );
3524
+ }
3525
+
3526
+ if (condition === null) {
3527
+ return validComputedResult("null");
3528
+ }
3529
+
3530
+ if (typeof condition !== "object") {
3531
+ if (typeof condition === "number") {
3532
+ if (Number.isFinite(condition)) {
3533
+ return validComputedResult("number");
3534
+ }
3535
+ return invalidFromErrorFactory(
3536
+ errorFactory,
3537
+ `${path} must use finite numeric literals`,
3538
+ );
3539
+ }
3540
+
3541
+ if (
3542
+ typeof condition === "string" ||
3543
+ typeof condition === "boolean"
3544
+ ) {
3545
+ return validComputedResult(typeof condition);
3546
+ }
3547
+
3548
+ return invalidFromErrorFactory(
3549
+ errorFactory,
3550
+ `${path} must use JSON-compatible primitive literals`,
3551
+ );
3552
+ }
3553
+
3554
+ if (Array.isArray(condition)) {
3555
+ return invalidFromErrorFactory(
3556
+ errorFactory,
3557
+ `${path} arrays must be wrapped in a condition operator or literal`,
3558
+ );
3559
+ }
3560
+
3561
+ if (ancestors.has(condition)) {
3562
+ return invalidFromErrorFactory(
3563
+ errorFactory,
3564
+ `${path} must not contain cyclic condition data`,
3565
+ );
3566
+ }
3567
+ const nextAncestors = new Set(ancestors);
3568
+ nextAncestors.add(condition);
3569
+
3570
+ const entries = Object.entries(condition);
3571
+ if (entries.length !== 1) {
3572
+ return invalidFromErrorFactory(
3573
+ errorFactory,
3574
+ `${path} must contain exactly one condition operator`,
3575
+ );
3576
+ }
3577
+
3578
+ const [[operator, operands]] = entries;
3579
+ if (operator === "var") {
3580
+ const result = validateComputedReferencePath({
3581
+ value: operands,
3582
+ path: `${path}.var`,
3583
+ errorFactory,
3584
+ variables,
3585
+ dependencies,
3586
+ });
3587
+ return result;
3588
+ }
3589
+ if (operator === "literal") {
3590
+ const result = validateComputedDataValue({
3591
+ value: operands,
3592
+ path: `${path}.literal`,
3593
+ errorFactory,
3594
+ });
3595
+ return result?.valid === false
3596
+ ? result
3597
+ : validComputedResult(getComputedValueType(operands));
3598
+ }
3599
+ if (operator === "call") {
3600
+ return invalidFromErrorFactory(
3601
+ errorFactory,
3602
+ `${path} function calls are not supported`,
3603
+ );
3604
+ }
3605
+ if (operator === "not") {
3606
+ const result = validateComputedCondition({
3607
+ condition: operands,
3608
+ path: `${path}.not`,
3609
+ errorFactory,
3610
+ variables,
3611
+ dependencies,
3612
+ isRoot: false,
3613
+ ancestors: nextAncestors,
3614
+ });
3615
+ return result?.valid === false ? result : validComputedResult("boolean");
3616
+ }
3617
+
3618
+ const fixedOperandCount = COMPUTED_CONDITION_FIXED_OPERAND_COUNTS[operator];
3619
+ const isVariadic = COMPUTED_CONDITION_VARIADIC_OPERATORS.has(operator);
3620
+ if (fixedOperandCount === undefined && !isVariadic) {
3621
+ return invalidFromErrorFactory(
3622
+ errorFactory,
3623
+ `${path} contains unsupported condition operator '${operator}'`,
3624
+ );
3625
+ }
3626
+ if (!Array.isArray(operands)) {
3627
+ return invalidFromErrorFactory(
3628
+ errorFactory,
3629
+ `${path}.${operator} must be an operand array`,
3630
+ );
3631
+ }
3632
+ if (
3633
+ (fixedOperandCount !== undefined &&
3634
+ operands.length !== fixedOperandCount) ||
3635
+ (isVariadic && operands.length === 0)
3636
+ ) {
3637
+ const operandRequirement = isVariadic
3638
+ ? "at least one operand"
3639
+ : `exactly ${fixedOperandCount} operands`;
3640
+ return invalidFromErrorFactory(
3641
+ errorFactory,
3642
+ `${path}.${operator} requires ${operandRequirement}`,
3643
+ );
3644
+ }
3645
+
3646
+ const operandTypes = [];
3647
+ for (const [index, operand] of operands.entries()) {
3648
+ const result = validateComputedCondition({
3649
+ condition: operand,
3650
+ path: `${path}.${operator}[${index}]`,
3651
+ errorFactory,
3652
+ variables,
3653
+ dependencies,
3654
+ isRoot: false,
3655
+ ancestors: nextAncestors,
3656
+ });
3657
+ if (result?.valid === false) {
3658
+ return result;
3659
+ }
3660
+ operandTypes.push(result.valueType);
3661
+ }
3662
+
3663
+ if (
3664
+ (operator === "add" || operator === "sub") &&
3665
+ operandTypes.some(
3666
+ (operandType) => operandType !== undefined && operandType !== "number",
3667
+ )
3668
+ ) {
3669
+ return invalidFromErrorFactory(
3670
+ errorFactory,
3671
+ `${path}.${operator} requires numeric operands`,
3672
+ );
3673
+ }
3674
+
3675
+ return validComputedResult(
3676
+ operator === "add" || operator === "sub" ? "number" : "boolean",
3677
+ );
3678
+ };
3679
+
3680
+ const validateComputedResultConfig = ({
3681
+ resultConfig,
3682
+ variableType,
3683
+ path,
3684
+ errorFactory,
3685
+ variables,
3686
+ dependencies,
3687
+ allowedKeys = ["expr", "value"],
3688
+ }) => {
3689
+ if (!isPlainObject(resultConfig)) {
3690
+ return invalidFromErrorFactory(errorFactory, `${path} must be an object`);
3691
+ }
3692
+
3693
+ {
3694
+ const result = validateAllowedKeys({
3695
+ value: resultConfig,
3696
+ allowedKeys,
3697
+ path,
3698
+ errorFactory,
3699
+ });
3700
+ if (result?.valid === false) {
3701
+ return result;
3702
+ }
3703
+ }
3704
+
3705
+ const hasExpression = Object.hasOwn(resultConfig, "expr");
3706
+ const hasValue = Object.hasOwn(resultConfig, "value");
3707
+ if (hasExpression === hasValue) {
3708
+ return invalidFromErrorFactory(
3709
+ errorFactory,
3710
+ `${path} must contain exactly one of expr or value`,
3711
+ );
3712
+ }
3713
+
3714
+ if (hasValue) {
3715
+ const dataResult = validateComputedDataValue({
3716
+ value: resultConfig.value,
3717
+ path: `${path}.value`,
3718
+ errorFactory,
3719
+ });
3720
+ if (dataResult?.valid === false) {
3721
+ return dataResult;
3722
+ }
3723
+ return validateVariableTypedValue({
3724
+ value: resultConfig.value,
3725
+ variableType,
3726
+ path: `${path}.value`,
3727
+ errorFactory,
3728
+ });
3729
+ }
3730
+
3731
+ const expressionResult = validateComputedExpression({
3732
+ expression: resultConfig.expr,
3733
+ path: `${path}.expr`,
3734
+ errorFactory,
3735
+ variables,
3736
+ dependencies,
3737
+ });
3738
+ if (expressionResult?.valid === false) {
3739
+ return expressionResult;
3740
+ }
3741
+ if (
3742
+ expressionResult.valueType !== undefined &&
3743
+ expressionResult.valueType !== variableType
3744
+ ) {
3745
+ return invalidFromErrorFactory(
3746
+ errorFactory,
3747
+ `${path}.expr must resolve to ${variableType}`,
3748
+ );
3749
+ }
3750
+ return VALID_RESULT;
3751
+ };
3752
+
3753
+ const validateVariableComputedConfig = ({
3754
+ computed,
3755
+ variableType,
3756
+ path,
3757
+ errorFactory,
3758
+ variables,
3759
+ dependencies,
3760
+ }) => {
3761
+ if (!isPlainObject(computed)) {
3762
+ return invalidFromErrorFactory(errorFactory, `${path} must be an object`);
3763
+ }
3764
+
3765
+ if (Object.hasOwn(computed, "branches")) {
3766
+ {
3767
+ const result = validateAllowedKeys({
3768
+ value: computed,
3769
+ allowedKeys: ["branches", "default"],
3770
+ path,
3771
+ errorFactory,
3772
+ });
3773
+ if (result?.valid === false) {
3774
+ return result;
3775
+ }
3776
+ }
3777
+ if (!Array.isArray(computed.branches) || computed.branches.length === 0) {
3778
+ return invalidFromErrorFactory(
3779
+ errorFactory,
3780
+ `${path}.branches must be a non-empty array`,
3781
+ );
3782
+ }
3783
+ if (!isPlainObject(computed.default)) {
3784
+ return invalidFromErrorFactory(
3785
+ errorFactory,
3786
+ `${path}.default must be an object`,
3787
+ );
3788
+ }
3789
+
3790
+ for (const [index, branch] of computed.branches.entries()) {
3791
+ const branchPath = `${path}.branches[${index}]`;
3792
+ if (!isPlainObject(branch)) {
3793
+ return invalidFromErrorFactory(
3794
+ errorFactory,
3795
+ `${branchPath} must be an object`,
3796
+ );
3797
+ }
3798
+ if (!Object.hasOwn(branch, "when")) {
3799
+ return invalidFromErrorFactory(
3800
+ errorFactory,
3801
+ `${branchPath}.when is required`,
3802
+ );
3803
+ }
3804
+ const conditionResult = validateComputedCondition({
3805
+ condition: branch.when,
3806
+ path: `${branchPath}.when`,
3807
+ errorFactory,
3808
+ variables,
3809
+ dependencies,
3810
+ });
3811
+ if (conditionResult?.valid === false) {
3812
+ return conditionResult;
3813
+ }
3814
+ const branchResult = validateComputedResultConfig({
3815
+ resultConfig: branch,
3816
+ variableType,
3817
+ path: branchPath,
3818
+ errorFactory,
3819
+ variables,
3820
+ dependencies,
3821
+ allowedKeys: ["when", "expr", "value"],
3822
+ });
3823
+ if (branchResult?.valid === false) {
3824
+ return branchResult;
3825
+ }
3826
+ }
3827
+
3828
+ return validateComputedResultConfig({
3829
+ resultConfig: computed.default,
3830
+ variableType,
3831
+ path: `${path}.default`,
3832
+ errorFactory,
3833
+ variables,
3834
+ dependencies,
3835
+ });
3836
+ }
3837
+
3838
+ return validateComputedResultConfig({
3839
+ resultConfig: computed,
3840
+ variableType,
3841
+ path,
3842
+ errorFactory,
3843
+ variables,
3844
+ dependencies,
3845
+ });
3846
+ };
3847
+
3848
+ const validateComputedVariableGraph = ({
3849
+ items,
3850
+ path,
3851
+ errorFactory,
3852
+ }) => {
3853
+ const dependencyGraph = new Map();
3854
+
3855
+ for (const [variableId, variable] of Object.entries(items)) {
3856
+ if (
3857
+ variable?.type !== "variable" ||
3858
+ !Object.hasOwn(variable, "computed")
3859
+ ) {
3860
+ continue;
3861
+ }
3862
+
3863
+ const dependencies = new Set();
3864
+ const result = validateVariableComputedConfig({
3865
+ computed: variable.computed,
3866
+ variableType: variable.variableType,
3867
+ path: `${path}.${variableId}.computed`,
3868
+ errorFactory,
3869
+ variables: items,
3870
+ dependencies,
3871
+ });
3872
+ if (result?.valid === false) {
3873
+ return result;
3874
+ }
3875
+ dependencyGraph.set(variableId, dependencies);
3876
+ }
3877
+
3878
+ const visited = new Set();
3879
+ for (const startVariableId of dependencyGraph.keys()) {
3880
+ if (visited.has(startVariableId)) {
3881
+ continue;
2828
3882
  }
2829
3883
 
2830
- {
2831
- const result = validateParticleModules({
2832
- modules: item.modules,
2833
- path: `${itemPath}.modules`,
2834
- errorFactory,
2835
- });
2836
- if (result?.valid === false) {
2837
- return result;
3884
+ const frames = [
3885
+ {
3886
+ variableId: startVariableId,
3887
+ dependencies: [...(dependencyGraph.get(startVariableId) ?? [])],
3888
+ nextDependencyIndex: 0,
3889
+ },
3890
+ ];
3891
+ const activeIndexes = new Map([[startVariableId, 0]]);
3892
+
3893
+ while (frames.length > 0) {
3894
+ const frame = frames.at(-1);
3895
+ if (frame.nextDependencyIndex >= frame.dependencies.length) {
3896
+ frames.pop();
3897
+ activeIndexes.delete(frame.variableId);
3898
+ visited.add(frame.variableId);
3899
+ continue;
3900
+ }
3901
+
3902
+ const dependencyId = frame.dependencies[frame.nextDependencyIndex];
3903
+ frame.nextDependencyIndex += 1;
3904
+ if (visited.has(dependencyId)) {
3905
+ continue;
3906
+ }
3907
+
3908
+ const cycleStartIndex = activeIndexes.get(dependencyId);
3909
+ if (cycleStartIndex !== undefined) {
3910
+ const cycle = [
3911
+ ...frames
3912
+ .slice(cycleStartIndex)
3913
+ .map(({ variableId }) => variableId),
3914
+ dependencyId,
3915
+ ].join(" -> ");
3916
+ return invalidFromErrorFactory(
3917
+ errorFactory,
3918
+ `${path} contains computed variable cycle: ${cycle}`,
3919
+ );
3920
+ }
3921
+
3922
+ if (!dependencyGraph.has(dependencyId)) {
3923
+ visited.add(dependencyId);
3924
+ continue;
2838
3925
  }
3926
+
3927
+ activeIndexes.set(dependencyId, frames.length);
3928
+ frames.push({
3929
+ variableId: dependencyId,
3930
+ dependencies: [...(dependencyGraph.get(dependencyId) ?? [])],
3931
+ nextDependencyIndex: 0,
3932
+ });
2839
3933
  }
2840
3934
  }
3935
+
3936
+ return VALID_RESULT;
2841
3937
  };
2842
3938
 
2843
- const validateVariableTypedValue = ({
2844
- value,
3939
+ const validateVariableStoredOrComputedData = ({
3940
+ data,
2845
3941
  variableType,
2846
3942
  path,
2847
3943
  errorFactory,
2848
3944
  }) => {
2849
- if (variableType === "string" && !isString(value)) {
2850
- return invalidFromErrorFactory(errorFactory, `${path} must be a string`);
2851
- }
2852
-
2853
- if (variableType === "number" && !isFiniteNumber(value)) {
2854
- return invalidFromErrorFactory(
3945
+ const isComputed = Object.hasOwn(data, "computed");
3946
+ if (isComputed) {
3947
+ if (Object.hasOwn(data, "default") || Object.hasOwn(data, "value")) {
3948
+ return invalidFromErrorFactory(
3949
+ errorFactory,
3950
+ `${path} computed variables must not contain default or value`,
3951
+ );
3952
+ }
3953
+ if (data.isEnum !== undefined || data.enumValues !== undefined) {
3954
+ return invalidFromErrorFactory(
3955
+ errorFactory,
3956
+ `${path} computed variables must not contain enum metadata`,
3957
+ );
3958
+ }
3959
+ return validateVariableComputedConfig({
3960
+ computed: data.computed,
3961
+ variableType,
3962
+ path: `${path}.computed`,
2855
3963
  errorFactory,
2856
- `${path} must be a finite number`,
2857
- );
3964
+ });
2858
3965
  }
2859
3966
 
2860
- if (variableType === "boolean" && typeof value !== "boolean") {
2861
- return invalidFromErrorFactory(errorFactory, `${path} must be a boolean`);
3967
+ {
3968
+ const result = validateVariableTypedValue({
3969
+ value: data.default,
3970
+ variableType,
3971
+ path: `${path}.default`,
3972
+ errorFactory,
3973
+ });
3974
+ if (result?.valid === false) {
3975
+ return result;
3976
+ }
2862
3977
  }
3978
+ return validateVariableTypedValue({
3979
+ value: data.value,
3980
+ variableType,
3981
+ path: `${path}.value`,
3982
+ errorFactory,
3983
+ });
2863
3984
  };
2864
3985
 
2865
3986
  const normalizeVariableEnumValues = (values = []) => {
@@ -2947,6 +4068,13 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
2947
4068
  const itemType = item?.type;
2948
4069
  const variableType = item?.variableType;
2949
4070
 
4071
+ if (itemId === "__proto__") {
4072
+ return invalidFromErrorFactory(
4073
+ errorFactory,
4074
+ `${itemPath} uses reserved variable id '__proto__'`,
4075
+ );
4076
+ }
4077
+
2950
4078
  if (itemType !== "folder" && itemType !== "variable") {
2951
4079
  return invalidFromErrorFactory(
2952
4080
  errorFactory,
@@ -2972,6 +4100,7 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
2972
4100
  "value",
2973
4101
  "isEnum",
2974
4102
  "enumValues",
4103
+ "computed",
2975
4104
  ],
2976
4105
  path: itemPath,
2977
4106
  errorFactory,
@@ -3013,7 +4142,7 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
3013
4142
  if (!VARIABLE_TYPE_KEYS.includes(variableType)) {
3014
4143
  return invalidFromErrorFactory(
3015
4144
  errorFactory,
3016
- `${itemPath}.variableType must be 'string', 'number', or 'boolean'`,
4145
+ `${itemPath}.variableType must be 'string', 'number', 'boolean', or 'object'`,
3017
4146
  );
3018
4147
  }
3019
4148
 
@@ -3041,7 +4170,14 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
3041
4170
  }
3042
4171
  }
3043
4172
 
3044
- if (!VARIABLE_SCOPE_KEYS.includes(item.scope)) {
4173
+ if (item.computed !== undefined) {
4174
+ if (item.scope !== undefined) {
4175
+ return invalidFromErrorFactory(
4176
+ errorFactory,
4177
+ `${itemPath}.scope must be omitted for computed variables`,
4178
+ );
4179
+ }
4180
+ } else if (!VARIABLE_SCOPE_KEYS.includes(item.scope)) {
3045
4181
  return invalidFromErrorFactory(
3046
4182
  errorFactory,
3047
4183
  `${itemPath}.scope must be 'context', 'device', or 'account'`,
@@ -3049,21 +4185,10 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
3049
4185
  }
3050
4186
 
3051
4187
  {
3052
- const result = validateVariableTypedValue({
3053
- value: item.default,
3054
- variableType,
3055
- path: `${itemPath}.default`,
3056
- errorFactory,
3057
- });
3058
- if (result?.valid === false) {
3059
- return result;
3060
- }
3061
- }
3062
- {
3063
- const result = validateVariableTypedValue({
3064
- value: item.value,
4188
+ const result = validateVariableStoredOrComputedData({
4189
+ data: item,
3065
4190
  variableType,
3066
- path: `${itemPath}.value`,
4191
+ path: itemPath,
3067
4192
  errorFactory,
3068
4193
  });
3069
4194
  if (result?.valid === false) {
@@ -3072,6 +4197,12 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
3072
4197
  }
3073
4198
  }
3074
4199
  }
4200
+
4201
+ return validateComputedVariableGraph({
4202
+ items,
4203
+ path,
4204
+ errorFactory,
4205
+ });
3075
4206
  };
3076
4207
 
3077
4208
  const validateTextStyleShadow = ({ shadow, path, errorFactory }) => {
@@ -3216,11 +4347,15 @@ const validateTextStyleItems = ({ items, path, errorFactory }) => {
3216
4347
  }
3217
4348
  }
3218
4349
 
3219
- if (!isNonEmptyString(item.fontId)) {
3220
- return invalidFromErrorFactory(
4350
+ {
4351
+ const result = validateRequiredStringOrUniqueIdArray({
4352
+ value: item.fontId,
4353
+ path: `${itemPath}.fontId`,
3221
4354
  errorFactory,
3222
- `${itemPath}.fontId must be a non-empty string`,
3223
- );
4355
+ });
4356
+ if (result?.valid === false) {
4357
+ return result;
4358
+ }
3224
4359
  }
3225
4360
 
3226
4361
  if (!isNonEmptyString(item.colorId)) {
@@ -7192,15 +8327,17 @@ export const assertInvariants = ({ state }) => {
7192
8327
  continue;
7193
8328
  }
7194
8329
 
7195
- const font = state.fonts.items[textStyle.fontId];
7196
- if (!isPlainObject(font) || font.type === "folder") {
7197
- return invalidInvariant(
7198
- "textStyle.fontId must reference an existing non-folder font",
7199
- {
7200
- textStyleId,
7201
- fontId: textStyle.fontId,
7202
- },
7203
- );
8330
+ for (const fontId of toIdArray(textStyle.fontId)) {
8331
+ const font = state.fonts.items[fontId];
8332
+ if (!isPlainObject(font) || font.type === "folder") {
8333
+ return invalidInvariant(
8334
+ "textStyle.fontId must reference an existing non-folder font",
8335
+ {
8336
+ textStyleId,
8337
+ fontId,
8338
+ },
8339
+ );
8340
+ }
7204
8341
  }
7205
8342
 
7206
8343
  const color = state.colors.items[textStyle.colorId];
@@ -8689,6 +9826,27 @@ const validateRequiredUniqueIdArray = ({ value, path, errorFactory }) => {
8689
9826
  }
8690
9827
  };
8691
9828
 
9829
+ const validateRequiredStringOrUniqueIdArray = ({
9830
+ value,
9831
+ path,
9832
+ errorFactory,
9833
+ }) => {
9834
+ if (isNonEmptyString(value)) {
9835
+ return VALID_RESULT;
9836
+ }
9837
+
9838
+ if (!Array.isArray(value)) {
9839
+ return invalidFromErrorFactory(
9840
+ errorFactory,
9841
+ `${path} must be a non-empty string or a non-empty array of strings`,
9842
+ );
9843
+ }
9844
+
9845
+ return validateRequiredUniqueIdArray({ value, path, errorFactory });
9846
+ };
9847
+
9848
+ const toIdArray = (value) => (Array.isArray(value) ? value : [value]);
9849
+
8692
9850
  const validateOptionalUniqueIdArray = ({
8693
9851
  value,
8694
9852
  path,
@@ -9978,7 +11136,15 @@ const validateFontCreateData = ({ data, errorFactory }) => {
9978
11136
  allowedKeys:
9979
11137
  data.type === "folder"
9980
11138
  ? ["type", "name", "description"]
9981
- : ["type", "name", "description", "tagIds", "fileId", "fontFamily"],
11139
+ : [
11140
+ "type",
11141
+ "name",
11142
+ "description",
11143
+ "tagIds",
11144
+ "fileId",
11145
+ "fontFamily",
11146
+ ...FONT_WEIGHT_KEYS,
11147
+ ],
9982
11148
  path: "payload.data",
9983
11149
  errorFactory,
9984
11150
  });
@@ -10026,6 +11192,17 @@ const validateFontCreateData = ({ data, errorFactory }) => {
10026
11192
  "payload.data.fontFamily must be a non-empty string",
10027
11193
  );
10028
11194
  }
11195
+
11196
+ {
11197
+ const result = validateFontWeightFields({
11198
+ value: data,
11199
+ path: "payload.data",
11200
+ errorFactory,
11201
+ });
11202
+ if (result?.valid === false) {
11203
+ return result;
11204
+ }
11205
+ }
10029
11206
  }
10030
11207
  };
10031
11208
 
@@ -10033,7 +11210,14 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
10033
11210
  {
10034
11211
  const result = validateAllowedKeys({
10035
11212
  value: data,
10036
- allowedKeys: ["name", "description", "tagIds", "fileId", "fontFamily"],
11213
+ allowedKeys: [
11214
+ "name",
11215
+ "description",
11216
+ "tagIds",
11217
+ "fileId",
11218
+ "fontFamily",
11219
+ ...FONT_WEIGHT_KEYS,
11220
+ ],
10037
11221
  path: "payload.data",
10038
11222
  errorFactory,
10039
11223
  });
@@ -10087,6 +11271,17 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
10087
11271
  "payload.data.fontFamily must be a non-empty string when provided",
10088
11272
  );
10089
11273
  }
11274
+
11275
+ {
11276
+ const result = validateFontWeightFields({
11277
+ value: data,
11278
+ path: "payload.data",
11279
+ errorFactory,
11280
+ });
11281
+ if (result?.valid === false) {
11282
+ return result;
11283
+ }
11284
+ }
10090
11285
  };
10091
11286
 
10092
11287
  const validateFileCreateData = ({ data, errorFactory }) => {
@@ -10958,6 +12153,7 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
10958
12153
  "value",
10959
12154
  "isEnum",
10960
12155
  "enumValues",
12156
+ "computed",
10961
12157
  ],
10962
12158
  path: "payload.data",
10963
12159
  errorFactory,
@@ -10985,7 +12181,7 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
10985
12181
  if (!VARIABLE_TYPE_KEYS.includes(data.variableType)) {
10986
12182
  return invalidFromErrorFactory(
10987
12183
  errorFactory,
10988
- "payload.data.variableType must be 'string', 'number', or 'boolean'",
12184
+ "payload.data.variableType must be 'string', 'number', 'boolean', or 'object'",
10989
12185
  );
10990
12186
  }
10991
12187
 
@@ -11012,7 +12208,14 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
11012
12208
  }
11013
12209
  }
11014
12210
 
11015
- if (!VARIABLE_SCOPE_KEYS.includes(data.scope)) {
12211
+ if (data.computed !== undefined) {
12212
+ if (data.scope !== undefined) {
12213
+ return invalidFromErrorFactory(
12214
+ errorFactory,
12215
+ "payload.data.scope must be omitted for computed variables",
12216
+ );
12217
+ }
12218
+ } else if (!VARIABLE_SCOPE_KEYS.includes(data.scope)) {
11016
12219
  return invalidFromErrorFactory(
11017
12220
  errorFactory,
11018
12221
  "payload.data.scope must be 'context', 'device', or 'account'",
@@ -11020,21 +12223,10 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
11020
12223
  }
11021
12224
 
11022
12225
  {
11023
- const result = validateVariableTypedValue({
11024
- value: data.default,
11025
- variableType: data.variableType,
11026
- path: "payload.data.default",
11027
- errorFactory,
11028
- });
11029
- if (result?.valid === false) {
11030
- return result;
11031
- }
11032
- }
11033
- {
11034
- const result = validateVariableTypedValue({
11035
- value: data.value,
12226
+ const result = validateVariableStoredOrComputedData({
12227
+ data,
11036
12228
  variableType: data.variableType,
11037
- path: "payload.data.value",
12229
+ path: "payload.data",
11038
12230
  errorFactory,
11039
12231
  });
11040
12232
  if (result?.valid === false) {
@@ -11057,6 +12249,7 @@ const validateVariableUpdateData = ({ data, errorFactory }) => {
11057
12249
  "value",
11058
12250
  "isEnum",
11059
12251
  "enumValues",
12252
+ "computed",
11060
12253
  ],
11061
12254
  path: "payload.data",
11062
12255
  errorFactory,
@@ -11105,6 +12298,15 @@ const validateVariableUpdateData = ({ data, errorFactory }) => {
11105
12298
  }
11106
12299
  }
11107
12300
 
12301
+ if (data.computed !== undefined) {
12302
+ if (!isPlainObject(data.computed)) {
12303
+ return invalidFromErrorFactory(
12304
+ errorFactory,
12305
+ "payload.data.computed must be an object when provided",
12306
+ );
12307
+ }
12308
+ }
12309
+
11108
12310
  if (data.scope !== undefined && !VARIABLE_SCOPE_KEYS.includes(data.scope)) {
11109
12311
  return invalidFromErrorFactory(
11110
12312
  errorFactory,
@@ -11283,7 +12485,18 @@ const validateTextStyleUpdateData = ({ data, errorFactory }) => {
11283
12485
  }
11284
12486
  }
11285
12487
 
11286
- for (const key of ["fontId", "colorId", "strokeColorId"]) {
12488
+ if (data.fontId !== undefined) {
12489
+ const result = validateRequiredStringOrUniqueIdArray({
12490
+ value: data.fontId,
12491
+ path: "payload.data.fontId",
12492
+ errorFactory,
12493
+ });
12494
+ if (result?.valid === false) {
12495
+ return result;
12496
+ }
12497
+ }
12498
+
12499
+ for (const key of ["colorId", "strokeColorId"]) {
11287
12500
  if (data[key] !== undefined && !isNonEmptyString(data[key])) {
11288
12501
  return invalidFromErrorFactory(
11289
12502
  errorFactory,
@@ -12915,6 +14128,7 @@ const createFolderedCollectionCommandDefinitions = ({
12915
14128
  validateDeleteState = () => {},
12916
14129
  afterDelete = () => {},
12917
14130
  includeUpdate = true,
14131
+ reservedItemIds = [],
12918
14132
  }) => {
12919
14133
  const existingMessage = `payload.${idField} must reference an existing ${itemLabel}`;
12920
14134
  const duplicateMessage = `payload.${idField} must not already exist`;
@@ -12955,6 +14169,12 @@ const createFolderedCollectionCommandDefinitions = ({
12955
14169
  );
12956
14170
  }
12957
14171
 
14172
+ if (reservedItemIds.includes(payload[idField])) {
14173
+ return invalidPayload(
14174
+ `payload.${idField} must not use reserved id '${payload[idField]}'`,
14175
+ );
14176
+ }
14177
+
12958
14178
  if (
12959
14179
  payload.parentId !== undefined &&
12960
14180
  payload.parentId !== null &&
@@ -17421,6 +18641,11 @@ const COMMAND_DEFINITIONS = [
17421
18641
  });
17422
18642
  nextFont.fileId = payload.data.fileId;
17423
18643
  nextFont.fontFamily = payload.data.fontFamily;
18644
+ for (const key of FONT_WEIGHT_KEYS) {
18645
+ if (payload.data[key] !== undefined) {
18646
+ nextFont[key] = payload.data[key];
18647
+ }
18648
+ }
17424
18649
  }
17425
18650
 
17426
18651
  state.fonts.items[payload.fontId] = nextFont;
@@ -17481,7 +18706,8 @@ const COMMAND_DEFINITIONS = [
17481
18706
  currentFont.type === "folder" &&
17482
18707
  (payload.data.tagIds !== undefined ||
17483
18708
  payload.data.fileId !== undefined ||
17484
- payload.data.fontFamily !== undefined)
18709
+ payload.data.fontFamily !== undefined ||
18710
+ FONT_WEIGHT_KEYS.some((key) => payload.data[key] !== undefined))
17485
18711
  ) {
17486
18712
  return invalidPrecondition(
17487
18713
  "folder font items cannot update font fields",
@@ -18385,6 +19611,7 @@ const COMMAND_DEFINITIONS = [
18385
19611
  itemLabel: "variable item",
18386
19612
  createDataValidator: validateVariableCreateData,
18387
19613
  updateDataValidator: validateVariableUpdateData,
19614
+ reservedItemIds: ["__proto__"],
18388
19615
  createItem: ({ payload }) => {
18389
19616
  const data = structuredClone(payload.data);
18390
19617
  if (!Array.isArray(data.tagIds) || data.tagIds.length === 0) {
@@ -18409,7 +19636,7 @@ const COMMAND_DEFINITIONS = [
18409
19636
  return;
18410
19637
  }
18411
19638
 
18412
- return validateTagIdsAgainstScope({
19639
+ const tagResult = validateTagIdsAgainstScope({
18413
19640
  state,
18414
19641
  tagIds: payload.data.tagIds,
18415
19642
  scopeKey: "variables",
@@ -18418,6 +19645,25 @@ const COMMAND_DEFINITIONS = [
18418
19645
  variableId: payload.variableId,
18419
19646
  },
18420
19647
  });
19648
+ if (!tagResult.valid) {
19649
+ return tagResult;
19650
+ }
19651
+
19652
+ if (!Object.hasOwn(payload.data, "computed")) {
19653
+ return VALID_RESULT;
19654
+ }
19655
+
19656
+ return validateComputedVariableGraph({
19657
+ items: {
19658
+ ...state.variables.items,
19659
+ [payload.variableId]: {
19660
+ id: payload.variableId,
19661
+ ...payload.data,
19662
+ },
19663
+ },
19664
+ path: "state.variables.items",
19665
+ errorFactory: createPreconditionValidationError,
19666
+ });
18421
19667
  },
18422
19668
  validateUpdateState: ({ state, payload, currentItem }) => {
18423
19669
  if (
@@ -18441,6 +19687,36 @@ const COMMAND_DEFINITIONS = [
18441
19687
  );
18442
19688
  }
18443
19689
 
19690
+ const currentItemIsComputed = Object.hasOwn(currentItem, "computed");
19691
+ if (
19692
+ currentItemIsComputed &&
19693
+ ["scope", "default", "value", "isEnum", "enumValues"].some((key) =>
19694
+ Object.hasOwn(payload.data, key),
19695
+ )
19696
+ ) {
19697
+ return invalidPrecondition(
19698
+ "computed variables cannot update scope, stored value, or enum fields",
19699
+ );
19700
+ }
19701
+
19702
+ if (!currentItemIsComputed && Object.hasOwn(payload.data, "computed")) {
19703
+ return invalidPrecondition(
19704
+ "stored variables cannot be converted to computed variables",
19705
+ );
19706
+ }
19707
+
19708
+ if (currentItemIsComputed && Object.hasOwn(payload.data, "computed")) {
19709
+ const result = validateVariableComputedConfig({
19710
+ computed: payload.data.computed,
19711
+ variableType: currentItem.variableType,
19712
+ path: "payload.data.computed",
19713
+ errorFactory: createPreconditionValidationError,
19714
+ });
19715
+ if (result?.valid === false) {
19716
+ return result;
19717
+ }
19718
+ }
19719
+
18444
19720
  if (currentItem.type !== "folder") {
18445
19721
  {
18446
19722
  const result = validateTagIdsAgainstScope({
@@ -18485,6 +19761,49 @@ const COMMAND_DEFINITIONS = [
18485
19761
  }
18486
19762
  }
18487
19763
  }
19764
+
19765
+ if (currentItemIsComputed && Object.hasOwn(payload.data, "computed")) {
19766
+ return validateComputedVariableGraph({
19767
+ items: {
19768
+ ...state.variables.items,
19769
+ [payload.variableId]: applyVariableUpdate({
19770
+ currentItem,
19771
+ data: payload.data,
19772
+ }),
19773
+ },
19774
+ path: "state.variables.items",
19775
+ errorFactory: createPreconditionValidationError,
19776
+ });
19777
+ }
19778
+
19779
+ return VALID_RESULT;
19780
+ },
19781
+ validateDeleteState: ({ state, payload }) => {
19782
+ const deletedIds = new Set();
19783
+ for (const variableId of payload.variableIds) {
19784
+ const node = findTreeNode({
19785
+ nodes: state.variables.tree,
19786
+ nodeId: variableId,
19787
+ });
19788
+ if (!node) {
19789
+ deletedIds.add(variableId);
19790
+ continue;
19791
+ }
19792
+ for (const descendantId of collectTreeDescendantIds({ node })) {
19793
+ deletedIds.add(descendantId);
19794
+ }
19795
+ }
19796
+
19797
+ const remainingItems = Object.fromEntries(
19798
+ Object.entries(state.variables.items).filter(
19799
+ ([variableId]) => !deletedIds.has(variableId),
19800
+ ),
19801
+ );
19802
+ return validateComputedVariableGraph({
19803
+ items: remainingItems,
19804
+ path: "state.variables.items",
19805
+ errorFactory: createPreconditionValidationError,
19806
+ });
18488
19807
  },
18489
19808
  }),
18490
19809
  ...createFolderedCollectionCommandDefinitions({
@@ -18531,16 +19850,24 @@ const COMMAND_DEFINITIONS = [
18531
19850
  }
18532
19851
  }
18533
19852
 
18534
- for (const field of ["fontId", "colorId", "strokeColorId"]) {
19853
+ for (const fontId of toIdArray(data.fontId)) {
19854
+ const item = state.fonts.items[fontId];
19855
+ if (!isPlainObject(item) || item.type === "folder") {
19856
+ return invalidPrecondition(
19857
+ "payload.data.fontId must reference an existing non-folder font",
19858
+ );
19859
+ }
19860
+ }
19861
+
19862
+ for (const field of ["colorId", "strokeColorId"]) {
18535
19863
  if (data[field] === undefined) {
18536
19864
  continue;
18537
19865
  }
18538
19866
 
18539
- const collectionKey = field === "fontId" ? "fonts" : "colors";
18540
- const item = state[collectionKey].items[data[field]];
19867
+ const item = state.colors.items[data[field]];
18541
19868
  if (!isPlainObject(item) || item.type === "folder") {
18542
19869
  return invalidPrecondition(
18543
- `payload.data.${field} must reference an existing non-folder ${collectionKey.slice(0, -1)}`,
19870
+ `payload.data.${field} must reference an existing non-folder color`,
18544
19871
  );
18545
19872
  }
18546
19873
  }
@@ -18583,16 +19910,26 @@ const COMMAND_DEFINITIONS = [
18583
19910
  }
18584
19911
  }
18585
19912
 
18586
- for (const field of ["fontId", "colorId", "strokeColorId"]) {
19913
+ if (payload.data.fontId !== undefined) {
19914
+ for (const fontId of toIdArray(payload.data.fontId)) {
19915
+ const item = state.fonts.items[fontId];
19916
+ if (!isPlainObject(item) || item.type === "folder") {
19917
+ return invalidPrecondition(
19918
+ "payload.data.fontId must reference an existing non-folder font",
19919
+ );
19920
+ }
19921
+ }
19922
+ }
19923
+
19924
+ for (const field of ["colorId", "strokeColorId"]) {
18587
19925
  if (payload.data[field] === undefined) {
18588
19926
  continue;
18589
19927
  }
18590
19928
 
18591
- const collectionKey = field === "fontId" ? "fonts" : "colors";
18592
- const item = state[collectionKey].items[payload.data[field]];
19929
+ const item = state.colors.items[payload.data[field]];
18593
19930
  if (!isPlainObject(item) || item.type === "folder") {
18594
19931
  return invalidPrecondition(
18595
- `payload.data.${field} must reference an existing non-folder ${collectionKey.slice(0, -1)}`,
19932
+ `payload.data.${field} must reference an existing non-folder color`,
18596
19933
  );
18597
19934
  }
18598
19935
  }