From ece6a19b15383560859c7a4276cf4f56d83f4a75 Mon Sep 17 00:00:00 2001 From: Brijesh Bittu Date: Mon, 27 May 2024 21:51:15 +0530 Subject: [PATCH] [core] Enable support for base and variant styles through css layers. Behind a feature-flag right now but the aim is to make this default before v1 release. --- packages/pigment-css-react/package.json | 2 + .../src/processors/styled.ts | 21 ++++++--- .../src/utils/cssFnValueToVariable.ts | 7 +++ .../src/utils/generateCss.ts | 8 +++- .../fixtures/styled-variants-layer.input.js | 34 ++++++++++++++ .../fixtures/styled-variants-layer.output.css | 31 ++++++++++++ .../fixtures/styled-variants-layer.output.js | 34 ++++++++++++++ .../tests/styled/styled.test.tsx | 17 +++++++ packages/pigment-css-react/tests/testUtils.ts | 16 +++---- packages/pigment-css-unplugin/src/index.ts | 4 +- packages/pigment-css-vite-plugin/src/index.ts | 2 +- pnpm-lock.yaml | 47 +++++++++++++++++-- 12 files changed, 199 insertions(+), 24 deletions(-) create mode 100644 packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.input.js create mode 100644 packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.output.css create mode 100644 packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.output.js diff --git a/packages/pigment-css-react/package.json b/packages/pigment-css-react/package.json index 4c5b47ed..b44732af 100644 --- a/packages/pigment-css-react/package.json +++ b/packages/pigment-css-react/package.json @@ -50,6 +50,8 @@ "cssesc": "^3.0.0", "csstype": "^3.1.3", "lodash": "^4.17.21", + "postcss": "^8.4.38", + "postcss-merge-rules": "^7.0.0", "stylis": "^4.3.1", "stylis-plugin-rtl": "^2.1.1" }, diff --git a/packages/pigment-css-react/src/processors/styled.ts b/packages/pigment-css-react/src/processors/styled.ts index 1168b9ae..b8001321 100644 --- a/packages/pigment-css-react/src/processors/styled.ts +++ b/packages/pigment-css-react/src/processors/styled.ts @@ -310,9 +310,9 @@ export class StyledProcessor extends BaseProcessor { * which we can use to generate our styles. * Order of processing styles - * 1. CSS directly declared in styled call - * 3. Variants declared in styled call - * 2. CSS declared in theme object's styledOverrides - * 3. Variants declared in theme object + * 2. Variants declared in styled call + * 3. CSS declared in theme object's styledOverrides + * 4. Variants declared in theme object */ build(values: ValueCache): void { if (this.isTemplateTag) { @@ -449,6 +449,7 @@ export class StyledProcessor extends BaseProcessor { variantsAccumulator, themeImportIdentifier, ); + const className = this.getClassName(); this.baseClasses.push(className); this.collectedStyles.push([className, finalStyle, styleArg]); @@ -523,8 +524,8 @@ export class StyledProcessor extends BaseProcessor { styleArg: ExpressionValue | null, variantsAccumulator?: VariantData[], themeImportIdentifier?: string, - ) { - const { themeArgs = {} } = this.options as IOptions; + ): string { + const { themeArgs = {}, experiments = {} } = this.options as IOptions; const styleObj = typeof styleObjOrFn === 'function' ? styleObjOrFn(themeArgs) : styleObjOrFn; if (!styleObj) { return ''; @@ -551,7 +552,15 @@ export class StyledProcessor extends BaseProcessor { if (res.length) { this.collectedVariables.push(...res); } - return processCssObject(styleObj, themeArgs); + const cssText = processCssObject(styleObj, themeArgs); + + if (experiments.styleLayers) { + if (variantsAccumulator) { + return `@layer pigment-base {${cssText}}`; + } + return `@layer pigment-variant {${cssText}}`; + } + return cssText; } public override get asSelector(): string { diff --git a/packages/pigment-css-react/src/utils/cssFnValueToVariable.ts b/packages/pigment-css-react/src/utils/cssFnValueToVariable.ts index c439ccad..f9bacd8f 100644 --- a/packages/pigment-css-react/src/utils/cssFnValueToVariable.ts +++ b/packages/pigment-css-react/src/utils/cssFnValueToVariable.ts @@ -38,6 +38,13 @@ export type PluginCustomOptions = { */ getDirSelector?: (dir: 'ltr' | 'rtl') => string; }; + experiments?: { + /** + * Wrap generated CSS in layers so that all the base styles come before all the variant styles. + * Experimental right now, but will be default by v1 release. + */ + styleLayers?: boolean; + }; }; type CssFnValueToVariableParams = { diff --git a/packages/pigment-css-react/src/utils/generateCss.ts b/packages/pigment-css-react/src/utils/generateCss.ts index b8d9394e..2383311a 100644 --- a/packages/pigment-css-react/src/utils/generateCss.ts +++ b/packages/pigment-css-react/src/utils/generateCss.ts @@ -1,13 +1,17 @@ import { serializeStyles } from '@emotion/serialize'; import { Theme } from './extendTheme'; +import { PluginCustomOptions } from './cssFnValueToVariable'; -export function generateTokenCss(theme?: Theme) { +export function generateTokenCss( + theme?: Theme, + experiments: PluginCustomOptions['experiments'] = {}, +) { if (!theme) { return ''; } // use emotion to serialize the object to css string const { styles } = serializeStyles(theme.generateStyleSheets?.() || []); - return styles; + return experiments.styleLayers ? `@layer pigment-base, pigment-variant;\n${styles}` : styles; } export function generateThemeTokens(theme?: Theme) { diff --git a/packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.input.js b/packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.input.js new file mode 100644 index 00000000..bf508f93 --- /dev/null +++ b/packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.input.js @@ -0,0 +1,34 @@ +import { styled } from '@pigment-css/react'; + +const SliderRail = styled('span', { + name: 'MuiSlider', + slot: 'Rail', +})({ + color: 'red', + variants: [ + { + props: { color: 'primary' }, + style: { + color: 'tomato', + }, + }, + { + props: ({ ownerState }) => ownerState.color === 'secondary', + style: { + color: 'salmon', + }, + }, + ], +}); + +export const SliderOverride = styled(SliderRail)({ + color: 'blue', + variants: [ + { + props: { color: 'primary' }, + style: { + color: 'indigo', + }, + }, + ], +}); diff --git a/packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.output.css b/packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.output.css new file mode 100644 index 00000000..00be8403 --- /dev/null +++ b/packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.output.css @@ -0,0 +1,31 @@ +@layer pigment-base, pigment-variant; +@layer pigment-base { + .srxh6zy { + color: red; + } +} +@layer pigment-variant { + .srxh6zy-1 { + color: tomato; + } +} +@layer pigment-variant { + .srxh6zy-2 { + color: salmon; + } +} +@layer pigment-base { + .srxh6zy-3 { + font-size: 1.5rem; + } +} +@layer pigment-base { + .s1q936we { + color: blue; + } +} +@layer pigment-variant { + .s1q936we-1 { + color: indigo; + } +} diff --git a/packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.output.js b/packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.output.js new file mode 100644 index 00000000..56032ecf --- /dev/null +++ b/packages/pigment-css-react/tests/styled/fixtures/styled-variants-layer.output.js @@ -0,0 +1,34 @@ +import { styled as _styled2 } from '@pigment-css/react'; +import _theme2 from '@pigment-css/react/theme'; +import { styled as _styled } from '@pigment-css/react'; +import _theme from '@pigment-css/react/theme'; +const SliderRail = /*#__PURE__*/ _styled('span', { + name: 'MuiSlider', + slot: 'Rail', +})({ + classes: ['srxh6zy', 'srxh6zy-3'], + variants: [ + { + props: { + color: 'primary', + }, + className: 'srxh6zy-1', + }, + { + props: ({ ownerState }) => ownerState.color === 'secondary', + className: 'srxh6zy-2', + }, + ], +}); +const _exp3 = /*#__PURE__*/ () => SliderRail; +export const SliderOverride = /*#__PURE__*/ _styled2(_exp3())({ + classes: ['s1q936we'], + variants: [ + { + props: { + color: 'primary', + }, + className: 's1q936we-1', + }, + ], +}); diff --git a/packages/pigment-css-react/tests/styled/styled.test.tsx b/packages/pigment-css-react/tests/styled/styled.test.tsx index 81eb252e..25c2c381 100644 --- a/packages/pigment-css-react/tests/styled/styled.test.tsx +++ b/packages/pigment-css-react/tests/styled/styled.test.tsx @@ -82,6 +82,23 @@ describe('Pigment CSS - styled', () => { expect(output.css).to.equal(fixture.css); }); + it('should work with variants with layers enabled', async () => { + const { output, fixture } = await runTransformation( + path.join(__dirname, 'fixtures/styled-variants-layer.input.js'), + { + themeArgs: { + theme, + }, + experiments: { + styleLayers: true, + }, + }, + ); + + expect(output.js).to.equal(fixture.js); + expect(output.css).to.equal(fixture.css); + }); + it('should work with theme styleOverrides', async () => { const { output, fixture } = await runTransformation( path.join(__dirname, 'fixtures/styled-theme-styleOverrides.input.js'), diff --git a/packages/pigment-css-react/tests/testUtils.ts b/packages/pigment-css-react/tests/testUtils.ts index fd7db17c..9408418a 100644 --- a/packages/pigment-css-react/tests/testUtils.ts +++ b/packages/pigment-css-react/tests/testUtils.ts @@ -8,7 +8,7 @@ import { transform as wywTransform, createFileReporter, } from '@wyw-in-js/transform'; -import { PluginCustomOptions, preprocessor } from '@pigment-css/react/utils'; +import { PluginCustomOptions, preprocessor, generateTokenCss } from '@pigment-css/react/utils'; import * as prettier from 'prettier'; import sxTransformPlugin from '../exports/sx-plugin'; @@ -24,10 +24,7 @@ function runSxTransform(code: string, filename: string) { }); } -export async function runTransformation( - absolutePath: string, - options?: { themeArgs?: { theme?: any }; css?: PluginCustomOptions['css'] }, -) { +export async function runTransformation(absolutePath: string, options?: PluginCustomOptions) { const cache = new TransformCacheCollection(); const { emitter: eventEmitter } = createFileReporter(false); const inputFilePath = absolutePath; @@ -43,9 +40,7 @@ export async function runTransformation( const babelResult = await runSxTransform(inputContent, inputFilePath); const pluginOptions = { - themeArgs: { - theme: options?.themeArgs?.theme, - }, + ...options, babelOptions: { configFile: false, babelrc: false, @@ -80,7 +75,10 @@ export async function runTransformation( ...prettierConfig, parser: 'babel', }); - const formattedCss = await prettier.format(result.cssText ?? '', { + const baseCss = generateTokenCss(options?.themeArgs?.theme ?? {}, options?.experiments); + const originalCss = baseCss + result.cssText ?? ''; + + const formattedCss = await prettier.format(originalCss, { ...prettierConfig, parser: 'css', }); diff --git a/packages/pigment-css-unplugin/src/index.ts b/packages/pigment-css-unplugin/src/index.ts index fe1d88b6..c2497ac5 100644 --- a/packages/pigment-css-unplugin/src/index.ts +++ b/packages/pigment-css-unplugin/src/index.ts @@ -345,7 +345,7 @@ export const plugin = createUnplugin((options) => { }, transform(_code, id) { if (id.endsWith('styles.css')) { - return theme ? generateTokenCss(theme) : _code; + return theme ? generateTokenCss(theme, options.experiments) : _code; } if (id.includes('pigment-css-react/theme')) { return `export default ${ @@ -370,7 +370,7 @@ export const plugin = createUnplugin((options) => { }, load(id) { if (id === VIRTUAL_CSS_FILE && theme) { - return generateTokenCss(theme); + return generateTokenCss(theme, options.experiments); } if (id === VIRTUAL_THEME_FILE) { return `export default ${ diff --git a/packages/pigment-css-vite-plugin/src/index.ts b/packages/pigment-css-vite-plugin/src/index.ts index 0e4e0583..5318fdde 100644 --- a/packages/pigment-css-vite-plugin/src/index.ts +++ b/packages/pigment-css-vite-plugin/src/index.ts @@ -62,7 +62,7 @@ export function pigment(options: PigmentOptions) { }, load(id) { if (id === VIRTUAL_CSS_FILE) { - return generateTokenCss(theme); + return generateTokenCss(theme, options.experiments); } if (id === VIRTUAL_THEME_FILE) { return `export default ${JSON.stringify(generateThemeTokens(theme))};`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c19774f5..a24746e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -472,6 +472,12 @@ importers: lodash: specifier: ^4.17.21 version: 4.17.21 + postcss: + specifier: ^8.4.38 + version: 8.4.38 + postcss-merge-rules: + specifier: ^7.0.0 + version: 7.0.0(postcss@8.4.38) stylis: specifier: ^4.3.1 version: 4.3.1 @@ -6108,6 +6114,15 @@ packages: engines: {node: '>=14.16'} dev: true + /caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + dependencies: + browserslist: 4.23.0 + caniuse-lite: 1.0.30001608 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + dev: false + /caniuse-lite@1.0.30001608: resolution: {integrity: sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==} @@ -6760,6 +6775,15 @@ packages: engines: {node: '>=10.0.0'} dev: false + /cssnano-utils@5.0.0(postcss@8.4.38): + resolution: {integrity: sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + dependencies: + postcss: 8.4.38 + dev: false + /cssstyle@4.0.1: resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==} engines: {node: '>=18'} @@ -10244,7 +10268,6 @@ packages: /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - dev: true /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -10266,6 +10289,10 @@ packages: resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} dev: true + /lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + dev: false + /lodash.upperfirst@4.3.1: resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} dev: true @@ -12063,6 +12090,19 @@ packages: yaml: 2.4.1 dev: true + /postcss-merge-rules@7.0.0(postcss@8.4.38): + resolution: {integrity: sha512-Zty3VlOsD6VSjBMu6PiHCVpLegtBT/qtZRVBcSeyEZ6q1iU5qTYT0WtEoLRV+YubZZguS5/ycfP+NRiKfjv6aw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + dependencies: + browserslist: 4.23.0 + caniuse-api: 3.0.0 + cssnano-utils: 5.0.0(postcss@8.4.38) + postcss: 8.4.38 + postcss-selector-parser: 6.0.16 + dev: false + /postcss-resolve-nested-selector@0.1.1: resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==} dev: true @@ -12082,7 +12122,6 @@ packages: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - dev: true /postcss-styled-syntax@0.6.4(postcss@8.4.38): resolution: {integrity: sha512-uWiLn+9rKgIghUYmTHvXMR6MnyPULMe9Gv3bV537Fg4FH6CA6cn21WMjKss2Qb98LUhT847tKfnRGG3FhSOgUQ==} @@ -12121,7 +12160,6 @@ packages: nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.2.0 - dev: true /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -14346,7 +14384,6 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} @@ -15011,6 +15048,8 @@ packages: cssesc: 3.0.0 csstype: 3.1.3 lodash: 4.17.21 + postcss: 8.4.38 + postcss-merge-rules: 7.0.0(postcss@8.4.38) react: 18.2.0 stylis: 4.3.1 stylis-plugin-rtl: 2.1.1(stylis@4.3.1)