diff --git a/packages/cli/package.json b/packages/cli/package.json index f5f7dea8..f29074a7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,7 +41,9 @@ "postcss-nested": "^5.0.6", "postcss-preset-env": "^7.0.2", "postcss-reporter": "^7.0.4", + "postcss-value-parser": "^4.2.0", "resolve": "^1.19.0", + "stylis": "^3.5.4", "terser-webpack-plugin": "^5.3.0", "thread-loader": "^3.0.4", "tslib": "^2.3.0", diff --git a/packages/cli/src/config/__tests__/__snapshots__/postcssLinariaPreprocessor.test.ts.snap b/packages/cli/src/config/__tests__/__snapshots__/postcssLinariaPreprocessor.test.ts.snap new file mode 100644 index 00000000..cddeba3e --- /dev/null +++ b/packages/cli/src/config/__tests__/__snapshots__/postcssLinariaPreprocessor.test.ts.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`postcss-linaria-preprocessor :global() selector 1`] = ` +" + .a { + color: red + } + page { + width: 50vw; + } + " +`; + +exports[`postcss-linaria-preprocessor escape breaking control characters 1`] = ` +" + .a { + content: '\\\\feff'; + } + " +`; + +exports[`postcss-linaria-preprocessor keyframes rename 1`] = ` +" + .a { + animation: 1s ease 1 backwards normal zoomIn-a; + @keyframes zoomIn-a { + from { + opacity: 0; + transform: scale(0.94); + } + + 50% { + opacity: var(--opacity); + } + + to { + transform: scale(var(--scale)); + } + } + } + " +`; diff --git a/packages/cli/src/config/__tests__/__snapshots__/postcssTransformUnit.test.ts.snap b/packages/cli/src/config/__tests__/__snapshots__/postcssTransformUnit.test.ts.snap new file mode 100644 index 00000000..e6fb0479 --- /dev/null +++ b/packages/cli/src/config/__tests__/__snapshots__/postcssTransformUnit.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`postcss-transform-unit px to rpx 1`] = ` +" + .a { + width: 20rpx; + height: 20rpx; + font-size: 30px; + } + " +`; + +exports[`postcss-transform-unit rpx to px 1`] = ` +" + .a { + width: 10px; + height: 10px; + font-size: 30rpx; + } + " +`; diff --git a/packages/cli/src/config/__tests__/postcssLinariaPreprocessor.test.ts b/packages/cli/src/config/__tests__/postcssLinariaPreprocessor.test.ts new file mode 100644 index 00000000..9a8f6946 --- /dev/null +++ b/packages/cli/src/config/__tests__/postcssLinariaPreprocessor.test.ts @@ -0,0 +1,57 @@ +import postcss from 'postcss'; + +const transform = async (css: string) => { + // eslint-disable-next-line global-require + const result = await postcss([require('../postcssLinariaPreprocessor')()]).process(css, { + from: '/path/to/file.css', + }); + return result.css; +}; + +describe('postcss-linaria-preprocessor', () => { + test(':global() selector', () => { + const css = ` + .a { + color: red; + :global() { + page { + width: 50vw; + } + } + } + `; + expect(transform(css)).resolves.toMatchSnapshot(); + }); + + test('keyframes rename', () => { + const css = ` + .a { + animation: 1s ease 1 backwards normal zoomIn; + @keyframes zoomIn { + from { + opacity: 0; + transform: scale(0.94); + } + + 50% { + opacity: var(--opacity); + } + + to { + transform: scale(var(--scale)); + } + } + } + `; + expect(transform(css)).resolves.toMatchSnapshot(); + }); + + test('escape breaking control characters', () => { + const css = ` + .a { + content: '\feff'; + } + `; + expect(transform(css)).resolves.toMatchSnapshot(); + }); +}); diff --git a/packages/cli/src/config/__tests__/postcssTransformUnit.test.ts b/packages/cli/src/config/__tests__/postcssTransformUnit.test.ts new file mode 100644 index 00000000..e47b35a9 --- /dev/null +++ b/packages/cli/src/config/__tests__/postcssTransformUnit.test.ts @@ -0,0 +1,49 @@ +import postcss from 'postcss'; + +describe('postcss-transform-unit', () => { + test('px to rpx', async () => { + const css = ` + .a { + width: 10px; + height: 20rpx; + font-size: 30px; /* no */ + } + `; + + const result = await postcss([ + // eslint-disable-next-line global-require + require('../postcssTransformUnit')({ + divisor: 1, + multiple: 2, + sourceUnit: 'px', + targetUnit: 'rpx', + }), + ]).process(css, { + from: '/path/to/file.css', + }); + expect(result.css).toMatchSnapshot(); + }); + + test('rpx to px', async () => { + const css = ` + .a { + width: 10px; + height: 20rpx; + font-size: 30rpx; /* no */ + } + `; + + const result = await postcss([ + // eslint-disable-next-line global-require + require('../postcssTransformUnit')({ + divisor: 2, + multiple: 1, + sourceUnit: 'rpx', + targetUnit: 'px', + }), + ]).process(css, { + from: '/path/to/file.css', + }); + expect(result.css).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/src/config/postcssConfig.ts b/packages/cli/src/config/postcssConfig.ts index bc7070fa..cfe07b60 100644 --- a/packages/cli/src/config/postcssConfig.ts +++ b/packages/cli/src/config/postcssConfig.ts @@ -33,12 +33,13 @@ const getTransformUnitsPlugin = (unit: TransformUnit) => { // to improve PostCSS performance, we should always use `require(name)(options)` rather than `[name, options]` // also we should set `postcssOptions.config` to `false` to avoid loading any `postcss.config.js` // TODO: hope PostCSS could fix this issue https://github.com/csstools/postcss-preset-env/issues/232#issuecomment-992263741 -export default ({ unit }: { unit: TransformUnit }) => { +export default ({ unit, linaria }: { unit: TransformUnit; linaria: boolean }) => { const transformUnitPlugin = getTransformUnitsPlugin(unit); return { plugins: [ require('postcss-each')(), + ...(linaria ? [require('./postcssLinariaPreprocessor')()] : []), // TODO: `postcss-nesting` from `postcss-preset-env` output `:is` pseudo-class that Mini Programs don't support. // We have to use `postcss-nested` manually before them to prevent `:is` being created. require('postcss-nested')(), diff --git a/packages/cli/src/config/postcssLinariaPreprocessor.ts b/packages/cli/src/config/postcssLinariaPreprocessor.ts new file mode 100644 index 00000000..16df9480 --- /dev/null +++ b/packages/cli/src/config/postcssLinariaPreprocessor.ts @@ -0,0 +1,91 @@ +/* eslint-disable import/no-import-module-exports */ +import type { PluginCreator } from 'postcss'; +import valueParser from 'postcss-value-parser'; + +const reserved = [ + 'none', + 'inherited', + 'initial', + 'unset', + /* single-timing-function */ + 'linear', + 'ease', + 'ease-in', + 'ease-in-out', + 'ease-out', + 'step-start', + 'step-end', + 'start', + 'end', + /* single-animation-iteration-count */ + 'infinite', + /* single-animation-direction */ + 'normal', + 'reverse', + 'alternate', + 'alternate-reverse', + /* single-animation-fill-mode */ + 'forwards', + 'backwards', + 'both', + /* single-animation-play-state */ + 'running', + 'paused', +]; + +interface Options {} + +const postcssLinariaPreprocessor: PluginCreator = () => ({ + postcssPlugin: 'postcss-linaria-preprocessor', + Once(root, postcss) { + const nodes = [...root.nodes]; + for (const node of nodes) { + if (node.type === 'rule' && node.selector.startsWith('.')) { + // use unique keyframe name to avoid conflict + // inspired from https://github.com/css-modules/postcss-icss-keyframes/blob/5f890e4068820daa80025d88a4f750a3a085dcc8/src/index.js + const keyframeNameMapping = new Map(); + node.walkAtRules(/keyframes$/, atRule => { + const name = atRule.params; + if (reserved.includes(name)) { + postcss.result.warn(`Unable to use reserve '${name}' animation name`, { + node: atRule, + }); + + return; + } + const newName = `${name}-${node.selector.replace(/^\./, '')}`; + keyframeNameMapping.set(name, newName); + atRule.params = newName; + }); + node.walkDecls(/animation$|animation-name$/, decl => { + const parsed = valueParser(decl.value); + for (const item of parsed.nodes) { + if (item.type === 'word' && keyframeNameMapping.has(item.value)) { + item.value = keyframeNameMapping.get(item.value)!; + } + } + decl.value = parsed.toString(); + }); + + // extract the global rule to the top of the root + node.walkRules(/^:global\(\)$/, globalRule => { + globalRule.remove(); + for (const globalNode of globalRule.nodes) { + root.insertAfter(node, globalNode); + } + }); + } + } + }, + Declaration(decl) { + // escape breaking control characters + // from: https://github.com/thysultan/stylis/blob/v3.5.4/tests/spec.js#L113C3-L116 + if (decl.value.match(/[\0\r\f]/)) { + decl.value = decl.value.replace(/\0/g, '\\0').replace(/\r/g, '\\r').replace(/\f/g, '\\f'); + } + }, +}); + +postcssLinariaPreprocessor.postcss = true; + +module.exports = postcssLinariaPreprocessor; diff --git a/packages/cli/src/config/webpack.config.ts b/packages/cli/src/config/webpack.config.ts index b980abc0..3cd20ea7 100644 --- a/packages/cli/src/config/webpack.config.ts +++ b/packages/cli/src/config/webpack.config.ts @@ -193,6 +193,7 @@ export const getWebpackConfig = ({ configFile: require.resolve('./linaria.config'), sourceMap: true, cacheProvider: require.resolve('./linariaFileCache'), + preprocessor: 'none', babelOptions: { // always use internal babel.config.js file configFile: require.resolve('./babel.config'), @@ -232,7 +233,7 @@ export const getWebpackConfig = ({ implementation: require.resolve('postcss'), postcssOptions: { config: false, - ...postcssConfig({ unit: cssUnit }), + ...postcssConfig({ unit: cssUnit, linaria: false }), }, }, }, @@ -269,7 +270,7 @@ export const getWebpackConfig = ({ implementation: require.resolve('postcss'), postcssOptions: { config: false, - ...postcssConfig({ unit: cssUnit }), + ...postcssConfig({ unit: cssUnit, linaria: true }), }, }, }, diff --git a/packages/demo-todomvc-linaria/src/components/MainSection.tsx b/packages/demo-todomvc-linaria/src/components/MainSection.tsx index 17b299b6..eccde02c 100644 --- a/packages/demo-todomvc-linaria/src/components/MainSection.tsx +++ b/packages/demo-todomvc-linaria/src/components/MainSection.tsx @@ -16,7 +16,7 @@ const main = css` const todosHeader = css` padding: 10px 0; width: 100%; - font-size: 100px; + font-size: 203rpx; font-weight: 100; text-align: center; color: rgba(175, 47, 47, 0.15); diff --git a/yarn.lock b/yarn.lock index e9638da4..7f9dc65f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2857,7 +2857,7 @@ "@docusaurus/theme-search-algolia" "2.4.3" "@docusaurus/types" "2.4.3" -"@docusaurus/react-loadable@5.5.2": +"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2": version "5.5.2" resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== @@ -14224,14 +14224,6 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" -"react-loadable@npm:@docusaurus/react-loadable@5.5.2": - version "5.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" - integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== - dependencies: - "@types/react" "*" - prop-types "^15.6.2" - react-reconciler@^0.26.2: version "0.26.2" resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.26.2.tgz#bbad0e2d1309423f76cf3c3309ac6c96e05e9d91"