diff --git a/flow-typed/npm/@csstools/postcss-cascade-layers_v5.x.x.js b/flow-typed/npm/@csstools/postcss-cascade-layers_v5.x.x.js new file mode 100644 index 000000000..de2fed84f --- /dev/null +++ b/flow-typed/npm/@csstools/postcss-cascade-layers_v5.x.x.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +declare module '@csstools/postcss-cascade-layers' { + import type { Plugin } from 'postcss'; + declare type PluginCreator = (opts?: T) => Plugin; + declare type PluginOptions = { + /** Emit a warning when the "revert" keyword is found in your CSS. default: "warn" */ + onRevertLayerKeyword?: 'warn' | false, + /** Emit a warning when conditional rules could change the layer order. default: "warn" */ + onConditionalRulesChangingLayerOrder?: 'warn' | false, + /** Emit a warning when "layer" is used in "@import". default: "warn" */ + onImportLayerRule?: 'warn' | false, + }; + + declare module.exports: PluginCreator; +} diff --git a/flow-typed/npm/postcss_v8.x.x.js b/flow-typed/npm/postcss_v8.x.x.js new file mode 100644 index 000000000..3e99d3eb3 --- /dev/null +++ b/flow-typed/npm/postcss_v8.x.x.js @@ -0,0 +1,128 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +declare module 'postcss' { + declare export type PluginCreator = (opts?: T) => Plugin; + + declare export interface Plugin { + postcssPlugin: string; + Once?: (root: Root, postcss: Postcss) => void; + OnceExit?: (root: Root, postcss: Postcss) => void; + Root?: (root: Root, postcss: Postcss) => void; + RootExit?: (root: Root, postcss: Postcss) => void; + AtRule?: (atRule: AtRule, postcss: Postcss) => void; + AtRuleExit?: (atRule: AtRule, postcss: Postcss) => void; + Rule?: (rule: Rule, postcss: Postcss) => void; + RuleExit?: (rule: Rule, postcss: Postcss) => void; + Declaration?: (decl: Declaration, postcss: Postcss) => void; + DeclarationExit?: (decl: Declaration, postcss: Postcss) => void; + Comment?: (comment: Comment, postcss: Postcss) => void; + CommentExit?: (comment: Comment, postcss: Postcss) => void; + } + + declare export interface Postcss { + version: string; + plugins: Array; + process: ( + css: string | { toString(): string }, + opts?: ProcessOptions, + ) => Promise; + (plugins?: Array): Postcss; + } + + declare export interface ProcessOptions { + from?: string; + to?: string; + map?: boolean | { inline: boolean, annotation: boolean }; + parser?: any; + stringifier?: any; + syntax?: any; + } + + declare export interface Result { + css: string; + map: any; + root: Root; + messages: Array; + processor: Postcss; + opts: ProcessOptions; + warnings(): Array; + toString(): string; + } + + declare export interface Root { + type: 'root'; + nodes: Array; + source: Source; + raws: any; + parent: null; + lastEach: number; + indexes: { [key: string]: number }; + rawCache: { [key: string]: string }; + [key: string]: any; + } + + declare export interface AtRule { + type: 'atrule'; + name: string; + params: string; + nodes?: Array; + parent: Container; + source: Source; + raws: any; + [key: string]: any; + } + + declare export interface Rule { + type: 'rule'; + selector: string; + nodes: Array; + parent: Container; + source: Source; + raws: any; + [key: string]: any; + } + + declare export interface Declaration { + type: 'decl'; + prop: string; + value: string; + parent: Container; + source: Source; + raws: any; + [key: string]: any; + } + + declare export interface Comment { + type: 'comment'; + text: string; + parent: Container; + source: Source; + raws: any; + [key: string]: any; + } + + declare export interface Container { + type: string; + nodes: Array; + parent: Container | null; + source: Source; + raws: any; + [key: string]: any; + } + + declare export type ChildNode = AtRule | Rule | Declaration | Comment; + + declare export interface Source { + start?: { line: number, column: number }; + end?: { line: number, column: number }; + input: { css: string, id?: string }; + } + + declare const postcss: Postcss; + declare export default typeof postcss; +} diff --git a/packages/@stylexjs/babel-plugin/__tests__/transform-process-test.js b/packages/@stylexjs/babel-plugin/__tests__/transform-process-test.js index 9b76e41e8..c9a57f9cb 100644 --- a/packages/@stylexjs/babel-plugin/__tests__/transform-process-test.js +++ b/packages/@stylexjs/babel-plugin/__tests__/transform-process-test.js @@ -94,19 +94,19 @@ export const styles = stylex.create({ describe('@stylexjs/babel-plugin', () => { describe('[transform] stylexPlugin.processStylexRules', () => { - test('no rules', () => { + test('no rules', async () => { const { code, metadata } = transform(` import * as stylex from '@stylexjs/stylex'; `); expect(code).toMatchInlineSnapshot( '"import * as stylex from \'@stylexjs/stylex\';"', ); - expect(stylexPlugin.processStylexRules(metadata)).toMatchInlineSnapshot( - '":root, .x1nqdfg0{--x1i1e39s:blue;}"', - ); + expect( + await stylexPlugin.processStylexRules(metadata), + ).toMatchInlineSnapshot('":root, .x1nqdfg0{--x1i1e39s:blue;}"'); }); - test('all rules (useLayers:false)', () => { + test('all rules (useLayers:false)', async () => { const { code, metadata } = transform(fixture); expect(code).toMatchInlineSnapshot(` "import * as stylex from '@stylexjs/stylex'; @@ -135,25 +135,25 @@ describe('@stylexjs/babel-plugin', () => { }] };" `); - expect(stylexPlugin.processStylexRules(metadata)).toMatchInlineSnapshot(` + expect(await stylexPlugin.processStylexRules(metadata)) + .toMatchInlineSnapshot(` "@property --color { syntax: "*"; inherits: false;} @keyframes x4ssjuf-B{0%{box-shadow:1px 2px 3px 4px red;color:yellow;}100%{box-shadow:10px 20px 30px 40px green;color:var(--orange);}} - @keyframes x4ssjuf-B{0%{box-shadow:-1px 2px 3px 4px red;color:yellow;}100%{box-shadow:-10px 20px 30px 40px green;color:var(--orange);}} - :root, .x1nqdfg0{--x1i1e39s:blue;} + @keyframes x4ssjuf-B{0%{box-shadow:-1px 2px 3px 4px red;color:yellow;}100%{box-shadow:-10px 20px 30px 40px green;color:var(--orange);}}:root, .x1nqdfg0{--x1i1e39s:blue;} .x1bg2uv5:not(#\\#){border-color:green} .xdmqw5o:not(#\\#):not(#\\#){animation-name:x4ssjuf-B} .xrkmrrc:not(#\\#):not(#\\#){background-color:red} .xfx01vb:not(#\\#):not(#\\#){color:var(--color)} - html:not([dir='rtl']) .x1skrh0i:not(#\\#):not(#\\#){text-shadow:1px 2px 3px 4px red} - html[dir='rtl'] .x1skrh0i:not(#\\#):not(#\\#){text-shadow:-1px 2px 3px 4px red} - @media (min-width:320px){html:not([dir='rtl']) .x1cmij7u.x1cmij7u:not(#\\#):not(#\\#){text-shadow:10px 20px 30px 40px green}} - @media (min-width:320px){html[dir='rtl'] .x1cmij7u.x1cmij7u:not(#\\#):not(#\\#){text-shadow:-10px 20px 30px 40px green}} + html:not([dir='rtl']):not(#\\#):not(#\\#) .x1skrh0i{text-shadow:1px 2px 3px 4px red} + html[dir='rtl']:not(#\\#):not(#\\#) .x1skrh0i{text-shadow:-1px 2px 3px 4px red} + @media (min-width:320px){html:not([dir='rtl']):not(#\\#):not(#\\#) .x1cmij7u.x1cmij7u{text-shadow:10px 20px 30px 40px green}} + @media (min-width:320px){html[dir='rtl']:not(#\\#):not(#\\#) .x1cmij7u.x1cmij7u{text-shadow:-10px 20px 30px 40px green}} @media (max-width: 1000px){.xwguixi.xwguixi:not(#\\#):not(#\\#):not(#\\#){border-color:var(--x1i1e39s)}} @media (max-width: 500px){@media (max-width: 1000px){.x5i7zo.x5i7zo.x5i7zo:not(#\\#):not(#\\#):not(#\\#):not(#\\#){border-color:yellow}}}" `); }); - test('all rules (useLayers:true)', () => { + test('all rules (useLayers:true)', async () => { const { code, metadata } = transform(fixture, { useLayers: true, }); @@ -184,7 +184,7 @@ describe('@stylexjs/babel-plugin', () => { }] };" `); - expect(stylexPlugin.processStylexRules(metadata, true)) + expect(await stylexPlugin.processStylexRules(metadata, true)) .toMatchInlineSnapshot(` " @layer priority1, priority2, priority3, priority4, priority5; diff --git a/packages/@stylexjs/babel-plugin/package.json b/packages/@stylexjs/babel-plugin/package.json index 6f9729943..2af4930e4 100644 --- a/packages/@stylexjs/babel-plugin/package.json +++ b/packages/@stylexjs/babel-plugin/package.json @@ -20,6 +20,7 @@ "@babel/core": "^7.26.8", "@babel/traverse": "^7.26.8", "@babel/types": "^7.26.8", + "@csstools/postcss-cascade-layers": "^5.0.1", "@dual-bundle/import-meta-resolve": "^4.1.0", "@stylexjs/stylex": "0.13.1", "postcss-value-parser": "^4.1.0" diff --git a/packages/@stylexjs/babel-plugin/src/index.js b/packages/@stylexjs/babel-plugin/src/index.js index fe80ddace..cba6240f2 100644 --- a/packages/@stylexjs/babel-plugin/src/index.js +++ b/packages/@stylexjs/babel-plugin/src/index.js @@ -31,6 +31,8 @@ import transformStylexCall, { } from './visitors/stylex-merge'; import transformStylexProps from './visitors/stylex-props'; import { skipStylexPropsChildren } from './visitors/stylex-props'; +import postcss from 'postcss'; +import cascadeLayers from '@csstools/postcss-cascade-layers'; import transformStyleXViewTransitionClass from './visitors/stylex-view-transition-class'; const NAME = 'stylex'; @@ -356,12 +358,12 @@ export type Rule = [ }, number, ]; -function processStylexRules( +async function processStylexRules( rules: Array, useLayers: boolean = false, -): string { +): Promise { if (rules.length === 0) { - return ''; + return Promise.resolve(''); } const constantRules = rules.filter( @@ -470,14 +472,9 @@ function processStylexRules( ) .flatMap((rule) => { const { ltr, rtl } = rule; - let ltrRule = ltr, + const ltrRule = ltr, rtlRule = rtl; - if (!useLayers) { - ltrRule = addSpecificityLevel(ltrRule, index); - rtlRule = rtlRule && addSpecificityLevel(rtlRule, index); - } - return rtlRule ? [ addAncestorSelector(ltrRule, "html:not([dir='rtl'])"), @@ -487,14 +484,22 @@ function processStylexRules( }) .join('\n'); + if (!useLayers) { + return `@layer priority${index + 1}{\n${collectedCSS}\n}`; + } // Don't put @property, @keyframe, @position-try in layers - return useLayers && pri > 0 + return pri > 0 ? `@layer priority${index + 1}{\n${collectedCSS}\n}` : collectedCSS; }) .join('\n'); - return header + collectedCSS; + if (!useLayers) { + const css = await transformCollectedCSS(header + collectedCSS); + return css.trim(); + } + + return Promise.resolve(header + collectedCSS); } styleXTransform.processStylexRules = processStylexRules; @@ -523,23 +528,11 @@ function addAncestorSelector( } /** - * Adds :not(#\#) to bump up specificity. as a polyfill for @layer + * Uses @csstools/postcss-cascade-layers to apply specificity adjustments + * via `:not(#\#)` as a polyfill for CSS @layer at-rules. */ -function addSpecificityLevel(selector: string, index: number): string { - if (selector.startsWith('@keyframes')) { - return selector; - } - const pseudo = Array.from({ length: index }) - .map(() => ':not(#\\#)') - .join(''); - - const lastOpenCurly = selector.includes('::') - ? selector.indexOf('::') - : selector.lastIndexOf('{'); - const beforeCurly = selector.slice(0, lastOpenCurly); - const afterCurly = selector.slice(lastOpenCurly); - - return `${beforeCurly}${pseudo}${afterCurly}`; +async function transformCollectedCSS(collectedCSS: string): Promise { + return (await postcss([cascadeLayers()]).process(collectedCSS)).css; } export type StyleXTransformObj = $ReadOnly<{ diff --git a/packages/@stylexjs/rollup-plugin/src/index.js b/packages/@stylexjs/rollup-plugin/src/index.js index 3af23053a..2ca035421 100644 --- a/packages/@stylexjs/rollup-plugin/src/index.js +++ b/packages/@stylexjs/rollup-plugin/src/index.js @@ -73,10 +73,10 @@ export default function stylexPlugin({ buildStart() { stylexRules = {}; }, - generateBundle(this: PluginContext) { + async generateBundle(this: PluginContext) { const rules: Array = Object.values(stylexRules).flat(); if (rules.length > 0) { - const collectedCSS = stylexBabelPlugin.processStylexRules( + const collectedCSS = await stylexBabelPlugin.processStylexRules( rules, useCSSLayers, ); diff --git a/packages/docs/scripts/make-stylex-sheet.js b/packages/docs/scripts/make-stylex-sheet.js index 4383d4735..055f9d98e 100644 --- a/packages/docs/scripts/make-stylex-sheet.js +++ b/packages/docs/scripts/make-stylex-sheet.js @@ -77,7 +77,9 @@ async function genSheet() { const ruleSets = await Promise.all(allFiles.map(transformFile)); - const generatedCSS = stylexBabelPlugin.processStylexRules(ruleSets.flat()); + const generatedCSS = await stylexBabelPlugin.processStylexRules( + ruleSets.flat(), + ); await mkdirp(path.join(__dirname, '../.stylex/'));