From ff9998880b4cde24e2639249b64edbb54ba447bc Mon Sep 17 00:00:00 2001 From: Brijesh Bittu Date: Thu, 12 Sep 2024 17:15:54 +0530 Subject: [PATCH] [react] Fix css extraction for trasformed styled tagged-template call --- .../src/processors/styled.ts | 143 ++++++++++++++---- ...led-swc-transformed-tagged-string.input.js | 88 +++++++++++ ...d-swc-transformed-tagged-string.output.css | 78 ++++++++++ ...ed-swc-transformed-tagged-string.output.js | 21 +++ .../tests/styled/styled.test.tsx | 32 ++++ packages/pigment-css-unplugin/src/index.ts | 2 +- 6 files changed, 334 insertions(+), 30 deletions(-) create mode 100644 packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.input.js create mode 100644 packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.output.css create mode 100644 packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.output.js diff --git a/packages/pigment-css-react/src/processors/styled.ts b/packages/pigment-css-react/src/processors/styled.ts index f439f24a..813f09eb 100644 --- a/packages/pigment-css-react/src/processors/styled.ts +++ b/packages/pigment-css-react/src/processors/styled.ts @@ -14,6 +14,7 @@ import { IOptions as IBaseOptions, } from '@wyw-in-js/processor-utils'; import { + type FunctionValue, ValueType, type ConstValue, type ExpressionValue, @@ -238,7 +239,35 @@ export class StyledProcessor extends BaseProcessor { }); } - private buildForTemplateTag(values: ValueCache): void { + private buildTemplateTag( + templateStrs: TemplateStringsArray, + templateExpressions: Primitive[], + values: ValueCache, + ) { + 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(); + } + + /** + * This handles transformation for direct tagged-template literal styled calls. + * + * @example + * ```js + * const Component = style('a')` + * color: red; + * `; + */ + private buildForTemplateTag(values: ValueCache) { const templateStrs: string[] = []; // @ts-ignore @TODO - Fix this. No idea how to initialize a Tagged String array. templateStrs.raw = []; @@ -274,14 +303,74 @@ export class StyledProcessor extends BaseProcessor { templateStrs.raw.push(item.value.raw); } }); - const cssClassName = css(templateStrs, ...templateExpressions); - const cssText = cache.registered[cssClassName] as string; + this.buildTemplateTag( + templateStrs as unknown as TemplateStringsArray, + templateExpressions, + values, + ); + } - const baseClass = this.getClassName(); - this.baseClasses.push(baseClass); - this.collectedStyles.push([baseClass, cssText, null]); - const variantsAccumulator: VariantData[] = []; + /** + * This handles transformation for tagged-template literal styled calls that have already been + * transformed through swc. See [styled-swc-transformed-tagged-string.input.js](../../tests/styled/fixtures/styled-swc-transformed-tagged-string.input.js) + * for sample code. + */ + private buildForTransformedTemplateTag(values: ValueCache) { + // the below types are already validated in isMaybeTransformedTemplateLiteral check + const [templateStrArg, ...restArgs] = this.styleArgs as (LazyValue | FunctionValue)[]; + const templateStrings = values.get(templateStrArg.ex.name) as string[]; + + const templateStrs: string[] = [...templateStrings]; + // @ts-ignore @TODO - Fix this. No idea how to initialize a Tagged String array. + templateStrs.raw = [...templateStrings]; + const templateExpressions: Primitive[] = []; + const { themeArgs } = this.options as IOptions; + + restArgs.forEach((item) => { + switch (item.kind) { + case ValueType.FUNCTION: { + const value = values.get(item.ex.name) as TemplateCallback; + templateExpressions.push(value(themeArgs)); + 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; + } + }); + this.buildTemplateTag( + templateStrs as unknown as TemplateStringsArray, + templateExpressions, + values, + ); + } + + private buildForStyledCall(values: ValueCache) { + const themeImportIdentifier = this.astService.addDefaultImport( + `${this.getImportPath()}/theme`, + 'theme', + ); + // all the variant definitions are collected here so that we can + // apply variant styles after base styles for more specific targetting. + let variantsAccumulator: VariantData[] = []; + (this.styleArgs as ExpressionValue[]).forEach((styleArg) => { + this.processStyle(values, styleArg, variantsAccumulator, themeImportIdentifier.name); + }); + // Generate CSS for default variants first + variantsAccumulator.forEach((variant) => { + this.processVariant(variant); + }); + variantsAccumulator = []; this.processOverrides(values, variantsAccumulator); + // Generate CSS for variants declared in `styleOverrides`, if any variantsAccumulator.forEach((variant) => { this.processVariant(variant); }); @@ -304,6 +393,18 @@ export class StyledProcessor extends BaseProcessor { this.replacer(this.astService.stringLiteral(this.asSelector), false); } + isMaybeTransformedTemplateLiteral(values: ValueCache): boolean { + const [firstArg, ...restArgs] = this.styleArgs; + if (!('kind' in firstArg) || firstArg.kind === ValueType.CONST) { + return false; + } + const firstArgVal = values.get(firstArg.ex.name); + if (Array.isArray(firstArgVal) && restArgs.length === firstArgVal.length - 1) { + return true; + } + return false; + } + /** * This is called by Wyw-in-JS after evaluating the code. Here, we * get access to the actual values of the `styled` arguments @@ -314,32 +415,16 @@ export class StyledProcessor extends BaseProcessor { * 2. CSS declared in theme object's styledOverrides * 3. Variants declared in theme object */ - build(values: ValueCache): void { + build(values: ValueCache) { if (this.isTemplateTag) { this.buildForTemplateTag(values); return; } - const themeImportIdentifier = this.astService.addDefaultImport( - `${this.getImportPath()}/theme`, - 'theme', - ); - // all the variant definitions are collected here so that we can - // apply variant styles after base styles for more specific targetting. - let variantsAccumulator: VariantData[] = []; - (this.styleArgs as ExpressionValue[]).forEach((styleArg) => { - this.processStyle(values, styleArg, variantsAccumulator, themeImportIdentifier.name); - }); - // Generate CSS for default variants first - variantsAccumulator.forEach((variant) => { - this.processVariant(variant); - }); - variantsAccumulator = []; - this.processOverrides(values, variantsAccumulator); - // Generate CSS for variants declared in `styleOverrides`, if any - variantsAccumulator.forEach((variant) => { - this.processVariant(variant); - }); - this.generateArtifacts(); + if (this.isMaybeTransformedTemplateLiteral(values)) { + this.buildForTransformedTemplateTag(values); + return; + } + this.buildForStyledCall(values); } /** diff --git a/packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.input.js b/packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.input.js new file mode 100644 index 00000000..7ec230c2 --- /dev/null +++ b/packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.input.js @@ -0,0 +1,88 @@ +/** + * This is a pre-transformed file for testing. + */ +import { _ as _tagged_template_literal } from "@swc/helpers/_/_tagged_template_literal"; +import { styled, keyframes } from '@pigment-css/react'; + +function _templateObject() { + const data = _tagged_template_literal(["\n 0% {\n transform: scale(0);\n opacity: 0.1;\n }\n\n 100% {\n transform: scale(1);\n opacity: 0.3;\n }\n"]); + _templateObject = function () { + return data; + }; + return data; +} +function _templateObject1() { + const data = _tagged_template_literal(["\n 0% {\n opacity: 1;\n }\n\n 100% {\n opacity: 0;\n }\n"]); + _templateObject1 = function () { + return data; + }; + return data; +} +function _templateObject2() { + const data = _tagged_template_literal(["\n 0% {\n transform: scale(1);\n }\n\n 50% {\n transform: scale(0.92);\n }\n\n 100% {\n transform: scale(1);\n }\n"]); + _templateObject2 = function () { + return data; + }; + return data; +} +function _templateObject3() { + const data = _tagged_template_literal(["\n opacity: 0;\n position: absolute;\n\n &.", " {\n opacity: 0.3;\n transform: scale(1);\n animation-name: ", ";\n animation-duration: ", "ms;\n animation-timing-function: ", ";\n }\n\n &.", " {\n animation-duration: ", "ms;\n }\n\n & .", " {\n opacity: 1;\n display: block;\n width: 100%;\n height: 100%;\n border-radius: 50%;\n background-color: currentColor;\n }\n\n & .", " {\n opacity: 0;\n animation-name: ", ";\n animation-duration: ", "ms;\n animation-timing-function: ", ";\n }\n\n & .", " {\n position: absolute;\n /* @noflip */\n left: 0px;\n top: 0;\n animation-name: ", ";\n animation-duration: 2500ms;\n animation-timing-function: ", ";\n animation-iteration-count: infinite;\n animation-delay: 200ms;\n }\n"]); + _templateObject3 = function () { + return data; + }; + return data; +} + +const touchRippleClasses = { + rippleVisible: 'MuiTouchRipple.rippleVisible', + ripplePulsate: 'MuiTouchRipple.ripplePulsate', + child: 'MuiTouchRipple.child', + childLeaving: 'MuiTouchRipple.childLeaving', + childPulsate: 'MuiTouchRipple.childPulsate', +}; + +const enterKeyframe = keyframes(_templateObject()); +const exitKeyframe = keyframes(_templateObject1()); +const pulsateKeyframe = keyframes(_templateObject2()); + +export const TouchRippleRoot = styled('span', { + name: 'MuiTouchRipple', + slot: 'Root' +})({ + overflow: 'hidden', + pointerEvents: 'none', + position: 'absolute', + zIndex: 0, + top: 0, + right: 0, + bottom: 0, + left: 0, + borderRadius: 'inherit' +}); + +// This `styled()` function invokes keyframes. `styled-components` only supports keyframes +// in string templates. Do not convert these styles in JS object as it will break. +export const TouchRippleRipple = styled(Ripple, { + name: 'MuiTouchRipple', + slot: 'Ripple' +})(_templateObject3(), touchRippleClasses.rippleVisible, enterKeyframe, DURATION, param => { + let { + theme + } = param; + return theme.transitions.easing.easeInOut; +}, touchRippleClasses.ripplePulsate, param => { + let { + theme + } = param; + return theme.transitions.duration.shorter; +}, touchRippleClasses.child, touchRippleClasses.childLeaving, exitKeyframe, DURATION, param => { + let { + theme + } = param; + return theme.transitions.easing.easeInOut; +}, touchRippleClasses.childPulsate, pulsateKeyframe, param => { + let { + theme + } = param; + return theme.transitions.easing.easeInOut; +}); diff --git a/packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.output.css b/packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.output.css new file mode 100644 index 00000000..6a99bc3e --- /dev/null +++ b/packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.output.css @@ -0,0 +1,78 @@ +@keyframes e19ejdhp { + 0% { + transform: scale(0); + opacity: 0.1; + } + 100% { + transform: scale(1); + opacity: 0.3; + } +} +@keyframes e1rvu9kp { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} +@keyframes p1yhz964 { + 0% { + transform: scale(1); + } + 50% { + transform: scale(0.92); + } + 100% { + transform: scale(1); + } +} +.ttz155n { + overflow: hidden; + pointer-events: none; + position: absolute; + z-index: 0; + top: 0; + right: 0; + bottom: 0; + left: 0; + border-radius: inherit; +} +.tm7990f { + opacity: 0; + position: absolute; +} +.tm7990f.MuiTouchRipple.rippleVisible { + opacity: 0.3; + transform: scale(1); + animation-name: e19ejdhp; + animation-duration: ms; + animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} +.tm7990f.MuiTouchRipple.ripplePulsate { + animation-duration: 200ms; +} +.tm7990f .MuiTouchRipple.child { + opacity: 1; + display: block; + width: 100%; + height: 100%; + border-radius: 50%; + background-color: currentColor; +} +.tm7990f .MuiTouchRipple.childLeaving { + opacity: 0; + animation-name: e1rvu9kp; + animation-duration: ms; + animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} +.tm7990f .MuiTouchRipple.childPulsate { + position: absolute; + left: 0px; + top: 0; + animation-name: p1yhz964; + animation-duration: 2500ms; + animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + animation-iteration-count: infinite; + animation-delay: 200ms; +} diff --git a/packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.output.js b/packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.output.js new file mode 100644 index 00000000..88887a5b --- /dev/null +++ b/packages/pigment-css-react/tests/styled/fixtures/styled-swc-transformed-tagged-string.output.js @@ -0,0 +1,21 @@ +import { styled as _styled, styled as _styled2 } from '@pigment-css/react'; +import _theme from '@pigment-css/react/theme'; +/** + * This is a pre-transformed file for testing. + */ +export const TouchRippleRoot = /*#__PURE__*/ _styled('span', { + name: 'MuiTouchRipple', + slot: 'Root', +})({ + classes: ['ttz155n'], +}); + +// This `styled()` function invokes keyframes. `styled-components` only supports keyframes +// in string templates. Do not convert these styles in JS object as it will break. +const _exp6 = /*#__PURE__*/ () => Ripple; +export const TouchRippleRipple = /*#__PURE__*/ _styled2(_exp6(), { + name: 'MuiTouchRipple', + slot: 'Ripple', +})({ + classes: ['tm7990f'], +}); diff --git a/packages/pigment-css-react/tests/styled/styled.test.tsx b/packages/pigment-css-react/tests/styled/styled.test.tsx index c12f42ed..933f8abd 100644 --- a/packages/pigment-css-react/tests/styled/styled.test.tsx +++ b/packages/pigment-css-react/tests/styled/styled.test.tsx @@ -154,4 +154,36 @@ describe('Pigment CSS - styled', () => { expect(output.js).to.equal(fixture.js); expect(output.css).to.equal(fixture.css); }); + + it('should properly transform a pre-transform tagged-template styled call', async () => { + const { output, fixture } = await runTransformation( + path.join(__dirname, 'fixtures/styled-swc-transformed-tagged-string.input.js'), + { + themeArgs: { + theme: { + transitions: { + easing: { + easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)', + easeOut: 'cubic-bezier(0.0, 0, 0.2, 1)', + easeIn: 'cubic-bezier(0.4, 0, 1, 1)', + sharp: 'cubic-bezier(0.4, 0, 0.6, 1)', + }, + duration: { + shortest: 150, + shorter: 200, + short: 250, + standard: 300, + complex: 375, + enteringScreen: 225, + leavingScreen: 195, + }, + }, + }, + }, + }, + ); + + expect(output.js).to.equal(fixture.js); + expect(output.css).to.equal(fixture.css); + }); }); diff --git a/packages/pigment-css-unplugin/src/index.ts b/packages/pigment-css-unplugin/src/index.ts index 45768174..08b426e0 100644 --- a/packages/pigment-css-unplugin/src/index.ts +++ b/packages/pigment-css-unplugin/src/index.ts @@ -116,7 +116,7 @@ export const plugin = createUnplugin((options) => { theme, meta, transformLibraries = [], - preprocessor, + preprocessor = basePreprocessor, asyncResolve: asyncResolveOpt, debug = false, sourceMap = false,