Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): flatten st-vars #2422

Merged
merged 12 commits into from
Mar 30, 2022
107 changes: 79 additions & 28 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,6 +237,16 @@ 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>,
Expand All @@ -208,6 +257,8 @@ export function getBoxValue(
return value;
} else if (value && customTypes[value.type]) {
return customTypes[value.type].getValue(path, value, node, customTypes);
} else if (value.type === 'st-string') {
return (value as Box<'string', string>).value;
tzachbon marked this conversation as resolved.
Show resolved Hide resolved
} else {
throw new Error('Unknown Type ' + JSON.stringify(value));
// return JSON.stringify(value);
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[];
idoros marked this conversation as resolved.
Show resolved Hide resolved
}

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) =>
tzachbon marked this conversation as resolved.
Show resolved Hide resolved
`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