Skip to content

Commit

Permalink
[zero] Add support for styled tagged-template literals (mui#41268)
Browse files Browse the repository at this point in the history
  • Loading branch information
brijeshb42 authored Feb 29, 2024
1 parent 086f2fa commit 2ba9b0f
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 59 deletions.
2 changes: 1 addition & 1 deletion packages/zero-runtime/src/keyframes.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CSSProperties } from './base';
import type { ThemeArgs } from './theme';

type Primitve = string | null | undefined | boolean | number;
export type Primitve = string | null | undefined | boolean | number;

interface KeyframesObject {
[key: string]: {
Expand Down
179 changes: 123 additions & 56 deletions packages/zero-runtime/src/processors/styled.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { parseExpression } from '@babel/parser';
import type { ObjectExpression, SourceLocation, Identifier, Expression } from '@babel/types';
import type {
ObjectExpression,
SourceLocation,
Identifier,
Expression,
TemplateElement,
} from '@babel/types';
import {
Params,
TailProcessorParams,
Expand All @@ -21,6 +27,8 @@ import { cssFnValueToVariable } from '../utils/cssFnValueToVariable';
import { processCssObject } from '../utils/processCssObject';
import { valueToLiteral } from '../utils/valueToLiteral';
import BaseProcessor from './base-processor';
import { Primitive, TemplateCallback } from './keyframes';
import { cache, css } from '../utils/emotion';

type Theme = { [key: 'unstable_sxConfig' | string]: string | number | Theme };

Expand Down Expand Up @@ -91,7 +99,7 @@ type ComponentMeta = {
* }
* ```
*
* For linaria tag processors, we need to implement 3 methods of BaseProcessor -
* For Wyw-in-JS tag processors, we need to implement 3 methods of BaseProcessor -
* 1. doEvaltimeReplacement
* 2. build
* 3. doRuntimeReplacement
Expand All @@ -103,7 +111,7 @@ export class StyledProcessor extends BaseProcessor {

componentMetaArg?: LazyValue;

styleArgs: ExpressionValue[];
styleArgs: ExpressionValue[] | (TemplateElement | ExpressionValue)[];

finalVariants: {
props: Record<string, string | number | boolean | null>;
Expand All @@ -124,6 +132,8 @@ export class StyledProcessor extends BaseProcessor {

originalLocation: SourceLocation | null = null;

isTemplateTag: boolean;

constructor(params: Params, ...args: TailProcessorParams) {
if (params.length <= 2) {
// no need to do any processing if it is an already transformed call or just a reference.
Expand All @@ -135,9 +145,10 @@ export class StyledProcessor extends BaseProcessor {
['callee', ['call', 'member'], ['call', 'template']],
`Invalid use of ${this.tagSource.imported} tag.`,
);
const [callee, memberOrCall, styleCall] = params;
const [callee, memberOrCall, styleCallOrTemplate] = params;
const [callType, componentArg, componentMetaArg] = memberOrCall;
const [, ...styleArgs] = styleCall;
const [, ...styleArgs] = styleCallOrTemplate;
this.isTemplateTag = styleCallOrTemplate[0] === 'template';
this.componentMetaArg =
componentMetaArg && componentMetaArg.kind === ValueType.LAZY ? componentMetaArg : undefined;
this.styleArgs = styleArgs as ExpressionValue[];
Expand Down Expand Up @@ -167,12 +178,11 @@ export class StyledProcessor extends BaseProcessor {
if (!this.component) {
throw new Error('Invalid usage of `styled` tag');
}
styleArgs.forEach((item) => {
if (!Array.isArray(item)) {
if ('kind' in item) {
// push item in dependencies so that they get evaluated and we receive its value in build call.
this.dependencies.push(item);
}

styleArgs.flat().forEach((item) => {
if ('kind' in item) {
// push item in dependencies so that they get evaluated and we receive its value in build call.
this.dependencies.push(item);
}
});
if (callee[0] === 'callee') {
Expand All @@ -192,8 +202,93 @@ export class StyledProcessor extends BaseProcessor {
}`;
}

private generateArtifacts() {
const artifacts: [Rules, Replacements][] = this.collectedStyles.map(([className, cssText]) => {
const rules: Rules = {
[`.${className}`]: {
className,
cssText,
displayName: this.displayName,
start: this.location?.start ?? null,
},
};
// @TODO - Refactor for finer location tracking in original code.
const replacements: Replacements = [
{
length: cssText.length,
original: {
start: {
column: this.location?.start.column ?? 0,
line: this.location?.start.line ?? 0,
},
end: {
column: this.location?.end.column ?? 0,
line: this.location?.end.line ?? 0,
},
},
},
];
return [rules, replacements];
});
artifacts.forEach((artifact) => {
// Wyw-in-JS accesses artifacts array to get the final
// css definitions which are then exposed to the bundler.
this.artifacts.push(['css', artifact]);
});
}

private buildForTemplateTag(values: ValueCache): void {
const templateStrs: string[] = [];
// @ts-ignore @TODO - Fix this. No idea how to initialize a Tagged String array.
templateStrs.raw = [];
const templateExpressions: Primitive[] = [];
const { themeArgs } = this.options as IOptions;

this.styleArgs.flat().forEach((item) => {
if ('kind' in item) {
switch (item.kind) {
case ValueType.FUNCTION: {
const value = values.get(item.ex.name) as TemplateCallback;
templateExpressions.push(value(themeArgs));
break;
}
case ValueType.CONST:
templateExpressions.push(item.value);
break;
case ValueType.LAZY: {
const evaluatedValue = values.get(item.ex.name);
if (typeof evaluatedValue === 'function') {
templateExpressions.push(evaluatedValue(themeArgs));
} else {
templateExpressions.push(evaluatedValue as Primitive);
}
break;
}
default:
break;
}
} else if (item.type === 'TemplateElement') {
templateStrs.push(item.value.cooked as string);
// @ts-ignore
templateStrs.raw.push(item.value.raw);
}
});
const cssClassName = css(templateStrs, ...templateExpressions);
const cssText = cache.registered[cssClassName] as string;

const baseClass = this.getClassName();
this.baseClasses.push(baseClass);
this.collectedStyles.push([baseClass, cssText, null]);
const variantsAccumulator: VariantData[] = [];
this.processOverrides(values, variantsAccumulator);
variantsAccumulator.forEach((variant) => {
this.processVariant(variant);
});
this.generateArtifacts();
}

/**
* There are 2 main phases in Linaria's processing, Evaltime and Runtime. During Evaltime, Linaria prepares minimal code that gets evaluated to get the actual values of the styled arguments. Here, we mostly want to replace the styled calls with a simple string/object of its classname. This is necessary for class composition. For ex, you could potentially do this -
* There are 2 main phases in Wyw-in-JS's processing, Evaltime and Runtime. During Evaltime, Wyw-in-JS prepares minimal code that gets evaluated to get the actual values of the styled arguments. Here, we mostly want to replace the styled calls with a simple string/object of its classname. This is necessary for class composition. For ex, you could potentially do this -
* ```js
* const Component = styled(...)(...)
* const Component2 = styled()({
Expand All @@ -209,7 +304,7 @@ export class StyledProcessor extends BaseProcessor {
}

/**
* This is called by linaria after evaluating the code. Here, we
* This is called by Wyw-in-JS after evaluating the code. Here, we
* get access to the actual values of the `styled` arguments
* which we can use to generate our styles.
* Order of processing styles -
Expand All @@ -219,52 +314,25 @@ export class StyledProcessor extends BaseProcessor {
* 3. Variants declared in theme object
*/
build(values: ValueCache): void {
if (this.isTemplateTag) {
this.buildForTemplateTag(values);
return;
}
const themeImportIdentifier = this.astService.addDefaultImport(
`${process.env.PACKAGE_NAME}/theme`,
'theme',
);
// all the variant definitions are collected here so that we can
// apply variant styles after base styles for more specific targetting.
const variantsAccumulator: VariantData[] = [];
this.styleArgs.forEach((styleArg) => {
(this.styleArgs as ExpressionValue[]).forEach((styleArg) => {
this.processStyle(values, styleArg, variantsAccumulator, themeImportIdentifier.name);
});
this.processOverrides(values, variantsAccumulator);
variantsAccumulator.forEach((variant) => {
this.processVariant(variant);
});
const artifacts: [Rules, Replacements][] = this.collectedStyles.map(([className, cssText]) => {
const rules: Rules = {
[`.${className}`]: {
className,
cssText,
displayName: this.displayName,
start: this.location?.start ?? null,
},
};
// @TODO - Refactor for finer location tracking in original code.
const replacements: Replacements = [
{
length: cssText.length,
original: {
start: {
column: this.location?.start.column ?? 0,
line: this.location?.start.line ?? 0,
},
end: {
column: this.location?.end.column ?? 0,
line: this.location?.end.line ?? 0,
},
},
},
];
return [rules, replacements];
});
artifacts.forEach((artifact) => {
// linaria accesses artifacts array to get the final
// css definitions which are then exposed to the bundler.
this.artifacts.push(['css', artifact]);
});
this.generateArtifacts();
}

/**
Expand Down Expand Up @@ -306,15 +374,15 @@ export class StyledProcessor extends BaseProcessor {
typeof t.objectProperty | typeof t.spreadElement | typeof t.objectMethod
>[] = [];

if (this.baseClasses.length) {
const classNames = Array.from(new Set([this.className, ...this.baseClasses]));
argProperties.push(
t.objectProperty(
t.identifier('classes'),
t.arrayExpression(classNames.map((cls) => t.stringLiteral(cls))),
),
);
}
const classNames = Array.from(
new Set([this.className, ...(this.baseClasses.length ? this.baseClasses : [])]),
);
argProperties.push(
t.objectProperty(
t.identifier('classes'),
t.arrayExpression(classNames.map((cls) => t.stringLiteral(cls))),
),
);

const varProperties: ReturnType<typeof t.objectProperty>[] = this.collectedVariables.map(
([variableId, expression, isUnitLess]) =>
Expand Down Expand Up @@ -350,7 +418,6 @@ export class StyledProcessor extends BaseProcessor {
componentMetaExpression ? [componentName, componentMetaExpression] : [componentName],
);
const mainCall = t.callExpression(styledCall, [t.objectExpression(argProperties)]);

this.replacer(mainCall, true);
}

Expand Down
6 changes: 6 additions & 0 deletions packages/zero-runtime/src/styled.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type * as React from 'react';
import type { CSSObject } from './base';
import type { ThemeArgs } from './theme';
import type { SxProp } from './sx';
import { Primitve } from './keyframes';

type Falsy = false | 0 | '' | null | undefined;

Expand Down Expand Up @@ -58,6 +59,11 @@ export interface CreateStyledComponent<
Component extends React.ElementType,
OuterProps extends object,
> {
(
styles: TemplateStringsArray,
...args: Array<(options: ThemeArgs) => Primitve | Primitve | React.ComponentClass>
): StyledComponent<OuterProps> & (Component extends string ? BaseDefaultProps : Component);

/**
* @typeparam Props: Additional props to add to the styled component
*/
Expand Down
25 changes: 24 additions & 1 deletion packages/zero-runtime/tests/fixtures/styled.input.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { styled, keyframes } from '@mui/zero-runtime';
import { styled, keyframes, css } from '@mui/zero-runtime';

const rotateKeyframe = keyframes({
from: {
Expand All @@ -13,3 +13,26 @@ const Component = styled.div(({ theme }) => ({
color: theme.palette.primary.main,
animation: `${rotateKeyframe} 2s ease-out 0s infinite`,
}));

const cls1 = css`
color: ${({ theme }) => theme.palette.primary.main};
font-size: ${({ theme }) => theme.size.font.h1};
`;

const SliderRail = styled('span', {
name: 'MuiSlider',
slot: 'Rail',
})`
display: block;
position: absolute;
border-radius: inherit;
background-color: currentColor;
opacity: 0.38;
font-size: ${({ theme }) => theme.size.font.h1};
`;

const SliderRail2 = styled.span`
display: block;
opacity: 0.38;
font-size: ${({ theme }) => theme.size.font.h1};
`;
4 changes: 4 additions & 0 deletions packages/zero-runtime/tests/fixtures/styled.output.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
@keyframes r1yjyf7p{from{transform:rotate(360deg);}to{transform:rotate(0deg);}}
.cir471u{color:red;animation:r1yjyf7p 2s ease-out 0s infinite;}
.c1xj10ek{color:red;font-size:3rem;}
.sefdpty{display:block;position:absolute;border-radius:inherit;background-color:currentColor;opacity:0.38;font-size:3rem;}
.sefdpty-1{font-size:3rem;}
.s13fhnbp{display:block;opacity:0.38;font-size:3rem;}
12 changes: 12 additions & 0 deletions packages/zero-runtime/tests/fixtures/styled.output.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { styled as _styled3 } from "@mui/zero-runtime";
import { styled as _styled2 } from "@mui/zero-runtime";
import { styled as _styled } from "@mui/zero-runtime";
import _theme from "@mui/zero-runtime/theme";
const Component = /*#__PURE__*/_styled("div")({
classes: ["cir471u"]
});
const cls1 = "c1xj10ek";
const SliderRail = /*#__PURE__*/_styled2("span", {
name: 'MuiSlider',
slot: 'Rail'
})({
classes: ["sefdpty", "sefdpty-1"]
});
const SliderRail2 = /*#__PURE__*/_styled3("span")({
classes: ["s13fhnbp"]
});
16 changes: 15 additions & 1 deletion packages/zero-runtime/tests/zero-runtime.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ const theme = {
main: 'red',
},
},
size: {
font: {
h1: '3rem',
},
},
components: {
MuiSlider: {
styleOverrides: {
rail: {
fontSize: '3rem',
},
},
},
},
};

describe('zero-runtime', () => {
Expand Down Expand Up @@ -59,8 +73,8 @@ describe('zero-runtime', () => {
asyncResolveFallback,
);

expect(result.code.trim()).to.equal(outputContent.trim());
expect(result.cssText).to.equal(outputCssContent);
expect(result.code.trim()).to.equal(outputContent.trim());
});
});
});

0 comments on commit 2ba9b0f

Please sign in to comment.