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: Allow stencil modifiers-in-selectors #2741

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions modules/codemod/lib/v11/spec/replaceStylesIconProp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('replaceStylesIconProp', () => {
<AccentIcon styles={{padding: '1rem'}} />
</>
`;
expectTransform(input, expected); //?
expectTransform(input, expected);
});

it('should rename styles to cs for Svg, SystemIcon, AccentIcon exported from the icon package', () => {
Expand Down Expand Up @@ -75,7 +75,7 @@ describe('replaceStylesIconProp', () => {
<AccentIcon cs={{padding: '1rem'}} />
</>
`;
expectTransform(input, expected); //?
expectTransform(input, expected);
});

it('should handle value as variable', () => {
Expand Down
2 changes: 1 addition & 1 deletion modules/css/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@workday/canvas-kit-css",
"version": "10.3.24",
"version": "10.3.36",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build process automates the syncing of these version strings.

"description": "The parent module that contains all Workday Canvas Kit CSS components",
"author": "Workday, Inc. (https://www.workday.com)",
"license": "Apache-2.0",
Expand Down
2 changes: 1 addition & 1 deletion modules/labs-css/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@workday/canvas-kit-labs-css",
"version": "10.3.24",
"version": "10.3.36",
"description": "The parent module that contains all Workday Canvas Kit Labs CSS components",
"author": "Workday, Inc. (https://www.workday.com)",
"license": "Apache-2.0",
Expand Down
2 changes: 1 addition & 1 deletion modules/preview-css/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@workday/canvas-kit-preview-css",
"version": "10.3.24",
"version": "10.3.36",
"description": "The parent module that contains all Workday Canvas Kit Preview CSS components",
"author": "Workday, Inc. (https://www.workday.com)",
"license": "Apache-2.0",
Expand Down
2 changes: 2 additions & 0 deletions modules/styling-transform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export {parseObjectToStaticValue} from './lib/utils/parseObjectToStaticValue';
export {createObjectTransform} from './lib/createObjectTransform';
export {createPropertyTransform} from './lib/createPropertyTransform';
export {styleTransformer};
export {withDefaultContext} from './lib/styleTransform';
export {getClassName} from './lib/utils/handleCreateStencil';

// be compatible with ttypescript which expects a default export
export default styleTransformer;
17 changes: 11 additions & 6 deletions modules/styling-transform/lib/styleTransform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export interface StyleTransformerOptions extends TransformerContext {
transformers?: NodeTransformer[];
}

let vars: TransformerContext['variables'] = {};
let vars: TransformerContext['names'] = {};
let extractedNames: TransformerContext['extractedNames'] = {};
Comment on lines +23 to +24
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

names and extractedNames are used by the static resolution system. The transformation process populates these caches with resolved names.

For example:

const foo = createStencil({
  modifiers: {
    size: {
      small: {},
      large: {}
    }
  }
})

const bar = createStencil({
  base: {
    // The type of `foo.modifiers.size.small` is `string` which the static
    // transform doesn't like. It will now be added to `names` so that
    // static resolution uses the actual class name
    [`.${foo.modifiers.size.small} :where(&)`]: {
    }
  }
})

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

names are a cache of the resolved names for React Kit. It will include hashes. The extractedNames are the names used in CSS Kit.

let styles: TransformerContext['styles'] = {};
let cache: TransformerContext['cache'] = {};
let loadedFallbacks = false;
Expand All @@ -32,6 +33,7 @@ let config: Config = {};
*/
export function _reset() {
vars = {};
extractedNames = {};
styles = {};
cache = {};
loadedFallbacks = false;
Expand Down Expand Up @@ -63,7 +65,7 @@ export default function styleTransformer(
configLoaded = true;
}

const {variables, ...transformContext} = withDefaultContext(program.getTypeChecker(), {
const {names, ...transformContext} = withDefaultContext(program.getTypeChecker(), {
...config,
...options,
});
Expand All @@ -83,8 +85,10 @@ export default function styleTransformer(
const fallbackVars = getVariablesFromFiles(files);
console.log(`Found ${Object.keys(fallbackVars).length} variables.`);

// eslint-disable-next-line no-param-reassign
vars = {...variables, ...fallbackVars};
// eslint-disable-next-line guard-for-in
for (const key in fallbackVars) {
names[key] = fallbackVars[key];
}
Comment on lines +88 to +91
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a bug where passing in names to the transform did not return the same reference. That's because a new reference was used internally. That means I couldn't properly test changes. This update instead mutates the name cache that was passed in so the cache can be properly tested against.

loadedFallbacks = true;
}

Expand All @@ -108,7 +112,7 @@ export default function styleTransformer(
}

const newNode = transformContext.transform(node, {
variables: vars,
names,
...transformContext,
});

Expand All @@ -126,7 +130,8 @@ export function withDefaultContext(
return {
prefix: 'css',
getPrefix: path => input.prefix || 'css',
variables: {},
names: vars,
extractedNames,
styles,
cache,
checker,
Expand Down
16 changes: 14 additions & 2 deletions modules/styling-transform/lib/utils/createStyleObjectNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function serializeStyles(
template: string,
context: TransformerContext
) {
const {getFileName, styles, cache} = context;
const {getFileName, styles, cache, names, extractedNames} = context;
const fileName = getFileName(node.getSourceFile().fileName);
const hash = getHash(node, context);
const serialized = {...serializedStylesEmotion([input]), name: hash} as ReturnType<
Expand All @@ -74,8 +74,20 @@ export function serializeStyles(
const styleOutput = compileCSS(
template.replace('%s', serialized.styles).replace('%n', serialized.name)
);

let extractedStyleOutput = styleOutput;

for (const key in names) {
if (extractedNames[names[key]]) {
// @ts-ignore replaceAll was added in es2021, but our lib versions don't go past es2019. replaceAll is available in node 15+
extractedStyleOutput = extractedStyleOutput.replaceAll(
names[key],
extractedNames[names[key]]
);
}
}
styles[fileName] = styles[fileName] || [];
styles[fileName].push(styleOutput);
styles[fileName].push(extractedStyleOutput);
cache[hash] = styleOutput;
}

Expand Down
16 changes: 8 additions & 8 deletions modules/styling-transform/lib/utils/getFallbackVariable.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/**
* Looks for a variable value that doesn't include a fallback and automatically adds one if found in
* the current cache of variables. This allows fallbacks to be automatically included in
* environments without the variables defined. This is most useful for Storybook or other sandboxes
* that may not have CSS Variables defined. The fallbacks will allow the UI to look correct without
* additional setup. Fallbacks come from the `fallbackFiles` transform configuration.
* the current cache of names. This allows fallbacks to be automatically included in environments
* without the names defined. This is most useful for Storybook or other sandboxes that may not have
* CSS Variables defined. The fallbacks will allow the UI to look correct without additional setup.
* Fallbacks come from the `fallbackFiles` transform configuration.
*/
export function getFallbackVariable(
variableName: string,
variables: Record<string, string>
names: Record<string, string>
): string | undefined {
const variable = variableName.includes('var(') ? variableName : variables[variableName];
const variable = variableName.includes('var(') ? variableName : names[variableName];
if (variable && variable.includes('var(')) {
return variable.replace(
/(var\(([A-Za-z0-9\-_]+)\))/,
Expand All @@ -18,9 +18,9 @@ export function getFallbackVariable(
/** the full match of the first group "var(--var-name)" */ varMatch,
/** the variable name - match of the second group "--var-name" */ cssVarName
) => {
const value = variables[cssVarName];
const value = names[cssVarName];
if (value && value.startsWith('var')) {
return getFallbackVariable(value, variables);
return getFallbackVariable(value, names);
}
return value || varMatch;
}
Expand Down
2 changes: 1 addition & 1 deletion modules/styling-transform/lib/utils/getVarName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import ts from 'typescript';
export function getVarName(node: ts.Node, parts: string[] = []): string {
// base case. Join all the parts
if (!node.parent || node.kind === ts.SyntaxKind.VariableStatement) {
return parts.join('-');
return parts.join('.');
}

// Any node with a `name` property that is an identifier can add to the var name
Expand Down
63 changes: 40 additions & 23 deletions modules/styling-transform/lib/utils/handleCreateStencil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import ts from 'typescript';
import {slugify} from '@workday/canvas-kit-styling';

import {getVarName} from './getVarName';
import {makeEmotionSafe} from './makeEmotionSafe';
import {parseObjectToStaticValue} from './parseObjectToStaticValue';
import {createStyleObjectNode, serializeStyles} from './createStyleObjectNode';
import {getValueFromAliasedSymbol, parseNodeToStaticValue} from './parseNodeToStaticValue';
import {NestedStyleObject, NodeTransformer, TransformerContext} from './types';
import {isImportedFromStyling} from './isImportedFromStyling';
import {getHash} from './getHash';

/**
* Handle all arguments of the CallExpression `createStencil()`
*/
export const handleCreateStencil: NodeTransformer = (node, context) => {
const {checker, prefix, variables} = context;
const {checker, prefix, names, extractedNames} = context;
/**
* This will match whenever a `createStencil()` call expression is encountered. It will loop
* over all the config to extract variables and styles.
Expand All @@ -33,7 +33,8 @@ export const handleCreateStencil: NodeTransformer = (node, context) => {
const stencilVariables: Record<string, string> = {};

// Stencil name is the variable name
const stencilName = slugify(getVarName(node.expression)).replace('-stencil', '');
const stencilName = getVarName(node.expression);
const stencilHash = getHash(node, context);

if (ts.isObjectLiteralExpression(config)) {
const extendedFrom = config.properties.reduce((result, property) => {
Expand All @@ -46,7 +47,7 @@ export const handleCreateStencil: NodeTransformer = (node, context) => {
ts.isIdentifier(property.initializer)
) {
const className = getClassName(property.initializer.text, context);
const extendsStencilName = className.split('-').slice(1).join('-');
const extendsStencilName = property.initializer.text;

if (
!Object.values(context.styles).some(fileStyles => {
Expand All @@ -58,11 +59,10 @@ export const handleCreateStencil: NodeTransformer = (node, context) => {
}

// attach all variables from extends stencil
Object.keys(context.variables).forEach(key => {
if (key.startsWith(`${extendsStencilName}-`)) {
Object.keys(names).forEach(key => {
if (key.startsWith(`${extendsStencilName}.`)) {
// We want to copy a new entry into variables that is the extended stencil with the same variable name as the base variable name
context.variables[key.replace(extendsStencilName, stencilName)] =
context.variables[key];
names[key.replace(extendsStencilName, stencilName)] = names[key];
}
});

Expand All @@ -81,30 +81,34 @@ export const handleCreateStencil: NodeTransformer = (node, context) => {
);
}) as ts.PropertyAssignment | undefined;

function extractVariables(node: ts.Node): any {
function extractNames(node: ts.Node): any {
if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
if (ts.isObjectLiteralExpression(node.initializer)) {
return node.initializer.properties.map(extractVariables);
return node.initializer.properties.map(extractNames);
}

const varName = `${stencilName}-${makeEmotionSafe(node.name.text)}`;
const varValue = `--${prefix}-${varName}`;
variables[`${varName}`] = varValue;
const varName = getVarName(node.name);
const varValue = `--${getClassName(varName, context)}`;
names[varName] = `--${node.name.text}-${slugify(stencilName).replace(
'-stencil',
''
)}-${stencilHash}`;

variables[makeEmotionSafe(node.name.text)] = varValue;
names[node.name.text] = names[varName];
extractedNames[names[varName]] = varValue;

// Evaluate the variable defaults
const value = parseNodeToStaticValue(node.initializer, context).toString();
if (value) {
// Only add the stencil variable if there's a value. An empty string means no default.
stencilVariables[varValue] = value;
stencilVariables[names[varName]] = value;
}
}
}

if (varsConfig && ts.isObjectLiteralExpression(varsConfig.initializer)) {
varsConfig.initializer.properties.forEach(variable => {
extractVariables(variable);
extractNames(variable);
});
}

Expand Down Expand Up @@ -156,9 +160,13 @@ export const handleCreateStencil: NodeTransformer = (node, context) => {
ts.factory.updateObjectLiteralExpression(
modifierProperty.initializer,
modifierProperty.initializer.properties.map(modifier => {
const styleObj = parseStyleBlock(modifier, context, stencilName);
const styleObj = parseStyleBlock(
modifier,
context,
getClassName(stencilName, context)
);

if (styleObj && modifier.name && Object.keys(styleObj).length) {
if (styleObj && modifier.name) {
const initializer = createStyleReplacementNode(
modifier,
styleObj,
Expand Down Expand Up @@ -331,7 +339,9 @@ export const handleCreateStencil: NodeTransformer = (node, context) => {

return ts.factory.updateCallExpression(node, node.expression, undefined, [
ts.factory.updateObjectLiteralExpression(config, configProperties),
ts.factory.createStringLiteral(`${prefix}-${stencilName}`),
ts.factory.createStringLiteral(
`${slugify(stencilName).replace('-stencil', '')}-${stencilHash}`
),
]);
}
}
Expand All @@ -353,7 +363,7 @@ function parseStyleBlock(
if (ts.isObjectLiteralExpression(property.initializer)) {
styleObj = parseObjectToStaticValue(property.initializer, {
...context,
variableScope: `${stencilName}-`,
nameScope: `${stencilName}.`,
});
}

Expand All @@ -362,7 +372,7 @@ function parseStyleBlock(
if (returnNode) {
styleObj = parseObjectToStaticValue(returnNode, {
...context,
variableScope: `${stencilName}-`,
nameScope: `${stencilName}.`,
});
}
}
Expand All @@ -373,7 +383,7 @@ function parseStyleBlock(
if (returnNode) {
styleObj = parseObjectToStaticValue(returnNode, {
...context,
variableScope: `${stencilName}-`,
nameScope: `${stencilName}.`,
});
}
}
Expand Down Expand Up @@ -413,15 +423,22 @@ function createStyleReplacementNode(
className: string,
context: TransformerContext
) {
const {prefix, names, extractedNames} = context;
const serialized = serializeStyles(node, styleObj, `.${className}{%s}`, context);

const varName = getVarName(node);
const value = `${prefix}-${serialized.name}`;
names[varName] = value;
extractedNames[value] = getClassName(varName, context);

return createStyleObjectNode(serialized.styles, serialized.name);
}

function getClassName(name: string, {prefix}: TransformerContext): string {
export function getClassName(name: string, {prefix}: TransformerContext): string {
return (
`${prefix}-` +
slugify(name)
.replace('-vars', '')
.replace('-stencil', '')
.replace('-base', '')
.replace('-modifiers', '-')
Expand Down
Loading
Loading