Skip to content

Commit

Permalink
[zero][system] Add support for keyframes (#39155)
Browse files Browse the repository at this point in the history
  • Loading branch information
brijeshb42 authored Oct 2, 2023
1 parent 6c70f79 commit 0124b79
Show file tree
Hide file tree
Showing 17 changed files with 325 additions and 79 deletions.
4 changes: 1 addition & 3 deletions apps/zero-runtime-next-app/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ const theme = createTheme({
// @ts-ignore
theme.applyDarkStyles = function applyDarkStyles(obj) {
return {
// @TODO - Use custom stylis plugin as in docs/src/createEmotionCache.ts
// so that we don't need to use *
'* :where([data-mui-color-scheme="dark"]) &': obj,
':where([data-mui-color-scheme="dark"]) &': obj,
};
};

Expand Down
34 changes: 32 additions & 2 deletions apps/zero-runtime-vite-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
import * as React from 'react';
import { styled } from '@mui/zero-runtime';
import { styled, keyframes } from '@mui/zero-runtime';
import Slider from './Slider/ZeroSlider';

const bounce = keyframes`
from, 20%, 53%, 80%, to {
transform: translate3d(0,0,0);
}
40%, 43% {
transform: translate3d(0, -30px, 0);
}
70% {
transform: translate3d(0, -15px, 0);
}
90% {
transform: translate3d(0,-4px,0);
}
`;

const bounceAnim = keyframes({
'from, 20%, 53%, 80%, to': {
transform: 'translate3d(0,0,0)',
},
'40%, 43%': {
transform: 'translate3d(0, -30px, 0)',
},
'70%': {
transform: 'translate3d(0, -15px, 0)',
},
'90%': {
transform: 'translate3d(0,-4px,0)',
},
});

const Button = styled('button', {
name: 'MuiButton',
slot: 'Root',
})(
'color:red',
({ theme }: any) => ({
fontFamily: 'sans-serif',
backgroundColor: [theme.palette.primary.main, 'text.primary', 'background.paper'],
Expand All @@ -30,6 +59,7 @@ const HalfWidth = styled.div({
maxHeight: 100,
padding: 20,
border: '1px solid #ccc',
animation: [`${bounce} 1s ease infinite`, `${bounceAnim} 1s ease infinite`],
});

export default function App({ isRed }: any) {
Expand Down
4 changes: 1 addition & 3 deletions apps/zero-runtime-vite-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ const theme = createTheme();
// @ts-ignore
theme.applyDarkStyles = function applyDarkStyles(obj) {
return {
// @TODO - Use custom stylis plugin as in docs/src/createEmotionCache.ts
// so that we don't need to use *
'* :where([data-mui-color-scheme="dark"]) &': obj,
':where([data-mui-color-scheme="dark"]) &': obj,
};
};

Expand Down
3 changes: 2 additions & 1 deletion packages/zero-next-plugin/src/loaders/transformLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import type { PluginOptions, Preprocessor, Result } from '@linaria/babel-preset';
import { transform } from '@linaria/babel-preset';
import path from 'path';
import { preprocessor as baseProcessor } from '@mui/zero-tag-processor/preprocessor';
import { transformAsync as babelTransformAsync } from '@babel/core';
import type { RawLoaderDefinitionFunction } from 'webpack';

Expand Down Expand Up @@ -55,7 +56,7 @@ const transformLoader: LoaderType = function transformLoader(content, inputSourc

const {
sourceMap = undefined,
preprocessor = undefined,
preprocessor = baseProcessor,
moduleStore,
...rest
} = this.getOptions() || {};
Expand Down
3 changes: 2 additions & 1 deletion packages/zero-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
"tags": {
"styled": "@mui/zero-tag-processor/styled",
"default": "@mui/zero-tag-processor/styled",
"sx": "@mui/zero-tag-processor/sx"
"sx": "@mui/zero-tag-processor/sx",
"keyframes": "@mui/zero-tag-processor/keyframes"
}
}
}
3 changes: 2 additions & 1 deletion packages/zero-runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import styled from './styled';
import sx from './sx';
import keyframes from './keyframes';

export { styled, sx };
export { styled, sx, keyframes };
export default styled;
4 changes: 4 additions & 0 deletions packages/zero-runtime/src/keyframes.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// @TODO - Implement correct style definitions
type Primitve = string | null | undefined | boolean | number;
export default function keyframes(arg: Record<string, any>): string;
export default function keyframes(arg: TemplateStringsArray, ...templateArgs: Primitve[]): string;
5 changes: 5 additions & 0 deletions packages/zero-runtime/src/keyframes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function keyframes() {
throw new Error(
'MUI: You were trying to call "keyframes" function without configuring your bundler. Make sure to install the bundler specific plugin and use it. @mui/zero-vite-plugin for Vite integration or @mui/zero-next-plugin for Next.js integration.',
);
}
6 changes: 4 additions & 2 deletions packages/zero-tag-processor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"@linaria/tags": "^4.5.4",
"@linaria/utils": "^4.5.3",
"@mui/system": "^5.14.11",
"lodash.get": "^4.4.2"
"lodash.get": "^4.4.2",
"stylis": "^4.2.0"
},
"devDependencies": {
"@mui/material": "^5.14.11",
Expand All @@ -58,7 +59,8 @@
"@types/chai": "^4.3.6",
"@types/lodash.get": "^4.4.7",
"@types/mocha": "^10.0.1",
"@types/node": "^18.18.0"
"@types/node": "^18.18.0",
"@types/stylis": "^4.2.0"
},
"sideEffects": false,
"publishConfig": {
Expand Down
44 changes: 44 additions & 0 deletions packages/zero-tag-processor/src/base-processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
BaseProcessor as LinariaBaseProcessor,
toValidCSSIdentifier,
buildSlug,
} from '@linaria/tags';
import { slugify, type IVariableContext } from '@linaria/utils';

export default abstract class BaseProcessor extends LinariaBaseProcessor {
variableIdx = 0;

// Implementation taken from Linaria - https://github.com/callstack/linaria/blob/master/packages/react/src/processors/styled.ts#L284
protected getCustomVariableId(cssKey: string, source: string, hasUnit: boolean) {
const context = this.getVariableContext(cssKey, source, hasUnit);
const customSlugFn = this.options.variableNameSlug;
if (!customSlugFn) {
return toValidCSSIdentifier(`${this.slug}-${context.index}`);
}

return typeof customSlugFn === 'function'
? customSlugFn(context)
: buildSlug(customSlugFn, { ...context });
}

// Implementation taken from Linaria - https://github.com/callstack/linaria/blob/master/packages/react/src/processors/styled.ts#L362
protected getVariableContext(cssKey: string, source: string, hasUnit: boolean): IVariableContext {
const getIndex = () => {
// eslint-disable-next-line no-plusplus
return this.variableIdx++;
};

return {
componentName: this.displayName,
componentSlug: this.slug,
get index() {
return getIndex();
},
precedingCss: cssKey,
processor: this.constructor.name,
source: '',
unit: '',
valueSlug: slugify(`${source}${hasUnit}`),
};
}
}
154 changes: 154 additions & 0 deletions packages/zero-tag-processor/src/keyframes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import type { Expression } from '@babel/types';
import { validateParams } from '@linaria/tags';
import type {
CallParam,
TemplateParam,
Params,
TailProcessorParams,
ValueCache,
} from '@linaria/tags';
import type { Replacements, Rules } from '@linaria/utils';
import { ValueType } from '@linaria/utils';
import type { CSSInterpolation } from '@emotion/css';
import BaseProcessor from './base-processor';
import type { IOptions } from './styled';
import { cache, keyframes } from './utils/emotion';

type Primitive = string | number | boolean | null | undefined;

export default class KeyframesProcessor extends BaseProcessor {
callParam: CallParam | TemplateParam;

constructor(params: Params, ...args: TailProcessorParams) {
super(params, ...args);
if (params.length < 2) {
throw BaseProcessor.SKIP;
}
validateParams(
params,
['callee', ['call', 'template']],
`Invalid use of ${this.tagSource.imported} tag.`,
);

const [, callParams] = params;
if (callParams[0] === 'call') {
this.dependencies.push(callParams[1]);
} else if (callParams[0] === 'template') {
callParams[1].forEach((element) => {
if ('kind' in element && element.kind !== ValueType.CONST) {
this.dependencies.push(element);
}
});
}
this.callParam = callParams;
}

build(values: ValueCache) {
if (this.artifacts.length > 0) {
throw new Error('Tag is already built');
}

const [callType] = this.callParam;

if (callType === 'template') {
this.handleTemplate(this.callParam, values);
} else {
this.handleCall(this.callParam, values);
}
}

private handleTemplate([, callArgs]: TemplateParam, values: ValueCache) {
const templateStrs: string[] = [];
const templateExpressions: Primitive[] = [];
callArgs.forEach((item) => {
if ('kind' in item) {
switch (item.kind) {
case ValueType.FUNCTION:
throw item.buildCodeFrameError(
'Functions are not allowed to be interpolated in keyframes tag.',
);
case ValueType.CONST:
templateExpressions.push(item.value);
break;
case ValueType.LAZY: {
const evaluatedValue = values.get(item.ex.name);
if (typeof evaluatedValue === 'function') {
throw item.buildCodeFrameError(
'Functions are not allowed to be interpolated in keyframes tag.',
);
} else {
templateExpressions.push(evaluatedValue as Primitive);
}
break;
}
default:
break;
}
} else if (item.type === 'TemplateElement') {
templateStrs.push(item.value.cooked as string);
}
});
this.generateArtifacts(templateStrs, ...templateExpressions);
}

generateArtifacts(styleObjOrTaggged: CSSInterpolation | string[], ...args: Primitive[]) {
const keyframeName = keyframes(styleObjOrTaggged, ...args);
const cacheCssText = cache.inserted[keyframeName.replace('animation-', '')] as string;
const cssText = cacheCssText.replaceAll(keyframeName, '');

const rules: Rules = {
[this.asSelector]: {
className: this.className,
cssText,
displayName: this.displayName,
start: this.location?.start ?? null,
},
};
const sourceMapReplacements: 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,
},
},
},
];
this.artifacts.push(['css', [rules, sourceMapReplacements]]);
}

private handleCall([, callArg]: CallParam, values: ValueCache) {
let styleObj: CSSInterpolation;
if (callArg.kind === ValueType.LAZY) {
styleObj = values.get(callArg.ex.name) as CSSInterpolation;
} else if (callArg.kind === ValueType.FUNCTION) {
const { themeArgs } = this.options as IOptions;
const value = values.get(callArg.ex.name) as Function;
styleObj = value(themeArgs) as CSSInterpolation;
}
if (styleObj) {
this.generateArtifacts(styleObj);
}
}

doEvaltimeReplacement() {
this.replacer(this.value, false);
}

doRuntimeReplacement() {
this.doEvaltimeReplacement();
}

get asSelector() {
return this.className;
}

get value(): Expression {
return this.astService.stringLiteral(this.className);
}
}
29 changes: 29 additions & 0 deletions packages/zero-tag-processor/src/preprocessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Element } from 'stylis';
import { serialize, compile, stringify, middleware } from 'stylis';

function globalSelector(element: Element) {
switch (element.type) {
case 'rule':
element.props = (element.props as string[]).map((value: any) => {
if (value.match(/(:where|:is)\(/)) {
value = value.replace(/\.[^:]+(:where|:is)/, '$1');
return value;
}
return value;
});
break;
default:
break;
}
}

const serializer = middleware([globalSelector, stringify]);

const stylis = (css: string) => serialize(compile(css), serializer);

export function preprocessor(selector: string, cssText: string) {
if (cssText.startsWith('@keyframes')) {
return stylis(cssText.replace('@keyframes', `@keyframes ${selector}`));
}
return stylis(`${selector}{${cssText}}`);
}
Loading

0 comments on commit 0124b79

Please sign in to comment.