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

fix(core): custom-value inside computed st-var #2402

Closed
Closed
55 changes: 43 additions & 12 deletions packages/core/src/custom-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,34 @@ import type { ParsedValue } from './types';
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 unbox<B extends Box<string, unknown>>(boxed: B | string): any {
export function unbox<B extends Box<string, unknown>>(
boxed: B | string,
customValues?: CustomTypes
): any {
if (typeof boxed === 'string') {
return boxed;
} else if (typeof boxed === 'object' && boxed.type && hasOwnProperty.call(boxed, 'value')) {
return cloneDeepWith(boxed.value, unbox);
} else if (typeof boxed === 'object' && boxed?.type) {
const customValue = customValues?.[boxed.type];
let value = boxed.value;
if (customValue?.flattenValue) {
value = customValue.getValue([], boxed, null, customValues!);
}
return cloneDeepWith(value, (v) => unbox(v, customValues));
}
}

Expand All @@ -44,7 +56,7 @@ export interface CustomValueExtension<T> {
getValue(
path: string[],
value: Box<string, T>,
node: ParsedValue,
node: ParsedValue | null,
customTypes: CustomTypes
): string;
}
Expand Down Expand Up @@ -171,7 +183,19 @@ export function createCustomValue<Value, Args>({
flattenValue,
evalVarAst(fnNode: ParsedValue, customTypes: CustomTypes) {
const args = processArgs(fnNode, customTypes);
return box(localTypeSymbol, createValue(args));
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,10 +205,7 @@ 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]);
Expand All @@ -198,6 +219,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 Down
14 changes: 7 additions & 7 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 { Box, deprecatedStFunctions } from '../custom-values';
import { generalDiagnostics } from './diagnostics';
import * as STSymbol from './st-symbol';
import type { StylableMeta } from '../stylable-meta';
Expand Down Expand Up @@ -28,10 +28,12 @@ export interface VarSymbol {
node: postcss.Node;
}

type Input = Box<string, Input | Record<string, Input | string> | Array<Input | string>>;

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

export const diagnostics = {
Expand Down Expand Up @@ -154,18 +156,16 @@ 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: topLevelType ? unbox(topLevelType, customValues) : outputValue,
diagnostics,
};

if (customValue?.flattenValue) {
computedStVar.input = unbox(topLevelType);
if (topLevelType) {
computedStVar.input = topLevelType;
}

computed[localName] = computedStVar;
Expand Down
138 changes: 133 additions & 5 deletions packages/core/test/features/st-var.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1284,7 +1284,42 @@ describe(`features/st-var`, () => {
});
expect(computedVars.c).to.containSubset({
value: ['red', 'gold'],
input: undefined,
input: {
type: 'st-array',
value: ['red', 'gold'],
tzachbon marked this conversation as resolved.
Show resolved Hide resolved
},
diagnostics: { reports: [] },
});
});

it('should get deep computed complex st-vars', () => {
const { stylable, sheets } = testStylableCore(`
:vars {
map: st-map(a st-map(b red));
}
`);

const { meta } = sheets['/entry.st.css'];
const computedVars = stylable.stVar.getComputed(meta);

expect(Object.keys(computedVars)).to.eql(['map']);
expect(computedVars.map).to.containSubset({
tzachbon marked this conversation as resolved.
Show resolved Hide resolved
value: {
a: {
b: 'red',
},
},
input: {
type: 'st-map',
value: {
a: {
type: 'st-map',
value: {
b: 'red',
},
},
},
},
diagnostics: { reports: [] },
});
});
Expand Down Expand Up @@ -1332,9 +1367,97 @@ describe(`features/st-var`, () => {
expect(computedVars.border).to.containSubset({
value: '1px solid red',
input: {
color: 'red',
size: '1px',
style: 'solid',
type: 'stBorder',
flatValue: '1px solid red',
tzachbon marked this conversation as resolved.
Show resolved Hide resolved
value: {
color: 'red',
size: '1px',
style: 'solid',
},
},
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(red, stBorder(1px, solid, blue));
map: st-map(border stBorder(1px, solid, value(array, 0)));
}
`,
// Stylable custom value
'/st-border.js': `
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],
};
},
})
`,
});

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: ['red', '1px solid blue'],
input: {
type: 'st-array',
flatValue: undefined,
value: [
'red',
{
type: 'stBorder',
flatValue: '1px solid blue',
value: {
color: 'blue',
size: '1px',
style: 'solid',
},
},
],
},
diagnostics: { reports: [] },
});
expect(computedVars.map).to.containSubset({
value: {
border: '1px solid red',
},
input: {
type: 'st-map',
flatValue: undefined,
value: {
border: {
type: 'stBorder',
flatValue: '1px solid red',
value: {
color: 'red',
size: '1px',
style: 'solid',
},
},
},
},
diagnostics: { reports: [] },
});
Expand Down Expand Up @@ -1373,7 +1496,12 @@ describe(`features/st-var`, () => {
});
expect(computedVars.b).to.containSubset({
value: { a: 'red' },
input: undefined,
input: {
type: 'st-map',
value: {
a: 'red',
},
},
diagnostics: { reports: [] },
});
});
Expand Down