├── .eslintrc.json ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── __tests__ ├── __snapshots__ │ └── incremental-classnames.js.snap ├── code │ ├── errors.js │ ├── fixtures.js │ ├── fixtures │ │ ├── classes │ │ │ ├── complex-ternary │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── destructuring-assignment │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── dynamic-bracket-access │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── empty-call │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── hoists-arrow-function-call │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── hoists-block-function-call │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── hoists-function-call │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── keeps-multiple-instances-of-same-value │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── member-expression-access │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── mixed │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── moves-test │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── no-keys │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── object │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── property-access │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── short-circuits-same-value │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── spread-assignment │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── spread-use │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── static-bracket-access │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── string-literal │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ └── ternary │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ ├── custom-properties │ │ │ ├── does-not-change-capitalization │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ └── does-not-convert-number │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ ├── import │ │ │ └── ignore-other-imports │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ ├── incremental-classnames │ │ │ ├── generated-classname │ │ │ │ ├── code.js │ │ │ │ ├── options.json │ │ │ │ └── output.js │ │ │ └── object-classname │ │ │ │ ├── code.js │ │ │ │ ├── options.json │ │ │ │ └── output.js │ │ ├── keyframes │ │ │ ├── basic │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── converts-from │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── converts-to │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ └── setting-animationName-directly │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ ├── minify-properties │ │ │ ├── does-not-minify-by-default │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── hashes-unknown-properties │ │ │ │ ├── code.js │ │ │ │ ├── options.json │ │ │ │ └── output.js │ │ │ ├── minifies-known-properties │ │ │ │ ├── code.js │ │ │ │ ├── options.json │ │ │ │ └── output.js │ │ │ └── minifies-nested-properties │ │ │ │ ├── code.js │ │ │ │ ├── options.json │ │ │ │ └── output.js │ │ ├── nesting │ │ │ ├── at-rule-key │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── at-rules │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── basic │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── deep │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ ├── generates-correct-class-names │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ │ └── translates-old-pseudo-element │ │ │ │ ├── code.js │ │ │ │ └── output.js │ │ ├── typescript │ │ │ └── casting │ │ │ │ ├── code.ts │ │ │ │ └── output.ts │ │ └── values │ │ │ ├── arrow-function │ │ │ ├── code.js │ │ │ └── output.js │ │ │ ├── expands-shorthand-in-nesting │ │ │ ├── code.js │ │ │ └── output.js │ │ │ ├── expands-shorthand │ │ │ ├── code.js │ │ │ └── output.js │ │ │ ├── keeps-longhand │ │ │ ├── code.js │ │ │ └── output.js │ │ │ └── removes-unused-keys │ │ │ ├── code.js │ │ │ └── output.js │ ├── import.js │ └── nesting.js ├── compile.js ├── css-sorter.js ├── incremental-classnames.js ├── resolver.js └── styles │ ├── comma-separated-properties.js │ ├── custom-properties.js │ ├── incremental-classnames.js │ ├── keyframes.js │ ├── multiple-imports.js │ ├── nesting.js │ ├── transition-property.js │ └── values.js ├── babel.js ├── docs ├── Background.md ├── Bundler-plugins.md ├── Ecosystem.md ├── FAQ.md ├── How-it-works.md ├── TypeScript.md └── Usage-guide.md ├── examples ├── gatsby │ ├── .gitignore │ ├── README.md │ ├── gatsby-config.js │ ├── package.json │ ├── src │ │ └── pages │ │ │ └── index.tsx │ ├── tsconfig.json │ └── yarn.lock ├── nextjs │ ├── .gitignore │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ └── index.tsx │ ├── shared │ │ └── styles.js │ ├── tsconfig.json │ └── yarn.lock ├── rollup │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ ├── rollup.config.js │ ├── src │ │ └── main.js │ └── yarn.lock ├── vite │ ├── index.html │ ├── package.json │ ├── src │ │ ├── dynamic.js │ │ ├── dynamic2.js │ │ └── main.js │ ├── vite.config.js │ └── yarn.lock ├── webpack4 │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ └── main.js │ ├── webpack.config.js │ └── yarn.lock └── webpack5 │ ├── README.md │ ├── package.json │ ├── public │ └── index.html │ ├── src │ └── main.js │ ├── webpack.config.js │ └── yarn.lock ├── gatsby ├── gatsby-node.js └── package.json ├── index.js ├── index.mjs ├── jest.config.json ├── next-legacy.js ├── next.js ├── package.json ├── rollup.d.ts ├── rollup.js ├── scripts └── test-examples.sh ├── src ├── helpers │ ├── flatten-at-rules.js │ ├── flatten-styles.js │ ├── generate-classes.js │ ├── generate-expression.js │ ├── generate-styles.js │ ├── get-style-object-value.js │ ├── list-dynamic-keys.js │ ├── list-function-call-keys.js │ ├── list-function-calls.js │ ├── list-references.js │ ├── list-static-keys.js │ ├── mutate-ast.js │ ├── normalize-arguments.js │ ├── strip-type-assertions.js │ └── validate.js ├── plugin-utils.js ├── process-css.js ├── process-references.js ├── transpilers │ ├── create.js │ └── keyframes.js └── utils │ ├── ast.js │ ├── constants.js │ ├── helpers.js │ ├── incremental-classnames.js │ ├── styles.js │ └── test-ast-shape.js ├── stryker.conf.json ├── types ├── Style.d.ts ├── index.d.ts ├── test │ └── at-rules.ts ├── ts4.3 │ ├── index.d.ts │ ├── test │ │ ├── at-rules.ts │ │ ├── basic.ts │ │ ├── custom-properties.ts │ │ ├── keyframes.ts │ │ ├── properties.ts │ │ └── pseudo.ts │ └── tsconfig.json ├── tsconfig.json └── tslint.json ├── vite.d.ts ├── vite.js ├── webpack ├── index.d.ts ├── index.js ├── loader-get-options.js ├── loader.d.ts ├── loader.js └── virtualModules.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "prettier", 5 | "plugin:import/recommended", 6 | "plugin:import/errors", 7 | "plugin:import/warnings" 8 | ], 9 | "plugins": ["import"], 10 | "parserOptions": { 11 | "ecmaVersion": 2020, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "curly": ["error", "multi-line"], 16 | "linebreak-style": ["error", "unix"], 17 | "max-len": [ 18 | "error", 19 | { 20 | "code": 80, 21 | "ignoreComments": true, 22 | "ignoreStrings": true, 23 | "ignoreTemplateLiterals": true 24 | } 25 | ], 26 | "new-cap": "off", 27 | "no-case-declarations": "error", 28 | "no-var": "error", 29 | "prefer-const": "error", 30 | "no-unused-vars": ["error", { "ignoreRestSiblings": true }], 31 | "import/order": "error" 32 | }, 33 | "env": { 34 | "node": true, 35 | "es6": true 36 | }, 37 | "ignorePatterns": [ 38 | "__tests__/code/fixtures/", 39 | "node_modules", 40 | "build", 41 | "coverage", 42 | "examples" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - '*' 5 | push: 6 | branches: 7 | - '*' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup Node.js environment 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: 14.x 20 | 21 | - name: Install packages 22 | run: yarn --frozen-lockfile 23 | 24 | - name: Check linting 25 | run: yarn lint:check 26 | 27 | - name: Check formatting 28 | run: yarn format:check 29 | 30 | - name: Test types 31 | run: yarn test:types 32 | 33 | - name: Tests 34 | run: yarn test 35 | 36 | - name: Test examples 37 | run: yarn test:examples 38 | 39 | - name: Mutation tests 40 | run: yarn test:mutation 41 | 42 | - name: Upload Stryker report 43 | if: ${{ always() }} 44 | uses: actions/upload-artifact@v2 45 | with: 46 | name: stryker-report 47 | path: reports/mutation/html/index.html 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .DS_Store 4 | *.log* 5 | coverage 6 | .stryker-tmp 7 | reports 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | examples 3 | scripts 4 | .github 5 | coverage 6 | .stryker-tmp 7 | reports 8 | .DS_Store 9 | *.log* 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.21.2 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | __tests__/code/fixtures/**/output.js 2 | node_modules 3 | build 4 | coverage 5 | examples/gatsby/.cache 6 | examples/gatsby/public 7 | examples/nextjs/.next 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Johan Holmerin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # style9 2 | 3 | CSS-in-JS compiler inspired by Meta's [StyleX][stylex], with near-zero runtime, atomic CSS extraction and TypeScript support. Framework agnostic. 4 | 5 | > [!NOTE] 6 | > [StyleX][stylex] was open-sourced on 2023-12-5. Consider using that instead 7 | 8 | ## Basic usage 9 | 10 | *For a complete walkthrough of the API, see [Usage guide](docs/Usage-guide.md).* 11 | 12 | ```javascript 13 | import style9 from 'style9'; 14 | 15 | const styles = style9.create({ 16 | blue: { 17 | color: 'blue', 18 | }, 19 | red: { 20 | color: 'red' 21 | } 22 | }); 23 | 24 | document.body.className = styles('blue', isRed && 'red'); 25 | ``` 26 | 27 | For the above input, the compiler will generate the following output: 28 | 29 | ```javascript 30 | /* JavaScript */ 31 | document.body.className = isRed ? 'cRCRUH ' : 'hxxstI '; 32 | 33 | /* CSS */ 34 | .hxxstI { color: blue } 35 | .cRCRUH { color: red } 36 | ``` 37 | 38 | ## Installation 39 | 40 | ```sh 41 | # Yarn 42 | yarn add style9 43 | 44 | # npm 45 | npm install style9 46 | ``` 47 | 48 | ## Compiler setup - required 49 | 50 | The following is the minimally required Webpack setup for extracting styles to a CSS file. For Webpack options and Rollup, Next.js, Gatsby,Vite, and Babel plugins, see [Bundler plugins](docs/Bundler-plugins.md). 51 | 52 | ```javascript 53 | const Style9Plugin = require('style9/webpack'); 54 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 55 | 56 | module.exports = { 57 | // Collect all styles in a single file - required 58 | optimization: { 59 | splitChunks: { 60 | cacheGroups: { 61 | styles: { 62 | name: 'styles', 63 | type: 'css/mini-extract', 64 | // For webpack@4 remove type and uncomment the line below 65 | // test: /\.css$/, 66 | chunks: 'all', 67 | enforce: true, 68 | } 69 | } 70 | } 71 | }, 72 | module: { 73 | rules: [ 74 | { 75 | test: /\.(tsx|ts|js|mjs|jsx)$/, 76 | use: Style9Plugin.loader, 77 | }, 78 | { 79 | test: /\.css$/i, 80 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 81 | } 82 | ] 83 | }, 84 | plugins: [ 85 | new Style9Plugin(), 86 | new MiniCssExtractPlugin() 87 | ] 88 | }; 89 | ``` 90 | 91 | ## Documentation 92 | 93 | 1. [Background](docs/Background.md) 94 | 1. [Usage guide](docs/Usage-guide.md) 95 | 1. [Bundler plugins](docs/Bundler-plugins.md) 96 | 1. [TypeScript](docs/TypeScript.md) 97 | 1. [Ecosystem](docs/Ecosystem.md) 98 | 1. [How it works](docs/How-it-works.md) 99 | 1. [FAQ](docs/FAQ.md) 100 | 1. [Example apps](examples) 101 | 102 | ## Have a question? 103 | 104 | Look at the [FAQ](docs/FAQ.md), [search][search] the repo, or ask in [discussions][discussions]. 105 | 106 | [stylex]: https://github.com/facebook/stylex 107 | [search]: https://github.com/johanholmerin/style9/search 108 | [discussions]: https://github.com/johanholmerin/style9/discussions 109 | -------------------------------------------------------------------------------- /__tests__/code/errors.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const compile = require('../compile.js'); 3 | 4 | it('only supports Member- and CallExpression on styles', () => { 5 | const input = ` 6 | import style9 from 'style9'; 7 | const styles = style9.create({ 8 | default: { 9 | color: 'blue' 10 | } 11 | }); 12 | foo(styles); 13 | `; 14 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 15 | "unknown: SyntaxError: Return value from style9.create has to be called as a function or accessed as an object 16 | 6 | } 17 | 7 | }); 18 | > 8 | foo(styles); 19 | | ^^^^^^ 20 | 9 | " 21 | `); 22 | }); 23 | 24 | it('supports React Hot Loader call', () => { 25 | const input = ` 26 | import style9 from 'style9'; 27 | const styles = style9.create({ 28 | default: { 29 | color: 'blue' 30 | } 31 | }); 32 | reactHotLoader.register(styles); 33 | `; 34 | expect(() => compile(input)).not.toThrow(); 35 | }); 36 | 37 | it('throws on invalid React Hot Loader call', () => { 38 | const input = ` 39 | import style9 from 'style9'; 40 | const styles = style9.create({ 41 | default: { 42 | color: 'blue' 43 | } 44 | }); 45 | foo.register(styles); 46 | `; 47 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 48 | "unknown: SyntaxError: Return value from style9.create has to be called as a function or accessed as an object 49 | 6 | } 50 | 7 | }); 51 | > 8 | foo.register(styles); 52 | | ^^^^^^ 53 | 9 | " 54 | `); 55 | }); 56 | 57 | it('throws on invalid React Hot Loader call2', () => { 58 | const input = ` 59 | import style9 from 'style9'; 60 | const styles = style9.create({ 61 | default: { 62 | color: 'blue' 63 | } 64 | }); 65 | reactHotLoader.foo(styles); 66 | `; 67 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 68 | "unknown: SyntaxError: Return value from style9.create has to be called as a function or accessed as an object 69 | 6 | } 70 | 7 | }); 71 | > 8 | reactHotLoader.foo(styles); 72 | | ^^^^^^ 73 | 9 | " 74 | `); 75 | }); 76 | 77 | it('throws on non-existing property import', () => { 78 | const input = ` 79 | import style9 from 'style9'; 80 | style9.foo; 81 | `; 82 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 83 | "unknown: Unsupported use. Supported uses are: style9(), style9.create(), and style9.keyframes() 84 | 1 | 85 | 2 | import style9 from 'style9'; 86 | > 3 | style9.foo; 87 | | ^^^^^^ 88 | 4 | " 89 | `); 90 | }); 91 | 92 | it('create throws when called without arguments', () => { 93 | const input = ` 94 | import style9 from 'style9'; 95 | style9.create(); 96 | `; 97 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 98 | "unknown: Unsupported use. Supported uses are: style9(), style9.create(), and style9.keyframes() 99 | 1 | 100 | 2 | import style9 from 'style9'; 101 | > 3 | style9.create(); 102 | | ^^^^^^ 103 | 4 | " 104 | `); 105 | }); 106 | 107 | it('create throws when called multiple arguments', () => { 108 | const input = ` 109 | import style9 from 'style9'; 110 | style9.create({}, {}); 111 | `; 112 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 113 | "unknown: Unsupported use. Supported uses are: style9(), style9.create(), and style9.keyframes() 114 | 1 | 115 | 2 | import style9 from 'style9'; 116 | > 3 | style9.create({}, {}); 117 | | ^^^^^^ 118 | 4 | " 119 | `); 120 | }); 121 | 122 | it('create throws non-object argument', () => { 123 | const input = ` 124 | import style9 from 'style9'; 125 | style9.create(1); 126 | `; 127 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 128 | "unknown: Unsupported use. Supported uses are: style9(), style9.create(), and style9.keyframes() 129 | 1 | 130 | 2 | import style9 from 'style9'; 131 | > 3 | style9.create(1); 132 | | ^^^^^^ 133 | 4 | " 134 | `); 135 | }); 136 | 137 | it('styles throws on non-existing style key', () => { 138 | const input = ` 139 | import style9 from 'style9'; 140 | const styles = style9.create({ 141 | default: { 142 | color: 'blue' 143 | } 144 | }); 145 | styles('blue'); 146 | `; 147 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 148 | "unknown: Property blue does not exist in style object 149 | 6 | } 150 | 7 | }); 151 | > 8 | styles('blue'); 152 | | ^^^^^^ 153 | 9 | " 154 | `); 155 | }); 156 | 157 | it('styles throws on unsupported operator', () => { 158 | const input = ` 159 | import style9 from 'style9'; 160 | const styles = style9.create({ 161 | default: { 162 | color: 'blue' 163 | } 164 | }); 165 | styles(foo & 'blue'); 166 | `; 167 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 168 | "unknown: Unsupported type BinaryExpression 169 | 6 | } 170 | 7 | }); 171 | > 8 | styles(foo & 'blue'); 172 | | ^^^^^^^^^^^^ 173 | 9 | " 174 | `); 175 | }); 176 | 177 | it('styles throws on failure to evaluate values', () => { 178 | const input = ` 179 | import style9 from 'style9'; 180 | const styles = style9.create({ 181 | default: { 182 | color: BLUE 183 | } 184 | }); 185 | styles('blue'); 186 | `; 187 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 188 | "unknown: Could not evaluate value 189 | 3 | const styles = style9.create({ 190 | 4 | default: { 191 | > 5 | color: BLUE 192 | | ^^^^ 193 | 6 | } 194 | 7 | }); 195 | 8 | styles('blue');" 196 | `); 197 | }); 198 | 199 | it('styles throws on spread', () => { 200 | const input = ` 201 | import style9 from 'style9'; 202 | const styles = style9.create({ 203 | default: { 204 | color: 'red' 205 | } 206 | }); 207 | styles({ ...foo }) 208 | `; 209 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 210 | "unknown: Unsupported type SpreadElement 211 | 6 | } 212 | 7 | }); 213 | > 8 | styles({ ...foo }) 214 | | ^^^^^^ 215 | 9 | " 216 | `); 217 | }); 218 | 219 | it('styles throws non-string logical right hand', () => { 220 | const input = ` 221 | import style9 from 'style9'; 222 | const styles = style9.create({ 223 | red: { 224 | color: 'red' 225 | } 226 | }); 227 | styles(foo && red) 228 | `; 229 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 230 | "unknown: Unsupported type Identifier 231 | 6 | } 232 | 7 | }); 233 | > 8 | styles(foo && red) 234 | | ^^^ 235 | 9 | " 236 | `); 237 | }); 238 | 239 | it('styles throws non-string ternary left hand', () => { 240 | const input = ` 241 | import style9 from 'style9'; 242 | const styles = style9.create({ 243 | red: { 244 | color: 'red' 245 | } 246 | }); 247 | styles(foo ? red : 'red') 248 | `; 249 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 250 | "unknown: Unsupported type Identifier 251 | 6 | } 252 | 7 | }); 253 | > 8 | styles(foo ? red : 'red') 254 | | ^^^ 255 | 9 | " 256 | `); 257 | }); 258 | 259 | it('styles throws non-string ternary right hand', () => { 260 | const input = ` 261 | import style9 from 'style9'; 262 | const styles = style9.create({ 263 | red: { 264 | color: 'red' 265 | } 266 | }); 267 | styles(foo ? 'red' : red) 268 | `; 269 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 270 | "unknown: Unsupported type Identifier 271 | 6 | } 272 | 7 | }); 273 | > 8 | styles(foo ? 'red' : red) 274 | | ^^^ 275 | 9 | " 276 | `); 277 | }); 278 | 279 | it('styles throws on identifier', () => { 280 | const input = ` 281 | import style9 from 'style9'; 282 | const styles = style9.create({ 283 | default: { 284 | color: 'red' 285 | } 286 | }); 287 | styles(foo) 288 | `; 289 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 290 | "unknown: Unsupported type Identifier 291 | 6 | } 292 | 7 | }); 293 | > 8 | styles(foo) 294 | | ^^^ 295 | 9 | " 296 | `); 297 | }); 298 | 299 | it('styles throws on dynamic key', () => { 300 | const input = ` 301 | import style9 from 'style9'; 302 | const styles = style9.create({ 303 | red: { 304 | color: 'red' 305 | } 306 | }); 307 | styles({ [red]: foo }) 308 | `; 309 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 310 | "unknown: Unsupported type ObjectProperty 311 | 6 | } 312 | 7 | }); 313 | > 8 | styles({ [red]: foo }) 314 | | ^^^^^^^^^^ 315 | 9 | " 316 | `); 317 | }); 318 | 319 | it('throws on unsupported logical expression', () => { 320 | const input = ` 321 | import style9 from 'style9'; 322 | const styles = style9.create({ 323 | red: { 324 | color: 'red' 325 | } 326 | }); 327 | styles(foo || red) 328 | `; 329 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 330 | "unknown: Unsupported type LogicalExpression 331 | 6 | } 332 | 7 | }); 333 | > 8 | styles(foo || red) 334 | | ^^^^^^^^^^ 335 | 9 | " 336 | `); 337 | }); 338 | -------------------------------------------------------------------------------- /__tests__/code/fixtures.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pluginTester = require('babel-plugin-tester').default; 3 | const plugin = require('../../babel.js'); 4 | 5 | pluginTester({ 6 | plugin, 7 | pluginName: 'style9', 8 | fixtures: path.join(__dirname, 'fixtures'), 9 | babelOptions: { 10 | parserOpts: { 11 | plugins: ['typescript'] 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/complex-ternary/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | not_used: { 4 | padding: '0', 5 | cursor: 'pointer' 6 | }, 7 | base: { 8 | pointerEvents: 'none', 9 | ':focus': { 10 | outline: 'none' 11 | } 12 | }, 13 | enabled: { 14 | color: 'green' 15 | }, 16 | disabled: { 17 | color: 'red', 18 | cursor: 'not-allowed' 19 | }, 20 | hover: { 21 | ':hover': { 22 | color: 'blue' 23 | } 24 | }, 25 | other: { 26 | backgroundColor: 'red' 27 | } 28 | }); 29 | 30 | styles('base', checked ? 'enabled' : 'disabled', 'hover', 'other'); 31 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/complex-ternary/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | 'fYFPyg eDssNQ larHMv ' + ((checked ? 'ciOZAH ' : 'RCRUH ') + 'kKPCVy cGdbor '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/destructuring-assignment/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const { blue } = style9.create({ 3 | blue: { 4 | color: 'blue' 5 | }, 6 | red: { 7 | color: 'red' 8 | } 9 | }); 10 | console.log(blue); 11 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/destructuring-assignment/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const { blue } = { 3 | blue: { 4 | color: 'hxxstI' 5 | } 6 | }; 7 | console.log(blue); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/dynamic-bracket-access/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | blue: { 4 | color: 'blue' 5 | }, 6 | red: { 7 | color: 'red' 8 | } 9 | })[blue]; 10 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/dynamic-bracket-access/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = { 3 | blue: { 4 | color: 'hxxstI' 5 | }, 6 | red: { 7 | color: 'RCRUH' 8 | } 9 | }[blue]; 10 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/empty-call/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue' 5 | } 6 | }); 7 | styles(); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/empty-call/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | (''); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/hoists-arrow-function-call/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue' 5 | } 6 | }); 7 | const get = state => styles(state() && 'default'); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/hoists-arrow-function-call/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | 4 | const get = state => { 5 | const _state = state(); 6 | 7 | return _state ? 'hxxstI ' : ''; 8 | }; 9 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/hoists-block-function-call/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue' 5 | } 6 | }); 7 | { 8 | styles({ 9 | default: foo() 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/hoists-block-function-call/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | { 4 | const _foo = foo(); 5 | 6 | _foo ? 'hxxstI ' : ''; 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/hoists-function-call/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue' 5 | } 6 | }); 7 | styles({ 8 | default: foo() 9 | }); 10 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/hoists-function-call/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | 4 | const _foo = foo(); 5 | 6 | _foo ? 'hxxstI ' : ''; 7 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/keeps-multiple-instances-of-same-value/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue' 5 | }, 6 | blue: { 7 | color: 'blue' 8 | } 9 | }); 10 | styles(false && 'default', true && 'blue'); 11 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/keeps-multiple-instances-of-same-value/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | const _false = false; 4 | const _true = true; 5 | _true ? 'hxxstI ' : _false ? 'hxxstI ' : ''; 6 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/member-expression-access/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const blue = style9.create({ 3 | blue: { 4 | color: 'blue' 5 | }, 6 | red: { 7 | color: 'red' 8 | } 9 | }).blue; 10 | console.log(blue); 11 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/member-expression-access/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const blue = { 3 | blue: { 4 | color: 'hxxstI' 5 | } 6 | }.blue; 7 | console.log(blue); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/mixed/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue', 5 | opacity: 1 6 | }, 7 | red: { 8 | color: 'red' 9 | } 10 | }); 11 | styles( 12 | { 13 | default: foo 14 | }, 15 | 'red' 16 | ); 17 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/mixed/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | 'RCRUH ' + (foo ? 'gOeSjL ' : ''); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/moves-test/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | 3 | const styles = style9.create({ 4 | default: { 5 | color: 'blue' 6 | } 7 | }); 8 | 9 | styles(foo() && 'default'); 10 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/moves-test/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | 4 | const _foo = foo(); 5 | 6 | _foo ? 'hxxstI ' : ''; 7 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/no-keys/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | 3 | const styles = style9.create({ 4 | default: { 5 | color: 'red' 6 | } 7 | }); 8 | 9 | styles(); 10 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/no-keys/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | (''); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/object/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue', 5 | opacity: 1 6 | }, 7 | red: { 8 | color: 'red' 9 | } 10 | }); 11 | styles({ 12 | default: foo, 13 | red: bar 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/object/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | (bar ? 'RCRUH ' : foo ? 'hxxstI ' : '') + (foo ? 'gOeSjL ' : ''); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/property-access/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles1 = style9.create({ 3 | default: { 4 | color: 'blue' 5 | } 6 | }); 7 | const styles2 = style9.create({ 8 | red: { 9 | color: 'red' 10 | } 11 | }); 12 | style9(styles1.default, styles2.red); 13 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/property-access/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles1 = { 3 | default: { 4 | color: 'hxxstI' 5 | } 6 | }; 7 | const styles2 = { 8 | red: { 9 | color: 'RCRUH' 10 | } 11 | }; 12 | style9(styles1.default, styles2.red); 13 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/short-circuits-same-value/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue' 5 | }, 6 | blue: { 7 | color: 'blue' 8 | }, 9 | red: { 10 | color: 'red' 11 | } 12 | }); 13 | styles('blue', foo && 'default', bar && 'red'); 14 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/short-circuits-same-value/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | bar ? 'RCRUH ' : 'hxxstI '; 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/spread-assignment/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const { ...styles } = style9.create({ 3 | blue: { 4 | color: 'blue' 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/spread-assignment/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const { ...styles } = { 3 | blue: { 4 | color: 'hxxstI' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/spread-use/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | blue: { 4 | color: 'blue' 5 | } 6 | }); 7 | console.log({ ...styles }); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/spread-use/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = { 3 | blue: { 4 | color: 'hxxstI' 5 | } 6 | }; 7 | console.log({ ...styles }); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/static-bracket-access/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const blue = style9.create({ 3 | blue: { 4 | color: 'blue' 5 | }, 6 | red: { 7 | color: 'red' 8 | } 9 | })['blue']; 10 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/static-bracket-access/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const blue = { 3 | blue: { 4 | color: 'hxxstI' 5 | } 6 | }['blue']; 7 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/string-literal/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | 3 | const styles = style9.create({ 4 | default: { 5 | color: 'blue', 6 | opacity: 1 7 | }, 8 | red: { 9 | color: 'red' 10 | } 11 | }); 12 | 13 | styles('default', 'red'); 14 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/string-literal/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('RCRUH gOeSjL '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/ternary/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue', 5 | opacity: 1 6 | }, 7 | red: { 8 | color: 'red' 9 | } 10 | }); 11 | styles(foo ? 'default' : 'red'); 12 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/classes/ternary/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | (foo ? 'hxxstI ' : 'RCRUH ') + (foo ? 'gOeSjL ' : ''); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/custom-properties/does-not-change-capitalization/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | '--backgroundColor': 'red' 5 | } 6 | }); 7 | styles('default'); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/custom-properties/does-not-change-capitalization/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('hJKoGo '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/custom-properties/does-not-convert-number/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | '--opacity': 1 5 | } 6 | }); 7 | styles('default'); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/custom-properties/does-not-convert-number/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('jVMKrZ '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/import/ignore-other-imports/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style8'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue' 5 | } 6 | }); 7 | styles('default'); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/import/ignore-other-imports/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style8'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue' 5 | } 6 | }); 7 | styles('default'); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/incremental-classnames/generated-classname/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | paddingLeft: 2, 5 | paddingTop: 1 6 | }, 7 | other: { 8 | paddingRight: 3 9 | } 10 | }); 11 | styles('default', 'other'); 12 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/incremental-classnames/generated-classname/options.json: -------------------------------------------------------------------------------- 1 | { "incrementalClassnames": true } 2 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/incremental-classnames/generated-classname/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('a b c '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/incremental-classnames/object-classname/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | paddingLeft: 2, 5 | paddingTop: 1 6 | }, 7 | other: { 8 | paddingRight: 3 9 | } 10 | }); 11 | styles.default; 12 | styles.other; 13 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/incremental-classnames/object-classname/options.json: -------------------------------------------------------------------------------- 1 | { "incrementalClassnames": true } 2 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/incremental-classnames/object-classname/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = { 3 | default: { 4 | paddingLeft: 'a', 5 | paddingTop: 'b' 6 | }, 7 | other: { 8 | paddingRight: 'c' 9 | } 10 | }; 11 | styles.default; 12 | styles.other; 13 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/keyframes/basic/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | style9.keyframes({ 3 | '0%': { 4 | color: 'blue' 5 | }, 6 | '100%': {} 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/keyframes/basic/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | ('duuCUn'); 3 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/keyframes/converts-from/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | style9.keyframes({ 3 | from: { 4 | color: 'blue' 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/keyframes/converts-from/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | ('duuCUn'); 3 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/keyframes/converts-to/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | style9.keyframes({ 3 | to: { 4 | color: 'blue' 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/keyframes/converts-to/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | ('lkltCV'); 3 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/keyframes/setting-animationName-directly/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | animationName: style9.keyframes({ 5 | '0%': { 6 | opacity: 0 7 | } 8 | }) 9 | } 10 | }); 11 | styles.default; 12 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/keyframes/setting-animationName-directly/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = { 3 | default: { 4 | animationName: 'flTQwj' 5 | } 6 | }; 7 | styles.default; 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/minify-properties/does-not-minify-by-default/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | opacity: 1 5 | } 6 | }); 7 | styles.default; 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/minify-properties/does-not-minify-by-default/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = { 3 | default: { 4 | opacity: 'gOeSjL' 5 | } 6 | }; 7 | styles.default; 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/minify-properties/hashes-unknown-properties/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | foo: 'bar' 5 | } 6 | }); 7 | styles.default; 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/minify-properties/hashes-unknown-properties/options.json: -------------------------------------------------------------------------------- 1 | { "minifyProperties": true } 2 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/minify-properties/hashes-unknown-properties/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = { 3 | default: { 4 | eicqJS: 'knxHJH' 5 | } 6 | }; 7 | styles.default; 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/minify-properties/minifies-known-properties/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | opacity: 1 5 | } 6 | }); 7 | styles.default; 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/minify-properties/minifies-known-properties/options.json: -------------------------------------------------------------------------------- 1 | { "minifyProperties": true } 2 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/minify-properties/minifies-known-properties/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = { 3 | default: { 4 | j9: 'gOeSjL' 5 | } 6 | }; 7 | styles.default; 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/minify-properties/minifies-nested-properties/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | '@media (max-width: 1000px)': { 5 | '::before': { 6 | opacity: 1 7 | } 8 | } 9 | } 10 | }); 11 | styles.default; 12 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/minify-properties/minifies-nested-properties/options.json: -------------------------------------------------------------------------------- 1 | { "minifyProperties": true } 2 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/minify-properties/minifies-nested-properties/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = { 3 | default: { 4 | cagpug: { 5 | gYHkBN: { 6 | j9: 'kvroMm' 7 | } 8 | } 9 | } 10 | }; 11 | styles.default; 12 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/at-rule-key/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | '@supports': { 5 | '(opacity: 1)': { 6 | opacity: 1, 7 | '@media': { 8 | '(max-width: 1000px)': { 9 | opacity: 1 10 | } 11 | } 12 | } 13 | } 14 | } 15 | }); 16 | styles.default; 17 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/at-rule-key/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = { 3 | default: { 4 | '@supports (opacity: 1)': { 5 | opacity: 'ksLciA', 6 | '@media (max-width: 1000px)': { 7 | opacity: 'keyTYY' 8 | } 9 | } 10 | } 11 | }; 12 | styles.default; 13 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/at-rules/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | '@media (max-width: 1000px)': { 5 | opacity: 1 6 | }, 7 | '@supports (color: blue)': { 8 | color: 'blue' 9 | } 10 | } 11 | }); 12 | styles('default'); 13 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/at-rules/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('Bbwnu cCpNNg '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/basic/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | '::before': { 5 | opacity: 1 6 | } 7 | } 8 | }); 9 | styles('default'); 10 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/basic/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('dLppjJ '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/deep/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | '@media (max-width: 1000px)': { 5 | ':hover': { 6 | '::before': { 7 | opacity: 1 8 | } 9 | } 10 | } 11 | } 12 | }); 13 | styles('default'); 14 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/deep/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('fViSUe '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/generates-correct-class-names/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | '::before': { 5 | opacity: 1 6 | } 7 | }, 8 | hidden: { 9 | '::before': { 10 | opacity: 0 11 | } 12 | } 13 | }); 14 | styles('default', 'hidden'); 15 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/generates-correct-class-names/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('diXuqL '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/translates-old-pseudo-element/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | ':before': { opacity: 1 }, 5 | ':after': { opacity: 1 }, 6 | ':first-letter': { opacity: 1 }, 7 | ':first-line': { opacity: 1 } 8 | } 9 | }); 10 | styles.default; 11 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/nesting/translates-old-pseudo-element/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = { 3 | default: { 4 | '::before': { 5 | opacity: 'dLppjJ' 6 | }, 7 | '::after': { 8 | opacity: 'kMNmYO' 9 | }, 10 | '::first-letter': { 11 | opacity: 'ezsObI' 12 | }, 13 | '::first-line': { 14 | opacity: 'iaGYxt' 15 | } 16 | } 17 | }; 18 | styles.default; 19 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/typescript/casting/code.ts: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | ['--bg-color' as any]: 'blue' 5 | } 6 | }); 7 | styles('default'); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/typescript/casting/output.ts: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('eMOIeF '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/values/arrow-function/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue' 5 | } 6 | }); 7 | const get = state => styles(state && 'default'); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/values/arrow-function/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | 4 | const get = state => (state ? 'hxxstI ' : ''); 5 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/values/expands-shorthand-in-nesting/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | '::before': { 5 | padding: '1rem' 6 | } 7 | } 8 | }); 9 | styles('default'); 10 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/values/expands-shorthand-in-nesting/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('gDvCuo cmluEy fyByHi isyUlq '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/values/expands-shorthand/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | padding: '1rem' 5 | } 6 | }); 7 | styles('default'); 8 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/values/expands-shorthand/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('jWWtke ftIldC bnHxUw iDuqPI '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/values/keeps-longhand/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | paddingTop: '.5rem', 5 | padding: '1rem', 6 | paddingLeft: '2rem' 7 | } 8 | }); 9 | styles('default'); 10 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/values/keeps-longhand/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = {}; 3 | ('lcGuBB ftIldC bnHxUw iigETV '); 4 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/values/removes-unused-keys/code.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = style9.create({ 3 | default: { 4 | color: 'blue' 5 | }, 6 | red: { 7 | color: 'red' 8 | }, 9 | yellow: { 10 | color: 'yellow' 11 | } 12 | }); 13 | styles('default'); 14 | styles.red; 15 | -------------------------------------------------------------------------------- /__tests__/code/fixtures/values/removes-unused-keys/output.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | const styles = { 3 | red: { 4 | color: 'RCRUH' 5 | } 6 | }; 7 | ('hxxstI '); 8 | styles.red; 9 | -------------------------------------------------------------------------------- /__tests__/code/import.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const compile = require('../compile.js'); 3 | 4 | it('ignores other imports', () => { 5 | const input = `import style9 from 'other';`; 6 | const { code } = compile(input); 7 | expect(code).toBe(input); 8 | }); 9 | -------------------------------------------------------------------------------- /__tests__/code/nesting.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const compile = require('../compile.js'); 3 | 4 | it('throws on invalid nesting', () => { 5 | const input = ` 6 | import style9 from 'style9'; 7 | const styles = style9.create({ 8 | default: { 9 | foo: { 10 | opacity: 1 11 | } 12 | } 13 | }); 14 | `; 15 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 16 | "unknown: Invalid key foo. Object keys must be at-rules or pseudo selectors 17 | 3 | const styles = style9.create({ 18 | 4 | default: { 19 | > 5 | foo: { 20 | | ^^^ 21 | 6 | opacity: 1 22 | 7 | } 23 | 8 | }" 24 | `); 25 | }); 26 | 27 | it('throws on invalid nesting with string literal key', () => { 28 | const input = ` 29 | import style9 from 'style9'; 30 | const styles = style9.create({ 31 | default: { 32 | 'foo': { 33 | opacity: 1 34 | } 35 | } 36 | }); 37 | `; 38 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 39 | "unknown: Invalid key foo. Object keys must be at-rules or pseudo selectors 40 | 3 | const styles = style9.create({ 41 | 4 | default: { 42 | > 5 | 'foo': { 43 | | ^^^^^ 44 | 6 | opacity: 1 45 | 7 | } 46 | 8 | }" 47 | `); 48 | }); 49 | 50 | it('throws on invalid nesting with dynamic key', () => { 51 | const input = ` 52 | import style9 from 'style9'; 53 | const foo = 'bar'; 54 | const styles = style9.create({ 55 | default: { 56 | [foo]: { 57 | opacity: 1 58 | } 59 | } 60 | }); 61 | `; 62 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 63 | "unknown: Invalid key bar. Object keys must be at-rules or pseudo selectors 64 | 4 | const styles = style9.create({ 65 | 5 | default: { 66 | > 6 | [foo]: { 67 | | ^^^ 68 | 7 | opacity: 1 69 | 8 | } 70 | 9 | }" 71 | `); 72 | }); 73 | 74 | it('throws when failing to evaluate key', () => { 75 | const input = ` 76 | import style9 from 'style9'; 77 | const styles = style9.create({ 78 | default: { 79 | [foo]: { 80 | opacity: 1 81 | } 82 | } 83 | }); 84 | `; 85 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 86 | "unknown: Could not evaluate value 87 | 3 | const styles = style9.create({ 88 | 4 | default: { 89 | > 5 | [foo]: { 90 | | ^^^ 91 | 6 | opacity: 1 92 | 7 | } 93 | 8 | }" 94 | `); 95 | }); 96 | 97 | it('throws on spread object', () => { 98 | const input = ` 99 | import style9 from 'style9'; 100 | const foo = {}; 101 | const styles = style9.create({ 102 | ...foo 103 | }); 104 | `; 105 | expect(() => compile(input)).toThrowErrorMatchingInlineSnapshot(` 106 | "unknown: Could not evaluate value 107 | 3 | const foo = {}; 108 | 4 | const styles = style9.create({ 109 | > 5 | ...foo 110 | | ^^^^^^ 111 | 6 | }); 112 | 7 | " 113 | `); 114 | }); 115 | -------------------------------------------------------------------------------- /__tests__/compile.js: -------------------------------------------------------------------------------- 1 | const babel = require('@babel/core'); 2 | const plugin = require('../babel.js'); 3 | 4 | function compile(input, opts = {}) { 5 | const { 6 | code, 7 | ast, 8 | metadata: { style9: styles } 9 | } = babel.transformSync(input, { 10 | plugins: [[plugin, opts]], 11 | highlightCode: false 12 | }); 13 | 14 | return { code, ast, styles }; 15 | } 16 | 17 | module.exports = compile; 18 | -------------------------------------------------------------------------------- /__tests__/css-sorter.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const processCSS = require('../src/process-css.js'); 3 | 4 | const CASES = [ 5 | { 6 | name: 'basic', 7 | input: '.a:hover{opacity:1}' + '.b{opacity:1}', 8 | expected: '.b{opacity:1}' + '.a:hover{opacity:1}' 9 | }, 10 | { 11 | name: 'pseudo order', 12 | input: 13 | '.i:active{opacity:1}' + 14 | '.k:disabled{opacity:1}' + 15 | '.f:even-child{opacity:1}' + 16 | '.c:first-child{opacity:1}' + 17 | '.b:focus-within{opacity:1}' + 18 | '.h:focus{opacity:1}' + 19 | '.g:hover{opacity:1}' + 20 | '.d:last-child{opacity:1}' + 21 | '.a:link{opacity:1}' + 22 | '.z:unknown{opacity:1}' + 23 | '.e:odd-child{opacity:1}' + 24 | '.j:visited{opacity:1}', 25 | expected: 26 | '.z:unknown{opacity:1}' + 27 | '.a:link{opacity:1}' + 28 | '.b:focus-within{opacity:1}' + 29 | '.c:first-child{opacity:1}' + 30 | '.d:last-child{opacity:1}' + 31 | '.e:odd-child{opacity:1}' + 32 | '.f:even-child{opacity:1}' + 33 | '.g:hover{opacity:1}' + 34 | '.h:focus{opacity:1}' + 35 | '.i:active{opacity:1}' + 36 | '.j:visited{opacity:1}' + 37 | '.k:disabled{opacity:1}' 38 | }, 39 | { 40 | name: 'first pseudo', 41 | input: '.b:active:hover{opacity:1}' + '.a:hover{opacity:1}', 42 | expected: '.a:hover{opacity:1}' + '.b:active:hover{opacity:1}' 43 | }, 44 | { 45 | name: 'mobile first', 46 | input: 47 | '@media (min-width: 200px){.b{opacity:1}}' + 48 | '@media (min-width: 100px){.a{opacity:1}}' + 49 | '.c{opacity:1}', 50 | expected: 51 | '.c{opacity:1}' + 52 | '@media (min-width: 100px){.a{opacity:1}}' + 53 | '@media (min-width: 200px){.b{opacity:1}}' 54 | }, 55 | { 56 | name: 'pseudo order before media query', 57 | input: 58 | '@media (max-width: 200px){.b:active{opacity:1}}' + 59 | '@media (max-width: 100px){.a:hover{opacity:1}}', 60 | expected: 61 | '@media (max-width: 100px){.a:hover{opacity:1}}' + 62 | '@media (max-width: 200px){.b:active{opacity:1}}' 63 | }, 64 | { 65 | name: 'nested media query', 66 | input: 67 | '@media (max-width: 100px){@media (min-height: 100px){.a{opacity:1}}}' + 68 | '@media (max-width: 200px){@media (min-height: 200px){.b{opacity:1}}}', 69 | expected: 70 | '@media (max-width: 200px){@media (min-height: 200px){.b{opacity:1}}}' + 71 | '@media (max-width: 100px){@media (min-height: 100px){.a{opacity:1}}}' 72 | }, 73 | { 74 | name: 'ignore @supports', 75 | input: '@supports (display: block){.b{opacity:1}}' + '.a{opacity:1}', 76 | expected: '@supports (display: block){.b{opacity:1}}' + '.a{opacity:1}' 77 | }, 78 | { 79 | name: 'splits multiple properties', 80 | input: '.a{color:red;opacity:1}', 81 | expected: '.a{color:red}' + '.a{opacity:1}' 82 | }, 83 | { 84 | name: 'preseveres same pseudo order', 85 | input: '.a:hover{opacity:1}' + '.b:hover{opacity:0}', 86 | expected: '.a:hover{opacity:1}' + '.b:hover{opacity:0}' 87 | }, 88 | { 89 | name: 'ignores pseudo element when sorting', 90 | input: '.a::before:hover{opacity:1}' + '.b:focus{opacity:0}', 91 | expected: '.a::before:hover{opacity:1}' + '.b:focus{opacity:0}' 92 | }, 93 | { 94 | name: 'sorts longhands after shorthands', 95 | input: 96 | '.a{padding-top:2px}' + 97 | '.b{padding:1px}' + 98 | '.c{border-top-width:2px}' + 99 | '.d{border-top:1px}' + 100 | '.e{border:2px solid red}', 101 | expected: 102 | '.b{padding:1px}' + 103 | '.e{border:2px solid red}' + 104 | '.a{padding-top:2px}' + 105 | '.d{border-top:1px}' + 106 | '.c{border-top-width:2px}' 107 | }, 108 | { 109 | name: 'ignore atrule', 110 | input: '@-ms-viewport {width:device-width}' + '.a{opacity:1}', 111 | expected: '@-ms-viewport {width:device-width}' + '.a{opacity:1}' 112 | }, 113 | { 114 | name: 'pseudo order in media queries', 115 | input: 116 | '@media (min-width: 200px){.b:disabled{opacity:1}}' + 117 | '@media (min-width: 100px){.a:disabled{opacity:1}}', 118 | expected: 119 | '@media (min-width: 100px){.a:disabled{opacity:1}}' + 120 | '@media (min-width: 200px){.b:disabled{opacity:1}}' 121 | } 122 | ]; 123 | 124 | const IGNORE = [ 125 | { 126 | name: 'selector list', 127 | input: '.foo, .bar{color:red} .bar, .foo{color:blue}' 128 | }, 129 | { 130 | name: 'non-class selectors', 131 | input: '[disabled]{color:red} .foo[disabled]{color:red}' 132 | }, 133 | { 134 | name: 'multiple selectors', 135 | input: '.foo.bar{color:red} .foo.bar.baz{color:red}' 136 | } 137 | ]; 138 | 139 | for (const { name, input, expected } of CASES) { 140 | it(name, () => { 141 | expect(processCSS(input).css).toEqual(expected); 142 | }); 143 | } 144 | 145 | for (const { name, input } of IGNORE) { 146 | it(name, () => { 147 | expect(processCSS(input).css).toEqual(input); 148 | }); 149 | } 150 | 151 | it('supports setting from parameter', () => { 152 | const from = 'testfile.css'; 153 | expect(processCSS('', { from }).result.opts.from).toEqual(from); 154 | }); 155 | -------------------------------------------------------------------------------- /__tests__/incremental-classnames.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const createGenerator = require('../src/utils/incremental-classnames'); 3 | 4 | it('generates unique classnames', () => { 5 | const { getIncrementalClass } = createGenerator(); 6 | const input = new Array(5000).fill().map((_, index) => String(index)); 7 | const output = input.map(getIncrementalClass); 8 | const duplicates = output.filter((cls, i) => output.indexOf(cls) !== i); 9 | expect(duplicates).toEqual([]); 10 | expect(output.join('')).toEqual(expect.not.stringContaining('undefined')); 11 | const startsWithNumber = output.filter(cls => /^[0-9]/.test(cls)); 12 | expect(startsWithNumber).toEqual([]); 13 | expect(output).toMatchSnapshot(); 14 | const ALL_CHARS = ( 15 | 'abcdefghijklmnopqrstuvwxyz' + 16 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 17 | '0123456789' 18 | ).split(''); 19 | const notIncluded = ALL_CHARS.filter(c => !output.join('').includes(c)); 20 | expect(notIncluded).toEqual([]); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/resolver.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const style9 = require('../index.js').default; 3 | 4 | it('combines different properties', () => { 5 | const input = { 6 | a: { 7 | foo: 'foo' 8 | }, 9 | b: { 10 | bar: 'bar' 11 | } 12 | }; 13 | expect(style9(input.a, input.b)).toBe('foo bar'); 14 | }); 15 | 16 | it('merges from right to left', () => { 17 | const input = { 18 | a: { 19 | foo: 'foo' 20 | }, 21 | b: { 22 | foo: 'bar' 23 | } 24 | }; 25 | expect(style9(input.a, input.b)).toBe('bar'); 26 | }); 27 | 28 | it('ignores falsy values', () => { 29 | const input = { 30 | a: { 31 | foo: 'foo' 32 | } 33 | }; 34 | expect(style9(input.a, false, undefined, null)).toBe('foo'); 35 | }); 36 | 37 | it('handles nested objects', () => { 38 | const input = { 39 | a: { 40 | foo: 'foo', 41 | first: { 42 | foo: 'baz' 43 | } 44 | }, 45 | b: { 46 | foo: 'bar' 47 | } 48 | }; 49 | expect(style9(input.a, input.b)).toBe('bar baz'); 50 | }); 51 | 52 | it('merges nested objects', () => { 53 | const input = { 54 | a: { 55 | foo: 'foo', 56 | first: { 57 | foo: 'baz' 58 | } 59 | }, 60 | b: { 61 | foo: 'bar', 62 | first: { 63 | foo: 'biz' 64 | } 65 | } 66 | }; 67 | expect(style9(input.a, input.b)).toBe('bar biz'); 68 | }); 69 | 70 | it('handles deeply nested objects', () => { 71 | const input = { 72 | a: { 73 | foo: 'foo', 74 | first: { 75 | foo: 'baz', 76 | second: { 77 | foo: 'bop' 78 | } 79 | } 80 | }, 81 | b: { 82 | foo: 'bar' 83 | } 84 | }; 85 | expect(style9(input.a, input.b)).toBe('bar baz bop'); 86 | }); 87 | 88 | it('merges deeply nested objects', () => { 89 | const input = { 90 | a: { 91 | foo: 'foo', 92 | first: { 93 | foo: 'baz', 94 | second: { 95 | foo: 'bop' 96 | } 97 | } 98 | }, 99 | b: { 100 | foo: 'bar', 101 | first: { 102 | foo: 'biz', 103 | second: { 104 | foo: 'bip' 105 | } 106 | } 107 | } 108 | }; 109 | expect(style9(input.a, input.b)).toBe('bar biz bip'); 110 | }); 111 | 112 | it('merges several deeply nested objects', () => { 113 | const input = { 114 | a: { 115 | foo: 'foo', 116 | first: { 117 | foo: 'baz', 118 | second: { 119 | foo: 'bop' 120 | } 121 | } 122 | }, 123 | b: { 124 | foo: 'bar', 125 | first: { 126 | foo: 'biz', 127 | second: { 128 | foo: 'bip' 129 | } 130 | } 131 | }, 132 | c: { 133 | foo: 'bup', 134 | first: { 135 | foo: 'bap' 136 | } 137 | } 138 | }; 139 | expect(style9(input.a, input.b, input.c)).toBe('bup bap bip'); 140 | }); 141 | 142 | it('does not modify objects', () => { 143 | const input = { 144 | a: { 145 | foo: 'foo', 146 | first: { 147 | foo: 'baz', 148 | second: { 149 | foo: 'bop' 150 | } 151 | } 152 | }, 153 | b: { 154 | foo: 'bar', 155 | first: { 156 | foo: 'biz', 157 | second: { 158 | foo: 'bip' 159 | } 160 | } 161 | } 162 | }; 163 | const clone = JSON.parse(JSON.stringify(input)); 164 | style9(input.a, input.b); 165 | expect(input).toEqual(clone); 166 | }); 167 | 168 | it('returns empty string when called without arguments', () => { 169 | expect(style9()).toBe(''); 170 | }); 171 | 172 | it('create should throw', () => { 173 | expect(() => style9.create({})).toThrow( 174 | new Error('style9.create calls should be compiled away') 175 | ); 176 | }); 177 | 178 | it('keyframes should throw', () => { 179 | expect(() => style9.keyframes({})).toThrow( 180 | new Error('style9.keyframes calls should be compiled away') 181 | ); 182 | }); 183 | -------------------------------------------------------------------------------- /__tests__/styles/comma-separated-properties.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const compile = require('../compile.js'); 3 | 4 | it('handles properties wich can be defined as lists correctly', () => { 5 | const input = ` 6 | import style9 from 'style9'; 7 | const styles = style9.create({ 8 | default: { 9 | transitionProperty: ['opacity', 'transform'], 10 | transitionDuration: ['200ms', '300ms', '400ms'], 11 | transitionDelay: ['100ms', '200ms', '300ms'], 12 | transitionTimingFunction: ['ease-in', 'ease-out', 'ease-in-out'], 13 | strokeDasharray: [10, 100, 200], 14 | scrollSnapType: ['none', 'mandatory'], 15 | scrollSnapAlign: ['start', 'end'] 16 | } 17 | }); 18 | styles('default'); 19 | `; 20 | const { styles } = compile(input); 21 | 22 | expect(styles).toBe( 23 | '.ivPgPH{transition-property:opacity,transform}' + 24 | '.cfCwqg{transition-duration:200ms,300ms,400ms}' + 25 | '.dEsdmn{transition-delay:100ms,200ms,300ms}' + 26 | '.genghA{transition-timing-function:ease-in,ease-out,ease-in-out}' + 27 | '.kViNob{stroke-dasharray:10 100 200}' + 28 | '.cwFPDc{scroll-snap-type:none mandatory}' + 29 | '.hboCrl{scroll-snap-align:start end}' 30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/styles/custom-properties.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const compile = require('../compile.js'); 3 | 4 | it('does not convert number', () => { 5 | const input = ` 6 | import style9 from 'style9'; 7 | const styles = style9.create({ 8 | default: { 9 | '--opacity': 1 10 | } 11 | }); 12 | styles('default'); 13 | `; 14 | const { styles } = compile(input); 15 | 16 | expect(styles).toBe('.jVMKrZ{--opacity:1}'); 17 | }); 18 | 19 | it('does not change capitalization', () => { 20 | const input = ` 21 | import style9 from 'style9'; 22 | const styles = style9.create({ 23 | default: { 24 | '--backgroundColor': 'red' 25 | } 26 | }); 27 | styles('default'); 28 | `; 29 | const { styles } = compile(input); 30 | 31 | expect(styles).toBe('.hJKoGo{--backgroundColor:red}'); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/styles/incremental-classnames.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const compile = require('../compile.js'); 3 | 4 | it('uses incremental classname for styles', () => { 5 | const input = ` 6 | import style9 from 'style9'; 7 | const styles = style9.create({ 8 | default: { 9 | paddingLeft: 2, 10 | paddingTop: 1 11 | }, 12 | other: { 13 | paddingRight: 3 14 | } 15 | }); 16 | styles('default', 'other'); 17 | `; 18 | const { styles } = compile(input, { 19 | incrementalClassnames: true 20 | }); 21 | 22 | expect(styles).toBe( 23 | '.a{padding-left:2px}.b{padding-top:1px}.c{padding-right:3px}' 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /__tests__/styles/keyframes.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const compile = require('../compile.js'); 3 | 4 | it('generates keyframes', () => { 5 | const input = ` 6 | import style9 from 'style9'; 7 | style9.keyframes({ 8 | '0%': { 9 | color: 'blue' 10 | }, 11 | '100%': { 12 | color: 'red' 13 | } 14 | }); 15 | `; 16 | const { styles } = compile(input); 17 | 18 | expect(styles).toBe('@keyframes gzOBtW{0%{color:blue}100%{color:red}}'); 19 | }); 20 | 21 | it('removes empty frame', () => { 22 | const input = ` 23 | import style9 from 'style9'; 24 | style9.keyframes({ 25 | '0%': { 26 | color: 'blue' 27 | }, 28 | '100%': { 29 | } 30 | }); 31 | `; 32 | const { styles } = compile(input); 33 | 34 | expect(styles).toBe('@keyframes duuCUn{0%{color:blue}}'); 35 | }); 36 | 37 | it('converts from', () => { 38 | const input = ` 39 | import style9 from 'style9'; 40 | style9.keyframes({ 41 | from: { 42 | color: 'blue' 43 | } 44 | }); 45 | `; 46 | const { styles } = compile(input); 47 | 48 | expect(styles).toBe('@keyframes duuCUn{0%{color:blue}}'); 49 | }); 50 | 51 | it('converts to', () => { 52 | const input = ` 53 | import style9 from 'style9'; 54 | style9.keyframes({ 55 | to: { 56 | color: 'blue' 57 | } 58 | }); 59 | `; 60 | const { styles } = compile(input); 61 | 62 | expect(styles).toBe('@keyframes lkltCV{100%{color:blue}}'); 63 | }); 64 | 65 | it('expands shorthand', () => { 66 | const input = ` 67 | import style9 from 'style9'; 68 | style9.keyframes({ 69 | '0%': { 70 | padding: '1rem', 71 | } 72 | }); 73 | `; 74 | const { styles } = compile(input); 75 | 76 | expect(styles).toBe( 77 | '@keyframes dyyvIk{0%{' + 78 | 'padding-top:1rem;' + 79 | 'padding-right:1rem;' + 80 | 'padding-bottom:1rem;' + 81 | 'padding-left:1rem' + 82 | '}}' 83 | ); 84 | }); 85 | 86 | it('keeps longhand', () => { 87 | const input = ` 88 | import style9 from 'style9'; 89 | style9.keyframes({ 90 | '0%': { 91 | padding: '1rem', 92 | paddingLeft: '2rem' 93 | } 94 | }); 95 | `; 96 | const { styles } = compile(input); 97 | 98 | expect(styles).toBe( 99 | '@keyframes eTcyEi{0%{' + 100 | 'padding-top:1rem;' + 101 | 'padding-right:1rem;' + 102 | 'padding-bottom:1rem;' + 103 | 'padding-left:2rem' + 104 | '}}' 105 | ); 106 | }); 107 | -------------------------------------------------------------------------------- /__tests__/styles/multiple-imports.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const compile = require('../compile.js'); 3 | 4 | it('supports multiple imports', () => { 5 | const input = ` 6 | import style0 from 'style9'; 7 | import style1 from 'style9'; 8 | const styles0 = style0.create({ 9 | first: { 10 | color: 'blue' 11 | } 12 | }); 13 | styles0('first'); 14 | const styles1 = style1.create({ 15 | second: { 16 | color: 'red' 17 | } 18 | }); 19 | styles1('second'); 20 | `; 21 | const { styles } = compile(input); 22 | 23 | expect(styles).toBe(`.hxxstI{color:blue}.RCRUH{color:red}`); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/styles/nesting.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const compile = require('../compile.js'); 3 | 4 | it('supports nesting', () => { 5 | const input = ` 6 | import style9 from 'style9'; 7 | const styles = style9.create({ 8 | default: { 9 | '::before': { 10 | opacity: 1 11 | } 12 | } 13 | }); 14 | styles('default'); 15 | `; 16 | const { styles } = compile(input); 17 | 18 | expect(styles).toBe('.dLppjJ::before{opacity:1}'); 19 | }); 20 | 21 | it('supports at rules', () => { 22 | const input = ` 23 | import style9 from 'style9'; 24 | const styles = style9.create({ 25 | default: { 26 | '@media (max-width: 1000px)': { 27 | opacity: 1 28 | }, 29 | '@supports (color: blue)': { 30 | opacity: 'blue' 31 | } 32 | } 33 | }); 34 | styles('default'); 35 | `; 36 | const { styles } = compile(input); 37 | 38 | expect(styles).toBe( 39 | '@media (max-width: 1000px){.Bbwnu{opacity:1}}' + 40 | '@supports (color: blue){.ilahpL{opacity:blue}}' 41 | ); 42 | }); 43 | 44 | it('supports deep nesting', () => { 45 | const input = ` 46 | import style9 from 'style9'; 47 | const styles = style9.create({ 48 | default: { 49 | '@media (max-width: 1000px)': { 50 | '@media (max-width: 200px)': { 51 | ':hover': { 52 | '::before': { 53 | opacity: 1 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }); 60 | styles('default'); 61 | `; 62 | const { styles } = compile(input); 63 | 64 | expect(styles).toBe( 65 | '@media (max-width: 1000px){@media (max-width: 200px){.ClOOj:hover::before{opacity:1}}}' 66 | ); 67 | }); 68 | 69 | it('generates correct class names', () => { 70 | const input = ` 71 | import style9 from 'style9'; 72 | const styles = style9.create({ 73 | default: { 74 | '::before': { 75 | opacity: 1 76 | } 77 | }, 78 | hidden: { 79 | '::before': { 80 | opacity: 0 81 | } 82 | } 83 | }); 84 | styles('default', 'hidden'); 85 | `; 86 | const { styles } = compile(input); 87 | 88 | expect(styles).toBe('.dLppjJ::before{opacity:1}.diXuqL::before{opacity:0}'); 89 | }); 90 | 91 | it('translates old pseudo element', () => { 92 | const input = ` 93 | import style9 from 'style9'; 94 | const styles = style9.create({ 95 | default: { 96 | ':before': { opacity: 1 }, 97 | ':after': { opacity: 1 }, 98 | ':first-letter': { opacity: 1 }, 99 | ':first-line': { opacity: 1 } 100 | } 101 | }); 102 | styles.default 103 | `; 104 | const { styles } = compile(input); 105 | 106 | expect(styles).toBe( 107 | '.dLppjJ::before{opacity:1}' + 108 | '.kMNmYO::after{opacity:1}' + 109 | '.ezsObI::first-letter{opacity:1}' + 110 | '.iaGYxt::first-line{opacity:1}' 111 | ); 112 | }); 113 | -------------------------------------------------------------------------------- /__tests__/styles/transition-property.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const compile = require('../compile.js'); 3 | 4 | it('converts transitionProperty to kebab-case', () => { 5 | const input = ` 6 | import style9 from 'style9'; 7 | const styles = style9.create({ 8 | default: { 9 | transitionProperty: 'backgroundColor', 10 | } 11 | }); 12 | styles('default'); 13 | `; 14 | const { styles } = compile(input); 15 | 16 | expect(styles).toBe('.diErbW{transition-property:background-color}'); 17 | }); 18 | 19 | it('converts transitionProperty list to kebab-case', () => { 20 | const input = ` 21 | import style9 from 'style9'; 22 | const styles = style9.create({ 23 | default: { 24 | transitionProperty: ['backgroundColor', 'borderColor', 'boxShadow'], 25 | } 26 | }); 27 | styles('default'); 28 | `; 29 | const { styles } = compile(input); 30 | 31 | expect(styles).toBe( 32 | '.gKERdg{transition-property:background-color,border-color,box-shadow}' 33 | ); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/styles/values.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const compile = require('../compile.js'); 3 | 4 | it('converts paddingLeft to pixels', () => { 5 | const input = ` 6 | import style9 from 'style9'; 7 | const styles = style9.create({ 8 | default: { 9 | paddingLeft: 2 10 | } 11 | }); 12 | styles('default'); 13 | `; 14 | const { styles } = compile(input); 15 | 16 | expect(styles).toBe(`.hhTkCv{padding-left:2px}`); 17 | }); 18 | 19 | it('does not convert opacity to pixels', () => { 20 | const input = ` 21 | import style9 from 'style9'; 22 | const styles = style9.create({ 23 | default: { 24 | opacity: 1 25 | } 26 | }); 27 | styles('default'); 28 | `; 29 | const { styles } = compile(input); 30 | 31 | expect(styles).toBe(`.gOeSjL{opacity:1}`); 32 | }); 33 | 34 | it('expands shorthand', () => { 35 | const input = ` 36 | import style9 from 'style9'; 37 | const styles = style9.create({ 38 | default: { 39 | padding: '1rem' 40 | } 41 | }); 42 | styles('default'); 43 | `; 44 | const { styles } = compile(input); 45 | 46 | expect(styles).toBe( 47 | '.jWWtke{padding-top:1rem}' + 48 | '.ftIldC{padding-right:1rem}' + 49 | '.bnHxUw{padding-bottom:1rem}' + 50 | '.iDuqPI{padding-left:1rem}' 51 | ); 52 | }); 53 | 54 | it('does not override longhand', () => { 55 | const input = ` 56 | import style9 from 'style9'; 57 | const styles = style9.create({ 58 | default: { 59 | paddingTop: '.5rem', 60 | padding: '1rem', 61 | paddingLeft: '2rem' 62 | } 63 | }); 64 | styles('default'); 65 | `; 66 | const { styles } = compile(input); 67 | 68 | expect(styles).toBe( 69 | '.lcGuBB{padding-top:.5rem}' + 70 | '.ftIldC{padding-right:1rem}' + 71 | '.bnHxUw{padding-bottom:1rem}' + 72 | '.iigETV{padding-left:2rem}' 73 | ); 74 | }); 75 | 76 | it('converts fontSize pixels', () => { 77 | const input = ` 78 | import style9 from 'style9'; 79 | const styles = style9.create({ 80 | default: { 81 | fontSize: 14 82 | } 83 | }); 84 | styles('default'); 85 | `; 86 | const { styles } = compile(input); 87 | 88 | expect(styles).toBe(`.kKRHCo{font-size:0.875rem}`); 89 | }); 90 | 91 | it('accepts an array', () => { 92 | const input = ` 93 | import style9 from 'style9'; 94 | const styles = style9.create({ 95 | default: { 96 | textDecorationLine: ['underline', 'overline'] 97 | } 98 | }); 99 | styles('default'); 100 | `; 101 | const { styles } = compile(input); 102 | 103 | expect(styles).toBe(`.jMUvdQ{text-decoration-line:underline overline}`); 104 | }); 105 | 106 | it('supports constants', () => { 107 | const input = ` 108 | import style9 from 'style9'; 109 | const BLUE = 'blue'; 110 | const styles = style9.create({ 111 | default: { 112 | color: BLUE 113 | } 114 | }); 115 | styles('default'); 116 | `; 117 | const { styles } = compile(input); 118 | 119 | expect(styles).toBe(`.hxxstI{color:blue}`); 120 | }); 121 | 122 | it('removes unused styles', () => { 123 | const input = ` 124 | import style9 from 'style9'; 125 | const styles = style9.create({ 126 | default: { 127 | color: 'blue' 128 | } 129 | }); 130 | `; 131 | const { styles } = compile(input); 132 | 133 | expect(styles).toBe(``); 134 | }); 135 | 136 | it('keeps styles used in styles()', () => { 137 | const input = ` 138 | import style9 from 'style9'; 139 | const styles = style9.create({ 140 | default: { 141 | color: 'blue' 142 | }, 143 | red: { 144 | color: 'red' 145 | } 146 | }); 147 | styles('default'); 148 | `; 149 | const { styles } = compile(input); 150 | 151 | expect(styles).toBe(`.hxxstI{color:blue}`); 152 | }); 153 | 154 | it('keeps styles used as object', () => { 155 | const input = ` 156 | import style9 from 'style9'; 157 | const styles = style9.create({ 158 | default: { 159 | color: 'blue' 160 | }, 161 | red: { 162 | color: 'red' 163 | } 164 | }); 165 | styles.default; 166 | `; 167 | const { styles } = compile(input); 168 | 169 | expect(styles).toBe(`.hxxstI{color:blue}`); 170 | }); 171 | 172 | it('supports static bracket access', () => { 173 | const input = ` 174 | import style9 from 'style9'; 175 | const styles = style9.create({ 176 | default: { 177 | color: 'blue' 178 | }, 179 | red: { 180 | color: 'red' 181 | } 182 | }); 183 | styles['default'] 184 | `; 185 | const { styles } = compile(input); 186 | 187 | expect(styles).toBe(`.hxxstI{color:blue}`); 188 | }); 189 | 190 | it('supports dynamic bracket access', () => { 191 | const input = ` 192 | import style9 from 'style9'; 193 | const styles = style9.create({ 194 | blue: { 195 | color: 'blue' 196 | }, 197 | red: { 198 | color: 'red' 199 | } 200 | }); 201 | styles[blue] 202 | `; 203 | const { styles } = compile(input); 204 | 205 | expect(styles).toBe(`.hxxstI{color:blue}.RCRUH{color:red}`); 206 | }); 207 | 208 | it('supports arrow function', () => { 209 | const input = ` 210 | import style9 from 'style9'; 211 | const styles = style9.create({ 212 | default: { 213 | color: 'blue' 214 | } 215 | }); 216 | const get = state => styles(state && 'default'); 217 | `; 218 | const { styles } = compile(input); 219 | 220 | expect(styles).toBe(`.hxxstI{color:blue}`); 221 | }); 222 | 223 | it('outputs no styles without declaration', () => { 224 | const input = ` 225 | import style9 from 'style9'; 226 | style9.create({ 227 | default: { 228 | color: 'blue' 229 | } 230 | }); 231 | `; 232 | const { styles } = compile(input); 233 | 234 | expect(styles).toBe(''); 235 | }); 236 | 237 | it('supports spread assignment', () => { 238 | const input = ` 239 | import style9 from 'style9'; 240 | const { ...styles } = style9.create({ 241 | blue: { 242 | color: 'blue' 243 | } 244 | }); 245 | `; 246 | const { styles } = compile(input); 247 | 248 | expect(styles).toBe(`.hxxstI{color:blue}`); 249 | }); 250 | 251 | it('removes unused destructured keys', () => { 252 | const input = ` 253 | import style9 from 'style9'; 254 | const { blue } = style9.create({ 255 | blue: { 256 | color: 'blue' 257 | }, 258 | red: { 259 | color: 'red' 260 | } 261 | }); 262 | `; 263 | const { styles } = compile(input); 264 | expect(styles).toBe('.hxxstI{color:blue}'); 265 | }); 266 | 267 | it('supports spread use', () => { 268 | const input = ` 269 | import style9 from 'style9'; 270 | const styles = style9.create({ 271 | blue: { 272 | color: 'blue' 273 | } 274 | }); 275 | console.log({ ...styles }); 276 | `; 277 | const { styles } = compile(input); 278 | 279 | expect(styles).toBe(`.hxxstI{color:blue}`); 280 | }); 281 | 282 | it('supports member expression access', () => { 283 | const input = ` 284 | import style9 from 'style9'; 285 | const blue = style9.create({ 286 | blue: { 287 | color: 'blue' 288 | }, 289 | red: { 290 | color: 'red' 291 | } 292 | }).blue; 293 | console.log(blue) 294 | `; 295 | const { styles } = compile(input); 296 | 297 | expect(styles).toBe(`.hxxstI{color:blue}`); 298 | }); 299 | 300 | it('does not output CSS when style9() is called', () => { 301 | const input = ` 302 | import style9 from 'style9'; 303 | style9(); 304 | `; 305 | const { styles } = compile(input); 306 | 307 | expect(styles).toBe(''); 308 | }); 309 | -------------------------------------------------------------------------------- /babel.js: -------------------------------------------------------------------------------- 1 | const NAME = require('./package.json').name; 2 | const processReferences = require('./src/process-references.js'); 3 | 4 | module.exports = function style9BabelPlugin() { 5 | return { 6 | name: NAME, 7 | visitor: { 8 | ImportDefaultSpecifier(path, state) { 9 | if (path.parent.source.value !== NAME) return; 10 | 11 | const importName = path.node.local.name; 12 | const bindings = path.scope.bindings[importName].referencePaths; 13 | 14 | const css = processReferences(bindings, state.opts).join(''); 15 | if (!state.file.metadata.style9) { 16 | state.file.metadata.style9 = ''; 17 | } 18 | state.file.metadata.style9 += css; 19 | } 20 | } 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /docs/Background.md: -------------------------------------------------------------------------------- 1 | # Background 2 | 3 | style9 was created to be an open-source implementation of the CSS-in-JS library used to develop the new version of Facebook, [stylex][stylex]. For that reason, the design principles are heavily influenced by stylex. 4 | 5 | ## Principles 6 | 7 | ### Optimized output 8 | 9 | Large JavaScript and CSS files are bad for performance, and embedding CSS in JavaScript is even worse. By only allowing values that can be inferred at compile time, combined with a carefully designed API, the CSS can be extracted to it's own file at compile time. Additionally, any styles that aren't used can be pruned from the output. And because most values are used many times the classes are outputted as atomic CSS, which avoids the issue of ever-growing files. 10 | 11 | ### No global styles, no cascade, no selectors 12 | 13 | No issues with specificity, no searching for definitions. Styles can be co-located with the code and referenced directly. 14 | 15 | ### Type safety 16 | 17 | Full type safety, courtesy of TypeScript. Values, properties and variables can be autocompleted and errors caught, all using toolchains already used. 18 | 19 | ### Low-level, framework agnostic, small API 20 | 21 | style9 deals with defining styles and generating class names, nothing else. This makes the API small and makes style9 easy to learn. It also makes it framework agnostic. It also makes it possible to build solutions on top, see [Ecosystem](Ecosystem.md). 22 | 23 | ## Tradeoffs 24 | 25 | ### No dynamic values 26 | 27 | There are two types of values that can be considered dynamic: the first is about choosing among pre-defined styles based on a runtime value. These are fully supported, for example: 28 | 29 | ```typescript 30 | import style9 from 'style9'; 31 | 32 | const styles = style9.create({ 33 | blue: { 34 | color: 'blue' 35 | }, 36 | red: { 37 | color: 'red' 38 | } 39 | }); 40 | 41 | export const getClass(color: keyof typeof styles) => style9(styles[color]); 42 | ``` 43 | 44 | The other is using a value that's only available at the runtime, a common example being setting a user-defined image as a background. These are not available while compiling, and are therefore not supported. However, these values are one-off and limited in use, and does not suffer from being set inline. Or, if reuse is required, they can be set as CSS Custom Properties: 45 | 46 | ```typescript 47 | import style9 from 'style9'; 48 | 49 | const styles = style9.create({ 50 | avatar: { 51 | backgroundColor: 'var(--my-personal-color)' 52 | } 53 | }); 54 | 55 | document.body.style = `--my-personal-color: blue;`; 56 | ``` 57 | 58 | ### No shorthands 59 | 60 | Because the generated CSS uses atomic classes, there is no single class name that can be toggled for each style. Instead, multiple classes are toggled, with the ones that are overridden removed. This means that it must be possible to know which properties set the same styles. Shorthand CSS properties, such as `background`, make this impossible since they set multiple values. 61 | 62 | [stylex]: https://engineering.fb.com/2020/05/08/web/facebook-redesign/ 63 | -------------------------------------------------------------------------------- /docs/Bundler-plugins.md: -------------------------------------------------------------------------------- 1 | # Bundler plugins 2 | 3 | ### Universal options 4 | 5 | - `minifyProperties` - minify the property names of style objects. Unless you pass custom objects to `style9()` not generated by `style9.create()`, this is safe to enable and will lead to smaller JavaScript output but mangled property names. Consider enabling it in production. Default: `false` 6 | - `incrementalClassnames` - use incremental names for classes(`.a`, `.b`) instead of a hash of the style value. This means that class names are not stable across builds, but leads to smaller JavaScript output. Default: `false` 7 | 8 | ## Webpack 9 | 10 | ```javascript 11 | const Style9Plugin = require('style9/webpack'); 12 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 13 | 14 | module.exports = { 15 | // Collect all styles in a single file - required 16 | optimization: { 17 | splitChunks: { 18 | cacheGroups: { 19 | styles: { 20 | name: 'styles', 21 | type: 'css/mini-extract', 22 | // For webpack@4 remove type and uncomment the line below 23 | // test: /\.css$/, 24 | chunks: 'all', 25 | enforce: true, 26 | } 27 | } 28 | } 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.(tsx|ts|js|mjs|jsx)$/, 34 | use: Style9Plugin.loader, 35 | options: { 36 | parserOptions?: BabelParserOpts; 37 | minifyProperties?: boolean; 38 | incrementalClassnames?: boolean; 39 | } 40 | }, 41 | { 42 | test: /\.css$/i, 43 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 44 | } 45 | ] 46 | }, 47 | plugins: [ 48 | new Style9Plugin(), 49 | new MiniCssExtractPlugin() 50 | ] 51 | }; 52 | ``` 53 | 54 | ## Rollup 55 | 56 | ```javascript 57 | import style9 from 'style9/rollup'; 58 | 59 | export default { 60 | // ... 61 | plugins: [ 62 | style9({ 63 | include?: (string | RegExp)[] | string | RegExp; 64 | exclude?: (string | RegExp)[] | string | RegExp; 65 | // fileName XOR name is required 66 | fileName: string; // will be emitted as fileName 67 | name: string; // will be emitted according to output.assetFileNames format 68 | parserOptions?: BabelParserOpts; 69 | minifyProperties?: boolean; 70 | incrementalClassnames?: boolean; 71 | }) 72 | ] 73 | }; 74 | ``` 75 | 76 | ## Next.js 77 | 78 | ```javascript 79 | const withTM = require('next-transpile-modules')(['style9']); 80 | // If you are using the latest version Next.js: 81 | const withStyle9 = require('style9/next'); 82 | // If you are using Next.js below 12.0.5: 83 | const withStyle9 = require('style9/next-legacy'); 84 | 85 | module.exports = withStyle9({ 86 | parserOptions?: BabelParserOpts; 87 | minifyProperties?: boolean; 88 | incrementalClassnames?: boolean; 89 | })(withTM()); 90 | ``` 91 | 92 | ## Gatsby 93 | 94 | ```javascript 95 | module.exports = { 96 | plugins: [ 97 | { 98 | resolve: 'style9/gatsby', 99 | options: { 100 | parserOptions?: BabelParserOpts; 101 | minifyProperties?: boolean; 102 | incrementalClassnames?: boolean; 103 | } 104 | } 105 | ] 106 | } 107 | ``` 108 | 109 | ## Vite 110 | 111 | ```javascript 112 | import { defineConfig } from 'vite'; 113 | import style9 from 'style9/vite'; 114 | export default defineConfig({ 115 | plugins: [ 116 | // ...other plugins. 117 | style9() 118 | ] 119 | }); 120 | ``` 121 | 122 | ## Babel 123 | 124 | When using the babel plugin you'll have to pass the generated CSS to the post-processor yourself. If compiling multiple files this should be done to the concatenated output of all babel transforms. 125 | 126 | ```javascript 127 | const babel = require('@babel/core'); 128 | const processCSS = require('style9/src/process-css'); 129 | 130 | const output = babel.transformFile('./file.js', { 131 | plugins: [['style9/babel', { 132 | minifyProperties?: boolean; 133 | incrementalClassnames?: boolean; 134 | }]] 135 | }); 136 | const { css } = processCSS(output.metadata.style9 || '', options?: PostCSSOptions); 137 | ``` 138 | -------------------------------------------------------------------------------- /docs/Ecosystem.md: -------------------------------------------------------------------------------- 1 | # Ecosystem 2 | 3 | ## [style9-components.macro](https://github.com/johanholmerin/style9-components.macro) 4 | 5 | styled-components API for React with support for prop-based styling 6 | 7 | ```javascript 8 | import styled from 'style9-components.macro'; 9 | 10 | // Create new component 11 | const Component = styled.div({ 12 | color: props => props.color, 13 | backgroundColor: 'red' 14 | }); 15 | 16 | // Extend existing component 17 | const WrappedComponent = styled(Component)({ 18 | backgroundColor: 'blue' 19 | }); 20 | 21 | // Support for overriding the element 22 | 23 | ``` 24 | 25 | ## [style9-jsx-prop](https://github.com/johanholmerin/style9-jsx-prop) 26 | 27 | JSX CSS-prop API with support for prop-based styling 28 | 29 | ```javascript 30 |
38 | ``` 39 | 40 | ## [css-to-js.macro](https://github.com/johanholmerin/css-to-js.macro) 41 | 42 | CSS macro for converting tagged template literal to object. Can be combined with any other library 43 | 44 | ```javascript 45 | import { css, keyframes } from 'css-to-js.macro'; 46 | 47 | css` 48 | color: red; 49 | font-size: ${props.fontSize}; 50 | ${props.isBlue && css` 51 | color: blue; 52 | `} 53 | animation-name: ${keyframes` 54 | from { opacity: 1; } 55 | `}; 56 | ` 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## Why can't I use shorthand CSS properties? 4 | 5 | See [Tradeoffs - No shorthands](Background.md#no-shorthands) 6 | 7 | ## Why can't I export the value from `style9.create`? 8 | 9 | The value returned from `style9.create` can be used as a function or as an object with properties. However, the generated code is just a plain object: the function call is transformed by the compiler to a ternary expression. To avoid issues when using the value as a function in places where babel can't track the reference, you have to explicitly make it into a plain object using the spread operator: 10 | 11 | ```javascript 12 | export const { ...styles } = style9.create({ 13 | // ... 14 | }); 15 | ``` 16 | 17 | ## Errors 18 | 19 | ### `Could not evaluate value` 20 | 21 | Babel failed to evaluate a value used in a style definition. Try moving the value directly to the create call. 22 | 23 | #### Unsupported uses 24 | 25 | ```javascript 26 | import style9 from 'style9'; 27 | import importedColor from './color'; 28 | 29 | const OBJECT = { 30 | BLUE: 'blue' 31 | }; 32 | 33 | const styles = style9.create({ 34 | imported: { 35 | color: importedColor 36 | }, 37 | object: { 38 | color: OBJECT.BLUE 39 | } 40 | }); 41 | ``` 42 | 43 | #### Supported use 44 | 45 | ```javascript 46 | import style9 from 'style9'; 47 | 48 | const COLOR = 'blue'; 49 | 50 | const styles = style9.create({ 51 | constant: { 52 | color: COLOR 53 | } 54 | }); 55 | ``` 56 | 57 | ### `Unsupported type` 58 | 59 | You're using an operator or value type that isn't supported. See [Usage guide](Usage-guide.md) for supported uses. 60 | 61 | ## Have another question? 62 | 63 | Look at the [FAQ](docs/FAQ.md), [search][search] the repo, or ask in [discussions][discussions]. 64 | 65 | [search]: https://github.com/johanholmerin/style9/search 66 | [discussions]: https://github.com/johanholmerin/style9/discussions 67 | -------------------------------------------------------------------------------- /docs/How-it-works.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | None of this is needed to use the library, it's purely for understanding how it works. 4 | 5 | ## Static value evaluation 6 | 7 | To be able to generate a static CSS file, the compiler needs to resolve the values defined in a call to `style9.create`. This is achieved using the `evaluate`-function from [@babel/traverse][babel-traverse]. Each property is then used to create a css class. 8 | 9 | ## Function call to string concatenation 10 | 11 | There are two ways of using the value returned from `style9.create`. The first is using as a function, passing the keys you want to apply. In this case, the compiler tracks all references and replaces them with simple string concatenation. For styles that are applied conditionally, the properties that would be overriden by a particular styles object are figured out and applied using ternary operators. In this case the entire create call can be removed, and the runtime is not needed. 12 | 13 | ### Input 14 | 15 | ```javascript 16 | import style9 from 'style9'; 17 | 18 | const styles = style9.create({ 19 | blue: { 20 | color: 'blue', 21 | }, 22 | red: { 23 | color: 'red' 24 | } 25 | }); 26 | 27 | document.body.className = styles('blue', isRed && 'red'); 28 | ``` 29 | 30 | ### Output 31 | 32 | ```javascript 33 | /* JavaScript */ 34 | document.body.className = isRed ? 'RCRUH ' : 'hxxstI '; 35 | 36 | /* CSS */ 37 | .hxxstI { color: blue } 38 | .RCRUH { color: red } 39 | ``` 40 | 41 | ## Merging composed styles 42 | 43 | For composing styles from different definitions, the properties object is needed to be able to resolve which properties should be applied. In this case the runtime is needed. 44 | 45 | ### Input 46 | 47 | ```javascript 48 | import style9 from 'style9'; 49 | 50 | const styles = style9.create({ 51 | blue: { 52 | color: 'blue', 53 | } 54 | }); 55 | 56 | const otherStyles = style9.create({ 57 | red: { 58 | color: 'red' 59 | } 60 | }); 61 | 62 | document.body.className = style9(styles.blue, otherStyles.red); 63 | ``` 64 | 65 | ### Output 66 | 67 | ```javascript 68 | /* JavaScript */ 69 | import style9 from 'style9'; 70 | 71 | const styles = { 72 | blue: { 73 | color: 'hxxstI', 74 | } 75 | }; 76 | 77 | const otherStyles = { 78 | red: { 79 | color: 'RCRUH' 80 | } 81 | }; 82 | document.body.className = style9(styles.blue, otherStyles.red); 83 | 84 | /* CSS */ 85 | .hxxstI { color: blue } 86 | .RCRUH { color: red } 87 | ``` 88 | 89 | ### CSS post-processing 90 | 91 | The application of normal properties are handled by the library and have no specificity. However, when using pseudo classes and at-rules, like media queries, the order of the classes in the style sheet can make a difference. To avoid issues, the generated classes are sorted in a deterministic order. 92 | 93 | [babel-traverse]: https://babeljs.io/docs/en/babel-traverse 94 | -------------------------------------------------------------------------------- /docs/TypeScript.md: -------------------------------------------------------------------------------- 1 | # TypeScript 2 | 3 | style9 has first-class TypeScript support, powered by [csstype](https://github.com/frenic/csstype). 4 | 5 | ## CSS Custom Properties 6 | 7 | Custom properties can be added to the type definition by augmenting the `CustomProperties` interface. This makes them available as properties and as values. 8 | 9 | ```typescript 10 | declare module 'style9' { 11 | interface CustomProperties { 12 | '--bg-color'?: string; 13 | } 14 | } 15 | 16 | style9.create({ 17 | declaration: { 18 | '--bg-color': 'blue' 19 | }, 20 | use: { 21 | backgroundColor: 'var(--bg-color)' 22 | } 23 | }); 24 | ``` 25 | 26 | ## Media queries 27 | 28 | For TypeScript < 4.4 the following syntax is required for media queries and @supports 29 | 30 | ```javascript 31 | style9.create({ 32 | mobile: { 33 | '@media': { 34 | '(min-width: 800px)': { 35 | display: 'none' 36 | } 37 | }, 38 | '@supports': { 39 | 'not (display: grid)': { 40 | float: 'right' 41 | } 42 | } 43 | }, 44 | }); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/Usage-guide.md: -------------------------------------------------------------------------------- 1 | # Usage guide 2 | 3 | ## Basic 4 | 5 | Styles are defined by calling `style9.create` with objects of style definitions. The return value is then a function which can be called with the names of the created style objects, and returns string of class names. 6 | 7 | ```javascript 8 | import style9 from 'style9'; 9 | 10 | const styles = style9.create({ 11 | blue: { 12 | color: 'blue', 13 | } 14 | }); 15 | 16 | document.body.className = styles('blue'); 17 | ``` 18 | 19 | ## Conditional 20 | 21 | Multiple styles can be applied by passing the names to the `styles` function. Style objects will be merged like with `Object.assign`, with styles to the right taking precedence. To conditionally apply styles, use logical AND or ternary operator. Alternatively, an object containing the keys of the style object can be used. Later keys take precedence. 22 | 23 | ```javascript 24 | import style9 from 'style9'; 25 | 26 | const styles = style9.create({ 27 | blue: { 28 | color: 'blue', 29 | }, 30 | red: { 31 | color: 'red' 32 | } 33 | }); 34 | 35 | document.body.className = styles('blue', 'red'); 36 | document.body.className = styles('blue', isRed && 'red'); 37 | document.body.className = styles(isRed ? 'red' : 'blue'); 38 | document.body.className = styles({ 39 | blue: true, 40 | red: isRed 41 | }); 42 | ``` 43 | 44 | ## Composition 45 | 46 | To compose styles from multiple declarations, `style9` can be called as a function with the properties of the generated style object. This is not subject to the same restrictions as using the `styles` function, and can be fully dynamic. 47 | 48 | ```javascript 49 | import style9 from 'style9'; 50 | 51 | const someStyles = style9.create({ 52 | blue: { 53 | color: 'blue', 54 | } 55 | }); 56 | 57 | const someOtherStyles = style9.create({ 58 | tilt: { 59 | transform: 'rotate(45deg)' 60 | } 61 | }); 62 | 63 | document.body.className = style9(someStyles.blue, someOtherStyles['ti' + 'lt']); 64 | ``` 65 | 66 | ## Pseudo selectors 67 | 68 | Both pseudo-classes and pseudo-elements are supported and can be nested. 69 | 70 | ```javascript 71 | import style9 from 'style9'; 72 | 73 | const styles = style9.create({ 74 | blue: { 75 | color: 'blue', 76 | ':hover': { 77 | color: 'purple' 78 | }, 79 | '::before': { 80 | content: '"some content"' 81 | } 82 | } 83 | }); 84 | 85 | document.body.className = styles('blue'); 86 | ``` 87 | 88 | ## Media queries 89 | 90 | Media queries are supported, and will be sorted mobile-first in the generated CSS. They can be nested and can contain pseudo selectors. 91 | 92 | **Note:** TypeScript users, see [Media queries with TypeScript](TypeScript.md#media-queries) 93 | 94 | ```javascript 95 | import style9 from 'style9'; 96 | 97 | const styles = style9.create({ 98 | blue: { 99 | color: 'blue', 100 | '@media (min-width: 80em)': { 101 | color: 'purple' 102 | } 103 | } 104 | }); 105 | 106 | document.body.className = styles('blue'); 107 | ``` 108 | 109 | ## Keyframes 110 | 111 | CSS animations are created by calling `style9.keyframes` with the desired keyframe definitions, which returns a string that can be used as `animationName`. 112 | 113 | ```javascript 114 | import style9 from 'style9'; 115 | 116 | const styles = style9.create({ 117 | blue: { 118 | animationName: style9.keyframes({ 119 | from: { color: 'blue' }, 120 | '50%': { color: 'yellow' }, 121 | to: { color: 'red' } 122 | }) 123 | } 124 | }); 125 | 126 | document.body.className = styles('blue'); 127 | ``` 128 | 129 | ## Shorthands 130 | 131 | To be able to confidently apply class names [shorthand CSS properties][mdn shorthands], like `background`, are not supported. Instead longhand properties, like `background-color` and `background-image` should be used. For some simple shorthands, [inline-style-expand-shorthand][inline-style-expand-shorthand] is used to automatically expand them into their longhand equivalents. 132 | 133 | #### Unsupported use 134 | 135 | ```javascript 136 | import style9 from 'style9'; 137 | 138 | const styles = style9.create({ 139 | shorthand: { 140 | background: 'blue center url("image.jpg")' 141 | } 142 | }); 143 | ``` 144 | 145 | #### Supported uses 146 | 147 | ```javascript 148 | import style9 from 'style9'; 149 | 150 | const styles = style9.create({ 151 | longhands: { 152 | backgroundColor: 'blue', 153 | backgroundImage: 'url("image.jpg")', 154 | backgroundPositionX: 'center', 155 | backgroundPositionY: 'center' 156 | }, 157 | automaticallyExpanded: { 158 | padding: '12px' 159 | } 160 | }); 161 | ``` 162 | 163 | ## FontSize in REM 164 | 165 | [For accessibility][accessible-typography], `font-size` should be declared in `REMs`, to allow users to change their base text size. style9 handles this automatically when defining `font-size` as a number. 166 | 167 | ```javascript 168 | import style9 from 'style9'; 169 | 170 | const styles = style9.create({ 171 | large: { 172 | fontSize: 32 173 | } 174 | }); 175 | ``` 176 | 177 | Generated CSS: 178 | 179 | ```css 180 | .c188tmoq { font-size: 2rem } 181 | ``` 182 | 183 | ## Shared styles 184 | 185 | It can be useful to define some styles in another file to be able to reuse them. To export a single property, it can be simply accessed from the returned object. If you wish to export all styles as an object you have use spread to show that you won't use the returned function. 186 | 187 | ```javascript 188 | import style9 from 'style9'; 189 | 190 | export const { specific } = style9.create({ 191 | // ... 192 | }); 193 | 194 | export const { ...all } = style9.create({ 195 | // ... 196 | }); 197 | ``` 198 | 199 | ## Autoprefixing 200 | 201 | style9 has no built-in autoprefixing, instead leaving that up to the user. Do it like you normally would, with your bundler of choice, for example with [Webpack][webpack-autoprefixing]. 202 | 203 | ## Theming 204 | 205 | Theming is most easily done with CSS Custom Properties. Declaration can be done either in separately or with style9. They can then be applied globally or for a specific part of your site. TypeScript users, see [CSS Custom Properties with TypeScript](TypeScript.md#css-custom-properties). 206 | 207 | ```javascript 208 | style9.create({ 209 | declaration: { 210 | '--bg-color': 'blue' 211 | }, 212 | use: { 213 | backgroundColor: 'var(--bg-color)' 214 | } 215 | }); 216 | ``` 217 | 218 | [mdn shorthands]: https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties 219 | [inline-style-expand-shorthand]: https://github.com/robinweser/inline-style-expand-shorthand 220 | [accessible-typography]: https://betterwebtype.com/articles/2019/06/16/5-keys-to-accessible-web-typography/ 221 | [webpack-autoprefixing]: https://webpack.js.org/loaders/postcss-loader/#autoprefixer 222 | -------------------------------------------------------------------------------- /examples/gatsby/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .cache/ 3 | public 4 | -------------------------------------------------------------------------------- /examples/gatsby/README.md: -------------------------------------------------------------------------------- 1 | # style9-gatsby-example 2 | 3 | Example using [style9](https://github.com/johanholmerin/style9) with Gatsby. 4 | See [gatsby-config.js](gatsby-config.js) for config and 5 | [src/pages/index.tsx](src/pages/index.tsx) for usage. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | yarn 11 | ``` 12 | 13 | ## Develop 14 | 15 | ```sh 16 | yarn develop 17 | ``` 18 | 19 | ## Build 20 | 21 | ```sh 22 | yarn build 23 | yarn serve 24 | ``` 25 | -------------------------------------------------------------------------------- /examples/gatsby/gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | { 4 | resolve: 'style9/gatsby' 5 | } 6 | ] 7 | }; 8 | -------------------------------------------------------------------------------- /examples/gatsby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "style9-gatsby-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "develop": "gatsby develop", 7 | "build": "gatsby build", 8 | "serve": "gatsby serve", 9 | "clean": "gatsby clean" 10 | }, 11 | "dependencies": { 12 | "gatsby": "^3.0.1", 13 | "react": "^17.0.1", 14 | "react-dom": "^17.0.1", 15 | "style9": "link:../.." 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/gatsby/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import style9 from 'style9'; 3 | 4 | const styles = style9.create({ 5 | container: { 6 | minHeight: '100vh', 7 | paddingLeft: '.5rem', 8 | paddingRight: '.5rem', 9 | display: 'flex', 10 | flexDirection: 'column', 11 | justifyContent: 'center', 12 | alignItems: 'center' 13 | }, 14 | title: { 15 | margin: 0, 16 | lineHeight: 1.15, 17 | fontSize: '4rem', 18 | textAlign: 'center' 19 | } 20 | }); 21 | 22 | export default function IndexPage() { 23 | return ( 24 |
25 |
26 |

27 | Hello world! 28 |

29 |
30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /examples/gatsby/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"], 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "moduleResolution": "node", 6 | "module": "esnext", 7 | "lib": ["dom", "es2017"], 8 | "jsx": "react", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "noEmit": true, 14 | "skipLibCheck": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | -------------------------------------------------------------------------------- /examples/nextjs/README.md: -------------------------------------------------------------------------------- 1 | # style9-nextjs-example 2 | 3 | Example using [style9](https://github.com/johanholmerin/style9) with Next.js. 4 | See [next.config.js](next.config.js) for config and 5 | [pages/index.tsx](pages/index.tsx) for usage. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | yarn 11 | ``` 12 | 13 | ## Development 14 | 15 | ```sh 16 | yarn dev 17 | ``` 18 | 19 | ## Build 20 | 21 | ```sh 22 | yarn build 23 | yarn start 24 | ``` 25 | -------------------------------------------------------------------------------- /examples/nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | const withTM = require('next-transpile-modules')(['style9']); 2 | const withStyle9 = require('style9/next'); 3 | 4 | module.exports = withStyle9()(withTM()); 5 | -------------------------------------------------------------------------------- /examples/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "style9-nextjs-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "next": "^12.1.0", 12 | "react": "^17.0.2", 13 | "react-dom": "^17.0.2", 14 | "style9": "file:../.." 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^17.0.2", 18 | "browserslist": "^4.16.3", 19 | "eslint-config-next": "^11.0.1", 20 | "next-transpile-modules": "^8.0.0", 21 | "typescript": "^4.2.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/nextjs/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import style9 from 'style9'; 3 | import { layout } from '../shared/styles'; 4 | 5 | const styles = style9.create({ 6 | container: { 7 | minHeight: '100vh', 8 | paddingLeft: '.5rem', 9 | paddingRight: '.5rem', 10 | }, 11 | title: { 12 | margin: 0, 13 | lineHeight: 1.15, 14 | fontSize: '4rem', 15 | textAlign: 'center' 16 | } 17 | }); 18 | 19 | export default function Home() { 20 | return ( 21 |
22 | 23 | Create Next App 24 | 25 | 26 |
27 |

28 | Hello world! 29 |

30 |
31 |
32 | ) 33 | } 34 | 35 | export const shared = { ...styles }; 36 | -------------------------------------------------------------------------------- /examples/nextjs/shared/styles.js: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | 3 | export const { ...layout } = style9.create({ 4 | center: { 5 | display: 'flex', 6 | flexDirection: 'column', 7 | justifyContent: 'center', 8 | alignItems: 'center' 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /examples/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ], 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/rollup/README.md: -------------------------------------------------------------------------------- 1 | # style9-rollup-example 2 | 3 | Example using [style9](https://github.com/johanholmerin/style9) with Rollup. 4 | See [rollup.config.js](rollup.config.js) for config and 5 | [src/main.js](src/main.js) for usage. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | yarn 11 | ``` 12 | 13 | ## Build 14 | 15 | ```sh 16 | yarn build 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/rollup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "style9-rollup-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c" 7 | }, 8 | "dependencies": { 9 | "style9": "link:../.." 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-node-resolve": "^11.2.0", 13 | "rollup": "^2.40.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/rollup/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Hello world! 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/rollup/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import style9 from 'style9/rollup'; 3 | 4 | export default { 5 | input: 'src/main.js', 6 | output: { 7 | file: 'build/main.js', 8 | format: 'es' 9 | }, 10 | plugins: [ 11 | resolve(), 12 | style9({ 13 | fileName: 'index.css' 14 | }) 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /examples/rollup/src/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import style9 from 'style9'; 3 | 4 | const styles = style9.create({ 5 | blue: { 6 | color: 'blue' 7 | } 8 | }); 9 | 10 | document.body.className = styles('blue'); 11 | -------------------------------------------------------------------------------- /examples/vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

Weclome Style9.

11 |

In some case we using dynamic import to load us resource.

12 |

Vite plugin will process css with right order.

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "style9-vite-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev":"vite", 7 | "build":"vite build" 8 | }, 9 | "dependencies": { 10 | "style9": "link:../.." 11 | }, 12 | "devDependencies": { 13 | "vite":"^4.0.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/vite/src/dynamic.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import style9 from 'style9'; 3 | 4 | const styles = style9.create({ 5 | size: { 6 | fontSize: '20px' 7 | }, 8 | color: { 9 | color: 'blue' 10 | } 11 | }); 12 | 13 | document 14 | .querySelectorAll('p') 15 | .forEach(node => (node.className = styles('color', 'size'))); 16 | -------------------------------------------------------------------------------- /examples/vite/src/dynamic2.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import style9 from 'style9'; 3 | 4 | const styles = style9.create({ 5 | size: { 6 | fontSize: '30px' 7 | }, 8 | color: { 9 | color: 'red' 10 | } 11 | }); 12 | 13 | document 14 | .querySelectorAll('p') 15 | .forEach(node => (node.className = styles('color', 'size'))); 16 | -------------------------------------------------------------------------------- /examples/vite/src/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import style9 from 'style9'; 3 | 4 | const styles = style9.create({ 5 | blue: { 6 | color: 'blue' 7 | } 8 | }); 9 | 10 | document.body.className = styles('blue'); 11 | 12 | if (Math.random() > 0.5) { 13 | import('./dynamic').then(() => import('./dynamic2')); 14 | } else { 15 | import('./dynamic2').then(() => import('./dynamic')); 16 | } 17 | -------------------------------------------------------------------------------- /examples/vite/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import style9 from 'style9/vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [style9()] 6 | }); 7 | -------------------------------------------------------------------------------- /examples/webpack4/README.md: -------------------------------------------------------------------------------- 1 | # style9-webpack4-example 2 | 3 | Example using [style9](https://github.com/johanholmerin/style9) with Webpack 4. 4 | See [webpack.config.js](webpack.config.js) for config and 5 | [src/main.js](src/main.js) for usage. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | yarn 11 | ``` 12 | 13 | ## Build 14 | 15 | ```sh 16 | yarn build 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/webpack4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "style9-webpack4-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "webpack --mode=production" 7 | }, 8 | "dependencies": { 9 | "style9": "link:../.." 10 | }, 11 | "devDependencies": { 12 | "css-loader": "^5.1.1", 13 | "mini-css-extract-plugin": "^1.3.9", 14 | "webpack": "^4.46", 15 | "webpack-cli": "^4.5.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/webpack4/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello world! 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/webpack4/src/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import style9 from 'style9'; 3 | 4 | const styles = style9.create({ 5 | blue: { 6 | color: 'blue' 7 | } 8 | }); 9 | 10 | document.body.className = styles('blue'); 11 | -------------------------------------------------------------------------------- /examples/webpack4/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Style9Plugin = require('style9/webpack'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | 5 | module.exports = { 6 | entry: './src/main.js', 7 | output: { 8 | filename: 'main.js', 9 | path: path.resolve(__dirname, 'build') 10 | }, 11 | optimization: { 12 | splitChunks: { 13 | cacheGroups: { 14 | styles: { 15 | name: 'styles', 16 | test: /\.css$/, 17 | chunks: 'all', 18 | enforce: true 19 | } 20 | } 21 | } 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(tsx|ts|js|mjs|jsx)$/, 27 | use: Style9Plugin.loader 28 | }, 29 | { 30 | test: /\.css$/i, 31 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 32 | } 33 | ] 34 | }, 35 | plugins: [ 36 | new Style9Plugin(), 37 | new MiniCssExtractPlugin({ 38 | filename: 'index.css' 39 | }) 40 | ] 41 | }; 42 | -------------------------------------------------------------------------------- /examples/webpack5/README.md: -------------------------------------------------------------------------------- 1 | # style9-webpack5-example 2 | 3 | Example using [style9](https://github.com/johanholmerin/style9) with Webpack 5. 4 | See [webpack.config.js](webpack.config.js) for config and 5 | [src/main.js](src/main.js) for usage. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | yarn 11 | ``` 12 | 13 | ## Build 14 | 15 | ```sh 16 | yarn build 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/webpack5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "style9-webpack5-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "webpack --mode=production" 7 | }, 8 | "dependencies": { 9 | "style9": "link:../.." 10 | }, 11 | "devDependencies": { 12 | "css-loader": "^5.2.0", 13 | "mini-css-extract-plugin": "^1.4.0", 14 | "webpack": "^5.28.0", 15 | "webpack-cli": "^4.5.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/webpack5/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello world! 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/webpack5/src/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import style9 from 'style9'; 3 | 4 | const styles = style9.create({ 5 | blue: { 6 | color: 'blue' 7 | } 8 | }); 9 | 10 | document.body.className = styles('blue'); 11 | -------------------------------------------------------------------------------- /examples/webpack5/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Style9Plugin = require('style9/webpack'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | 5 | module.exports = { 6 | entry: './src/main.js', 7 | output: { 8 | filename: 'main.js', 9 | path: path.resolve(__dirname, 'build') 10 | }, 11 | optimization: { 12 | splitChunks: { 13 | cacheGroups: { 14 | styles: { 15 | name: 'styles', 16 | type: 'css/mini-extract', 17 | chunks: 'all', 18 | enforce: true 19 | } 20 | } 21 | } 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(tsx|ts|js|mjs|jsx)$/, 27 | use: Style9Plugin.loader 28 | }, 29 | { 30 | test: /\.css$/i, 31 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 32 | } 33 | ] 34 | }, 35 | plugins: [ 36 | new Style9Plugin(), 37 | new MiniCssExtractPlugin({ 38 | filename: 'index.css' 39 | }) 40 | ] 41 | }; 42 | -------------------------------------------------------------------------------- /gatsby/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const { stringifyCssRequest } = require('../src/plugin-utils.js'); 2 | const Style9Plugin = require('../webpack/index.js'); 3 | 4 | exports.onCreateWebpackConfig = ( 5 | { stage, loaders, actions, getConfig }, 6 | pluginOptions 7 | ) => { 8 | if (stage === 'develop-html') return; 9 | 10 | const config = getConfig(); 11 | 12 | const outputCSS = !stage.includes('html'); 13 | const inlineLoader = stringifyCssRequest([ 14 | loaders.miniCssExtract(), 15 | { loader: 'css-loader' } 16 | ]); 17 | 18 | config.module.rules.unshift({ 19 | test: /\.(tsx|ts|js|mjs|jsx)$/, 20 | use: [ 21 | { 22 | loader: Style9Plugin.loader, 23 | options: { inlineLoader, outputCSS, ...pluginOptions } 24 | } 25 | ] 26 | }); 27 | 28 | if (outputCSS) { 29 | config.plugins.push(new Style9Plugin()); 30 | } 31 | 32 | actions.replaceWebpackConfig(config); 33 | }; 34 | -------------------------------------------------------------------------------- /gatsby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "style9" 3 | } 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { default } from './index.mjs'; 2 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | function merge(target, source) { 2 | for (const key in source) { 3 | if (typeof source[key] === 'object') { 4 | target[key] = merge({ ...target[key] }, source[key]); 5 | } else { 6 | target[key] = source[key]; 7 | } 8 | } 9 | 10 | return target; 11 | } 12 | 13 | function getValues(obj) { 14 | const values = []; 15 | 16 | for (const key in obj) { 17 | const val = obj[key]; 18 | if (typeof val === 'object') { 19 | values.push(...getValues(val)); 20 | } else { 21 | values.push(val); 22 | } 23 | } 24 | 25 | return values; 26 | } 27 | 28 | export default function style9(...styles) { 29 | const merged = styles.reduce(merge, {}); 30 | return getValues(merged).join(' '); 31 | } 32 | 33 | style9.create = () => { 34 | throw new Error('style9.create calls should be compiled away'); 35 | }; 36 | 37 | style9.keyframes = () => { 38 | throw new Error('style9.keyframes calls should be compiled away'); 39 | }; 40 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "node", 3 | "testPathIgnorePatterns": [ 4 | "__tests__/compile.js", 5 | "__tests__/code/fixtures/" 6 | ], 7 | "transform": { 8 | "\\/index\\.m?js$": [ 9 | "babel-jest", 10 | { 11 | "plugins": ["@babel/plugin-transform-modules-commonjs"] 12 | } 13 | ] 14 | }, 15 | "collectCoverage": true, 16 | "coverageThreshold": { 17 | "global": { 18 | "branches": 100, 19 | "functions": 100, 20 | "lines": 100, 21 | "statements": 100 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /next-legacy.js: -------------------------------------------------------------------------------- 1 | const { 2 | getClientStyleLoader 3 | } = require('next/dist/build/webpack/config/blocks/css/loaders/client'); 4 | const { stringifyCssRequest } = require('./src/plugin-utils.js'); 5 | const Style9Plugin = require('./webpack/index.js'); 6 | 7 | const cssLoader = (() => { 8 | try { 9 | // v10 & v11 10 | return require.resolve('next/dist/compiled/css-loader'); 11 | } catch (_) { 12 | try { 13 | // v12+ 14 | return require.resolve('next/dist/build/webpack/loaders/css-loader/src'); 15 | } catch (_) { 16 | return 'css-loader'; 17 | } 18 | } 19 | })(); 20 | 21 | function getInlineLoader(options, MiniCssExtractPlugin) { 22 | const outputLoaders = [{ loader: cssLoader }]; 23 | 24 | if (!options.isServer) { 25 | outputLoaders.unshift({ 26 | // Logic adopted from https://git.io/JfD9r 27 | ...getClientStyleLoader({ 28 | // In development model Next.js uses style-loader, which inserts each 29 | // CSS file as its own style tag, which means the CSS won't be sorted 30 | // and causes issues with determinism when using media queries and 31 | // pseudo selectors. Setting isDevelopment means MiniCssExtractPlugin is 32 | // used instead. 33 | isDevelopment: false, 34 | assetPrefix: options.config.assetPrefix 35 | }), 36 | loader: MiniCssExtractPlugin.loader 37 | }); 38 | } 39 | 40 | return stringifyCssRequest(outputLoaders); 41 | } 42 | 43 | module.exports = (pluginOptions = {}) => (nextConfig = {}) => { 44 | return { 45 | ...nextConfig, 46 | webpack(config, options) { 47 | const outputCSS = !options.isServer; 48 | 49 | // The style9 compiler must run on source code, which means it must be 50 | // configured as the last loader in webpack so that it runs before any 51 | // other transformation. 52 | 53 | if (typeof nextConfig.webpack === 'function') { 54 | config = nextConfig.webpack(config, options); 55 | } 56 | 57 | // For some reason, Next 11.0.1 has `config.optimization.splitChunks` 58 | // set to `false` when webpack 5 is enabled. 59 | config.optimization.splitChunks = config.optimization.splitChunks || { 60 | cacheGroups: {} 61 | }; 62 | 63 | // Use own MiniCssExtractPlugin to ensure HMR works 64 | // v9 has issues when using own plugin in production 65 | // v10.2.1 has issues when using built-in plugin in development since it 66 | // doesn't bundle HMR files 67 | const MiniCssExtractPlugin = options.dev 68 | ? require('mini-css-extract-plugin') 69 | : require('next/dist/build/webpack/plugins/mini-css-extract-plugin') 70 | .default; 71 | 72 | config.module.rules.push({ 73 | test: /\.(tsx|ts|js|mjs|jsx)$/, 74 | use: [ 75 | { 76 | loader: Style9Plugin.loader, 77 | options: { 78 | inlineLoader: getInlineLoader(options, MiniCssExtractPlugin), 79 | outputCSS, 80 | ...pluginOptions 81 | } 82 | } 83 | ] 84 | }); 85 | 86 | if (outputCSS) { 87 | config.optimization.splitChunks.cacheGroups.styles = { 88 | name: 'styles', 89 | test: /\.css$/, 90 | chunks: 'all', 91 | enforce: true 92 | }; 93 | 94 | // HMR reloads the CSS file when the content changes but does not use 95 | // the new file name, which means it can't contain a hash. 96 | const filename = options.dev 97 | ? 'static/css/[name].css' 98 | : 'static/css/[contenthash].css'; 99 | 100 | config.plugins.push( 101 | // Logic adopted from https://git.io/JtdBy 102 | new MiniCssExtractPlugin({ 103 | filename, 104 | chunkFilename: filename, 105 | ignoreOrder: true 106 | }), 107 | new Style9Plugin() 108 | ); 109 | } 110 | 111 | return config; 112 | } 113 | }; 114 | }; 115 | -------------------------------------------------------------------------------- /next.js: -------------------------------------------------------------------------------- 1 | const { 2 | getClientStyleLoader 3 | } = require('next/dist/build/webpack/config/blocks/css/loaders/client'); 4 | const NextMiniCssExtractPlugin = require('next/dist/build/webpack/plugins/mini-css-extract-plugin') 5 | .default; 6 | const { browserslist } = require('next/dist/compiled/browserslist'); 7 | const { lazyPostCSS } = require('next/dist/build/webpack/config/blocks/css'); 8 | 9 | const Style9Plugin = require('./webpack/index.js'); 10 | 11 | // Adopted from https://github.com/vercel/next.js/blob/1f1632979c78b3edfe59fd85d8cce62efcdee688/packages/next/build/webpack-config.ts#L60-L72 12 | function getSupportedBrowsers(dir, isDevelopment) { 13 | let browsers; 14 | try { 15 | browsers = browserslist.loadConfig({ 16 | path: dir, 17 | env: isDevelopment ? 'development' : 'production' 18 | }); 19 | } catch (_) { 20 | /** */ 21 | } 22 | return browsers; 23 | } 24 | 25 | const cssLoader = (() => { 26 | try { 27 | // v12+ 28 | return require.resolve('next/dist/build/webpack/loaders/css-loader/src'); 29 | } catch (_) { 30 | return 'css-loader'; 31 | } 32 | })(); 33 | 34 | const getNextMiniCssExtractPlugin = isDev => { 35 | // Use own MiniCssExtractPlugin to ensure HMR works 36 | // v9 has issues when using own plugin in production 37 | // v10.2.1 has issues when using built-in plugin in development since it 38 | // doesn't bundle HMR files 39 | // v12.1.7 finaly fixes the issue by adding the missing hmr/hotModuleReplacement.js file 40 | if (isDev) { 41 | try { 42 | // Check if hotModuleReplacement exists 43 | require('next/dist/compiled/mini-css-extract-plugin/hmr/hotModuleReplacement'); 44 | return NextMiniCssExtractPlugin; 45 | } catch (_) { 46 | return require('mini-css-extract-plugin'); 47 | } 48 | } 49 | // Always use Next.js built-in MiniCssExtractPlugin in production 50 | return NextMiniCssExtractPlugin; 51 | }; 52 | 53 | function getStyle9VirtualCssLoader(options, MiniCssExtractPlugin) { 54 | const outputLoaders = [ 55 | { 56 | loader: cssLoader, 57 | options: { 58 | // A simplify version of https://github.com/vercel/next.js/blob/88a5f263f11cb55907f0d89a4cd53647ee8e96ac/packages/next/build/webpack/config/blocks/css/index.ts#L142-L147 59 | postcss: () => 60 | lazyPostCSS( 61 | options.dir, 62 | getSupportedBrowsers(options.dir, options.dev) 63 | ) 64 | } 65 | } 66 | ]; 67 | 68 | if (!options.isServer) { 69 | outputLoaders.unshift({ 70 | // Logic adopted from https://git.io/JfD9r 71 | ...getClientStyleLoader({ 72 | // In development model Next.js uses style-loader, which inserts each 73 | // CSS file as its own style tag, which means the CSS won't be sorted 74 | // and causes issues with determinism when using media queries and 75 | // pseudo selectors. Setting isDevelopment means MiniCssExtractPlugin is 76 | // used instead. 77 | isDevelopment: false, 78 | assetPrefix: options.config.assetPrefix 79 | }), 80 | loader: MiniCssExtractPlugin.loader 81 | }); 82 | } 83 | 84 | return outputLoaders; 85 | } 86 | 87 | module.exports = (pluginOptions = {}) => (nextConfig = {}) => { 88 | return { 89 | ...nextConfig, 90 | webpack(config, options) { 91 | const outputCSS = !options.isServer; 92 | 93 | // The style9 compiler must run on source code, which means it must be 94 | // configured as the last loader in webpack so that it runs before any 95 | // other transformation. 96 | 97 | if (typeof nextConfig.webpack === 'function') { 98 | config = nextConfig.webpack(config, options); 99 | } 100 | 101 | // For some reason, Next 11.0.1 has `config.optimization.splitChunks` 102 | // set to `false` when webpack 5 is enabled. 103 | config.optimization.splitChunks = config.optimization.splitChunks || { 104 | cacheGroups: {} 105 | }; 106 | 107 | const MiniCssExtractPlugin = getNextMiniCssExtractPlugin(options.dev); 108 | 109 | config.module.rules.push({ 110 | test: /\.(tsx|ts|js|mjs|jsx)$/, 111 | exclude: /node_modules/, 112 | use: [ 113 | { 114 | loader: Style9Plugin.loader, 115 | options: { 116 | // Here we configure a custom virtual css file name, for later matches 117 | virtualFileName: '[path][name].[hash:base64:7].style9.css', 118 | // We will not pass a inline loader, instead we will add a specfic rule for /\.style9.css$/ 119 | inlineLoader: '', 120 | outputCSS, 121 | ...pluginOptions 122 | } 123 | } 124 | ] 125 | }); 126 | 127 | // Based on https://github.com/vercel/next.js/blob/88a5f263f11cb55907f0d89a4cd53647ee8e96ac/packages/next/build/webpack/config/helpers.ts#L12-L18 128 | const cssRules = config.module.rules.find( 129 | rule => 130 | Array.isArray(rule.oneOf) && 131 | rule.oneOf.some( 132 | ({ test }) => 133 | typeof test === 'object' && 134 | typeof test.test === 'function' && 135 | test.test('filename.css') 136 | ) 137 | ).oneOf; 138 | 139 | // Here we matches virtual css file emitted by Style9Plugin 140 | cssRules.unshift({ 141 | test: /\.style9.css$/, 142 | use: getStyle9VirtualCssLoader(options, MiniCssExtractPlugin) 143 | }); 144 | 145 | if (outputCSS) { 146 | config.optimization.splitChunks.cacheGroups.style9 = { 147 | name: 'style9', 148 | // We apply cacheGroups to style9 virtual css only 149 | test: /\.style9.css$/, 150 | chunks: 'all', 151 | enforce: true 152 | }; 153 | 154 | // Style9 need to emit the css file on both server and client, both during the 155 | // development and production. 156 | // However, Next.js only add MiniCssExtractPlugin on client + production. 157 | // 158 | // To simplify the logic at our side, we will add MiniCssExtractPlugin based on 159 | // the "instanceof" check (We will only add our required MiniCssExtractPlugin if 160 | // Next.js hasn't added it yet). 161 | // This also prevent multiple MiniCssExtractPlugin being added (which will cause 162 | // RealContentHashPlugin to panic) 163 | if ( 164 | !config.plugins.some(plugin => plugin instanceof MiniCssExtractPlugin) 165 | ) { 166 | // HMR reloads the CSS file when the content changes but does not use 167 | // the new file name, which means it can't contain a hash. 168 | const filename = options.dev 169 | ? 'static/css/[name].css' 170 | : 'static/css/[contenthash].css'; 171 | 172 | // Logic adopted from https://git.io/JtdBy 173 | config.plugins.push( 174 | new MiniCssExtractPlugin({ 175 | filename, 176 | chunkFilename: filename 177 | }) 178 | ); 179 | } 180 | 181 | config.plugins.push(new Style9Plugin()); 182 | } 183 | 184 | return config; 185 | } 186 | }; 187 | }; 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "style9", 3 | "version": "0.18.2", 4 | "description": "CSS-in-JS compiler", 5 | "author": "Johan Holmerin ", 6 | "license": "MIT", 7 | "main": "./index.mjs", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/johanholmerin/style9.git" 11 | }, 12 | "engines": { 13 | "node": ">=14" 14 | }, 15 | "types": "./types/index.d.ts", 16 | "typesVersions": { 17 | "<4.4": { 18 | "*": [ 19 | "./types/ts4.3/index.d.ts" 20 | ] 21 | } 22 | }, 23 | "keywords": [ 24 | "styles", 25 | "css", 26 | "css-in-js", 27 | "babel", 28 | "babel-plugin", 29 | "gatsby", 30 | "gatsby-plugin", 31 | "next", 32 | "nextjs", 33 | "nextjs-plugin", 34 | "webpack", 35 | "rollup", 36 | "rollup-plugin" 37 | ], 38 | "dependencies": { 39 | "@babel/core": "^7.10.4", 40 | "@babel/types": "^7.8.3", 41 | "@rollup/pluginutils": "^3.0.4", 42 | "csstype": "^3.0.6", 43 | "fast-json-stable-stringify": "^2.1.0", 44 | "inline-style-expand-shorthand": "1.6.0", 45 | "json5": "^2.2.3", 46 | "known-css-properties": "^0.19.0", 47 | "loader-utils": "^3.2.1", 48 | "mini-css-extract-plugin": "^1.6.0", 49 | "murmurhash-js": "^1.0.0", 50 | "postcss": "^8.4.7", 51 | "postcss-discard-duplicates": "^5.1.0", 52 | "postcss-selector-parser": "^6.0.2", 53 | "sort-css-media-queries": "^2.0.4", 54 | "webpack-sources": "^2.2.0", 55 | "webpack-virtual-modules": "^0.4.1" 56 | }, 57 | "devDependencies": { 58 | "@babel/plugin-transform-modules-commonjs": "^7.10.4", 59 | "@stryker-mutator/core": "^5.0.1", 60 | "@stryker-mutator/jest-runner": "^5.0.1", 61 | "babel-jest": "^26.1.0", 62 | "babel-plugin-tester": "^10.0.0", 63 | "dtslint": "^4.0.7", 64 | "eslint": "^6.8.0", 65 | "eslint-config-prettier": "^6.15.0", 66 | "eslint-plugin-import": "^2.22.1", 67 | "husky": "^4.3.8", 68 | "jest": "^26.6.3", 69 | "lint-staged": "^10.5.4", 70 | "prettier": "2.2.1", 71 | "rollup": "^1.29.0", 72 | "typescript": "^4.4.2", 73 | "vite": "^4.0.1", 74 | "webpack": "^4.43.0", 75 | "webpack-cli": "^3.3.11" 76 | }, 77 | "scripts": { 78 | "test": "jest", 79 | "test:types": "dtslint types", 80 | "test:mutation": "stryker run", 81 | "test:examples": "./scripts/test-examples.sh", 82 | "lint": "eslint \"**/*.js\" --fix", 83 | "lint:check": "eslint \"**/*.js\"", 84 | "format": "prettier \"**/*.{js,ts}\" --write", 85 | "format:check": "prettier \"**/*.{js,ts}\" --check" 86 | }, 87 | "husky": { 88 | "hooks": { 89 | "pre-commit": "lint-staged" 90 | } 91 | }, 92 | "lint-staged": { 93 | "*.js": [ 94 | "npm run lint", 95 | "npm run format" 96 | ], 97 | "*.ts": [ 98 | "npm run format" 99 | ] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /rollup.d.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'rollup'; 2 | import { FilterPattern } from '@rollup/pluginutils'; 3 | import { ParserOptions } from '@babel/core'; 4 | 5 | interface BabelOptions { 6 | minifyProperties?: boolean; 7 | incrementalClassnames?: boolean; 8 | } 9 | 10 | interface CommonOptions extends BabelOptions { 11 | include?: FilterPattern; 12 | exclude?: FilterPattern; 13 | parserOptions?: ParserOptions; 14 | } 15 | 16 | interface FileNameOptions extends CommonOptions { 17 | fileName: string; 18 | name?: never; 19 | } 20 | 21 | interface NameOptions extends CommonOptions { 22 | name: string; 23 | fileName?: never; 24 | } 25 | 26 | type RollupOptions = FileNameOptions | NameOptions; 27 | 28 | export default function style9Plugin(rollupOptions: RollupOptions): Plugin; 29 | -------------------------------------------------------------------------------- /rollup.js: -------------------------------------------------------------------------------- 1 | const babel = require('@babel/core'); 2 | const { createFilter } = require('@rollup/pluginutils'); 3 | const babelPlugin = require('./babel.js'); 4 | const NAME = require('./package.json').name; 5 | const processCSS = require('./src/process-css.js'); 6 | 7 | module.exports = function style9Plugin({ 8 | include, 9 | exclude, 10 | fileName, 11 | name, 12 | parserOptions = { 13 | plugins: ['typescript', 'jsx'] 14 | }, 15 | ...options 16 | } = {}) { 17 | // Default name required to ensure extension 18 | if (!fileName && !name) name = 'index.css'; 19 | 20 | const filter = createFilter(include, exclude); 21 | const styles = Object.create(null); 22 | 23 | return { 24 | name: NAME, 25 | async transform(input, filename) { 26 | if (!filter(filename)) return; 27 | 28 | const { code, map, metadata } = await babel.transformAsync(input, { 29 | plugins: [[babelPlugin, options]], 30 | filename, 31 | sourceMaps: true, 32 | parserOpts: parserOptions, 33 | babelrc: false 34 | }); 35 | 36 | styles[filename] = metadata.style9 || ''; 37 | 38 | return { code, map }; 39 | }, 40 | generateBundle(options, bundles) { 41 | let css = ''; 42 | // Collect css from files that are included in this build 43 | for (const bundle in bundles) { 44 | for (const id in bundles[bundle].modules) { 45 | if (id in styles) { 46 | css += styles[id]; 47 | } 48 | } 49 | } 50 | 51 | this.emitFile({ 52 | type: 'asset', 53 | source: processCSS(css, { from: undefined }).css, 54 | fileName, 55 | name 56 | }); 57 | } 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /scripts/test-examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | for file in `ls -1 examples`; 6 | do 7 | (cd "examples/$file" && yarn --frozen-lockfile && yarn build); 8 | done; 9 | -------------------------------------------------------------------------------- /src/helpers/flatten-at-rules.js: -------------------------------------------------------------------------------- 1 | const { mapObject, mapObjectValues } = require('../utils/helpers'); 2 | const { isNestedStyles, isAtRuleObject } = require('../utils/styles'); 3 | 4 | function flatten(type, object) { 5 | return mapObject(object, ([key, value]) => { 6 | return [`${type} ${key}`, flattenObjectAtRules(value)]; 7 | }); 8 | } 9 | 10 | function flattenObjectAtRules(styles) { 11 | const entries = Object.entries(styles).flatMap(([name, value]) => { 12 | if (!isNestedStyles(value)) { 13 | return [[name, value]]; 14 | } 15 | 16 | if (isAtRuleObject(name)) { 17 | return Object.entries(flatten(name, value)); 18 | } 19 | 20 | return [[name, flattenObjectAtRules(value)]]; 21 | }); 22 | 23 | return Object.fromEntries(entries); 24 | } 25 | 26 | function flattenAtRules(obj) { 27 | return mapObjectValues(obj, flattenObjectAtRules); 28 | } 29 | 30 | module.exports = flattenAtRules; 31 | -------------------------------------------------------------------------------- /src/helpers/flatten-styles.js: -------------------------------------------------------------------------------- 1 | const { 2 | normalizePseudoElements, 3 | isAtRule, 4 | isPseudoSelector 5 | } = require('../utils/styles'); 6 | 7 | function flattenStyle({ name, value, atRules, pseudoSelectors }) { 8 | if (isAtRule(name)) { 9 | return flattenStyles(value, { 10 | atRules: [...atRules, name], 11 | pseudoSelectors 12 | }); 13 | } 14 | 15 | if (isPseudoSelector(name)) { 16 | const normalizedName = normalizePseudoElements(name); 17 | return flattenStyles(value, { 18 | pseudoSelectors: [...pseudoSelectors, normalizedName], 19 | atRules 20 | }); 21 | } 22 | 23 | return { name, value, atRules, pseudoSelectors }; 24 | } 25 | 26 | function flattenStyles(styles, { atRules = [], pseudoSelectors = [] } = {}) { 27 | return Object.entries(styles).flatMap(([name, value]) => 28 | flattenStyle({ name, value, atRules, pseudoSelectors }) 29 | ); 30 | } 31 | 32 | module.exports = flattenStyles; 33 | -------------------------------------------------------------------------------- /src/helpers/generate-classes.js: -------------------------------------------------------------------------------- 1 | const { mapObject, mapObjectValues } = require('../utils/helpers'); 2 | const { 3 | getClass, 4 | normalizePseudoElements, 5 | isAtRule, 6 | isPseudoSelector 7 | } = require('../utils/styles'); 8 | 9 | function getClassValues( 10 | styles, 11 | incremental, 12 | { atRules = [], pseudoSelectors = [] } = {} 13 | ) { 14 | return mapObject(styles, ([name, value]) => { 15 | if (isAtRule(name)) { 16 | const newValue = getClassValues(value, incremental, { 17 | atRules: [...atRules, name], 18 | pseudoSelectors 19 | }); 20 | return [name, newValue]; 21 | } 22 | 23 | if (isPseudoSelector(name)) { 24 | const normalizedName = normalizePseudoElements(name); 25 | const newValue = getClassValues(value, incremental, { 26 | pseudoSelectors: [...pseudoSelectors, normalizedName], 27 | atRules 28 | }); 29 | return [normalizedName, newValue]; 30 | } 31 | 32 | const newValue = getClass( 33 | { name, value, atRules, pseudoSelectors }, 34 | incremental 35 | ); 36 | return [name, newValue]; 37 | }); 38 | } 39 | 40 | function generateClasses(obj, incremental = false) { 41 | return mapObjectValues(obj, value => getClassValues(value, incremental)); 42 | } 43 | 44 | module.exports = generateClasses; 45 | -------------------------------------------------------------------------------- /src/helpers/generate-expression.js: -------------------------------------------------------------------------------- 1 | const t = require('@babel/types'); 2 | const { mapObject, removeDuplicates } = require('../utils/helpers'); 3 | 4 | function getConditionalArgs(args, classes) { 5 | const newArgs = []; 6 | let prevValue; 7 | 8 | // Iterate over args backwards. If a string literal is found, it means the 9 | // property is applied unconditionally, and the rest can be skipped 10 | for (let n = args.length - 1; n >= 0; n--) { 11 | const arg = args[n]; 12 | const name = typeof arg === 'string' ? arg : arg.value; 13 | const cls = classes[name]; 14 | 15 | if (cls === undefined) continue; 16 | 17 | if (typeof arg === 'string') { 18 | if (prevValue === cls) { 19 | // If the last last value is the same as the static value, the last 20 | // conditional can be skipped since both sides would be the same 21 | const last = newArgs.pop(); 22 | newArgs.push(last.value); 23 | } else { 24 | newArgs.push(t.stringLiteral(cls + ' ')); 25 | } 26 | 27 | return newArgs; 28 | } 29 | 30 | newArgs.push({ 31 | test: arg.test, 32 | value: t.stringLiteral(cls + ' ') 33 | }); 34 | prevValue = cls; 35 | } 36 | 37 | newArgs.push(t.stringLiteral('')); 38 | 39 | return newArgs; 40 | } 41 | 42 | function listObjectsProperties(classObj) { 43 | return removeDuplicates( 44 | Object.values(classObj).flatMap(obj => Object.keys(obj)) 45 | ); 46 | } 47 | 48 | function getObjectsProp(object, prop) { 49 | return mapObject(object, ([key, val]) => [key, val[prop]]); 50 | } 51 | 52 | function generateExpression(args, classObject) { 53 | const originalConditionals = listObjectsProperties(classObject) 54 | .map(prop => getObjectsProp(classObject, prop)) 55 | .map(classes => getConditionalArgs(args, classes)) 56 | .filter(conditionalArgs => conditionalArgs.length) 57 | .map(conditionalArgs => 58 | conditionalArgs.reduceRight((acc, prop) => 59 | t.conditionalExpression(prop.test, prop.value, acc) 60 | ) 61 | ); 62 | 63 | const simplifiedConditionals = []; 64 | let stringBuffer = ''; 65 | 66 | for (let i = 0; i < originalConditionals.length; i++) { 67 | const conditional = originalConditionals[i]; 68 | if (t.isStringLiteral(conditional)) { 69 | stringBuffer += conditional.value; 70 | } else { 71 | if (stringBuffer !== '') { 72 | simplifiedConditionals.push(t.stringLiteral(stringBuffer)); 73 | stringBuffer = ''; 74 | } 75 | simplifiedConditionals.push(conditional); 76 | } 77 | } 78 | 79 | if (stringBuffer !== '') { 80 | simplifiedConditionals.push(t.stringLiteral(stringBuffer)); 81 | } 82 | 83 | if (simplifiedConditionals.length === 0) { 84 | return t.expressionStatement(t.stringLiteral('')); 85 | } 86 | 87 | const binaryExpression = simplifiedConditionals.reduceRight((acc, expr) => { 88 | return t.binaryExpression('+', expr, acc); 89 | }); 90 | 91 | return t.expressionStatement(binaryExpression); 92 | } 93 | 94 | module.exports = generateExpression; 95 | -------------------------------------------------------------------------------- /src/helpers/generate-styles.js: -------------------------------------------------------------------------------- 1 | const { getDeclaration } = require('../utils/styles'); 2 | const flattenStyles = require('./flatten-styles'); 3 | 4 | function generateStyles(styles, incremental) { 5 | return Object.values(styles).flatMap(props => 6 | flattenStyles(props).map(obj => getDeclaration(obj, incremental)) 7 | ); 8 | } 9 | 10 | module.exports = generateStyles; 11 | -------------------------------------------------------------------------------- /src/helpers/get-style-object-value.js: -------------------------------------------------------------------------------- 1 | const { expandProperty } = require('inline-style-expand-shorthand'); 2 | const { evaluateNodePath } = require('../utils/ast'); 3 | const { mapObjectValues } = require('../utils/helpers'); 4 | const { isNestedStyles, normalizeValue } = require('../utils/styles'); 5 | 6 | function mapObject(object, cb) { 7 | const expanded = {}; 8 | 9 | for (const key in object) { 10 | const value = object[key]; 11 | Object.assign(expanded, cb([key, value])); 12 | } 13 | 14 | return expanded; 15 | } 16 | 17 | function expandStyleProperties(styles) { 18 | return mapObject(styles, ([key, value]) => { 19 | const current = { [key]: value }; 20 | 21 | if (isNestedStyles(value)) { 22 | return expandProperties(current); 23 | } 24 | 25 | const expandedProps = Object.entries(expandProperty(key, value) || current) 26 | .filter(([prop]) => !(prop in styles && prop !== key)) 27 | .map(([prop, propValue]) => [prop, normalizeValue(prop, propValue)]); 28 | 29 | return Object.fromEntries(expandedProps); 30 | }); 31 | } 32 | 33 | // Recursively expands shorthand properties 34 | // Alternating levels of objects are treated as having CSS properties 35 | function expandProperties(styleContainer) { 36 | return mapObjectValues(styleContainer, expandStyleProperties); 37 | } 38 | 39 | function getStyleObjectValue(nodePath) { 40 | return expandProperties(evaluateNodePath(nodePath)); 41 | } 42 | 43 | module.exports = getStyleObjectValue; 44 | -------------------------------------------------------------------------------- /src/helpers/list-dynamic-keys.js: -------------------------------------------------------------------------------- 1 | const { isDynamicKey, getStaticKey } = require('../utils/ast'); 2 | 3 | function listDynamicKeys(references, allKeys) { 4 | // Stryker disable next-line ArrayDeclaration: only actual keys are checked 5 | const dynamicKeys = []; 6 | 7 | for (const ref of references) { 8 | if (ref.parentPath.isSpreadElement()) return allKeys; 9 | 10 | if (ref.parentPath.isMemberExpression()) { 11 | if (isDynamicKey(ref.parentPath)) return allKeys; 12 | 13 | dynamicKeys.push(getStaticKey(ref.parentPath)); 14 | } 15 | } 16 | 17 | return dynamicKeys; 18 | } 19 | 20 | module.exports = listDynamicKeys; 21 | -------------------------------------------------------------------------------- /src/helpers/list-function-call-keys.js: -------------------------------------------------------------------------------- 1 | function listFunctionCallKeys(functionCallArgs) { 2 | return functionCallArgs.flatMap(args => { 3 | return args.map(arg => { 4 | const isString = typeof arg === 'string'; 5 | return isString ? arg : arg.value; 6 | }); 7 | }); 8 | } 9 | 10 | module.exports = listFunctionCallKeys; 11 | -------------------------------------------------------------------------------- /src/helpers/list-function-calls.js: -------------------------------------------------------------------------------- 1 | function listFunctionCalls(references) { 2 | return references.filter(ref => { 3 | return ref.parentPath.isCallExpression() && ref.parent.callee === ref.node; 4 | }); 5 | } 6 | 7 | module.exports = listFunctionCalls; 8 | -------------------------------------------------------------------------------- /src/helpers/list-references.js: -------------------------------------------------------------------------------- 1 | function listReferences(declarator) { 2 | if (declarator.get('id').isIdentifier()) { 3 | return declarator.scope.bindings[declarator.node.id.name].referencePaths; 4 | } 5 | 6 | return []; 7 | } 8 | 9 | module.exports = listReferences; 10 | -------------------------------------------------------------------------------- /src/helpers/list-static-keys.js: -------------------------------------------------------------------------------- 1 | const { isDynamicKey, getStaticKey } = require('../utils/ast'); 2 | 3 | function listStaticKeys(callExpr, allKeys) { 4 | const { parentPath } = callExpr; 5 | 6 | if (parentPath.isMemberExpression()) { 7 | return isDynamicKey(parentPath) ? allKeys : [getStaticKey(parentPath)]; 8 | } 9 | 10 | if (parentPath.get('id').isObjectPattern()) { 11 | const properties = parentPath.get('id.properties'); 12 | const hasRestElement = properties.some(prop => prop.isRestElement()); 13 | if (hasRestElement) return allKeys; 14 | 15 | return properties.map(prop => prop.node.key.name); 16 | } 17 | 18 | // Stryker disable next-line ArrayDeclaration: only actual keys are checked 19 | return []; 20 | } 21 | 22 | module.exports = listStaticKeys; 23 | -------------------------------------------------------------------------------- /src/helpers/mutate-ast.js: -------------------------------------------------------------------------------- 1 | const t = require('@babel/types'); 2 | const { mapObjectValues } = require('../utils/helpers'); 3 | const flattenStyles = require('./flatten-styles'); 4 | const generateExpression = require('./generate-expression'); 5 | 6 | function objectToAST(object) { 7 | const properties = Object.entries(object).map(([name, value]) => { 8 | const isObject = typeof value === 'object'; 9 | const isIdentifier = t.isValidIdentifier(name); 10 | 11 | const astValue = isObject ? objectToAST(value) : t.stringLiteral(value); 12 | const key = isIdentifier ? t.identifier(name) : t.stringLiteral(name); 13 | 14 | return t.objectProperty(key, astValue); 15 | }); 16 | 17 | return t.objectExpression(properties); 18 | } 19 | 20 | function replaceCreateCall(callExpr, minifiedDefinitions) { 21 | callExpr.replaceWith(objectToAST(minifiedDefinitions)); 22 | } 23 | 24 | function flattenClasses(classes) { 25 | return mapObjectValues(classes, value => { 26 | return Object.fromEntries( 27 | flattenStyles(value).map(({ value, ...rest }) => [ 28 | JSON.stringify(rest), 29 | value 30 | ]) 31 | ); 32 | }); 33 | } 34 | 35 | function extractNode(path, node) { 36 | if (t.isIdentifier(node)) return node; 37 | 38 | const name = path.scope.generateUidBasedOnNode(node); 39 | 40 | if (!Array.isArray(path.scope.path.get('body'))) { 41 | path.scope.path.ensureBlock(); 42 | } 43 | 44 | path 45 | .getStatementParent() 46 | .insertBefore( 47 | t.variableDeclaration('const', [ 48 | t.variableDeclarator(t.identifier(name), node) 49 | ]) 50 | ); 51 | 52 | return t.identifier(name); 53 | } 54 | 55 | function extractArgumentIdentifiers(parentPath, args) { 56 | return args.map(arg => { 57 | if (typeof arg === 'string') return arg; 58 | 59 | return { 60 | value: arg.value, 61 | test: extractNode(parentPath, arg.test) 62 | }; 63 | }); 64 | } 65 | 66 | function replaceFunctionCalls(normalizedFuncCalls, styleClasses) { 67 | for (const [callExpr, args] of normalizedFuncCalls) { 68 | const flatClasses = flattenClasses(styleClasses); 69 | const replacedArguments = extractArgumentIdentifiers(callExpr, args); 70 | 71 | callExpr.replaceWith(generateExpression(replacedArguments, flatClasses)); 72 | } 73 | } 74 | 75 | module.exports = { replaceCreateCall, replaceFunctionCalls }; 76 | -------------------------------------------------------------------------------- /src/helpers/normalize-arguments.js: -------------------------------------------------------------------------------- 1 | const t = require('@babel/types'); 2 | 3 | function assertType(nodePath, type, opts) { 4 | if (!t[`is${type}`](nodePath.node, opts)) { 5 | throw nodePath.buildCodeFrameError(`Unsupported type ${nodePath.type}`); 6 | } 7 | } 8 | 9 | function assertInStyles(stringOrIdentifier, styleNames) { 10 | const value = t.isStringLiteral(stringOrIdentifier) 11 | ? stringOrIdentifier.node.value 12 | : stringOrIdentifier.node.name; 13 | 14 | if (!styleNames.includes(value)) { 15 | throw stringOrIdentifier.buildCodeFrameError( 16 | `Property ${value} does not exist in style object` 17 | ); 18 | } 19 | } 20 | 21 | function normalizeObjectExpression(objectExpr, styleNames) { 22 | return objectExpr.get('properties').map(prop => { 23 | assertType(prop, 'ObjectProperty', { computed: false }); 24 | assertInStyles(prop.get('key'), styleNames); 25 | 26 | return { 27 | test: prop.node.value, 28 | value: prop.node.key.name 29 | }; 30 | }); 31 | } 32 | 33 | function normalizeLogicalExpression(logicalExpr, styleNames) { 34 | assertType(logicalExpr.get('right'), 'StringLiteral'); 35 | assertInStyles(logicalExpr.get('right'), styleNames); 36 | 37 | return { 38 | test: logicalExpr.node.left, 39 | value: logicalExpr.node.right.value 40 | }; 41 | } 42 | 43 | function normalizeConditionalExpression(conditionalExpr, styleNames) { 44 | assertType(conditionalExpr.get('consequent'), 'StringLiteral'); 45 | assertType(conditionalExpr.get('alternate'), 'StringLiteral'); 46 | assertInStyles(conditionalExpr.get('alternate'), styleNames); 47 | assertInStyles(conditionalExpr.get('consequent'), styleNames); 48 | 49 | return [ 50 | conditionalExpr.node.alternate.value, 51 | { 52 | test: conditionalExpr.node.test, 53 | value: conditionalExpr.node.consequent.value 54 | } 55 | ]; 56 | } 57 | 58 | function normalizeStringLiteral(stringLiteral, styleNames) { 59 | assertInStyles(stringLiteral, styleNames); 60 | return stringLiteral.node.value; 61 | } 62 | 63 | // Map resolver arguments to strings and logical ANDs 64 | function normalizeArguments(callExpr, styleNames) { 65 | return callExpr.get('arguments').flatMap(arg => { 66 | if (t.isObjectExpression(arg.node)) { 67 | return normalizeObjectExpression(arg, styleNames); 68 | } 69 | 70 | if (t.isStringLiteral(arg.node)) { 71 | return normalizeStringLiteral(arg, styleNames); 72 | } 73 | 74 | if (t.isLogicalExpression(arg.node, { operator: '&&' })) { 75 | return normalizeLogicalExpression(arg, styleNames); 76 | } 77 | 78 | if (t.isConditionalExpression(arg.node)) { 79 | return normalizeConditionalExpression(arg, styleNames); 80 | } 81 | 82 | throw arg.buildCodeFrameError(`Unsupported type ${arg.node.type}`); 83 | }); 84 | } 85 | 86 | module.exports = normalizeArguments; 87 | -------------------------------------------------------------------------------- /src/helpers/strip-type-assertions.js: -------------------------------------------------------------------------------- 1 | function stripTypeAssertions(nodePath) { 2 | nodePath.traverse({ 3 | TSAsExpression(path) { 4 | path.replaceWith(path.get('expression')); 5 | } 6 | }); 7 | } 8 | 9 | module.exports = stripTypeAssertions; 10 | -------------------------------------------------------------------------------- /src/helpers/validate.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const testASTShape = require('../utils/test-ast-shape'); 3 | const { 4 | isAtRuleObject, 5 | isAtRule, 6 | isPseudoSelector 7 | } = require('../utils/styles'); 8 | 9 | function isHMR(identifier) { 10 | return testASTShape(identifier.parentPath, { 11 | type: 'CallExpression', 12 | callee: { 13 | type: 'MemberExpression', 14 | object: { 15 | name: 'reactHotLoader' 16 | }, 17 | property: { 18 | name: 'register' 19 | } 20 | } 21 | }); 22 | } 23 | 24 | function validateReferences(references) { 25 | references.forEach(ref => { 26 | const { parentPath, parent, node } = ref; 27 | if (parentPath.isCallExpression() && parent.callee === node) return; 28 | if (parentPath.isSpreadElement()) return; 29 | if (parentPath.isMemberExpression()) return; 30 | 31 | // The return value from `style9.create` should be a function, but the 32 | // compiler turns it into an object. Therefore only access to properties 33 | // is allowed. React Hot Loader accesses all bindings, so a temporary 34 | // workaround is required. React Fast Refresh does not have this problem. 35 | assert( 36 | isHMR(ref), 37 | ref.buildCodeFrameError( 38 | 'Return value from style9.create has to be called as a function or accessed as an object' 39 | ) 40 | ); 41 | }); 42 | } 43 | 44 | function evalKey(objProp) { 45 | const keyPath = objProp.get('key'); 46 | if (objProp.node.computed) { 47 | return keyPath.evaluate(); 48 | } 49 | 50 | if (keyPath.isStringLiteral()) { 51 | return { value: keyPath.node.value, confident: true }; 52 | } 53 | 54 | return { value: keyPath.node.name, confident: true }; 55 | } 56 | 57 | function validateStyleObjectInner(objProp) { 58 | objProp.get('value').traverse({ 59 | ObjectProperty(path) { 60 | const { value, confident } = evalKey(path); 61 | if (!confident) return; 62 | 63 | if (!path.get('value').isObjectExpression()) return; 64 | 65 | if (isAtRuleObject(value)) { 66 | // Skip direct props 67 | validateStyleObject(path.get('value')); 68 | path.skip(); 69 | } else if (!isAtRule(value) && !isPseudoSelector(value)) { 70 | throw path 71 | .get('key') 72 | .buildCodeFrameError( 73 | `Invalid key ${value}. Object keys must be at-rules or pseudo selectors` 74 | ); 75 | } 76 | } 77 | }); 78 | } 79 | 80 | // Does not validate spread elements 81 | function validateStyleObject(objExpr) { 82 | objExpr.get('properties').forEach(validateStyleObjectInner); 83 | } 84 | 85 | module.exports = { validateReferences, validateStyleObject }; 86 | -------------------------------------------------------------------------------- /src/plugin-utils.js: -------------------------------------------------------------------------------- 1 | function stringifyCssRequest(outputLoaders) { 2 | const cssLoaders = outputLoaders.map(stringifyLoaderRequest).join('!'); 3 | 4 | return `!${cssLoaders}!`; 5 | } 6 | 7 | function stringifyLoaderRequest({ loader, options = {} }) { 8 | return `${loader}?${JSON.stringify(options)}`; 9 | } 10 | 11 | module.exports = { stringifyCssRequest, stringifyLoaderRequest }; 12 | -------------------------------------------------------------------------------- /src/process-css.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const discardDuplicates = require('postcss-discard-duplicates'); 3 | const selectorParser = require('postcss-selector-parser'); 4 | const sortCSSmq = require('sort-css-media-queries'); 5 | const { PROPERTY_PRIORITY } = require('./utils/constants'); 6 | 7 | const DEFAULT_PRIORITY = 1; 8 | 9 | const PSEUDO_ORDER = [ 10 | ':link', 11 | ':focus-within', 12 | ':first-child', 13 | ':last-child', 14 | ':odd-child', 15 | ':even-child', 16 | ':hover', 17 | ':focus', 18 | ':active', 19 | ':visited', 20 | ':disabled' 21 | ]; 22 | 23 | const EXCLUDE_RULE_TYPE = ['atrule']; 24 | 25 | function getPriority(prop) { 26 | return PROPERTY_PRIORITY[prop] || DEFAULT_PRIORITY; 27 | } 28 | 29 | function isValidSelector({ nodes: [firstNode, ...restNodes] }) { 30 | if (restNodes.length) return false; 31 | 32 | if (firstNode.nodes[0].type !== 'class') return false; 33 | 34 | for (let index = 1; index < firstNode.nodes.length; index++) { 35 | const node = firstNode.nodes[index]; 36 | if (node.type !== 'pseudo') return false; 37 | } 38 | 39 | return true; 40 | } 41 | 42 | function parseSelector(selector) { 43 | return selectorParser(selector => selector).transformSync(selector); 44 | } 45 | 46 | function getPseudoClasses(selector) { 47 | return selector 48 | .filter(selector => selector.type === 'pseudo' && selector.value[1] !== ':') 49 | .map(selector => selector.value); 50 | } 51 | 52 | function removeWithContext(rule) { 53 | let decl; 54 | let onlyChild = true; 55 | 56 | do { 57 | const { parent } = rule; 58 | if (rule.nodes && rule.nodes.length) onlyChild = false; 59 | const clone = onlyChild ? rule.remove() : rule.clone(); 60 | if (rule.type !== 'decl') clone.removeAll(); 61 | if (decl) clone.append(decl); 62 | decl = clone; 63 | rule = parent; 64 | } while (rule && rule.type !== 'root'); 65 | 66 | return decl; 67 | } 68 | 69 | function getMediaQueries(rule) { 70 | const mediaQueries = []; 71 | 72 | while (rule) { 73 | if ( 74 | // Stryker disable next-line ConditionalExpression: extra test safety 75 | rule.type === 'atrule' && 76 | rule.name === 'media' 77 | ) { 78 | mediaQueries.push(rule.params); 79 | } 80 | rule = rule.parent; 81 | } 82 | 83 | return mediaQueries; 84 | } 85 | 86 | function getDecls(root) { 87 | const decls = []; 88 | 89 | root.walkDecls(rule => { 90 | decls.push(rule); 91 | }); 92 | 93 | return decls; 94 | } 95 | 96 | function extractDecls(decls) { 97 | const nodes = []; 98 | 99 | decls.forEach(rule => { 100 | if (EXCLUDE_RULE_TYPE.includes(rule.parent.type)) { 101 | return; 102 | } 103 | const selectors = parseSelector(rule.parent); 104 | const isStyle9Selector = isValidSelector(selectors); 105 | if (!isStyle9Selector) return; 106 | const pseudoClasses = getPseudoClasses(selectors.nodes[0]); 107 | const mediaQueries = getMediaQueries(rule.parent); 108 | const decl = removeWithContext(rule); 109 | const node = { decl, mediaQueries, pseudoClasses }; 110 | nodes.push(node); 111 | }); 112 | 113 | return nodes; 114 | } 115 | 116 | function sortNodes(nodes) { 117 | nodes.sort((a, b) => { 118 | if (a.pseudoClasses.length !== b.pseudoClasses.length) { 119 | return a.pseudoClasses.length - b.pseudoClasses.length; 120 | } 121 | 122 | // Stryker disable next-line EqualityOperator: out-of-bounds array access 123 | for (let index = 0; index < a.pseudoClasses.length; index++) { 124 | const clsA = a.pseudoClasses[index]; 125 | const clsB = b.pseudoClasses[index]; 126 | if (clsA !== clsB) { 127 | return PSEUDO_ORDER.indexOf(clsA) - PSEUDO_ORDER.indexOf(clsB); 128 | } 129 | } 130 | 131 | if (a.mediaQueries.length !== b.mediaQueries.length) { 132 | return a.mediaQueries.length - b.mediaQueries.length; 133 | } 134 | 135 | if (a.mediaQueries.length) { 136 | return sortCSSmq( 137 | // Stryker disable StringLiteral: strict, but not needed for sortCSSmq 138 | a.mediaQueries.join(' and '), 139 | b.mediaQueries.join(' and ') 140 | // Stryker restore all 141 | ); 142 | } 143 | 144 | const propA = a.decl.nodes[0].prop; 145 | const propB = b.decl.nodes[0].prop; 146 | 147 | return getPriority(propA) - getPriority(propB); 148 | }); 149 | 150 | return nodes; 151 | } 152 | 153 | /** 154 | * Sort declarations first by pseudo-classes, then by media queries mobile 155 | * first, and finally by longhand prioerity 156 | * Only sort rules that are generated by style9, which should have a selector 157 | * that consists of a class and optionally of pseudo-elements & classes 158 | */ 159 | function sortPseudo(root) { 160 | const decls = getDecls(root); 161 | const nodes = sortNodes(extractDecls(decls)); 162 | 163 | nodes.forEach(({ decl }) => { 164 | root.append(decl); 165 | }); 166 | } 167 | 168 | module.exports = function processCSS(css, options) { 169 | return postcss([discardDuplicates, sortPseudo]).process(css, options); 170 | }; 171 | -------------------------------------------------------------------------------- /src/process-references.js: -------------------------------------------------------------------------------- 1 | const { transpileCreate } = require('./transpilers/create'); 2 | const { transpileKeyframes } = require('./transpilers/keyframes'); 3 | const testASTShape = require('./utils/test-ast-shape'); 4 | 5 | function isPropertyCall(node, name) { 6 | return testASTShape(node, { 7 | parent: { 8 | type: 'MemberExpression', 9 | parent: { 10 | type: 'CallExpression', 11 | callee: { 12 | property: { name } 13 | }, 14 | arguments: { 15 | length: 1, 16 | 0: { 17 | type: 'ObjectExpression' 18 | } 19 | } 20 | } 21 | } 22 | }); 23 | } 24 | 25 | function processReference(node, options) { 26 | // style9() calls are left as-is 27 | if (node.parentPath.isCallExpression()) return []; 28 | 29 | if (isPropertyCall(node, 'create')) return transpileCreate(node, options); 30 | if (isPropertyCall(node, 'keyframes')) return transpileKeyframes(node); 31 | 32 | throw node.buildCodeFrameError( 33 | 'Unsupported use. Supported uses are: style9(), style9.create(), and style9.keyframes()' 34 | ); 35 | } 36 | 37 | // Keyframes needs to be processed first because the result is used in create. 38 | // The correct solution would be to process the references in the order they 39 | // would be executed, but it's easier to just sort keyframes first 40 | function sortReferences(references) { 41 | return references.sort(reference => 42 | isPropertyCall(reference, 'keyframes') ? -1 : 1 43 | ); 44 | } 45 | 46 | function processReferences(references, options) { 47 | return sortReferences(references).flatMap(node => { 48 | return processReference(node, options); 49 | }); 50 | } 51 | 52 | module.exports = processReferences; 53 | -------------------------------------------------------------------------------- /src/transpilers/create.js: -------------------------------------------------------------------------------- 1 | const generateClasses = require('../helpers/generate-classes'); 2 | const generateStyles = require('../helpers/generate-styles'); 3 | const getStyleObjectValue = require('../helpers/get-style-object-value'); 4 | const listDynamicKeys = require('../helpers/list-dynamic-keys'); 5 | const listFunctionCallKeys = require('../helpers/list-function-call-keys'); 6 | const listFunctionCalls = require('../helpers/list-function-calls'); 7 | const listReferences = require('../helpers/list-references'); 8 | const listStaticKeys = require('../helpers/list-static-keys'); 9 | const { 10 | replaceCreateCall, 11 | replaceFunctionCalls 12 | } = require('../helpers/mutate-ast'); 13 | const normalizeArguments = require('../helpers/normalize-arguments'); 14 | const { 15 | validateReferences, 16 | validateStyleObject 17 | } = require('../helpers/validate'); 18 | const { 19 | mapObject, 20 | mapObjectValues, 21 | filterObjectKeys 22 | } = require('../utils/helpers'); 23 | const { minifyProperty } = require('../utils/styles'); 24 | const stripTypeAssertions = require('../helpers/strip-type-assertions'); 25 | const flattenAtRules = require('../helpers/flatten-at-rules'); 26 | 27 | function normalizeFunctionCalls(callExpressions, styleNames) { 28 | const entries = callExpressions.map(id => { 29 | return [id.parentPath, normalizeArguments(id.parentPath, styleNames)]; 30 | }); 31 | return new Map(entries); 32 | } 33 | 34 | function minifyProperties(classes) { 35 | return mapObject(classes, ([key, value]) => { 36 | const minifiedName = minifyProperty(key); 37 | const isObject = typeof value === 'object'; 38 | const minifiedValue = isObject ? minifyProperties(value) : value; 39 | 40 | return [minifiedName, minifiedValue]; 41 | }); 42 | } 43 | 44 | function transpileCreate(identifier, options) { 45 | const callExpr = identifier.parentPath.parentPath; 46 | const objExpr = callExpr.get('arguments.0'); 47 | 48 | stripTypeAssertions(objExpr); 49 | validateStyleObject(objExpr); 50 | 51 | const styleDefinitions = flattenAtRules(getStyleObjectValue(objExpr)); 52 | const styleClasses = generateClasses( 53 | styleDefinitions, 54 | options.incrementalClassnames 55 | ); 56 | const references = listReferences(callExpr.parentPath); 57 | 58 | validateReferences(references); 59 | 60 | const funcCalls = listFunctionCalls(references); 61 | const styleNames = Object.keys(styleDefinitions); 62 | const normalizedFuncCalls = normalizeFunctionCalls(funcCalls, styleNames); 63 | 64 | const staticKeys = listStaticKeys(callExpr, styleNames); 65 | const dynamicKeys = listDynamicKeys(references, styleNames); 66 | const funcCallKeys = listFunctionCallKeys([...normalizedFuncCalls.values()]); 67 | 68 | const propKeys = [...staticKeys, ...dynamicKeys]; 69 | const filteredStyleValues = filterObjectKeys(styleClasses, propKeys); 70 | const allKeys = [...funcCallKeys, ...propKeys]; 71 | const filteredDefinitions = filterObjectKeys(styleDefinitions, allKeys); 72 | 73 | const minifiedStyleValues = options.minifyProperties 74 | ? mapObjectValues(filteredStyleValues, minifyProperties) 75 | : filteredStyleValues; 76 | 77 | replaceCreateCall(callExpr, minifiedStyleValues); 78 | replaceFunctionCalls(normalizedFuncCalls, styleClasses); 79 | 80 | return generateStyles(filteredDefinitions, options.incrementalClassnames); 81 | } 82 | 83 | module.exports = { transpileCreate }; 84 | -------------------------------------------------------------------------------- /src/transpilers/keyframes.js: -------------------------------------------------------------------------------- 1 | const t = require('@babel/types'); 2 | const getStyleObjectValue = require('../helpers/get-style-object-value'); 3 | const { getKeyframes } = require('../utils/styles'); 4 | 5 | function transpileKeyframes(identifier) { 6 | const callExpr = identifier.parentPath.parentPath; 7 | const objExpr = callExpr.get('arguments.0'); 8 | 9 | const rules = getStyleObjectValue(objExpr); 10 | const { name, declaration } = getKeyframes(rules); 11 | 12 | callExpr.replaceWith(t.stringLiteral(name)); 13 | 14 | return declaration; 15 | } 16 | 17 | module.exports = { transpileKeyframes }; 18 | -------------------------------------------------------------------------------- /src/utils/ast.js: -------------------------------------------------------------------------------- 1 | function evaluateNodePath(path) { 2 | const { value, confident, deopt } = path.evaluate(); 3 | if (confident) return value; 4 | throw deopt.buildCodeFrameError('Could not evaluate value'); 5 | } 6 | 7 | function isDynamicKey(memberExpr) { 8 | const property = memberExpr.get('property'); 9 | 10 | return memberExpr.node.computed && !property.isLiteral(); 11 | } 12 | 13 | function getStaticKey(memberExpr) { 14 | return memberExpr.node.property.name || memberExpr.node.property.value; 15 | } 16 | 17 | module.exports = { isDynamicKey, getStaticKey, evaluateNodePath }; 18 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) Johan Holmerin. 5 | * Copyright (c) Nicolas Gallagher. 6 | * Copyright (c) Facebook, Inc. and its affiliates. 7 | * Copyright (c) Robin Weser 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | */ 27 | 28 | // https://github.com/necolas/react-native-web/blob/36dacb2052efdab2a28655773dc76934157d9134/packages/react-native-web/src/modules/unitlessNumbers/index 29 | const UNITLESS_NUMBERS = [ 30 | 'animationIterationCount', 31 | 'borderImageOutset', 32 | 'borderImageSlice', 33 | 'borderImageWidth', 34 | 'boxFlex', 35 | 'boxFlexGroup', 36 | 'boxOrdinalGroup', 37 | 'columnCount', 38 | 'flex', 39 | 'flexGrow', 40 | 'flexOrder', 41 | 'flexPositive', 42 | 'flexShrink', 43 | 'flexNegative', 44 | 'fontWeight', 45 | 'gridRowEnd', 46 | 'gridRowSpan', 47 | 'gridRowStart', 48 | 'gridColumnEnd', 49 | 'gridColumnSpan', 50 | 'gridColumnStart', 51 | 'lineClamp', 52 | 'lineHeight', 53 | 'opacity', 54 | 'order', 55 | 'orphans', 56 | 'tabSize', 57 | 'widows', 58 | 'zIndex', 59 | 'zoom', 60 | 'fillOpacity', 61 | 'floodOpacity', 62 | 'stopOpacity', 63 | 'strokeDasharray', 64 | 'strokeDashoffset', 65 | 'strokeMiterlimit', 66 | 'strokeOpacity', 67 | 'strokeWidth', 68 | 'scale', 69 | 'scaleX', 70 | 'scaleY', 71 | 'scaleZ' 72 | ]; 73 | 74 | // https://github.com/robinweser/fela/blob/2c6c50bad8d0bcf704f1727a3dcf67bdf26fbb5c/packages/fela-enforce-longhands/src/index.js 75 | const PROPERTY_PRIORITY = { 76 | 'margin-left': 2, 77 | 'margin-right': 2, 78 | 'margin-top': 2, 79 | 'margin-bottom': 2, 80 | 'padding-left': 2, 81 | 'padding-right': 2, 82 | 'padding-bottom': 2, 83 | 'padding-top': 2, 84 | 'flex-wrap': 2, 85 | 'flex-shrink': 2, 86 | 'flex-basis': 2, 87 | 'background-color': 2, 88 | 'backgound-repeat': 2, 89 | 'background-position': 2, 90 | 'background-image': 2, 91 | 'background-origin': 2, 92 | 'background-clip': 2, 93 | 'background-size': 2, 94 | 'transition-property': 2, 95 | 'transition-timing-function': 2, 96 | 'transition-duration': 2, 97 | 'transition-delay': 2, 98 | 'animation-delay': 2, 99 | 'animation-direction': 2, 100 | 'animation-duration': 2, 101 | 'animation-fill-mode': 2, 102 | 'animation-iteration-count': 2, 103 | 'animation-name': 2, 104 | 'animation-play-state': 2, 105 | 'animation-timing-function': 2, 106 | 'border-width': 2, 107 | 'border-style': 2, 108 | 'border-color': 2, 109 | 'border-top': 2, 110 | 'border-right': 2, 111 | 'border-bottom': 2, 112 | 'border-left': 2, 113 | 'border-top-width': 3, 114 | 'border-top-style': 3, 115 | 'border-top-color': 3, 116 | 'border-right-width': 3, 117 | 'border-right-style': 3, 118 | 'border-right-color': 3, 119 | 'border-bottom-width': 3, 120 | 'border-bottom-style': 3, 121 | 'border-bottom-color': 3, 122 | 'border-left-width': 3, 123 | 'border-left-style': 3, 124 | 'border-left-color': 3, 125 | 'border-bottom-left-radius': 2, 126 | 'border-bottom-right-radius': 2, 127 | 'border-top-left-radius': 2, 128 | 'border-top-right-radius': 2, 129 | 'border-image-outset': 2, 130 | 'border-image-repeat': 2, 131 | 'border-image-slice': 2, 132 | 'border-image-source': 2, 133 | 'border-image-width': 2, 134 | 'column-width': 2, 135 | 'column-count': 2, 136 | 'list-style-image': 2, 137 | 'list-style-position': 2, 138 | 'list-style-type': 2, 139 | 'outline-width': 2, 140 | 'outline-style': 2, 141 | 'outline-color': 2, 142 | 'overflow-x': 2, 143 | 'overflow-y': 2, 144 | 'text-decoration-line': 2, 145 | 'text-decoration-style': 2, 146 | 'text-decoration-color': 2 147 | }; 148 | 149 | // Defined in Style.d.ts 150 | const COMMA_SEPARATED_LIST_PROPERTIES = [ 151 | 'transitionProperty', 152 | 'transitionDuration', 153 | 'transitionTimingFunction', 154 | 'transitionDelay' 155 | ]; 156 | 157 | module.exports = { 158 | UNITLESS_NUMBERS, 159 | PROPERTY_PRIORITY, 160 | COMMA_SEPARATED_LIST_PROPERTIES 161 | }; 162 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | function mapObject(object, cb) { 2 | return Object.fromEntries(Object.entries(object).map(cb)); 3 | } 4 | 5 | function mapObjectValues(object, cb) { 6 | return mapObject(object, ([key, value]) => [key, cb(value)]); 7 | } 8 | 9 | function removeDuplicates(list) { 10 | return list.filter((prop, index, array) => array.indexOf(prop) === index); 11 | } 12 | 13 | function filterObjectKeys(obj, keys) { 14 | const newObj = {}; 15 | 16 | // Iterate in existing order 17 | for (const key in obj) { 18 | if (keys.includes(key)) { 19 | newObj[key] = obj[key]; 20 | } 21 | } 22 | 23 | return newObj; 24 | } 25 | 26 | module.exports = { 27 | mapObject, 28 | mapObjectValues, 29 | removeDuplicates, 30 | filterObjectKeys 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/incremental-classnames.js: -------------------------------------------------------------------------------- 1 | function createGenerator() { 2 | const CLASS_CACHE = Object.create(null); 3 | let CLASS_INDEX = 0; 4 | 5 | const CHRS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); 6 | const REST_CHRS = [...CHRS, ...'0123456789'.split('')]; 7 | 8 | function isSafeClassname(cls) { 9 | return !/(ad|fa)/gi.test(cls); 10 | } 11 | 12 | function generateClassname(index) { 13 | const logCharsRadix = parseInt(Math.log(REST_CHRS.length)); 14 | let i = parseInt(index / CHRS.length); 15 | const n = parseInt(Math.log(i * REST_CHRS.length) / logCharsRadix); 16 | 17 | let cssNameChars = CHRS[index % CHRS.length]; 18 | 19 | for (let k = 1; k <= n; k++) { 20 | cssNameChars += REST_CHRS[i % REST_CHRS.length]; 21 | i = parseInt(i / REST_CHRS.length); 22 | } 23 | 24 | return cssNameChars; 25 | } 26 | 27 | function getIncrementalClass(key) { 28 | let value = CLASS_CACHE[key]; 29 | if (value) return value; 30 | 31 | do { 32 | value = generateClassname(CLASS_INDEX++); 33 | } while (!isSafeClassname(value)); 34 | 35 | CLASS_CACHE[key] = value; 36 | 37 | return value; 38 | } 39 | 40 | return { getIncrementalClass }; 41 | } 42 | 43 | module.exports = createGenerator; 44 | -------------------------------------------------------------------------------- /src/utils/styles.js: -------------------------------------------------------------------------------- 1 | const cssProperties = require('known-css-properties').all; 2 | const fastJsonStableStringify = require('fast-json-stable-stringify'); 3 | const hash = require('murmurhash-js'); 4 | const { getIncrementalClass } = require('./incremental-classnames')(); 5 | 6 | const { 7 | UNITLESS_NUMBERS, 8 | COMMA_SEPARATED_LIST_PROPERTIES 9 | } = require('./constants'); 10 | 11 | const BASE_FONT_SIZE_PX = 16; 12 | 13 | function isCustomProperty(name) { 14 | return name.startsWith('--'); 15 | } 16 | 17 | function mapValue(prop, value) { 18 | if (typeof value === 'number') { 19 | if (prop === 'fontSize') return `${value / BASE_FONT_SIZE_PX}rem`; 20 | if (!UNITLESS_NUMBERS.includes(prop)) return `${value}px`; 21 | } 22 | 23 | if (prop === 'transitionProperty') { 24 | return camelToHyphen(value); 25 | } 26 | 27 | return value; 28 | } 29 | 30 | function joinValues(prop, list) { 31 | const separator = COMMA_SEPARATED_LIST_PROPERTIES.includes(prop) ? ',' : ' '; 32 | 33 | return list.join(separator); 34 | } 35 | 36 | function normalizeValue(prop, value) { 37 | if (isCustomProperty(prop)) return value; 38 | 39 | if (Array.isArray(value)) { 40 | const mappedValues = value.map(val => mapValue(prop, val)); 41 | return joinValues(prop, mappedValues); 42 | } 43 | 44 | return mapValue(prop, value); 45 | } 46 | 47 | // given a code (0 <= code <= 51), return a character in a-zA-Z 48 | const getAlphabeticChar = code => 49 | String.fromCharCode(code + (code > 25 ? 39 /* 65 - 26 */ : 97)); 50 | 51 | function getHashClass(...args) { 52 | const code = hash(fastJsonStableStringify(args)); 53 | 54 | let className = ''; 55 | let x = 0; 56 | 57 | for (x = Math.abs(code); x > 52; x = (x / 52) | 0) { 58 | className = getAlphabeticChar(x % 52) + className; 59 | } 60 | 61 | className = getAlphabeticChar(x % 52) + className; 62 | 63 | // replace ad with a_d 64 | return className.replace(/(a)(d)/gi, '$1_$2'); 65 | } 66 | 67 | function getClass(args, incremental) { 68 | const cls = getHashClass(args); 69 | if (!incremental) return cls; 70 | return getIncrementalClass(cls); 71 | } 72 | 73 | function camelToHyphen(string) { 74 | if (isCustomProperty(string)) return string; 75 | return string.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`); 76 | } 77 | 78 | function getDeclaration( 79 | { name, value, atRules, pseudoSelectors }, 80 | incremental 81 | ) { 82 | const cls = getClass({ name, value, atRules, pseudoSelectors }, incremental); 83 | 84 | return ( 85 | atRules.map(rule => rule + '{').join('') + 86 | '.' + 87 | cls + 88 | pseudoSelectors.join('') + 89 | '{' + 90 | camelToHyphen(name) + 91 | ':' + 92 | normalizeValue(name, value) + 93 | '}' + 94 | atRules.map(() => '}').join('') 95 | ); 96 | } 97 | 98 | function normalizeTime(time) { 99 | if (time === 'from') return '0%'; 100 | if (time === 'to') return '100%'; 101 | return time; 102 | } 103 | 104 | function stringifyKeyframe(time, frame) { 105 | if (!Object.keys(frame).length) return ''; 106 | 107 | const props = Object.entries(frame).map(([key, value]) => { 108 | return `${camelToHyphen(key)}:${normalizeValue(key, value)}`; 109 | }); 110 | 111 | return `${normalizeTime(time)}{${props.join(';')}}`; 112 | } 113 | 114 | function stringifyKeyframes(rules) { 115 | return Object.entries(rules) 116 | .map(([time, frame]) => stringifyKeyframe(time, frame)) 117 | .join(''); 118 | } 119 | 120 | function getKeyframes(rules) { 121 | const rulesString = stringifyKeyframes(rules); 122 | const name = getClass(rulesString); 123 | const declaration = `@keyframes ${name}{${rulesString}}`; 124 | return { name, declaration }; 125 | } 126 | 127 | const LEGACY_PSEUDO_ELEMENTS = [ 128 | ':before', 129 | ':after', 130 | ':first-letter', 131 | ':first-line' 132 | ]; 133 | 134 | function normalizePseudoElements(string) { 135 | if (LEGACY_PSEUDO_ELEMENTS.includes(string)) { 136 | return ':' + string; 137 | } 138 | 139 | return string; 140 | } 141 | 142 | function minifyProperty(name) { 143 | const hyphenName = camelToHyphen(name); 144 | if (cssProperties.includes(hyphenName)) { 145 | return cssProperties.indexOf(hyphenName).toString(36); 146 | } 147 | 148 | return getHashClass(hyphenName); 149 | } 150 | 151 | // Values can be primitives or arrays, nested styles are plain objects 152 | function isNestedStyles(item) { 153 | return typeof item === 'object' && !Array.isArray(item); 154 | } 155 | 156 | function isAtRule(string) { 157 | return string.startsWith('@'); 158 | } 159 | 160 | function isPseudoSelector(string) { 161 | return string.startsWith(':'); 162 | } 163 | 164 | function isAtRuleObject(name) { 165 | return name === '@media' || name === '@supports'; 166 | } 167 | 168 | module.exports = { 169 | getClass, 170 | getDeclaration, 171 | getKeyframes, 172 | normalizePseudoElements, 173 | minifyProperty, 174 | isNestedStyles, 175 | normalizeValue, 176 | isAtRule, 177 | isPseudoSelector, 178 | isAtRuleObject 179 | }; 180 | -------------------------------------------------------------------------------- /src/utils/test-ast-shape.js: -------------------------------------------------------------------------------- 1 | function getAst(ast, path) { 2 | if (Array.isArray(ast)) return ast[path]; 3 | if (path === 'parent') return ast.parentPath; 4 | return ast.get(path); 5 | } 6 | 7 | function getValue(ast, path) { 8 | if (Array.isArray(ast)) return ast[path]; 9 | return ast && ast.node[path]; 10 | } 11 | 12 | function testASTShape(ast, shape) { 13 | for (const key in shape) { 14 | if (typeof shape[key] === 'object') { 15 | if (!testASTShape(getAst(ast, key), shape[key])) return false; 16 | } else { 17 | if (shape[key] !== getValue(ast, key)) return false; 18 | } 19 | } 20 | 21 | return true; 22 | } 23 | 24 | module.exports = testASTShape; 25 | -------------------------------------------------------------------------------- /stryker.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", 3 | "packageManager": "yarn", 4 | "reporters": [ 5 | "html", 6 | "clear-text", 7 | "json", 8 | "progress" 9 | ], 10 | "mutate": [ 11 | "src/**/*.js", 12 | "!src/plugin-utils.js", 13 | "!src/utils/constants.js", 14 | "index.js", 15 | "babel.js" 16 | ], 17 | "testRunner": "jest", 18 | "coverageAnalysis": "perTest", 19 | "jest": { 20 | "projectType": "custom", 21 | "configFile": "./jest.config.json" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /types/Style.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | StandardShorthandProperties, 3 | StandardLonghandProperties, 4 | VendorLonghandProperties, 5 | SimplePseudos 6 | } from 'csstype'; 7 | 8 | export type AtRules = '@media' | '@supports'; 9 | 10 | export type Falsy = false | null | undefined; 11 | 12 | export type Style = StyleProperties & 13 | { 14 | [key in SimplePseudos]?: Style; 15 | } & 16 | { 17 | [key in AtRules]?: Record>; 18 | } & 19 | Extra; 20 | 21 | export {}; 22 | 23 | type FilteredStandardLonghandProperties = Omit< 24 | StandardLonghandProperties, 25 | // Not longhands 26 | | 'backgroundPosition' 27 | | 'inset' 28 | | 'insetBlock' 29 | | 'insetInline' 30 | | 'marginBlock' 31 | | 'marginInline' 32 | | 'overscrollBehavior' 33 | | 'paddingBlock' 34 | | 'paddingInline' 35 | | 'scrollMargin' 36 | | 'scrollMarginBlock' 37 | | 'scrollMarginInline' 38 | | 'scrollPadding' 39 | | 'scrollPaddingBlock' 40 | | 'scrollPaddingInline' 41 | | 'scrollMargin' 42 | // Custom definitions 43 | | keyof ExtendedStyleProperties 44 | >; 45 | 46 | interface ArrayProperties { 47 | fontVariant?: 48 | | FilteredStandardLonghandProperties['fontVariant'] 49 | | Array; 50 | textDecorationLine?: 51 | | FilteredStandardLonghandProperties['textDecorationLine'] 52 | | Array; 53 | transitionDuration?: 54 | | FilteredStandardLonghandProperties['transitionDuration'] 55 | | Array; 56 | transitionTimingFunction?: 57 | | FilteredStandardLonghandProperties['transitionTimingFunction'] 58 | | Array; 59 | transitionDelay?: 60 | | FilteredStandardLonghandProperties['transitionDelay'] 61 | | Array; 62 | } 63 | 64 | interface ArrayStandardLonghandProperties 65 | extends Omit, 66 | ArrayProperties {} 67 | 68 | interface SvgProperties { 69 | alignmentBaseline?: 70 | | 'auto' 71 | | 'baseline' 72 | | 'before-edge' 73 | | 'text-before-edge' 74 | | 'middle' 75 | | 'central' 76 | | 'after-edge' 77 | | 'text-after-edge' 78 | | 'ideographic' 79 | | 'alphabetic' 80 | | 'hanging' 81 | | 'mathematical'; 82 | baselineShift?: number | string; 83 | color?: string; 84 | colorInterpolation?: 'auto' | 'sRGB' | 'linearRGB'; 85 | colorRendering?: 'auto' | 'optimizeSpeed' | 'optimizeQuality'; 86 | dominantBaseline?: 87 | | 'auto' 88 | | 'text-bottom' 89 | | 'alphabetic' 90 | | 'ideographic' 91 | | 'middle' 92 | | 'central' 93 | | 'mathematical' 94 | | 'hanging' 95 | | 'text-top'; 96 | fill?: string; 97 | fillOpacity?: string; 98 | fillRule?: 'nonzero' | 'evenodd'; 99 | imageRendering?: 'auto' | 'optimizeSpeed' | 'optimizeQuality'; 100 | shapeRendering?: 101 | | 'auto' 102 | | 'optimizeSpeed' 103 | | 'crispEdges' 104 | | 'geometricPrecision'; 105 | stopColor?: string; 106 | stopOpacity?: number; 107 | stroke?: string; 108 | strokeDasharray?: number | string | Array; 109 | strokeDashoffset?: number | string; 110 | strokeLinecap?: 'butt' | 'round' | 'square'; 111 | strokeLinejoin?: 'miter' | 'round' | 'bevel'; 112 | strokeMiterlimit?: number; 113 | strokeOpacity?: number | string; 114 | strokeWidth?: number | string; 115 | textAnchor?: 'start' | 'middle' | 'end'; 116 | vectorEffect?: 'none' | 'non-scaling-stroke'; 117 | writingMode?: 'lr-tb' | 'rl-tb' | 'tb-rl' | 'lr' | 'rl' | 'tb'; 118 | } 119 | 120 | export interface CustomProperties {} 121 | 122 | export interface StyleProperties 123 | extends StylePropertiesType, 124 | Partial {} 125 | 126 | type StylePropertiesType = { 127 | [k in keyof StylePropertiesInternal]?: 128 | | `var(${keyof CustomProperties})` 129 | | StylePropertiesInternal[k]; 130 | }; 131 | 132 | interface StylePropertiesInternal 133 | extends ArrayStandardLonghandProperties, 134 | VendorLonghandProperties, 135 | ExpandedShorthands, 136 | ExtendedStyleProperties, 137 | Omit {} 138 | 139 | type ScrollSnapType = 'x' | 'y' | 'block' | 'inline' | 'both'; 140 | type ScrollSnapAlign = 'none' | 'start' | 'end' | 'center'; 141 | 142 | interface ExtendedStyleProperties { 143 | transitionProperty?: 144 | | StandardLonghandProperties['transitionProperty'] 145 | | keyof StylePropertiesInternal 146 | | Array; 147 | gridAutoFlow?: 148 | | StandardLonghandProperties['gridAutoFlow'] 149 | | ['row', 'dense'] 150 | | ['column', 'dense']; 151 | scrollSnapType?: 152 | | StandardLonghandProperties['scrollSnapType'] 153 | | [ScrollSnapType, ('mandatory' | 'proximity')?]; 154 | scrollSnapAlign?: 155 | | StandardLonghandProperties['scrollSnapAlign'] 156 | | [ScrollSnapAlign, ScrollSnapAlign?]; 157 | } 158 | 159 | type ExpandedShorthands = Pick< 160 | StandardShorthandProperties, 161 | | 'border' 162 | | 'borderRadius' 163 | | 'borderTop' 164 | | 'borderRight' 165 | | 'borderBottom' 166 | | 'borderLeft' 167 | | 'borderWidth' 168 | | 'borderStyle' 169 | | 'borderColor' 170 | | 'padding' 171 | | 'margin' 172 | | 'outline' 173 | | 'flex' 174 | | 'flexFlow' 175 | | 'textDecoration' 176 | | 'overflow' 177 | | 'gap' 178 | | 'placeItems' 179 | | 'placeSelf' 180 | | 'transition' 181 | // Incorrectly classified as longhand in csstype 182 | // | 'overscrollBehavior' 183 | // | 'paddingBlock' 184 | // | 'paddingInline' 185 | // | 'marginBlock' 186 | // | 'marginInline' 187 | // | 'placeContent' 188 | // | 'inset' 189 | // | 'scrollMargin' 190 | // | 'scrollPadding' 191 | >; 192 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Minimum TypeScript Version: 4.1 2 | import { Style, StyleProperties, AtRules, Falsy } from './Style'; 3 | 4 | type AtRulesKey = `${AtRules}${string}`; 5 | 6 | interface AtRulesProperties { 7 | [key: AtRulesKey]: StyleWithAtRules; 8 | } 9 | 10 | interface StylePropertiesObject { 11 | [key: string]: StyleProperties; 12 | } 13 | 14 | export type StyleWithAtRules = Style; 15 | 16 | // Should be kept in sync with ./4.3/index.d.ts 17 | declare function style9(...names: Array): string; 18 | declare namespace style9 { 19 | function create( 20 | styles: { [key in keyof T]: StyleWithAtRules } 21 | ): { [key in keyof T]: StyleWithAtRules } & 22 | (( 23 | ...names: Array< 24 | keyof T | Falsy | { [key in keyof T]?: boolean | undefined | null } 25 | > 26 | ) => string); 27 | function keyframes(rules: StylePropertiesObject): string; 28 | } 29 | 30 | export default style9; 31 | export * from './Style'; 32 | -------------------------------------------------------------------------------- /types/test/at-rules.ts: -------------------------------------------------------------------------------- 1 | import style9 from 'style9'; 2 | 3 | style9.create({ 4 | media: { 5 | '@media (min-width: 80em)': { 6 | opacity: 0 7 | } 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /types/ts4.3/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Style, StyleProperties, Falsy } from '../Style'; 2 | 3 | interface StylePropertiesObject { 4 | [key: string]: StyleProperties; 5 | } 6 | 7 | // Copied from ../index.d.ts, with modified Style type 8 | declare function style9(...names: Array