diff --git a/packages/core/src/custom-values.ts b/packages/core/src/custom-values.ts index 42f248125..1bac63279 100644 --- a/packages/core/src/custom-values.ts +++ b/packages/core/src/custom-values.ts @@ -3,25 +3,49 @@ import postcssValueParser from 'postcss-value-parser'; import { getFormatterArgs, getNamedArgs, getStringValue } from './helpers/value'; import type { ParsedValue } from './types'; +export class CustomValueError extends Error { + constructor(message: string, public fallbackValue: string) { + super(message); + } +} + export interface Box { type: Type; value: Value; + flatValue: string | undefined; } -export function box(type: Type, value: Value): Box { +export function box( + type: Type, + value: Value, + flatValue?: string +): Box { return { type, value, + flatValue, }; } -const { hasOwnProperty } = Object.prototype; +export function boxString(value: string) { + return box('st-string', value, value); +} -export function unbox>(boxed: B | string): any { +export function unbox>( + boxed: B | string, + unboxPrimitives = true, + customValues?: CustomTypes, + node?: ParsedValue +): any { if (typeof boxed === 'string') { - return boxed; - } else if (typeof boxed === 'object' && boxed.type && hasOwnProperty.call(boxed, 'value')) { - return cloneDeepWith(boxed.value, unbox); + return unboxPrimitives ? boxed : boxString(boxed); + } else if (typeof boxed === 'object' && boxed) { + const customValue = customValues?.[boxed.type]; + let value = boxed.value; + if (customValue?.flattenValue && node) { + value = customValue.getValue([], boxed, node, customValues!); + } + return cloneDeepWith(value, (v) => unbox(v, unboxPrimitives, customValues, node)); } } @@ -39,7 +63,8 @@ export interface CustomValueExtension { valueAst: ParsedValue, customTypes: { [typeID: string]: CustomValueExtension; - } + }, + boxPrimitive?: boolean ): Box; getValue( path: string[], @@ -51,8 +76,8 @@ export interface CustomValueExtension { function createStArrayCustomFunction() { return createCustomValue({ - processArgs: (node, customTypes) => { - return CustomValueStrategy.args(node, customTypes); + processArgs: (node, customTypes, boxPrimitive) => { + return CustomValueStrategy.args(node, customTypes, boxPrimitive); }, createValue: (args) => { return args; @@ -63,8 +88,8 @@ function createStArrayCustomFunction() { function createStMapCustomFunction() { return createCustomValue({ - processArgs: (node, customTypes) => { - return CustomValueStrategy.named(node, customTypes); + processArgs: (node, customTypes, boxPrimitive) => { + return CustomValueStrategy.named(node, customTypes, boxPrimitive); }, createValue: (args) => { return args; @@ -80,6 +105,7 @@ export const stTypes: CustomTypes = { stMap: createStMapCustomFunction().register('stMap'), 'st-array': createStArrayCustomFunction().register('st-array'), 'st-map': createStMapCustomFunction().register('st-map'), + 'st-string': createStMapCustomFunction().register('st-string'), } as const; export const deprecatedStFunctions: Record = { @@ -92,7 +118,7 @@ export const deprecatedStFunctions: Record }; export const CustomValueStrategy = { - args: (fnNode: ParsedValue, customTypes: CustomTypes) => { + args: (fnNode: ParsedValue, customTypes: CustomTypes, boxPrimitive?: boolean) => { const pathArgs = getFormatterArgs(fnNode); const outputArray = []; for (const arg of pathArgs) { @@ -100,13 +126,13 @@ export const CustomValueStrategy = { const ct = parsedArg.type === 'function' && parsedArg.value; const resolvedValue = typeof ct === 'string' && customTypes[ct] - ? customTypes[ct].evalVarAst(parsedArg, customTypes) - : arg; + ? customTypes[ct].evalVarAst(parsedArg, customTypes, boxPrimitive) + : unbox(arg, !boxPrimitive); outputArray.push(resolvedValue); } return outputArray; }, - named: (fnNode: ParsedValue, customTypes: CustomTypes) => { + named: (fnNode: ParsedValue, customTypes: CustomTypes, boxPrimitive?: boolean) => { const outputMap: BoxedValueMap = {}; const s = getNamedArgs(fnNode); for (const [prop, space, ...valueNodes] of s) { @@ -124,13 +150,13 @@ export const CustomValueStrategy = { if (!resolvedValue) { const ct = customTypes[valueNode.value]; if (valueNode.type === 'function' && ct) { - resolvedValue = ct.evalVarAst(valueNode, customTypes); + resolvedValue = ct.evalVarAst(valueNode, customTypes, boxPrimitive); } else { - resolvedValue = getStringValue(valueNode); + resolvedValue = unbox(getStringValue(valueNode), !boxPrimitive); } } } else { - resolvedValue = getStringValue(valueNodes); + resolvedValue = unbox(getStringValue(valueNodes), !boxPrimitive); } if (resolvedValue) { @@ -152,7 +178,7 @@ type FlattenValue = (v: Box) => { }; interface ExtensionApi { - processArgs: (fnNode: ParsedValue, customTypes: CustomTypes) => Args; + processArgs: (fnNode: ParsedValue, customTypes: CustomTypes, boxPrimitive?: boolean) => Args; createValue: (args: Args) => Value; getValue: (v: Value, key: string) => string | Box; flattenValue?: FlattenValue; @@ -169,9 +195,21 @@ export function createCustomValue({ register(localTypeSymbol: string) { return { flattenValue, - evalVarAst(fnNode: ParsedValue, customTypes: CustomTypes) { - const args = processArgs(fnNode, customTypes); - return box(localTypeSymbol, createValue(args)); + evalVarAst(fnNode: ParsedValue, customTypes: CustomTypes, boxPrimitive?: boolean) { + const args = processArgs(fnNode, customTypes, boxPrimitive); + const value = createValue(args); + let flatValue: string | undefined; + + if (flattenValue) { + flatValue = getFlatValue( + flattenValue, + box(localTypeSymbol, value), + fnNode, + customTypes + ); + } + + return box(localTypeSymbol, value, flatValue); }, getValue( path: string[], @@ -181,13 +219,14 @@ export function createCustomValue({ ): string { if (path.length === 0) { if (flattenValue) { - const { delimiter, parts } = flattenValue(obj); - return parts - .map((v) => getBoxValue([], v, fallbackNode, customTypes)) - .join(delimiter); + return getFlatValue(flattenValue, obj, fallbackNode, customTypes); } else { - // TODO: add diagnostics - return getStringValue([fallbackNode]); + const stringifiedValue = getStringValue([fallbackNode]); + + throw new CustomValueError( + `/* Error trying to flat -> */${stringifiedValue}`, + stringifiedValue + ); } } const value = getValue(obj.value, path[0]); @@ -198,14 +237,24 @@ export function createCustomValue({ }; } +function getFlatValue( + flattenValue: FlattenValue, + obj: Box, + fallbackNode: ParsedValue, + customTypes: CustomTypes +) { + const { delimiter, parts } = flattenValue(obj); + return parts.map((v) => getBoxValue([], v, fallbackNode, customTypes)).join(delimiter); +} + export function getBoxValue( path: string[], value: string | Box, node: ParsedValue, customTypes: CustomTypes ): string { - if (typeof value === 'string') { - return value; + if (typeof value === 'string' || value.type === 'st-string') { + return unbox(value, true, customTypes); } else if (value && customTypes[value.type]) { return customTypes[value.type].getValue(path, value, node, customTypes); } else { diff --git a/packages/core/src/features/st-var.ts b/packages/core/src/features/st-var.ts index 8fd073c85..91cae8384 100644 --- a/packages/core/src/features/st-var.ts +++ b/packages/core/src/features/st-var.ts @@ -1,5 +1,5 @@ import { createFeature, FeatureContext, FeatureTransformContext } from './feature'; -import { deprecatedStFunctions } from '../custom-values'; +import { unbox, Box, deprecatedStFunctions, boxString } from '../custom-values'; import { generalDiagnostics } from './diagnostics'; import * as STSymbol from './st-symbol'; import type { StylableMeta } from '../stylable-meta'; @@ -14,7 +14,6 @@ import type { ImmutablePseudoClass, PseudoClass } from '@tokey/css-selector-pars import type * as postcss from 'postcss'; import { processDeclarationFunctions } from '../process-declaration-functions'; import { Diagnostics } from '../diagnostics'; -import { unbox } from '../custom-values'; import type { ParsedValue } from '../types'; import type { Stylable } from '../stylable'; import type { RuntimeStVar } from '../stylable-transformer'; @@ -28,10 +27,20 @@ export interface VarSymbol { node: postcss.Node; } +export type CustomValueInput = Box< + string, + CustomValueInput | Record | Array +>; + export interface ComputedStVar { value: RuntimeStVar; diagnostics: Diagnostics; - input?: any; + input: CustomValueInput; +} + +export interface FlatComputedStVar { + value: string; + path: string[]; } export const diagnostics = { @@ -47,8 +56,8 @@ export const diagnostics = { .map((s, i) => (i === cyclicChain.length - 1 ? '↻ ' : i === 0 ? '→ ' : '↪ ') + s) .join('\n')}"`, MISSING_VAR_IN_VALUE: () => `invalid value() with no var identifier`, - COULD_NOT_RESOLVE_VALUE: (args: string) => - `cannot resolve value function using the arguments provided: "${args}"`, + COULD_NOT_RESOLVE_VALUE: (args?: string) => + `cannot resolve value function${args ? ` using the arguments provided: "${args}"` : ''}`, MULTI_ARGS_IN_VALUE: (args: string) => `value function accepts only a single argument: "value(${args})"`, CANNOT_USE_AS_VALUE: (type: string, varName: string) => @@ -133,13 +142,13 @@ export class StylablePublicApi { topLevelDiagnostics ); - const { var: stVars, customValues } = getResolvedSymbols(meta); + const { var: stVars } = getResolvedSymbols(meta); const computed: Record = {}; for (const [localName, resolvedVar] of Object.entries(stVars)) { const diagnostics = new Diagnostics(); - const { outputValue, topLevelType } = evaluator.evaluateValue( + const { outputValue, topLevelType, runtimeValue } = evaluator.evaluateValue( { getResolvedSymbols, resolver: this.stylable.resolver, @@ -154,25 +163,53 @@ export class StylablePublicApi { } ); - const customValue = customValues[topLevelType?.type]; const computedStVar: ComputedStVar = { - /** - * In case of custom value that could be flat, we will use the "outputValue" which is a flat value. - */ - value: - topLevelType && !customValue?.flattenValue ? unbox(topLevelType) : outputValue, + value: runtimeValue ?? outputValue, + input: topLevelType ?? unbox(outputValue, false), diagnostics, }; - if (customValue?.flattenValue) { - computedStVar.input = unbox(topLevelType); - } - computed[localName] = computedStVar; } return computed; } + + public flatten(meta: StylableMeta) { + const computed = this.getComputed(meta); + + const flatStVars: FlatComputedStVar[] = []; + + for (const [symbol, stVar] of Object.entries(computed)) { + flatStVars.push(...this.flatSingle(stVar.input, [symbol])); + } + + return flatStVars; + } + + private flatSingle(input: CustomValueInput, path: string[]) { + const currentVars: FlatComputedStVar[] = []; + + if (input.flatValue) { + currentVars.push({ + value: input.flatValue, + path, + }); + } + + if (typeof input.value === `object` && input.value !== null) { + for (const [key, innerInput] of Object.entries(input.value)) { + currentVars.push( + ...this.flatSingle( + typeof innerInput === 'string' ? boxString(innerInput) : innerInput, + [...path, key] + ) + ); + } + } + + return currentVars; + } } function collectVarSymbols(context: FeatureContext, rule: postcss.Rule) { @@ -286,17 +323,14 @@ function evaluateValueCall( args: restArgs, node: resolvedVarSymbol.node, meta: resolvedVar.meta, + rootArgument: varName, + initialNode: node, } ); // report errors if (node) { const argsAsString = parsedArgs.join(', '); - if (typeError) { - context.diagnostics.warn( - node, - diagnostics.COULD_NOT_RESOLVE_VALUE(argsAsString) - ); - } else if (!topLevelType && parsedArgs.length > 1) { + if (!typeError && !topLevelType && parsedArgs.length > 1) { context.diagnostics.warn(node, diagnostics.MULTI_ARGS_IN_VALUE(argsAsString)); } } diff --git a/packages/core/src/functions.ts b/packages/core/src/functions.ts index 2866ab3f5..fed554db3 100644 --- a/packages/core/src/functions.ts +++ b/packages/core/src/functions.ts @@ -12,11 +12,12 @@ import { createSymbolResolverWithCache, MetaResolvedSymbols, } from './stylable-resolver'; -import type { replaceValueHook, StylableTransformer } from './stylable-transformer'; +import type { replaceValueHook, RuntimeStVar, StylableTransformer } from './stylable-transformer'; import { getFormatterArgs, getStringValue, stringifyFunction } from './helpers/value'; import type { ParsedValue } from './types'; import type { FeatureTransformContext } from './features/feature'; import { CSSCustomProperty, STVar } from './features'; +import { unbox, CustomValueError } from './custom-values'; export type ValueFormatter = (name: string) => string; export type ResolvedFormatter = Record; @@ -30,10 +31,13 @@ export interface EvalValueData { tsVarOverride?: Record | null; cssVarsMapping?: Record; args?: string[]; + rootArgument?: string; + initialNode?: postcss.Node; } export interface EvalValueResult { topLevelType: any; + runtimeValue: RuntimeStVar; outputValue: string; typeError?: Error; } @@ -58,7 +62,9 @@ export class StylableEvaluator { context.diagnostics, data.passedThrough, data.cssVarsMapping, - data.args + data.args, + data.rootArgument, + data.initialNode ); } } @@ -111,7 +117,9 @@ export function processDeclarationValue( diagnostics: Diagnostics = new Diagnostics(), passedThrough: string[] = [], cssVarsMapping: Record = {}, - args: string[] = [] + args: string[] = [], + rootArgument?: string, + initialNode?: postcss.Node ): EvalValueResult { const evaluator = new StylableEvaluator({ tsVarOverride: variableOverride }); const resolvedSymbols = getResolvedSymbols(meta); @@ -137,6 +145,8 @@ export function processDeclarationValue( tsVarOverride: variableOverride, cssVarsMapping, args, + rootArgument, + initialNode, }, node: parsedNode, }); @@ -204,6 +214,8 @@ export function processDeclarationValue( tsVarOverride: variableOverride, cssVarsMapping, args, + rootArgument, + initialNode, }, node: parsedNode, }); @@ -220,23 +232,46 @@ export function processDeclarationValue( let outputValue = ''; let topLevelType = null; + let runtimeValue = null; let typeError: Error | undefined = undefined; for (const n of parsedValue.nodes) { if (n.type === 'function') { const matchingType = resolvedSymbols.customValues[n.value]; if (matchingType) { - topLevelType = matchingType.evalVarAst(n, resolvedSymbols.customValues); try { - outputValue += matchingType.getValue( - args, - topLevelType, - n, - resolvedSymbols.customValues - ); + topLevelType = matchingType.evalVarAst(n, resolvedSymbols.customValues, true); + runtimeValue = unbox(topLevelType, true, resolvedSymbols.customValues, n); + try { + outputValue += matchingType.getValue( + args, + topLevelType, + n, + resolvedSymbols.customValues + ); + } catch (error) { + if (error instanceof CustomValueError) { + outputValue += error.fallbackValue; + } else { + throw error; + } + } } catch (e) { typeError = e as Error; - // catch broken variable resolutions + + const invalidNode = initialNode || node; + + if (invalidNode) { + diagnostics.warn( + invalidNode, + STVar.diagnostics.COULD_NOT_RESOLVE_VALUE( + [...(rootArgument ? [rootArgument] : []), ...args].join(', ') + ), + { word: value } + ); + } else { + // TODO: catch broken variable resolutions without a node + } } } else { outputValue += getStringValue([n]); @@ -245,7 +280,7 @@ export function processDeclarationValue( outputValue += getStringValue([n]); } } - return { outputValue, topLevelType, typeError }; + return { outputValue, topLevelType, typeError, runtimeValue }; } export function evalDeclarationValue( diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9c3ee770a..183909660 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -20,7 +20,7 @@ export type { } from './features'; export { reservedKeyFrames } from './features/css-keyframes'; export { scopeCSSVar } from './features/css-custom-property'; -export type { ComputedStVar } from './features/st-var'; +export type { ComputedStVar, FlatComputedStVar } from './features/st-var'; export { StylableProcessor, createEmptyMeta, diff --git a/packages/core/src/stylable-transformer.ts b/packages/core/src/stylable-transformer.ts index c9a00874f..32f974a65 100644 --- a/packages/core/src/stylable-transformer.ts +++ b/packages/core/src/stylable-transformer.ts @@ -204,6 +204,7 @@ export class StylableTransformer { node: atRule, valueHook: this.replaceValueHook, passedThrough: path.slice(), + initialNode: atRule, }).outputValue; } else if (name === 'property') { CSSCustomProperty.hooks.transformAtRuleNode({ diff --git a/packages/core/test/features/st-var.spec.ts b/packages/core/test/features/st-var.spec.ts index 99df56364..e595084c1 100644 --- a/packages/core/test/features/st-var.spec.ts +++ b/packages/core/test/features/st-var.spec.ts @@ -10,6 +10,31 @@ import postcssValueParser from 'postcss-value-parser'; chai.use(chaiSubset); describe(`features/st-var`, () => { + const stBorderDefinitionMock = ` + const { createCustomValue, CustomValueStrategy } = require("@stylable/core"); + exports.stBorder = createCustomValue({ + processArgs: (node, customTypes) => { + return CustomValueStrategy.args(node, customTypes); + }, + createValue: ([size, style, color]) => { + return { + size, + style, + color, + }; + }, + getValue: (value, index) => { + return value[index]; + }, + flattenValue: ({ value: { size, style, color } }) => { + return { + delimiter: ' ', + parts: [size, style, color], + }; + }, + }) + `; + it(`should process :vars definitions`, () => { const { sheets } = testStylableCore(` /* @transform-remove */ @@ -648,6 +673,21 @@ describe(`features/st-var`, () => { } `); }); + it.skip('should report on invalid input', () => { + /** + * TODO: test invalid input in built-in custom values (st-map, st-array) + */ + testStylableCore(` + :vars { + /* @transform-error ${ + STVar.diagnostics.COULD_NOT_RESOLVE_VALUE( + `keyWithoutValue` + ) /** TODO - add custom diagnostic for this case */ + }*/ + keyWithoutValueMap: stMap(keyWithoutValue); + } + `); + }); it(`*** st-map and st-array contract test ***`, () => { const test = ({ label, @@ -1259,162 +1299,564 @@ describe(`features/st-var`, () => { }); }); describe('introspection', () => { - it('should get computed st-vars', () => { - const { stylable, sheets } = testStylableCore(` - :vars { - a: red; - b: blue; - c: st-array(value(a), gold); - } - `); - - const { meta } = sheets['/entry.st.css']; - const computedVars = stylable.stVar.getComputed(meta); - - expect(Object.keys(computedVars)).to.eql(['a', 'b', 'c']); - expect(computedVars.a).to.containSubset({ - value: 'red', - input: undefined, - diagnostics: { reports: [] }, - }); - expect(computedVars.b).to.containSubset({ - value: 'blue', - input: undefined, - diagnostics: { reports: [] }, - }); - expect(computedVars.c).to.containSubset({ - value: ['red', 'gold'], - input: undefined, - diagnostics: { reports: [] }, + describe('getComputed', () => { + it('should get computed st-vars', () => { + const { stylable, sheets } = testStylableCore(` + :vars { + a: red; + b: blue; + c: st-array(value(a), gold); + } + `); + + const { meta } = sheets['/entry.st.css']; + const computedVars = stylable.stVar.getComputed(meta); + + expect(Object.keys(computedVars)).to.eql(['a', 'b', 'c']); + expect(computedVars.a).to.containSubset({ + value: 'red', + input: { + flatValue: 'red', + type: 'st-string', + value: 'red', + }, + diagnostics: { reports: [] }, + }); + expect(computedVars.b).to.containSubset({ + value: 'blue', + input: { + flatValue: 'blue', + type: 'st-string', + value: 'blue', + }, + diagnostics: { reports: [] }, + }); + expect(computedVars.c).to.containSubset({ + value: ['red', 'gold'], + input: { + type: 'st-array', + value: [ + { + flatValue: 'red', + type: 'st-string', + value: 'red', + }, + { + flatValue: 'gold', + type: 'st-string', + value: 'gold', + }, + ], + }, + diagnostics: { reports: [] }, + }); }); - }); - - it('should get computed custom value st-var', () => { - const { stylable, sheets } = testStylableCore({ - '/entry.st.css': ` - @st-import [stBorder] from './st-border.js'; + it('should get deep computed complex st-vars', () => { + const { stylable, sheets } = testStylableCore(` :vars { - border: stBorder(1px, solid, red); + map: st-map(a st-map(b red)); } - `, - // Stylable custom value - '/st-border.js': ` - const { createCustomValue, CustomValueStrategy } = require("@stylable/core"); - exports.stBorder = createCustomValue({ - processArgs: (node, customTypes) => { - return CustomValueStrategy.args(node, customTypes); + `); + + const { meta } = sheets['/entry.st.css']; + const computedVars = stylable.stVar.getComputed(meta); + + expect(Object.keys(computedVars)).to.eql(['map']); + expect(computedVars.map.diagnostics.reports.length).to.eql(0); + expect(computedVars.map.value).to.eql({ + a: { + b: 'red', + }, + }); + expect(computedVars.map.input).to.eql({ + type: 'st-map', + flatValue: undefined, + value: { + a: { + type: 'st-map', + flatValue: undefined, + value: { + b: { + flatValue: 'red', + type: 'st-string', + value: 'red', + }, + }, }, - createValue: ([size, style, color]) => { - return { - size, - style, - color, - }; + }, + }); + }); + + it('should get computed custom value st-var', () => { + const { stylable, sheets } = testStylableCore({ + '/entry.st.css': ` + @st-import [stBorder as createBorder] from './st-border.js'; + + :vars { + border: createBorder(1px, solid, red); + } + `, + // Stylable custom value + '/st-border.js': stBorderDefinitionMock, + }); + + const { meta } = sheets['/entry.st.css']; + const computedVars = stylable.stVar.getComputed(meta); + + expect(Object.keys(computedVars)).to.eql(['border']); + expect(computedVars.border).to.containSubset({ + value: '1px solid red', + input: { + type: 'createBorder', + flatValue: '1px solid red', + value: { + color: 'red', + size: '1px', + style: 'solid', }, - getValue: (value, index) => { - return value[index]; + }, + diagnostics: { reports: [] }, + }); + }); + + it('should get deep computed custom value st-var', () => { + const { stylable, sheets } = testStylableCore({ + '/entry.st.css': ` + @st-import [stBorder] from './st-border.js'; + + :vars { + array: st-array(blue, stBorder(1px, solid, blue)); + map: st-map( + border stBorder( + value(array, 1, size), + solid, + value(array, 0) + ) + ); + } + `, + // Stylable custom value + '/st-border.js': stBorderDefinitionMock, + }); + + const { meta } = sheets['/entry.st.css']; + const computedVars = stylable.stVar.getComputed(meta); + + expect(Object.keys(computedVars)).to.eql(['array', 'map']); + expect(computedVars.array).to.containSubset({ + value: ['blue', '1px solid blue'], + input: { + type: 'st-array', + flatValue: undefined, + value: [ + { + flatValue: 'blue', + type: 'st-string', + value: 'blue', + }, + { + type: 'stBorder', + flatValue: '1px solid blue', + value: { + color: 'blue', + size: '1px', + style: 'solid', + }, + }, + ], + }, + diagnostics: { reports: [] }, + }); + expect(computedVars.map).to.containSubset({ + value: { + border: '1px solid blue', + }, + input: { + type: 'st-map', + flatValue: undefined, + value: { + border: { + type: 'stBorder', + flatValue: '1px solid blue', + value: { + color: 'blue', + size: '1px', + style: 'solid', + }, + }, }, - flattenValue: ({ value: { size, style, color } }) => { - return { - delimiter: ' ', - parts: [size, style, color], - }; + }, + diagnostics: { reports: [] }, + }); + }); + + it('should get imported computed st-vars', () => { + const { stylable, sheets } = testStylableCore({ + '/entry.st.css': ` + @st-import [imported-var as imported] from './imported.st.css'; + + :vars { + a: value(imported); + b: st-map(a value(imported)); + } + `, + 'imported.st.css': ` + :vars { + imported-var: red; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + const computedVars = stylable.stVar.getComputed(meta); + + expect(Object.keys(computedVars)).to.eql(['imported', 'a', 'b']); + expect(computedVars.imported).to.containSubset({ + value: 'red', + input: { + flatValue: 'red', + type: 'st-string', + value: 'red', + }, + diagnostics: { reports: [] }, + }); + expect(computedVars.a).to.containSubset({ + value: 'red', + input: { + flatValue: 'red', + type: 'st-string', + value: 'red', + }, + diagnostics: { reports: [] }, + }); + expect(computedVars.b).to.containSubset({ + value: { a: 'red' }, + input: { + type: 'st-map', + value: { + a: 'red', }, - }) - `, + }, + diagnostics: { reports: [] }, + }); }); - const { meta } = sheets['/entry.st.css']; - const computedVars = stylable.stVar.getComputed(meta); - - expect(Object.keys(computedVars)).to.eql(['border']); - expect(computedVars.border).to.containSubset({ - value: '1px solid red', - input: { - color: 'red', - size: '1px', - style: 'solid', - }, - diagnostics: { reports: [] }, + it('should emit diagnostics only on invalid computed st-vars', () => { + const { stylable, sheets } = testStylableCore( + ` + :vars { + validBefore: red; + invalid: invalid-func(imported); + validAfter: green; + } + ` + ); + + const { meta } = sheets['/entry.st.css']; + + const computedVars = stylable.stVar.getComputed(meta); + + expect(Object.keys(computedVars)).to.eql(['validBefore', 'invalid', 'validAfter']); + expect(computedVars.validBefore).to.containSubset({ + value: 'red', + input: { + flatValue: 'red', + type: 'st-string', + value: 'red', + }, + diagnostics: { reports: [] }, + }); + expect(computedVars.validAfter).to.containSubset({ + value: 'green', + input: { + flatValue: 'green', + type: 'st-string', + value: 'green', + }, + diagnostics: { reports: [] }, + }); + expect(computedVars.invalid).to.containSubset({ + value: 'invalid-func(imported)', + input: { + flatValue: 'invalid-func(imported)', + type: 'st-string', + value: 'invalid-func(imported)', + }, + diagnostics: { + reports: [ + { + message: functionWarnings.UNKNOWN_FORMATTER('invalid-func'), + type: 'warning', + }, + ], + }, + }); + }); + + it('should emit diagnostics only on invalid custom st-vars', () => { + const { stylable, sheets } = testStylableCore({ + '/entry.st.css': ` + @st-import [stBorder] from './st-border.js'; + + :vars { + border: stBorder(st-array(1px, 2px), solid, red); + } + `, + // Stylable custom value + '/st-border.js': stBorderDefinitionMock, + }); + + const { meta } = sheets['/entry.st.css']; + + const computedVars = stylable.stVar.getComputed(meta); + + expect(computedVars.border).to.containSubset({ + value: '', + input: { + flatValue: '', + type: 'st-string', + value: '', + }, + diagnostics: { + reports: [ + { + message: STVar.diagnostics.COULD_NOT_RESOLVE_VALUE(), + type: 'warning', + }, + ], + }, + }); }); }); - it('should get imported computed st-vars', () => { - const { stylable, sheets } = testStylableCore({ - '/entry.st.css': ` - @st-import [imported-var as imported] from './imported.st.css'; + describe('flatten', () => { + it('should flat simple st vars', () => { + const { stylable, sheets } = testStylableCore(` + :vars { + a: red; + b: blue; + } + `); - :vars { - a: value(imported); - b: st-map(a value(imported)); - } - `, - 'imported.st.css': ` - :vars { - imported-var: red; - } - `, + const meta = sheets['/entry.st.css'].meta; + const flattenStVars = stylable.stVar.flatten(meta); + + expect(flattenStVars).to.eql([ + { + value: 'red', + path: ['a'], + }, + { + value: 'blue', + path: ['b'], + }, + ]); }); + it('should not flat native css function inside st vars', () => { + const { stylable, sheets } = testStylableCore(` + :vars { + a: linear-gradient(to right, red, blue); + } + `); - const { meta } = sheets['/entry.st.css']; - const computedVars = stylable.stVar.getComputed(meta); + const meta = sheets['/entry.st.css'].meta; + const flattenStVars = stylable.stVar.flatten(meta); - expect(Object.keys(computedVars)).to.eql(['imported', 'a', 'b']); - expect(computedVars.imported).to.containSubset({ - value: 'red', - input: undefined, - diagnostics: { reports: [] }, + expect(flattenStVars).to.eql([ + { + path: ['a'], + value: 'linear-gradient(to right, red, blue)', + }, + ]); }); - expect(computedVars.a).to.containSubset({ - value: 'red', - input: undefined, - diagnostics: { reports: [] }, + it('should flat imported simple st vars', () => { + const { stylable, sheets } = testStylableCore({ + '/entry.st.css': ` + @st-import [color as myColor] from './imported.st.css'; + .root { color: value(myColor); } + `, + '/imported.st.css': ` + :vars { + color: red; + } + `, + }); + + const meta = sheets['/entry.st.css'].meta; + const flattenStVars = stylable.stVar.flatten(meta); + + expect(flattenStVars).to.eql([ + { + value: 'red', + path: ['myColor'], + }, + ]); }); - expect(computedVars.b).to.containSubset({ - value: { a: 'red' }, - input: undefined, - diagnostics: { reports: [] }, + + it('should flat st-array st vars', () => { + const { stylable, sheets } = testStylableCore( + ` + :vars { + array: st-array(1px, 2px); + } + + ` + ); + + const meta = sheets['/entry.st.css'].meta; + const flattenStVars = stylable.stVar.flatten(meta); + + expect(flattenStVars).to.eql([ + { + value: '1px', + path: ['array', '0'], + }, + { + value: '2px', + path: ['array', '1'], + }, + ]); }); - }); - it('should emit diagnostics only on invalid computed st-vars', () => { - const { stylable, sheets } = testStylableCore( - ` - :vars { - validBefore: red; - invalid: invalid-func(imported); - validAfter: green; - } - ` - ); + it('should flat st-map st vars', () => { + const { stylable, sheets } = testStylableCore( + ` + :vars { + map: st-map(first 1px,second 2px); + } - const { meta } = sheets['/entry.st.css']; + ` + ); + + const meta = sheets['/entry.st.css'].meta; + const flattenStVars = stylable.stVar.flatten(meta); + + expect(flattenStVars).to.eql([ + { + value: '1px', + path: ['map', 'first'], + }, + { + value: '2px', + path: ['map', 'second'], + }, + ]); + }); - const computedVars = stylable.stVar.getComputed(meta); + it('should flat custom value st vars', () => { + const { stylable, sheets } = testStylableCore({ + '/entry.st.css': ` + @st-import [stBorder] from './st-border.js'; - expect(Object.keys(computedVars)).to.eql(['validBefore', 'invalid', 'validAfter']); - expect(computedVars.validBefore).to.containSubset({ - value: 'red', - input: undefined, - diagnostics: { reports: [] }, + :vars { + border: stBorder(1px, solid, red); + } + `, + '/st-border.js': stBorderDefinitionMock, + }); + + const meta = sheets['/entry.st.css'].meta; + const flattenStVars = stylable.stVar.flatten(meta); + + expect(flattenStVars).to.eql([ + { + value: '1px solid red', + path: ['border'], + }, + { + value: '1px', + path: ['border', 'size'], + }, + { + value: 'solid', + path: ['border', 'style'], + }, + { + value: 'red', + path: ['border', 'color'], + }, + ]); }); - expect(computedVars.validAfter).to.containSubset({ - value: 'green', - input: undefined, - diagnostics: { reports: [] }, + it('should flat nested st-array st vars', () => { + const { stylable, sheets } = testStylableCore( + ` + :vars { + nestedArray: st-array( + red, + st-array(red, green), + st-map(color1 gold, color2 blue), + ); + } + + ` + ); + + const meta = sheets['/entry.st.css'].meta; + const flattenStVars = stylable.stVar.flatten(meta); + + expect(flattenStVars).to.eql([ + { + value: 'red', + path: ['nestedArray', '0'], + }, + { + value: 'red', + path: ['nestedArray', '1', '0'], + }, + { + value: 'green', + path: ['nestedArray', '1', '1'], + }, + { + value: 'gold', + path: ['nestedArray', '2', 'color1'], + }, + { + value: 'blue', + path: ['nestedArray', '2', 'color2'], + }, + ]); }); - expect(computedVars.invalid).to.containSubset({ - value: 'invalid-func(imported)', - input: undefined, - diagnostics: { - reports: [ - { - message: functionWarnings.UNKNOWN_FORMATTER('invalid-func'), - type: 'warning', - }, - ], - }, + it('should flat nested st-map st vars', () => { + const { stylable, sheets } = testStylableCore( + ` + :vars { + nestedMap: st-map( + simple red, + array st-array(red, green), + map st-map(color1 gold, color2 blue) + ); + } + + ` + ); + + const meta = sheets['/entry.st.css'].meta; + const flattenStVars = stylable.stVar.flatten(meta); + + expect(flattenStVars).to.eql([ + { + value: 'red', + path: ['nestedMap', 'simple'], + }, + { + value: 'red', + path: ['nestedMap', 'array', '0'], + }, + { + value: 'green', + path: ['nestedMap', 'array', '1'], + }, + { + value: 'gold', + path: ['nestedMap', 'map', 'color1'], + }, + { + value: 'blue', + path: ['nestedMap', 'map', 'color2'], + }, + ]); }); }); });