Skip to content

Commit

Permalink
feat(core): flatten st-vars (#2422)
Browse files Browse the repository at this point in the history
  • Loading branch information
tzachbon authored Mar 30, 2022
1 parent ede0449 commit 82c9c64
Show file tree
Hide file tree
Showing 6 changed files with 757 additions and 196 deletions.
109 changes: 79 additions & 30 deletions packages/core/src/custom-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 extends string, Value> {
type: Type;
value: Value;
flatValue: string | undefined;
}

export function box<Type extends string, Value>(type: Type, value: Value): Box<Type, Value> {
export function box<Type extends string, Value>(
type: Type,
value: Value,
flatValue?: string
): Box<Type, Value> {
return {
type,
value,
flatValue,
};
}

const { hasOwnProperty } = Object.prototype;
export function boxString(value: string) {
return box('st-string', value, value);
}

export function unbox<B extends Box<string, unknown>>(boxed: B | string): any {
export function unbox<B extends Box<string, unknown>>(
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));
}
}

Expand All @@ -39,7 +63,8 @@ export interface CustomValueExtension<T> {
valueAst: ParsedValue,
customTypes: {
[typeID: string]: CustomValueExtension<unknown>;
}
},
boxPrimitive?: boolean
): Box<string, T>;
getValue(
path: string[],
Expand All @@ -51,8 +76,8 @@ export interface CustomValueExtension<T> {

function createStArrayCustomFunction() {
return createCustomValue<BoxedValueArray, BoxedValueArray>({
processArgs: (node, customTypes) => {
return CustomValueStrategy.args(node, customTypes);
processArgs: (node, customTypes, boxPrimitive) => {
return CustomValueStrategy.args(node, customTypes, boxPrimitive);
},
createValue: (args) => {
return args;
Expand All @@ -63,8 +88,8 @@ function createStArrayCustomFunction() {

function createStMapCustomFunction() {
return createCustomValue<BoxedValueMap, BoxedValueMap>({
processArgs: (node, customTypes) => {
return CustomValueStrategy.named(node, customTypes);
processArgs: (node, customTypes, boxPrimitive) => {
return CustomValueStrategy.named(node, customTypes, boxPrimitive);
},
createValue: (args) => {
return args;
Expand All @@ -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<string, { alternativeName: string }> = {
Expand All @@ -92,21 +118,21 @@ export const deprecatedStFunctions: Record<string, { alternativeName: string }>
};

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) {
const parsedArg = postcssValueParser(arg).nodes[0];
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) {
Expand All @@ -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) {
Expand All @@ -152,7 +178,7 @@ type FlattenValue<Value> = (v: Box<string, Value>) => {
};

interface ExtensionApi<Value, Args> {
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<string, unknown>;
flattenValue?: FlattenValue<Value>;
Expand All @@ -169,9 +195,21 @@ export function createCustomValue<Value, Args>({
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[],
Expand All @@ -181,13 +219,14 @@ export function createCustomValue<Value, Args>({
): 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]);
Expand All @@ -198,14 +237,24 @@ export function createCustomValue<Value, Args>({
};
}

function getFlatValue<Value>(
flattenValue: FlattenValue<Value>,
obj: Box<string, Value>,
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<string, unknown>,
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 {
Expand Down
80 changes: 57 additions & 23 deletions packages/core/src/features/st-var.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -28,10 +27,20 @@ export interface VarSymbol {
node: postcss.Node;
}

export type CustomValueInput = Box<
string,
CustomValueInput | Record<string, CustomValueInput | string> | Array<CustomValueInput | string>
>;

export interface ComputedStVar {
value: RuntimeStVar;
diagnostics: Diagnostics;
input?: any;
input: CustomValueInput;
}

export interface FlatComputedStVar {
value: string;
path: string[];
}

export const diagnostics = {
Expand All @@ -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) =>
Expand Down Expand Up @@ -133,13 +142,13 @@ export class StylablePublicApi {
topLevelDiagnostics
);

const { var: stVars, customValues } = getResolvedSymbols(meta);
const { var: stVars } = getResolvedSymbols(meta);

const computed: Record<string, ComputedStVar> = {};

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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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));
}
}
Expand Down
Loading

0 comments on commit 82c9c64

Please sign in to comment.