├── .eslintignore ├── .eslintrc ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── babel.config.js ├── babel.js ├── lib ├── cjs │ ├── createElement.js │ ├── factory.js │ └── index.js └── esm │ ├── createElement.js │ ├── factory.js │ └── index.js ├── package.json ├── rollup.config.js ├── src ├── babel.js ├── compile.js ├── createElement.js ├── createOrderedCSSStyleSheet.js ├── data.js ├── factory.js ├── index.js ├── sort-mq.js ├── source-maps.js └── validate.js ├── test ├── _register.js ├── _utils.js ├── babel │ ├── fixtures │ │ ├── constants.js │ │ ├── i18n.js │ │ ├── missingImport.js │ │ └── simple.js │ ├── index.js │ └── snapshots │ │ ├── index.js.md │ │ └── index.js.snap ├── createElement.js ├── createOrderedCSSStyleSheet.js ├── e2e │ ├── _setup.js │ ├── fixtures │ │ └── external.css │ ├── test.html │ └── test.js ├── i18n.js ├── index.js ├── snapshots │ ├── createElement.js.md │ ├── createElement.js.snap │ ├── createOrderedCSSStyleSheet.js.md │ ├── createOrderedCSSStyleSheet.js.snap │ ├── i18n.js.md │ ├── i18n.js.snap │ ├── index.js.md │ ├── index.js.snap │ ├── server.js.md │ └── server.js.snap └── validate.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | test/lib/_helpers.js 2 | test/_register.js 3 | lib/**/*.prod.js 4 | lib/**/*.dev.js 5 | lib/*.js 6 | test/e2e/lib/ 7 | fixtures 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:ava/recommended" 5 | ], 6 | "parserOptions": { 7 | "ecmaVersion": 10, 8 | "sourceType": "module" 9 | }, 10 | "globals": { 11 | "process": "readonly", 12 | "window": "readonly", 13 | "document": "readonly", 14 | "Map": "readonly", 15 | "require": "readonly" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: giuseppeg 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/**/*.prod.js 2 | lib/**/*.dev.js 3 | lib/*.js 4 | test/e2e/lib 5 | node_modules 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-Present Giuseppe Gurgone. 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StyleSheet ⚡️💨 2 | 3 | Build Status source code 4 | 5 | StyleSheet is a library to author styles in JavaScript. 6 | 7 | It is **fast** and generates optimized, tiny bundles by compiling rules to **atomic CSS** that can then be **extracted to .css file** with a Babel plugin. 8 | 9 | [Play with StyleSheet on **CodeSandbox**](https://codesandbox.io/s/crazy-dubinsky-nrs7n?fontsize=14)! 10 | 11 | ```js 12 | import { StyleSheet, StyleResolver } from 'style-sheet' 13 | 14 | const styles = StyleSheet.create({ 15 | one: { 16 | color: 'red', 17 | }, 18 | another: { 19 | color: 'green' 20 | } 21 | }) 22 | 23 | const className = StyleResolver.resolve([styles.one, styles.another]) 24 | ``` 25 | 26 | Instead of making use of the Cascade, StyleSheet resolves styles deterministically based on their application order. 27 | 28 | ```js 29 | StyleResolver.resolve([styles.one, styles.another]) 30 | // color is green 31 | 32 | StyleResolver.resolve([styles.another, styles.one]) 33 | // color is red 34 | ``` 35 | 36 | `StyleResolver.resolve` works like `Object.assign` and merges rules right to left. 37 | 38 | StyleSheet comes with built-in support for pseudo classes and media queries, i18n, React and customizable `css` prop. 39 | 40 | The StyleSheet library API is highly inspired to React Native and React Native for Web's and implements a styling solution that is similar to the one used in the new facebook.com website: 41 | 42 | > This sounds very similar to what we use internally at Facebook for the new version of the site :) "Atomic" CSS via a CSS-in-JS library, that's extracted to static CSS files. 43 | [Building the New facebook.com](https://developers.facebook.com/videos/2019/building-the-new-facebookcom-with-react-graphql-and-relay/) touches on it (around 28:40 in the video).

— Daniel Lo Nigro (@Daniel15) Software Engineer at Facebook 44 | > [August 12, 2019](https://twitter.com/Daniel15/status/1160980442041896961) 45 | 46 | 47 | 48 | ## Getting started 49 | 50 | Firstly, install the package: 51 | 52 | ``` 53 | npm i --save style-sheet 54 | ``` 55 | 56 | The package exposes a `StyleSheet` and `StyleResolver` instances that are used to respectively create rulesets and resolve (apply) them to class names. 57 | 58 | ```js 59 | import { StyleSheet, StyleResolver } from 'style-sheet' 60 | ``` 61 | 62 | Use `StyleSheet.create` to create a style object of rules. 63 | 64 | ```js 65 | const styles = StyleSheet.create({ 66 | one: { 67 | color: 'red', 68 | }, 69 | another: { 70 | color: 'green' 71 | } 72 | }) 73 | ``` 74 | 75 | And `StyleResolver.resolve` to consume the `styles`: 76 | 77 | ```js 78 | StyleResolver.resolve([styles.one, styles.another]) 79 | ``` 80 | 81 | Remember the order in which you pass rules to `StyleResolver.resolve` matters! 82 | 83 | ### Pseudo classes, media queries and other *selectors* 84 | 85 | StyleSheet supports simple state selectors, media queries and shallow combinator selectors like: 86 | 87 | ```js 88 | const styles = StyleSheet.create({ 89 | root: { 90 | color: 'red', 91 | '&:hover' { // state selector 92 | color: 'green' 93 | }, 94 | ':focus > &': { // shallow combinator selector 95 | color: 'green' 96 | }, 97 | ':focus + &': { // shallow combinator selector 98 | color: 'blue' 99 | }, 100 | '@media (min-width: 678px)': { // media query 101 | color: 'yellow' 102 | } 103 | }, 104 | }) 105 | ``` 106 | 107 | ## Styles resolution 108 | 109 | `StyleSheet.create` converts rules to arrays of atomic CSS classes. Every atomic CSS class corresponds to a declaration inside of the rule: 110 | 111 | ```js 112 | const rules = StyleSheet.create({ 113 | rule: { 114 | display: 'block', // declaration 115 | color: 'green' // declaration 116 | } 117 | }) 118 | ``` 119 | 120 | `StyleResolver.resolve` then, accepts a single rule or an array of rules and it will merge them deterministically in application order (left to right). Finally it inserts the computed styles into the page. 121 | 122 | To make sure that styles are resolved deterministically some rules apply: 123 | 124 | 1. Shorthand properties are inserted first. 125 | 2. Longhand properties override shorthands, always! 126 | 3. States are sorted as follow: `link`, `visited`, `hover`, `focus-within`, `focus-visible`, `focus`, `active` meaning that `active` overrides `focus` and `hover` for example. 127 | 4. Shorthand and longhand properties used inside of combinator selectors are inserted after their corrispective regular groups. 128 | 5. Media queries are sorted in a mobile first manner. 129 | 130 | For simplicity sake, generally we encourage not use these advanced selectors and simply resolve rules conditionally at runtime based on application state. Note that this won't stop you from extracting styles to .css file! 131 | 132 | 133 | ## Server side rendering 134 | 135 | To render on the server, you can access the underlying style sheet that the library is using at any time with `StyleResolver.getStyleSheet()`. 136 | 137 | This method returns an ordered StyleSheet that exploses two methods: 138 | 139 | * `getTextContent` to get the atomic CSS for the rules that have been resolved. 140 | * `flush` to `getTextContent` and clear the stylesheet - useful when a server deamon is rendering multiple pages. 141 | 142 | ```js 143 | import { StyleResolver } from 'style-sheet' 144 | 145 | const html = ` 146 | 147 | 148 | 149 | 150 | my app 151 | 152 | 153 | 154 |
${renderedHTML}
155 | 156 | 157 | ` 158 | ``` 159 | 160 | By setting the `id` attribute to `__style_sheet__` StyleSheet can hydrate styles automatically on the client. 161 | 162 | ## Extracting to static .css file 163 | 164 | StyleSheet comes with a Babel plugin that can extract static rules. This means that your styles are not computed at runtime or in JavaScript and can be served via `link` tag. 165 | 166 | Just add `style-sheet/babel` to `plugins` in your babel configuration: 167 | 168 | ```json 169 | { 170 | "plugins": [ 171 | "style-sheet/babel" 172 | ] 173 | } 174 | ``` 175 | 176 | and compile your JavaScript files with Babel. 177 | 178 | Once Babel is done compiling you can import the `getCss` function from `style-sheet/babel` to get the extracted CSS: 179 | 180 | ```js 181 | import { writeFileSync } from 'fs' 182 | import { getCss } from 'style-sheet/babel' 183 | 184 | const bundleFilePath = './build/bundle.css' 185 | writeFileSync(bundleFilePath, getCss()) 186 | ``` 187 | 188 | In your page then you can reference the `bundleFilePath`: 189 | 190 | ```diff 191 | const html = ` 192 | 193 | 194 | 195 | 196 | my app 197 | + 198 | 199 | 200 |
${renderedHTML}
201 | 202 | 203 | ` 204 | ``` 205 | 206 | Note that StyleSheet **can also reconcile extracted styles!!!** You just need to make sure that the `link` tag has the `__style_sheet__` set, and keep in mind that CORS apply. 207 | 208 | When the Babel plugin can't resolve styles to static, it flags them as dynamic and it leaves them in JavaScript. For this reason it is always a good idea to define dynamic styles in separate rules. 209 | 210 | ### Configuration 211 | 212 | By default the plugin looks for references to `StyleSheet` when they are imported from `style-sheet`. However both can be configured, via plugin options: 213 | 214 | * `importName` - default is `StyleSheet` and the plugin looks for `StyleSheet.create`. 215 | * `packageName` - default is `style-sheet` but when using advanced features (see below) you can point to your custom setup. 216 | * `stylePropName` - default is `css`. In React the plugin looks for inline styles defined via this prop and extracts them. 217 | * `stylePropPackageName` - mandatory. When using the style prop you need to set this path to point to where you setup your custom `createElement` (see below). 218 | * `rtl` - boolean. When set generates I18n styles and extracts them too. 219 | 220 | ```json 221 | { 222 | "plugins": [ 223 | [ 224 | "style-sheet/babel", 225 | { 226 | "importName": "StyleSheet", 227 | "packageName": "./path/to/customInstance", 228 | "stylePropName": "css", 229 | "stylePropPackageName": "./path/to/createElement.js", 230 | "rtl": true 231 | } 232 | ] 233 | ] 234 | } 235 | ``` 236 | 237 | This is useful when StyleSheet is useds in custom ways like described in the advanced usage section. 238 | 239 | ### Extracting styles with webpack 240 | 241 | In your webpack configuration create a small plugin to wrap the `getCss` function: 242 | 243 | ```js 244 | const styleSheet = require('style-sheet/babel') 245 | const { RawSource } = require('webpack-sources') 246 | 247 | const bundleFilenamePath = 'style-sheet-bundle.css' 248 | class StyleSheetPlugin { 249 | apply(compiler) { 250 | compiler.plugin('emit', (compilation, cb) => { 251 | compilation.assets[bundleFilenamePath] = new RawSource(styleSheet.getCss()) 252 | cb() 253 | }) 254 | } 255 | } 256 | ``` 257 | 258 | and register an instance of it in the `plugins` section of the webpack configuration: 259 | 260 | ```js 261 | // class StyleSheetPlugin { 262 | // ... 263 | // } 264 | 265 | module.exports = { 266 | // ... 267 | module: { 268 | rules: { 269 | test: /\.jsx?$/, 270 | use: { 271 | loader: 'babel-loader', 272 | options: { 273 | plugins: [styleSheet.default] 274 | } 275 | } 276 | } 277 | }, 278 | plugins: [ 279 | new StyleSheetPlugin() 280 | ], 281 | // ... 282 | } 283 | ``` 284 | 285 | Remember to also register the Babel plugin if you are using Babel via webpack. 286 | 287 | That's it! webpack will write `bundleFilenamePath` in your public assets folder. 288 | 289 | ## Advanced usage 290 | 291 | StyleSheet ships with CommonJS, ESM and UMD bundles respectively available at: 292 | 293 | * `lib/cjs` 294 | * `lib/esm` 295 | * `lib/umd` 296 | 297 | Throughout the readme we will use `lib/esm` in the examples that require you to point to individual modules manually. 298 | 299 | StyleSheet comes with a factory to generate an instance of `StyleSheet` and `StyleResolver`. The factory available at `style-sheet/lib/umd/factory` and can be used to have fine control over the style sheets creation and support unusual cases like rendering inside of iframes. 300 | 301 | More documentation to come, please refer to the implementation in `src/factory.js` and see how it is used in `src/index.js`. 302 | 303 | ## Using StyleSheet with React 304 | 305 | StyleSheet is framework agnostic but it works well with React. 306 | 307 | ```jsx 308 | import React from 'react' 309 | import { StyleSheet, StyleResolver } from 'style-sheet' 310 | 311 | export default ({ children }) => { 312 | const className = StyleResolver.resolve([styles.root, styles.another]) 313 | return ( 314 |
{children}
315 | ) 316 | } 317 | 318 | const styles = StyleSheet.create({ 319 | root: { 320 | color: 'red', 321 | }, 322 | another: { 323 | color: 'green' 324 | } 325 | }) 326 | ``` 327 | 328 | ### The style (`css`) prop 329 | 330 | StyleSheet provides an helper to create a custom `createElement` function that adds support for a styling prop to React. By default this prop is called `css` (but its name can be configured) and it allows you to define "inline styles" that get compiled to real CSS and removed from the element. These are also vendor prefixed and scoped. 331 | 332 | Note that when applying styles, `className` takes always precedence over the style prop. This allows parent components to pass styles such as overrides to children. 333 | 334 | To use this feature you need to create an empty file in your project, name it `createElement.js` and add the following code: 335 | 336 | ```jsx 337 | import * as StyleSheet from 'style-sheet' 338 | import setup from 'style-sheet/lib/esm/createElement' 339 | 340 | const stylePropName = 'css' 341 | export const createElement = setup(StyleSheet, stylePropName) 342 | ``` 343 | 344 | and then instruct Babel to use this method instead of the default `React.createElement`. This can be done in two ways: 345 | 346 | * Adding the `/* @jsx createElement */` at the top of every file 347 | 348 | ```jsx 349 | /* @jsx createElement */ 350 | 351 | import React from 'react' 352 | import createElement from './path/to/createElement.js' 353 | 354 | export default ({ children }) => ( 355 |
{children}
356 | ) 357 | ``` 358 | 359 | * In your Babel configuration 360 | 361 | ```js 362 | { 363 | "plugins": [ 364 | ["@babel/plugin-transform-react-jsx", { 365 | "pragma": "createElement" // React will use style-sheet's createElement 366 | }], 367 | ["style-sheet/babel", { 368 | "stylePropName": "css", 369 | "stylePropPackageName": "./path/to/createElement.js" 370 | }] 371 | ] 372 | } 373 | ``` 374 | 375 | or if you use `@babel/preset-react` 376 | 377 | ```js 378 | { 379 | "presets": [ 380 | [ 381 | "@babel/preset-react", 382 | { 383 | "pragma": "createElement" // React will use style-sheet's createElement 384 | } 385 | ] 386 | ], 387 | "plugins": [ 388 | ["style-sheet/babel", { 389 | "stylePropName": "css", 390 | "stylePropPackageName": "./path/to/createElement.js" 391 | }] 392 | ] 393 | } 394 | ``` 395 | 396 | When possible, the Babel plugin will hoist and extract to .css file the style prop! 397 | 398 | ## i18n 399 | 400 | StyleSheet comes with built-in support for i18n and RTL. 401 | 402 | In order for i18n to work StyleSheet requires you to define and set an i18n manager that is an object with two properties: 403 | 404 | ```js 405 | import { setI18nManager } from 'style-sheet' 406 | const i18nManager = { 407 | isRTL: true, // Boolean 408 | doLeftAndRightSwapInRTL: true // Boolean 409 | } 410 | 411 | setI18nManager(i18nManager) 412 | ``` 413 | 414 | In your app you can then toggle `isRTL` and (important) you need to re-render the application yourself i.e. `isRTL` is not a reactive property and styles don't resolve automatically when you change direction in the i18n manager. In a React application the i18n manager would likely be kept in state and consumed via context. 415 | 416 | I18n works with server side rendering and static extraction too! 417 | 418 | 419 | ## Contributing 420 | 421 | Please refer to the [contributing guidelines document](https://github.com/giuseppeg/contributing). 422 | 423 | ### Roadmap 424 | 425 | Feel free to contact [me](https://twitter.com/giuseppegurgone) if you want to help with any of the following tasks (sorted in terms on priority/dependency): 426 | 427 | - [ ] Find a better/smaller deterministic name scheme for classes (right now it is `dss_hashedProperty-hashedValue`) 428 | - [ ] Consider adding support for i18n properties like `marginHorizontal` 429 | - [ ] Add support for `StyleSheet.createPrimitive` (or `createRule`) to generate non-atomic rules that can be used for the primitives' base styling (and avoid too many atomic classes on elements) 430 | 431 | ## Credits 432 | 433 | Thanks to: 434 | 435 | * [Matt Hamlin](https://twitter.com/immatthamlin) for transferring ownership of the npm package to us. 436 | * [Callstack.io/linaria](https://github.com/callstack/linaria/issues/242) for providing the evaluation library to extract styles to static. 437 | 438 | ## License 439 | 440 | MIT 441 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | test: { 4 | presets: [['@babel/preset-env', { targets: { node: true } }]], 5 | }, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /babel.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/babel.js') 2 | -------------------------------------------------------------------------------- /lib/cjs/createElement.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | process.env.NODE_ENV === 'production' 3 | ? require('./createElement.prod.js') 4 | : require('./createElement.dev.js') 5 | -------------------------------------------------------------------------------- /lib/cjs/factory.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | process.env.NODE_ENV === 'production' 3 | ? require('./factory.prod.js') 4 | : require('./factory.dev.js') 5 | -------------------------------------------------------------------------------- /lib/cjs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | process.env.NODE_ENV === 'production' 3 | ? require('./index.prod.js') 4 | : require('./index.dev.js') 5 | -------------------------------------------------------------------------------- /lib/esm/createElement.js: -------------------------------------------------------------------------------- 1 | import dev from './createElement.dev.js' 2 | import prod from './createElement.prod.js' 3 | 4 | export default process.env.NODE_ENV === 'production' ? prod : dev 5 | -------------------------------------------------------------------------------- /lib/esm/factory.js: -------------------------------------------------------------------------------- 1 | import * as dev from './factory.dev.js' 2 | import * as prod from './factory.prod.js' 3 | 4 | export const create = 5 | process.env.NODE_ENV === 'production' ? prod.create : dev.create 6 | export const createSheet = 7 | process.env.NODE_ENV === 'production' ? prod.createSheet : dev.createSheet 8 | -------------------------------------------------------------------------------- /lib/esm/index.js: -------------------------------------------------------------------------------- 1 | import * as dev from './index.dev.js' 2 | import * as prod from './index.prod.js' 3 | 4 | export const StyleResolver = 5 | process.env.NODE_ENV === 'production' ? prod.StyleResolver : dev.StyleResolver 6 | export const StyleSheet = 7 | process.env.NODE_ENV === 'production' ? prod.StyleSheet : dev.StyleSheet 8 | export const setI18nManager = 9 | process.env.NODE_ENV === 'production' 10 | ? prod.setI18nManager 11 | : dev.setI18nManager 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "style-sheet", 3 | "version": "4.0.4", 4 | "description": "Fast CSS in JS library with support for static CSS extraction.", 5 | "funding": { 6 | "type": "github", 7 | "url": "https://github.com/sponsors/giuseppeg" 8 | }, 9 | "main": "lib/cjs/index.js", 10 | "module": "lib/esm/index.js", 11 | "unpkg": "lib/umd/index.prod.js", 12 | "files": [ 13 | "lib", 14 | "babel.js" 15 | ], 16 | "scripts": { 17 | "prepublish": "npm run clean && npm run build", 18 | "clean": "rm -rf lib/**/*.dev.js && rm -rf lib/**/*.prod.js && rm -rf test/e2e/lib", 19 | "prebuild": "npm run clean", 20 | "build": "npm run build:dev && npm run build:prod", 21 | "build:dev": "rollup -c --environment NODE_ENV:development", 22 | "build:prod": "rollup -c --environment NODE_ENV:production", 23 | "ava": "ava", 24 | "ava:e2e": "ava test/e2e/test.js", 25 | "test:copy:lib": "npm run clean && rollup -c --environment NODE_ENV:test && mkdir -p test/e2e/lib && cp lib/umd/index.prod.js test/e2e/lib/_styleSheet.js && cp lib/umd/factory.prod.js test/e2e/lib/_styleSheetFactory.js", 26 | "pretest": "npm run lint && npm run test:copy:lib", 27 | "test": "run-p --race test:e2e:server ava", 28 | "test:unit": "ava test/*.js", 29 | "pretest:e2e": "npm run test:copy:lib", 30 | "test:e2e": "run-p --race test:e2e:server ava:e2e", 31 | "test:e2e:server": "serve ./test/e2e", 32 | "test:babel": "ava test/babel/*.js", 33 | "lint": "eslint src test", 34 | "format": "prettier --single-quote --trailing-comma=es5 --no-semi --write all *.js {src,test}/*.js {src,test}/**/*.js" 35 | }, 36 | "keywords": [ 37 | "css-in-js", 38 | "css in js", 39 | "stylesheet", 40 | "css", 41 | "react native styles", 42 | "babel-plugin", 43 | "atomic css" 44 | ], 45 | "author": "Giuseppe Gurgone", 46 | "license": "MIT", 47 | "dependencies": { 48 | "@babel/plugin-proposal-export-namespace-from": "^7.2.0", 49 | "@babel/plugin-syntax-jsx": "^7.2.0", 50 | "@babel/plugin-transform-modules-commonjs": "^7.4.4", 51 | "babel-helper-evaluate-path": "^0.5.0", 52 | "error-stack-parser": "^2.0.2", 53 | "fnv1a": "^1.0.1", 54 | "inline-style-prefixer": "^5.0.1", 55 | "linaria": "^1.3.1" 56 | }, 57 | "devDependencies": { 58 | "@babel/cli": "^7.1.0", 59 | "@babel/core": "^7.1.2", 60 | "@babel/plugin-transform-react-jsx": "^7.3.0", 61 | "@babel/preset-env": "^7.1.0", 62 | "@babel/register": "^7.0.0", 63 | "ava": "^1.0.0", 64 | "eslint": "^6.0.0", 65 | "eslint-plugin-ava": "^8.0.0", 66 | "npm-run-all": "^4.1.5", 67 | "prettier": "^1.14.3", 68 | "puppeteer": "^1.8.0", 69 | "react": "^16.10.1", 70 | "rollup": "^1.11.2", 71 | "rollup-plugin-babel": "^4.0.3", 72 | "rollup-plugin-commonjs": "^10.0.0", 73 | "rollup-plugin-node-resolve": "^5.0.0", 74 | "rollup-plugin-replace": "^2.0.0", 75 | "rollup-plugin-terser": "^5.0.0", 76 | "serve": "^11.0.0" 77 | }, 78 | "ava": { 79 | "require": [ 80 | "./test/_register.js" 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import replace from 'rollup-plugin-replace' 3 | import resolve from 'rollup-plugin-node-resolve' 4 | import commonjs from 'rollup-plugin-commonjs' 5 | import { terser } from 'rollup-plugin-terser' 6 | 7 | const ENV = process.env.NODE_ENV || 'development' 8 | const ENV_ALIASES = { 9 | production: 'prod', 10 | development: 'dev', 11 | test: 'prod', 12 | } 13 | const ENTRY_FILES = ['factory', 'index', 'createElement'] 14 | 15 | const plugins = format => { 16 | return [ 17 | replace({ 18 | 'process.env.NODE_ENV': JSON.stringify(ENV), 19 | }), 20 | babel({ 21 | exclude: 'node_modules/**', 22 | babelrc: false, 23 | presets: [ 24 | ['@babel/preset-env', { targets: { esmodules: format === 'esm' } }], 25 | ], 26 | }), 27 | resolve({ 28 | browser: true, 29 | }), 30 | commonjs(), 31 | ENV !== 'development' ? terser() : null, 32 | ].filter(Boolean) 33 | } 34 | 35 | const confCreators = ENTRY_FILES.map(entryName => [ 36 | entryName, 37 | format => ({ 38 | input: `./src/${entryName}.js`, 39 | output: { 40 | format, 41 | file: `lib/${format}/${entryName}.${ENV_ALIASES[ENV]}.js`, 42 | compact: ENV === 'production', 43 | }, 44 | plugins: plugins(format), 45 | }), 46 | ]) 47 | 48 | const EXTERNAL = { 49 | createElement: ['react'], 50 | index: ['inline-style-prefixer', 'fnv1a'], 51 | factory: ['inline-style-prefixer', 'fnv1a'], 52 | } 53 | const CJS_CONFIG = confCreators.map(([entryName, creator]) => ({ 54 | ...creator('cjs'), 55 | external: EXTERNAL[entryName], 56 | })) 57 | 58 | const ESM_CONFIG = confCreators.map(([entryName, creator]) => ({ 59 | ...creator('esm'), 60 | external: EXTERNAL[entryName], 61 | })) 62 | 63 | const GLOBALS = { 64 | createElement: { 65 | react: 'React', 66 | }, 67 | } 68 | const UMD_CONFIG = confCreators.map(([entryName, creator]) => { 69 | const config = creator('umd') 70 | config.output.name = `styleSheet${ 71 | entryName === 'index' 72 | ? '' 73 | : entryName.charAt(0).toUpperCase() + entryName.slice(1) 74 | }` 75 | config.output.globals = GLOBALS[entryName] 76 | return config 77 | }) 78 | 79 | const BABEL_PLUGIN_CONFIG = { 80 | input: './src/babel.js', 81 | output: { 82 | file: './lib/babel.js', 83 | format: 'cjs', 84 | exports: 'named', 85 | }, 86 | plugins: [ 87 | resolve({ 88 | browser: false, 89 | }), 90 | commonjs(), 91 | ], 92 | external: ['linaria/lib/babel/evaluate', 'babel-helper-evaluate-path'], 93 | } 94 | 95 | export default [ 96 | ...CJS_CONFIG, 97 | ...ESM_CONFIG, 98 | ...UMD_CONFIG, 99 | BABEL_PLUGIN_CONFIG, 100 | ] 101 | -------------------------------------------------------------------------------- /src/babel.js: -------------------------------------------------------------------------------- 1 | import evaluateSimple from 'babel-helper-evaluate-path' 2 | import evaluateComplex from 'linaria/lib/babel/evaluate' 3 | import jsx from '@babel/plugin-syntax-jsx' 4 | import { create } from './factory' 5 | const { StyleSheet, StyleResolver, setI18nManager } = create() 6 | 7 | // This function returns the extracted CSS to save in a .css file. 8 | // It must be called after all the files are processed by Babel. 9 | export function getCss() { 10 | return StyleResolver.getStyleSheet().flush() 11 | } 12 | 13 | export default function(babel) { 14 | let setI18n = false 15 | return { 16 | name: 'style-sheet/babel', 17 | inherits: jsx, 18 | visitor: { 19 | Program: { 20 | enter(path, state) { 21 | if (!setI18n && typeof state.opts.rtl === 'boolean') { 22 | setI18n = true 23 | setI18nManager({ 24 | isRTL: state.opts.rtl, 25 | doLeftAndRightSwapInRTL: state.opts.rtl, 26 | }) 27 | } 28 | }, 29 | exit(path, state) { 30 | const { types: t } = babel 31 | if (!state.hasStyleSheetImport && state.needsStyleSheetImport) { 32 | const importSpecifier = t.identifier( 33 | state.opts.importName || 'StyleSheet' 34 | ) 35 | const importDeclaration = t.importDeclaration( 36 | [t.importSpecifier(importSpecifier, importSpecifier)], 37 | t.stringLiteral(state.opts.packageName || 'style-sheet') 38 | ) 39 | path.node.body.unshift(importDeclaration) 40 | } 41 | if (!state.hasStylePropImport && state.needsStylePropImport) { 42 | const importSpecifier = t.identifier('createElement') 43 | const importDeclaration = t.importDeclaration( 44 | [t.importSpecifier(importSpecifier, importSpecifier)], 45 | t.stringLiteral(state.opts.stylePropPackageName) 46 | ) 47 | path.node.body.unshift(importDeclaration) 48 | } 49 | }, 50 | }, 51 | JSXAttribute(path, state) { 52 | if (!state.opts.stylePropName) { 53 | state.opts.stylePropName = 'css' 54 | } 55 | if (path.node.name.name !== state.opts.stylePropName) { 56 | return 57 | } 58 | 59 | const value = path.get('value') 60 | if (!value.isJSXExpressionContainer()) { 61 | return 62 | } 63 | 64 | let expression = value.get('expression') 65 | 66 | const { types: t } = babel 67 | const cloneNode = t.cloneNode || t.cloneDeep 68 | const importName = state.opts.importName || 'StyleSheet' 69 | 70 | let isExpressionArray = false 71 | let expressions 72 | if (expression.isArrayExpression()) { 73 | isExpressionArray = true 74 | expressions = expression.get('elements') 75 | } else { 76 | expressions = [expression] 77 | } 78 | 79 | const hoisted = expressions 80 | .map(expression => { 81 | if (!expression.isPure()) { 82 | return 83 | } 84 | 85 | const replacement = t.callExpression( 86 | t.memberExpression( 87 | t.identifier(importName), 88 | t.identifier('create') 89 | ), 90 | [ 91 | t.objectExpression([ 92 | t.objectProperty( 93 | t.identifier('__styleProp'), 94 | cloneNode(expression.node) 95 | ), 96 | ]), 97 | ] 98 | ) 99 | expression.replaceWith(replacement) 100 | processReferencePath(babel, expression, state) 101 | return expression.hoist() 102 | }) 103 | .filter(Boolean) 104 | 105 | if (isExpressionArray && hoisted.length === expressions.length) { 106 | expression.hoist() 107 | } 108 | 109 | state.needsStylePropImport = true 110 | 111 | if (!state.opts.stylePropPackageName) { 112 | throw path.buildCodeFrameError( 113 | ` 114 | Found \`${state.opts.stylePropName}\` prop but you didn't specify the path to the custom createElement in the Babel configuration. 115 | Please set the \`stylePropPackageName\` option. 116 | 117 | { 118 | "plugins": [ 119 | [ 120 | "style-sheet/babel", 121 | { 122 | "stylePropName": "${state.opts.stylePropName}", 123 | "stylePropPackageName": "./path/to/createElement.js" 124 | } 125 | ] 126 | ] 127 | } 128 | 129 | Read more about how to create the style prop package at https://github.com/giuseppeg/style-sheet 130 | ` 131 | ) 132 | } 133 | }, 134 | ImportDeclaration(path, state) { 135 | const stylePropPackageName = state.opts.stylePropPackageName 136 | state.needsStylePropImport = Boolean(stylePropPackageName) 137 | state.hasStylePropImport = 138 | path.node.source.value === stylePropPackageName 139 | 140 | const packageName = state.opts.packageName || 'style-sheet' 141 | if (path.node.source.value !== packageName) { 142 | return 143 | } 144 | const importName = state.opts.importName || 'StyleSheet' 145 | const specifier = path.get('specifiers').find(specifier => { 146 | return ( 147 | specifier.isImportSpecifier() && 148 | specifier.get('imported').node.name === importName 149 | ) 150 | }) 151 | if (!specifier) { 152 | return 153 | } 154 | 155 | state.hasStyleSheetImport = true 156 | 157 | // Find all the references to StyleSheet.create. 158 | const binding = path.scope.getBinding(specifier.node.local.name) 159 | 160 | if (!binding || !Array.isArray(binding.referencePaths)) { 161 | return 162 | } 163 | 164 | binding.referencePaths 165 | .map(referencePath => referencePath.parentPath.parentPath) 166 | .forEach(path => { 167 | if (path.isCallExpression()) { 168 | processReferencePath(babel, path, state) 169 | } 170 | }) 171 | }, 172 | }, 173 | } 174 | } 175 | 176 | function processReferencePath(babel, path, state) { 177 | const t = babel.types 178 | const cloneNode = t.cloneNode || t.cloneDeep 179 | // From 180 | // 181 | // StyleSheet.create({ 182 | // root: { 183 | // color: 'red' 184 | // } 185 | // }) 186 | // 187 | // grabs 188 | // 189 | // { 190 | // root: { 191 | // color: 'red' 192 | // } 193 | // } 194 | const rulesPath = path.get('arguments')[0] 195 | const extractableProperties = [] 196 | 197 | // For each property 198 | // 199 | // root: { 200 | // color: 'red' 201 | // } 202 | const properties = rulesPath.get('properties') 203 | properties.forEach(property => { 204 | // Ignore complex stuff like spread elements for now. 205 | if (!property.isObjectProperty()) { 206 | return 207 | } 208 | // Try to resolve to static... 209 | // evaluate() will also compile static styles, which are the ones 210 | // that we will extract to file. 211 | const evaluated = evaluate(babel, property.get('value'), state) 212 | 213 | if (evaluated.value === null) { 214 | return 215 | } 216 | extractableProperties.push( 217 | t.objectProperty( 218 | cloneNode(property.get('key').node), 219 | t.arrayExpression(evaluated.value.map(value => t.stringLiteral(value))) 220 | ) 221 | ) 222 | property.remove() 223 | }) 224 | 225 | // If we couldn't resolve anything we exit. 226 | if (extractableProperties.length === 0) { 227 | state.needsStyleSheetImport = true 228 | return 229 | } 230 | 231 | const extractedStylesObjectLiteral = t.objectExpression(extractableProperties) 232 | 233 | // When some rules could not be extracted (maybe there are dynamic styles) 234 | // we will spread StyleSheet.create({...}) to the replacement object 235 | // 236 | // ({ 237 | // static: [/* ... */], 238 | // ...StyleSheet.create({ 239 | // someDynamicRule: { 240 | // color: props.color, 241 | // } 242 | // }) 243 | // }) 244 | if (properties.length !== extractableProperties.length) { 245 | state.needsStyleSheetImport = true 246 | extractedStylesObjectLiteral.properties.push( 247 | t.spreadElement(cloneNode(path.node)) 248 | ) 249 | } 250 | path.replaceWith(extractedStylesObjectLiteral) 251 | } 252 | 253 | function compileRule(rule) { 254 | const compiled = StyleSheet.create({ static: rule }).static 255 | StyleResolver.resolve(compiled) 256 | return compiled 257 | } 258 | 259 | function evaluate(babel, path, state) { 260 | let result = evaluateSimple(path) 261 | if (result.confident) { 262 | return { 263 | value: compileRule(result.value), 264 | dependencies: [], 265 | } 266 | } 267 | 268 | try { 269 | result = evaluateComplex( 270 | path, 271 | babel.types, 272 | state.file.opts.filename, 273 | text => { 274 | return babel.transformSync(text, { 275 | babelrc: false, 276 | filename: state.file.opts.filename, 277 | plugins: [ 278 | // Include this plugin to avoid extra config when using { module: false } for webpack 279 | '@babel/plugin-transform-modules-commonjs', 280 | '@babel/plugin-proposal-export-namespace-from', 281 | // We don't support dynamic imports when evaluating, but don't wanna syntax error 282 | // This will replace dynamic imports with an object that does nothing 283 | // eslint-disable-next-line no-undef 284 | require.resolve('linaria/lib/babel/dynamic-import-noop'), 285 | ], 286 | exclude: /node_modules/, 287 | }) 288 | } 289 | ) 290 | 291 | if (result.value !== null) { 292 | result.value = compileRule(result.value) 293 | } 294 | } catch (error) { 295 | result = { value: null, dependencies: [] } 296 | } 297 | 298 | return result 299 | } 300 | -------------------------------------------------------------------------------- /src/compile.js: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/jxnblk/object-style 2 | // which is MIT (c) jxnblk 3 | 4 | import hashFn from 'fnv1a' 5 | import { prefix } from 'inline-style-prefixer' 6 | import { unitless, i18n, shortHandProperties } from './data' 7 | import { STYLE_GROUPS } from './createOrderedCSSStyleSheet' 8 | 9 | export function createClassName(property, value, descendants, media) { 10 | const ruleType = getRuleType(property, media, descendants) 11 | return `dss${ruleType}_${hashFn(property + descendants + media).toString( 12 | 36 13 | )}-${hashFn(String(value)).toString(36)}` 14 | } 15 | 16 | const hyphenate = s => s.replace(/[A-Z]|^ms/g, '-$&').toLowerCase() 17 | const strigifyDeclaration = dec => { 18 | let stringified = '' 19 | 20 | for (const prop in dec) { 21 | const value = dec[prop] 22 | if (Array.isArray(value)) { 23 | for (let i = 0; i < value.length; i++) { 24 | stringified += hyphenate(prop) + ':' + value[i] + ';' 25 | } 26 | } else { 27 | stringified += hyphenate(prop) + ':' + value + ';' 28 | } 29 | } 30 | return stringified 31 | } 32 | export function createRule(className, declaration, descendants, media) { 33 | const cls = '.' + className.replace('.', '\\.') 34 | const selector = descendants 35 | ? descendants.replace(/^&/, cls).replace(/&/g, cls) 36 | : cls 37 | const rule = selector + '{' + strigifyDeclaration(declaration) + '}' 38 | if (!media) return rule 39 | return media + '{' + rule + '}' 40 | } 41 | 42 | const order = { 43 | pseudo: [ 44 | 'link', 45 | 'visited', 46 | 'hover', 47 | 'focus-within', 48 | 'focus-visible', 49 | 'focus', 50 | 'active', 51 | ], 52 | } 53 | 54 | function getRuleType(prop, media, descendants) { 55 | let name = '' 56 | if (shortHandProperties.indexOf(prop) > -1) { 57 | name = media ? 'mediaShorthand' : 'shorthand' 58 | } else { 59 | name = media ? 'mediaAtomic' : 'atomic' 60 | } 61 | let subGroup = 0 62 | if (descendants) { 63 | let subGroupPart 64 | // is a combinator selector eg :hover > & 65 | if (descendants.substr(0, 2) !== '&:') { 66 | name += 'Combinator' 67 | subGroupPart = descendants.slice(1).split(/\s*[+>~]\s*/g)[0] 68 | } else { 69 | subGroupPart = descendants.slice(2) 70 | } 71 | const index = order.pseudo.indexOf(subGroupPart.split(':').slice(-1)[0]) 72 | if (index > -1) { 73 | subGroup = index + 1 74 | } 75 | } 76 | 77 | return subGroup > 0 ? STYLE_GROUPS[name] + '.' + subGroup : STYLE_GROUPS[name] 78 | } 79 | 80 | function normalizeValue(value) { 81 | if (typeof value === 'number') { 82 | if (value !== 0) { 83 | return value + 'px' 84 | } 85 | } else if (Array.isArray(value)) { 86 | return value.map(v => { 87 | if (typeof v === 'number' && v !== 0) { 88 | return v + 'px' 89 | } 90 | return v 91 | }) 92 | } 93 | 94 | return value 95 | } 96 | 97 | function toI18n(lookup, thing) { 98 | return Object.prototype.hasOwnProperty.call(lookup, thing) 99 | ? lookup[thing] 100 | : null 101 | } 102 | 103 | const cache = {} 104 | const parse = (obj, descendants, media, opts) => { 105 | const rules = {} 106 | 107 | for (let key in obj) { 108 | let value = obj[key] 109 | if (value === null || value === undefined) continue 110 | switch (Object.prototype.toString.call(value)) { 111 | case '[object Object]': { 112 | const parsed = 113 | key.charAt(0) === '@' 114 | ? parse(value, descendants, key, opts) 115 | : parse(value, descendants + key, media, opts) 116 | Object.assign(rules, parsed) 117 | break 118 | } 119 | default: { 120 | const cacheKey = key + value + descendants + media 121 | const cached = cache[cacheKey] 122 | if (cached) { 123 | Object.assign(rules, cached) 124 | break 125 | } 126 | let className = createClassName(key, value, descendants, media) 127 | if (rules[className]) { 128 | break 129 | } 130 | if (!unitless[key]) { 131 | value = normalizeValue(value) 132 | } 133 | const declaration = prefix({ [key]: value }) 134 | let rule = createRule(className, declaration, descendants, media) 135 | 136 | if (opts.i18n) { 137 | const originalProp = key 138 | const originalValue = value 139 | key = toI18n(i18n.properties, originalProp) 140 | value = toI18n(i18n.values, originalValue) 141 | if (key !== null || value !== null) { 142 | key = key || originalProp 143 | value = value || originalValue 144 | const i18nClassName = createClassName( 145 | key, 146 | value, 147 | descendants, 148 | media 149 | ) 150 | // i18n classNames contain both the ltr and rtl version 151 | // this is resolved at runtime by the StyleResolver 152 | className = `${className}|${i18nClassName}` 153 | 154 | const declaration = prefix({ [key]: value }) 155 | // i18n rule is an array with two rules the ltr and the rtl one 156 | // eg. ['.left { margin-left: 10px }', '.right { margin-right: 10px }'] 157 | // At runtime the StyleResolver will pick the correct one. 158 | rule = [ 159 | rule, 160 | createRule(i18nClassName, declaration, descendants, media), 161 | ] 162 | } 163 | } 164 | rules[className] = rule 165 | cache[cacheKey] = { [className]: rule } 166 | break 167 | } 168 | } 169 | } 170 | 171 | return rules 172 | } 173 | 174 | export default (obj, opts) => { 175 | if (!obj) { 176 | throw new Error('DSS parser invoked without a mandatory styles object.') 177 | } 178 | return parse(obj, '', '', opts) 179 | } 180 | -------------------------------------------------------------------------------- /src/createElement.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function createCreateElement( 4 | { StyleSheet, StyleResolver }, 5 | stylePropName = 'css' 6 | ) { 7 | return function(tag, props, ...children) { 8 | if (props && props[stylePropName]) { 9 | const styles = props[stylePropName] 10 | delete props[stylePropName] 11 | const className = props.className 12 | delete props.className 13 | 14 | let rules = [] 15 | if (Array.isArray(styles)) { 16 | rules = styles.reduce((rules, rule) => { 17 | if (!rule) { 18 | return rules 19 | } 20 | if (rule.__styleProp) { 21 | rules.push(rule.__styleProp) 22 | } else if (Array.isArray(rule)) { 23 | rules.push(...rule) 24 | } else { 25 | rules.push(StyleSheet.create({ rule }).rule) 26 | } 27 | return rules 28 | }, []) 29 | } else if (styles.__styleProp) { 30 | rules.push(styles.__styleProp) 31 | } else { 32 | rules.push(StyleSheet.create({ rule: styles }).rule) 33 | } 34 | if (className) { 35 | // className takes precedence over the style prop 36 | // this allows parent components to style the current one. 37 | rules.push( 38 | /dss[\d.]+_/.test(className) ? className.split(' ') : [className] 39 | ) 40 | } 41 | props.className = StyleResolver.resolve(rules) 42 | } 43 | return React.createElement(tag, props, ...children) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/createOrderedCSSStyleSheet.js: -------------------------------------------------------------------------------- 1 | import sortMq from './sort-mq' 2 | /** 3 | * This module is a fork of and modifies: https://git.io/fjceH 4 | * 5 | * The original source is (c) Nicolas Gallagher 6 | * and licensed under the MIT license found a thttps://git.io/fjceS 7 | */ 8 | 9 | /** 10 | * Order-based insertion of CSS. 11 | * 12 | * Each rule is associated with a numerically defined group. 13 | * Groups are ordered within the style sheet according to their number, with the 14 | * lowest first. 15 | * 16 | * Groups are implemented using marker rules. The selector of the first rule of 17 | * each group is used only to encode the group number for hydration. An 18 | * alternative implementation could rely on CSSMediaRule, allowing groups to be 19 | * treated as a sub-sheet, but the Edge implementation of CSSMediaRule is 20 | * broken. 21 | * https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule 22 | * https://gist.github.com/necolas/aa0c37846ad6bd3b05b727b959e82674 23 | */ 24 | export default function createOrderedCSSStyleSheet(sheet) { 25 | let groups = {} 26 | let selectors = {} 27 | 28 | /** 29 | * Hydrate approximate record from any existing rules in the sheet. 30 | */ 31 | if (sheet != null) { 32 | let group 33 | Array.prototype.forEach.call(sheet.cssRules, function(cssRule, i) { 34 | const cssText = cssRule.cssText 35 | // Create record of existing selectors and rules 36 | if (cssText.indexOf('style-sheet-group') > -1) { 37 | group = decodeGroupRule(cssRule) 38 | groups[group] = { start: i, rules: [cssText], mq: [] } 39 | } else { 40 | const selectorText = getSelectorText(cssText) 41 | if (selectorText != null) { 42 | selectors[selectorText.selector] = true 43 | let index = groups[group].rules.length - 1 44 | if (selectorText.media) { 45 | index = groups[group].mq.indexOf(selectorText.media) 46 | if (index === -1) { 47 | groups[group].mq.push(selectorText.media) 48 | groups[group].mq.sort(sortMq) 49 | index = groups[group].mq.indexOf(selectorText.media) 50 | } 51 | } 52 | groups[group].rules.splice(index + 1, 0, cssText) 53 | } 54 | } 55 | }) 56 | } 57 | 58 | function sheetInsert(sheet, group, text, index) { 59 | const orderedGroups = getOrderedGroups(groups) 60 | const groupIndex = orderedGroups.indexOf(group) 61 | const nextGroupIndex = groupIndex + 1 62 | const nextGroup = orderedGroups[nextGroupIndex] 63 | // Insert rule before the next group, or at the end of the stylesheet 64 | const position = 65 | nextGroup != null && groups[nextGroup].start != null 66 | ? groups[nextGroup].start - typeof index === 'number' 67 | ? groups[group].rules.length - index 68 | : 0 69 | : sheet.cssRules.length 70 | const isInserted = insertRuleAt(sheet, text, position) 71 | 72 | if (isInserted) { 73 | // Set the starting index of the new group 74 | if (groups[group].start == null) { 75 | groups[group].start = position 76 | } 77 | // Increment the starting index of all subsequent groups 78 | for (let i = nextGroupIndex; i < orderedGroups.length; i += 1) { 79 | const groupNumber = orderedGroups[i] 80 | const previousStart = groups[groupNumber].start 81 | groups[groupNumber].start = previousStart + 1 82 | } 83 | } 84 | 85 | return isInserted 86 | } 87 | 88 | function getTextContent() { 89 | return getOrderedGroups(groups).reduce(function(text, group, index) { 90 | const rules = groups[group].rules 91 | return text + (index > 0 ? '\n' : '') + rules.join('\n') 92 | }, '') 93 | } 94 | 95 | const OrderedCSSStyleSheet = { 96 | /** 97 | * The textContent of the style sheet. 98 | */ 99 | getTextContent, 100 | 101 | /** 102 | * Returns the textContent of the style sheet and removes all the rules from it. 103 | */ 104 | flush() { 105 | const textContent = getTextContent() 106 | groups = {} 107 | selectors = {} 108 | if (sheet != null) { 109 | Array.prototype.forEach.call(sheet.cssRules, function(_, i) { 110 | sheet.deleteRule(i) 111 | }) 112 | } 113 | return textContent 114 | }, 115 | 116 | /** 117 | * Insert a rule into the style sheet 118 | */ 119 | insertRule(cssText, groupValue, index) { 120 | const group = Number(groupValue) 121 | 122 | if (isNaN(group)) { 123 | throw new Error( 124 | `${groupValue} - Invalid group. Use OrderedCSSStyleSheet.insertRule(cssText, groupId)` 125 | ) 126 | } 127 | 128 | // Create a new group. 129 | if (groups[group] == null) { 130 | const markerRule = encodeGroupRule(group) 131 | // Create the internal record. 132 | groups[group] = { start: null, rules: [markerRule], mq: [] } 133 | // Update CSSOM. 134 | if (sheet != null) { 135 | sheetInsert(sheet, group, markerRule) 136 | } 137 | } 138 | 139 | // selectorText is more reliable than cssText for insertion checks. The 140 | // browser excludes vendor-prefixed properties and rewrites certain values 141 | // making cssText more likely to be different from what was inserted. 142 | const selectorText = getSelectorText(cssText) 143 | if (selectorText != null && selectors[selectorText.selector] == null) { 144 | selectors[selectorText.selector] = true 145 | if (typeof index !== 'number') { 146 | index = groups[group].rules.length - 1 147 | if (selectorText.media) { 148 | index = groups[group].mq.indexOf(selectorText.media) 149 | if (index === -1) { 150 | groups[group].mq.push(selectorText.media) 151 | groups[group].mq.sort(sortMq) 152 | index = groups[group].mq.indexOf(selectorText.media) 153 | } 154 | } 155 | } 156 | if (index > groups[group].rules.length - 1) { 157 | throw new Error(`index ${index} out of bound for group ${group}`) 158 | } 159 | groups[group].rules.splice(index + 1, 0, cssText) 160 | 161 | // Update CSSOM. 162 | if (sheet != null) { 163 | const isInserted = sheetInsert(sheet, group, cssText, index) 164 | if (!isInserted) { 165 | // Revert internal record change if a rule was rejected (e.g., 166 | // unrecognized pseudo-selector) 167 | groups[group].rules.splice(index + 1, 1) 168 | } 169 | } 170 | } 171 | }, 172 | } 173 | 174 | return OrderedCSSStyleSheet 175 | } 176 | 177 | /** 178 | * Helper functions 179 | */ 180 | 181 | function encodeGroupRule(group) { 182 | return `[style-sheet-group="${group}"]{}` 183 | } 184 | 185 | function decodeGroupRule(cssRule) { 186 | return Number(cssRule.selectorText.split(/["']/)[1]) 187 | } 188 | 189 | function getOrderedGroups(obj) { 190 | return Object.keys(obj) 191 | .map(Number) 192 | .sort((a, b) => (a > b ? 1 : -1)) 193 | } 194 | 195 | const pattern = /\s*([,])\s*/g 196 | function getSelectorText(cssText) { 197 | const split = cssText.split('{') 198 | let selector = split[0].trim() 199 | let media = null 200 | if (selector.startsWith('@media')) { 201 | media = selector.substring(6).trim() 202 | selector = split[1].trim() 203 | } 204 | return selector !== '' 205 | ? { media, selector: selector.replace(pattern, '$1') } 206 | : null 207 | } 208 | 209 | function insertRuleAt(root, cssText, position) { 210 | try { 211 | root.insertRule(cssText, position) 212 | return true 213 | } catch (e) { 214 | // JSDOM doesn't support `CSSSMediaRule#insertRule`. 215 | // Also ignore errors that occur from attempting to insert vendor-prefixed selectors. 216 | return false 217 | } 218 | } 219 | 220 | export const STYLE_GROUPS = [ 221 | 'classic', 222 | 'mediaClassic', 223 | 224 | 'shorthand', 225 | 'mediaShorthand', 226 | 227 | 'shorthandCombinator', 228 | 'mediaShorthandCombinator', 229 | 230 | 'i18nShorthand', 231 | 'mediaI18nShorthand', 232 | 233 | 'i18nShorthandCombinator', 234 | 'mediaI18nShorthandCombinator', 235 | 236 | 'atomic', 237 | 'mediaAtomic', 238 | 239 | 'atomicCombinator', 240 | 'mediaAtomicCombinator', 241 | ].reduce((groups, name, index) => { 242 | groups[name] = index 243 | return groups 244 | }, {}) 245 | -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | export const unitless = { 2 | animationIterationCount: true, 3 | borderImageOutset: true, 4 | borderImageSlice: true, 5 | borderImageWidth: true, 6 | boxFlex: true, 7 | boxFlexGroup: true, 8 | boxOrdinalGroup: true, 9 | columnCount: true, 10 | columns: true, 11 | flex: true, 12 | flexGrow: true, 13 | flexPositive: true, 14 | flexShrink: true, 15 | flexNegative: true, 16 | flexOrder: true, 17 | gridRow: true, 18 | gridRowEnd: true, 19 | gridRowSpan: true, 20 | gridRowStart: true, 21 | gridColumn: true, 22 | gridColumnEnd: true, 23 | gridColumnSpan: true, 24 | gridColumnStart: true, 25 | fontWeight: true, 26 | lineClamp: true, 27 | lineHeight: true, 28 | opacity: true, 29 | order: true, 30 | orphans: true, 31 | tabSize: true, 32 | widows: true, 33 | zIndex: true, 34 | zoom: true, 35 | 36 | // SVG-related properties 37 | fillOpacity: true, 38 | floodOpacity: true, 39 | stopOpacity: true, 40 | strokeDasharray: true, 41 | strokeDashoffset: true, 42 | strokeMiterlimit: true, 43 | strokeOpacity: true, 44 | strokeWidth: true, 45 | } 46 | 47 | export const shortHandProperties = [ 48 | 'animation', 49 | 'background', 50 | 'border', 51 | 'border-bottom', 52 | 'border-left', 53 | 'border-radius', 54 | 'border-right', 55 | 'border-top', 56 | 'column-rule', 57 | 'columns', 58 | 'flex', 59 | 'flex-flow', 60 | 'font', 61 | 'grid', 62 | 'grid-area', 63 | 'grid-column', 64 | 'grid-row', 65 | 'grid-template', 66 | 'list-style', 67 | 'margin', 68 | 'offset', 69 | 'outline', 70 | 'overflow', 71 | 'padding', 72 | 'place-content', 73 | 'place-items', 74 | 'place-self', 75 | 'text-decoration', 76 | 'transition', 77 | ] 78 | 79 | export const i18n = { 80 | properties: { 81 | borderTopLeftRadius: 'borderTopRightRadius', 82 | borderTopRightRadius: 'borderTopLeftRadius', 83 | borderBottomLeftRadius: 'borderBottomRightRadius', 84 | borderBottomRightRadius: 'borderBottomLeftRadius', 85 | borderLeftColor: 'borderRightColor', 86 | borderLeftStyle: 'borderRightStyle', 87 | borderLeftWidth: 'borderRightWidth', 88 | borderRightColor: 'borderLeftColor', 89 | borderRightStyle: 'borderLeftStyle', 90 | borderRightWidth: 'borderLeftWidth', 91 | left: 'right', 92 | marginLeft: 'marginRight', 93 | marginRight: 'marginLeft', 94 | paddingLeft: 'paddingRight', 95 | paddingRight: 'paddingLeft', 96 | right: 'left', 97 | }, 98 | values: { 99 | ltr: 'rtl', 100 | rtl: 'ltr', 101 | left: 'right', 102 | right: 'left', 103 | wResize: 'eResize', 104 | eResize: 'wResize', 105 | swResize: 'seResize', 106 | seResize: 'swResize', 107 | nwResize: 'neResize', 108 | neResize: 'nwResize', 109 | }, 110 | } 111 | -------------------------------------------------------------------------------- /src/factory.js: -------------------------------------------------------------------------------- 1 | import compile from './compile' 2 | import validate from './validate' 3 | import createOrderedCSSStyleSheet from './createOrderedCSSStyleSheet' 4 | import { createSourceMapsEngine } from './source-maps' 5 | 6 | const isBrowser = typeof window !== 'undefined' 7 | const isProd = process.env.NODE_ENV === 'production' 8 | const isTest = process.env.NODE_ENV === 'test' 9 | 10 | function createStyleSheet(rules, opts) { 11 | const cache = typeof Map === 'undefined' ? null : new Map() 12 | let sourceMapsEngine 13 | if (!isProd && !isTest && !isBrowser && typeof Worker !== 'undefined') { 14 | sourceMapsEngine = createSourceMapsEngine() 15 | } 16 | 17 | return { 18 | create: styles => { 19 | if (cache) { 20 | const cached = cache.get(styles) 21 | if (cached) { 22 | return cached 23 | } 24 | } 25 | const locals = {} 26 | 27 | for (const token in styles) { 28 | const rule = styles[token] 29 | if (!isProd) { 30 | validate(rule, null) 31 | } 32 | const compiled = compile(rule, opts) 33 | Object.assign(rules, compiled) 34 | 35 | locals[token] = Object.keys(compiled) 36 | 37 | // In dev add source maps 38 | if (!isProd && sourceMapsEngine) { 39 | locals[token].unshift( 40 | sourceMapsEngine.create((prefix, id) => 41 | opts.sourceMaps.className({ prefix, key: token, id }) 42 | ) 43 | ) 44 | } 45 | } 46 | 47 | if (cache) { 48 | cache.set(styles, locals) 49 | } 50 | 51 | return locals 52 | }, 53 | } 54 | } 55 | 56 | function concatClassName(dest, className) { 57 | if (className.substr(0, 3) !== 'dss') { 58 | return { 59 | shouldInject: false, 60 | className: `${className} ${dest}`, 61 | group: null, 62 | } 63 | } 64 | const property = className.substr(0, className.indexOf('-')) 65 | if (dest.indexOf(property) > -1) { 66 | return { shouldInject: false, className: dest, group: null } 67 | } 68 | return { 69 | shouldInject: true, 70 | className: `${dest} ${className}`, 71 | group: Number( 72 | className.substring(3, className.indexOf('_')).replace('\\', '') 73 | ), 74 | } 75 | } 76 | 77 | function createStyleResolver(sheet, rules, opts) { 78 | let resolved = {} 79 | let injected = {} 80 | 81 | return { 82 | getStyleSheet() { 83 | // On the server we reset the caches. 84 | if (typeof window === 'undefined') { 85 | resolved = {} 86 | injected = {} 87 | } 88 | return sheet 89 | }, 90 | resolve(style) { 91 | const i18n = opts.i18n || {} 92 | const stylesToString = 93 | i18n.isRTL + i18n.doLeftAndRightSwapInRTL + style.join() 94 | 95 | if (resolved[stylesToString]) { 96 | return resolved[stylesToString] 97 | } 98 | 99 | let resolvedClassName = '' 100 | 101 | for (let i = style.length - 1; i >= 0; i--) { 102 | let current = style[i] 103 | if (!current) { 104 | continue 105 | } 106 | if (typeof current === 'string') { 107 | current = [current] 108 | } 109 | for (let j = 0; j < current.length; j++) { 110 | let className = current[j] 111 | let rule 112 | 113 | // resolve i18n rules 114 | const i18nClassNames = className.split('|') 115 | let i18nRules 116 | let i18nIndex 117 | if (i18nClassNames.length > 1) { 118 | if (i18n.isRTL && i18n.doLeftAndRightSwapInRTL) { 119 | i18nIndex = 1 120 | } else { 121 | i18nIndex = 0 122 | } 123 | i18nRules = rules[className] 124 | className = i18nClassNames[i18nIndex] 125 | rule = i18nRules[i18nIndex] 126 | } else { 127 | rule = rules[className] 128 | } 129 | 130 | const result = concatClassName(resolvedClassName, className) 131 | resolvedClassName = result.className 132 | 133 | if (result.shouldInject && !injected[className]) { 134 | if (rule) { 135 | sheet.insertRule(rule, result.group) 136 | if (i18nRules && !isBrowser) { 137 | const i18nIndexInverse = i18nIndex ? 0 : 1 138 | sheet.insertRule(i18nRules[i18nIndexInverse], result.group) 139 | injected[i18nClassNames[i18nIndexInverse]] = true 140 | } 141 | } 142 | injected[className] = true 143 | } 144 | } 145 | } 146 | 147 | resolvedClassName = resolvedClassName.trim() 148 | resolved[stylesToString] = resolvedClassName 149 | return resolvedClassName 150 | }, 151 | } 152 | } 153 | 154 | export function createSheet(document) { 155 | document = document || typeof window === 'undefined' ? null : window.document 156 | let sheet = null 157 | 158 | if (document) { 159 | const style = document.createElement('style') 160 | document.head.appendChild(style) 161 | sheet = style.sheet 162 | } 163 | 164 | return sheet 165 | } 166 | 167 | export function create(options = {}) { 168 | let i18n 169 | function setI18nManager(manager) { 170 | i18n = manager 171 | if (i18n && !isProd) { 172 | if (typeof i18n.isRTL !== 'boolean') { 173 | throw new Error('i18n.isRTL must be a boolean.') 174 | } 175 | if (typeof i18n.doLeftAndRightSwapInRTL !== 'boolean') { 176 | throw new Error('i18n.doLeftAndRightSwapInRTL must be a boolean.') 177 | } 178 | } 179 | } 180 | setI18nManager(options.i18n) 181 | 182 | const sheet = createOrderedCSSStyleSheet(options.sheet || createSheet()) 183 | const rules = {} 184 | 185 | const opts = { 186 | get i18n() { 187 | return i18n 188 | }, 189 | } 190 | 191 | if (!isProd) { 192 | opts.sourceMaps = Object.assign( 193 | { 194 | className: ({ prefix, key, id }) => `${prefix}__${key}-${id}`, 195 | }, 196 | options.sourceMaps || {} 197 | ) 198 | } 199 | 200 | return { 201 | StyleSheet: createStyleSheet(rules, opts), 202 | StyleResolver: createStyleResolver(sheet, rules, opts), 203 | setI18nManager, 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { create } from './factory' 2 | 3 | function getSheet() { 4 | if (typeof window === 'undefined') { 5 | return null 6 | } 7 | let element = document.querySelector('#__style_sheet__') 8 | if (!element) { 9 | element = document.createElement('style') 10 | element.id = '__style_sheet__' 11 | document.head.appendChild(element) 12 | } 13 | return element.sheet 14 | } 15 | 16 | export const { StyleSheet, StyleResolver, setI18nManager } = create({ 17 | sheet: getSheet(), 18 | }) 19 | -------------------------------------------------------------------------------- /src/sort-mq.js: -------------------------------------------------------------------------------- 1 | // adapted from styletron - https://raw.githubusercontent.com/styletron/styletron/c157e2a3a2592d639ae665342b2c0be8774e916b/packages/styletron-engine-atomic/src/sort-css-media-queries.js 2 | 3 | const minMaxWidth = /(!?\(\s*min(-device-)?-width).+\(\s*max(-device)?-width/i 4 | const minWidth = /\(\s*min(-device)?-width/i 5 | const maxMinWidth = /(!?\(\s*max(-device)?-width).+\(\s*min(-device)?-width/i 6 | const maxWidth = /\(\s*max(-device)?-width/i 7 | 8 | const isMinWidth = _testQuery(minMaxWidth, maxMinWidth, minWidth) 9 | const isMaxWidth = _testQuery(maxMinWidth, minMaxWidth, maxWidth) 10 | 11 | const minMaxHeight = /(!?\(\s*min(-device)?-height).+\(\s*max(-device)?-height/i 12 | const minHeight = /\(\s*min(-device)?-height/i 13 | const maxMinHeight = /(!?\(\s*max(-device)?-height).+\(\s*min(-device)?-height/i 14 | const maxHeight = /\(\s*max(-device)?-height/i 15 | 16 | const isMinHeight = _testQuery(minMaxHeight, maxMinHeight, minHeight) 17 | const isMaxHeight = _testQuery(maxMinHeight, minMaxHeight, maxHeight) 18 | 19 | const isPrint = /print/i 20 | const isPrintOnly = /^print$/i 21 | const maxValue = Number.MAX_VALUE 22 | 23 | function _getQueryLength(length) { 24 | const matches = /(-?\d*\.?\d+)(ch|em|ex|px|rem)/.exec(length) 25 | if (matches === null) { 26 | return maxValue 27 | } 28 | let number = matches[1] 29 | const unit = matches[2] 30 | switch (unit) { 31 | case 'ch': 32 | number = parseFloat(number) * 8.8984375 33 | break 34 | case 'em': 35 | case 'rem': 36 | number = parseFloat(number) * 16 37 | break 38 | case 'ex': 39 | number = parseFloat(number) * 8.296875 40 | break 41 | case 'px': 42 | number = parseFloat(number) 43 | break 44 | } 45 | return +number 46 | } 47 | 48 | function _testQuery(doubleTestTrue, doubleTestFalse, singleTest) { 49 | return function(query) { 50 | if (doubleTestTrue.test(query)) { 51 | return true 52 | } else if (doubleTestFalse.test(query)) { 53 | return false 54 | } 55 | return singleTest.test(query) 56 | } 57 | } 58 | 59 | function _testIsPrint(a, b) { 60 | const isPrintA = isPrint.test(a) 61 | const isPrintOnlyA = isPrintOnly.test(a) 62 | const isPrintB = isPrint.test(b) 63 | const isPrintOnlyB = isPrintOnly.test(b) 64 | 65 | if (isPrintA && isPrintB) { 66 | if (!isPrintOnlyA && isPrintOnlyB) { 67 | return 1 68 | } 69 | if (isPrintOnlyA && !isPrintOnlyB) { 70 | return -1 71 | } 72 | return a.localeCompare(b) 73 | } 74 | if (isPrintA) { 75 | return 1 76 | } 77 | if (isPrintB) { 78 | return -1 79 | } 80 | return null 81 | } 82 | 83 | export default function sortCSSmq(a, b) { 84 | if (a === '') { 85 | return -1 86 | } 87 | if (b === '') { 88 | return 1 89 | } 90 | const testIsPrint = _testIsPrint(a, b) 91 | if (testIsPrint !== null) { 92 | return testIsPrint 93 | } 94 | 95 | const minA = isMinWidth(a) || isMinHeight(a) 96 | const maxA = isMaxWidth(a) || isMaxHeight(a) 97 | const minB = isMinWidth(b) || isMinHeight(b) 98 | const maxB = isMaxWidth(b) || isMaxHeight(b) 99 | 100 | if (minA && maxB) { 101 | return -1 102 | } 103 | if (maxA && minB) { 104 | return 1 105 | } 106 | 107 | const lengthA = _getQueryLength(a) 108 | const lengthB = _getQueryLength(b) 109 | 110 | if (lengthA === maxValue && lengthB === maxValue) { 111 | return a.localeCompare(b) 112 | } else if (lengthA === maxValue) { 113 | return 1 114 | } else if (lengthB === maxValue) { 115 | return -1 116 | } 117 | 118 | if (lengthA > lengthB) { 119 | if (maxA) { 120 | return -1 121 | } 122 | return 1 123 | } 124 | 125 | if (lengthA < lengthB) { 126 | if (maxA) { 127 | return 1 128 | } 129 | return -1 130 | } 131 | 132 | return a.localeCompare(b) 133 | } 134 | -------------------------------------------------------------------------------- /src/source-maps.js: -------------------------------------------------------------------------------- 1 | /* global Blob, Worker, URL, module */ 2 | import ErrorStackParser from 'error-stack-parser' 3 | 4 | export function createSourceMapsEngine({ 5 | baseUrl = 'https://unpkg.com/css-to-js-sourcemap-worker@2.0.5', 6 | renderInterval = 120, 7 | } = {}) { 8 | const workerBlob = new Blob([`importScripts("${baseUrl}/worker.js")`], { 9 | type: 'application/javascript', 10 | }) 11 | let worker = new Worker(URL.createObjectURL(workerBlob)) 12 | worker.postMessage({ 13 | id: 'init_wasm', 14 | url: `${baseUrl}/mappings.wasm`, 15 | }) 16 | const style = document.createElement('style') 17 | document.head.appendChild(style) 18 | worker.postMessage({ 19 | id: 'set_render_interval', 20 | interval: renderInterval, 21 | }) 22 | if (module && module.hot) { 23 | module.hot.addStatusHandler(status => { 24 | if (status === 'dispose') { 25 | worker.postMessage({ id: 'invalidate' }) 26 | } 27 | }) 28 | } 29 | 30 | worker.onmessage = msg => { 31 | const { id, css } = msg.data 32 | if (id === 'render_css' && css) { 33 | style.appendChild(document.createTextNode(css)) 34 | } 35 | } 36 | 37 | let counter = 0 38 | return { 39 | create(className = `__debug`) { 40 | const stackIndex = 3 41 | const error = new Error('stacktrace source') 42 | const prefix = getDebugClassName(error, stackIndex) 43 | const cls = 44 | typeof className === 'function' 45 | ? className(prefix, counter) 46 | : className + '-' + counter 47 | counter++ 48 | worker.postMessage({ 49 | id: 'add_mapped_class', 50 | className: cls, 51 | stackInfo: { 52 | stack: error.stack, 53 | message: error.message, 54 | }, 55 | stackIndex, 56 | }) 57 | return cls 58 | }, 59 | } 60 | } 61 | 62 | export function getDebugClassName(error, stackIndex = 1) { 63 | const line = ErrorStackParser.parse(error)[stackIndex] 64 | if (!line || !line.fileName) { 65 | return '__dss-debug' 66 | } 67 | const parts = line.fileName.split('/') 68 | let name = parts.pop().replace(/\..*$/, '') 69 | if (name === 'index') { 70 | name = parts.pop() 71 | } 72 | name = name.replace(/\W/g, '-') 73 | return name.charAt(0).toUpperCase() + name.slice(1) 74 | } 75 | -------------------------------------------------------------------------------- /src/validate.js: -------------------------------------------------------------------------------- 1 | function error(message) { 2 | throw new Error(`style-sheet: ${message}`) 3 | } 4 | 5 | export default function validate(obj) { 6 | for (const k in obj) { 7 | const key = k.trim() 8 | const value = obj[key] 9 | if (value === null) continue 10 | const isDeclaration = 11 | Object.prototype.toString.call(value) !== '[object Object]' 12 | validateStr(key, isDeclaration) 13 | if (!isDeclaration) { 14 | validate(value) 15 | } else if (typeof value === 'string' && /!\s*important/.test(value)) { 16 | error('!important is not allowed') 17 | } 18 | } 19 | } 20 | 21 | export function validateStr(key, isDeclaration) { 22 | if (isDeclaration) { 23 | return 24 | } 25 | 26 | if (key.charAt(0) === '@') { 27 | return 28 | } 29 | 30 | // Selector 31 | 32 | if (key.split(',').length > 1) { 33 | error(`Invalid nested selector: '${key}'. Selectors cannot be grouped.`) 34 | } 35 | 36 | if (/::?(after|before|first-letter|first-line)/.test(key)) { 37 | error( 38 | `Detected pseudo-element: '${key}'. Pseudo-elements are not supported. Please use regular elements.` 39 | ) 40 | } 41 | 42 | if (/:(matches|has|not|lang|any|current)/.test(key)) { 43 | error(`Detected unsupported pseudo-class: '${key}'.`) 44 | } 45 | 46 | const split = key.split(/\s*[+>~]\s*/g) 47 | 48 | switch (split.length) { 49 | case 2: 50 | if (split[0].charAt(0) !== ':') { 51 | error( 52 | `Invalid nested selector: '${key}'. ` + 53 | 'The left part of a combinator selector must be a pseudo-class eg. `:hover`.' 54 | ) 55 | } 56 | if (split[1] !== '&') { 57 | error( 58 | `Invalid nested selector: '${key}'. ` + 59 | 'The right part of a combinator selector must be `&`.' 60 | ) 61 | } 62 | break 63 | case 1: 64 | if (split[0].indexOf(' ') > -1) { 65 | error( 66 | `Invalid nested selector: ${key}. Complex selectors are not supported.` 67 | ) 68 | } 69 | if (split[0].charAt(0) !== '&') { 70 | error( 71 | `Invalid nested selector: '${key}'. ` + 72 | 'A pseudo-class selector should reference its parent with `&` eg. `&:hover {}`.' 73 | ) 74 | } 75 | break 76 | default: 77 | error(`Invalid nested selector: ${key}.`) 78 | } 79 | 80 | if (/\[/.test(key)) { 81 | error( 82 | `Invalid selector: ${key}. Cannot use attribute selectors, please use only class selectors.` 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/_register.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | ignore: ['node_modules/*', 'test/**/fixtures/*'], 3 | }) 4 | -------------------------------------------------------------------------------- /test/_utils.js: -------------------------------------------------------------------------------- 1 | export function resolverToString(resolver) { 2 | return resolver.getStyleSheet().getTextContent() 3 | } 4 | -------------------------------------------------------------------------------- /test/babel/fixtures/constants.js: -------------------------------------------------------------------------------- 1 | export const TEST = 10 2 | -------------------------------------------------------------------------------- /test/babel/fixtures/i18n.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'style-sheet' 2 | 3 | const styles1 = StyleSheet.create({ 4 | root: { 5 | color: 'red', 6 | marginLeft: 10, 7 | '&:hover': { 8 | paddingRight: 5 9 | } 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/babel/fixtures/missingImport.js: -------------------------------------------------------------------------------- 1 | /* @jsx createElement */ 2 | 3 | const ComponentStatic = () =>
4 | const ComponentDynamic = ({ margin }) =>
5 | -------------------------------------------------------------------------------- /test/babel/fixtures/simple.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'style-sheet' 2 | import { TEST } from './constants' 3 | 4 | const backgroundColor = 'hotpink' 5 | const marginTop = 10; 6 | const rule = { 7 | display: 'block' 8 | } 9 | const small = true 10 | 11 | const styles1 = StyleSheet.create({ 12 | root: { 13 | color: 'red', 14 | marginTop: marginTop, 15 | marginBottom: marginTop + 'px', 16 | paddingTop: marginTop / 2, 17 | fontSize: (small ? marginTop : 4), 18 | val: TEST, 19 | backgroundColor, 20 | filter: 'blur(10px)' 21 | }, 22 | foo: rule, 23 | notExtractable: props.foo 24 | }) 25 | 26 | const styles2 = StyleSheet.create({ 27 | root: { 28 | display: 'flex' 29 | }, 30 | }) 31 | 32 | const ComponentStatic = () =>
33 | const ComponentStaticArray = () =>
34 | const ComponentConstant = () =>
35 | const ComponentConstantImported = () =>
36 | const ComponentDynamic = ({ margin }) =>
37 | const ComponentDynamicArray = ({ margin, padding }) =>
38 | const ComponentMixedStaticDynamicArray = ({ margin }) =>
39 | -------------------------------------------------------------------------------- /test/babel/index.js: -------------------------------------------------------------------------------- 1 | /* global __dirname:readonly */ 2 | import path from 'path' 3 | import test from 'ava' 4 | import { transformFileSync } from '@babel/core' 5 | import _plugin, { getCss } from '../../src/babel' 6 | 7 | const plugin = [_plugin, { stylePropPackageName: './lib/createElement' }] 8 | 9 | const transform = (file, opts = {}) => 10 | transformFileSync(path.resolve(__dirname, file), { 11 | plugins: [plugin], 12 | babelrc: false, 13 | ...opts, 14 | }) 15 | 16 | test.serial('plugin', async t => { 17 | const { code } = await transform('./fixtures/simple.js') 18 | t.snapshot(code) 19 | t.snapshot(getCss()) 20 | }) 21 | 22 | test.serial('missing import', async t => { 23 | const { code } = await transform('./fixtures/missingImport.js') 24 | t.snapshot(code) 25 | t.snapshot(getCss()) 26 | }) 27 | 28 | test.serial('missing import - jsx', async t => { 29 | const { code } = await transform('./fixtures/missingImport.js', { 30 | plugins: [plugin, '@babel/plugin-transform-react-jsx'], 31 | }) 32 | t.snapshot(code) 33 | t.snapshot(getCss()) 34 | }) 35 | 36 | test.serial('generates i18n styles', async t => { 37 | const { code } = await transform('./fixtures/i18n.js', { 38 | plugins: [ 39 | [_plugin, { stylePropPackageName: './lib/createElement', rtl: true }], 40 | ], 41 | }) 42 | t.snapshot(code) 43 | t.snapshot(getCss()) 44 | }) 45 | -------------------------------------------------------------------------------- /test/babel/snapshots/index.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/babel/index.js` 2 | 3 | The actual snapshot is saved in `index.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## generates i18n styles 8 | 9 | > Snapshot 1 10 | 11 | `"use strict";␊ 12 | ␊ 13 | var _createElement = require("./lib/createElement");␊ 14 | ␊ 15 | var _styleSheet = require("style-sheet");␊ 16 | ␊ 17 | const styles1 = {␊ 18 | root: ["dss10_h28rbs-i0tgik", "dss10_1b1ksw2-7qvd50|dss10_107tc4v-oyp9nw", "dss10.3_13j2o7y-dby8y8|dss10.3_1qpbkkv-vwgfuw"]␊ 19 | };` 20 | 21 | > Snapshot 2 22 | 23 | `[style-sheet-group="10"]{}␊ 24 | .dss10_107tc4v-oyp9nw{margin-right:10px;}␊ 25 | .dss10_1b1ksw2-7qvd50{margin-left:10px;}␊ 26 | .dss10_h28rbs-i0tgik{color:red;}␊ 27 | [style-sheet-group="10.3"]{}␊ 28 | .dss10\\.3_1qpbkkv-vwgfuw:hover{padding-left:5px;}␊ 29 | .dss10\\.3_13j2o7y-dby8y8:hover{padding-right:5px;}` 30 | 31 | ## missing import 32 | 33 | > Snapshot 1 34 | 35 | `"use strict";␊ 36 | ␊ 37 | var _createElement = require("./lib/createElement");␊ 38 | ␊ 39 | var _styleSheet = require("style-sheet");␊ 40 | ␊ 41 | var _ref = {␊ 42 | __styleProp: ["dss10_h28rbs-i0tgik"]␊ 43 | };␊ 44 | ␊ 45 | /* @jsx createElement */␊ 46 | const ComponentStatic = () =>
;␊ 47 | ␊ 48 | const ComponentDynamic = ({␊ 49 | margin␊ 50 | }) =>
;` 55 | 56 | > Snapshot 2 57 | 58 | `[style-sheet-group="10"]{}␊ 59 | .dss10_h28rbs-i0tgik{color:red;}` 60 | 61 | ## missing import - jsx 62 | 63 | > Snapshot 1 64 | 65 | `"use strict";␊ 66 | ␊ 67 | var _createElement = require("./lib/createElement");␊ 68 | ␊ 69 | var _styleSheet = require("style-sheet");␊ 70 | ␊ 71 | var _ref = {␊ 72 | __styleProp: ["dss10_h28rbs-i0tgik"]␊ 73 | };␊ 74 | ␊ 75 | /* @jsx createElement */␊ 76 | const ComponentStatic = () => (0, _createElement.createElement)("div", {␊ 77 | css: _ref␊ 78 | });␊ 79 | ␊ 80 | const ComponentDynamic = ({␊ 81 | margin␊ 82 | }) => (0, _createElement.createElement)("div", {␊ 83 | css: _styleSheet.StyleSheet.create({␊ 84 | __styleProp: {␊ 85 | margin␊ 86 | }␊ 87 | })␊ 88 | });` 89 | 90 | > Snapshot 2 91 | 92 | `[style-sheet-group="10"]{}␊ 93 | .dss10_h28rbs-i0tgik{color:red;}` 94 | 95 | ## plugin 96 | 97 | > Snapshot 1 98 | 99 | `"use strict";␊ 100 | ␊ 101 | var _createElement = require("./lib/createElement");␊ 102 | ␊ 103 | var _styleSheet = require("style-sheet");␊ 104 | ␊ 105 | var _constants = require("./constants");␊ 106 | ␊ 107 | const backgroundColor = 'hotpink';␊ 108 | const marginTop = 10;␊ 109 | const rule = {␊ 110 | display: 'block'␊ 111 | };␊ 112 | const small = true;␊ 113 | const styles1 = {␊ 114 | root: ["dss10_h28rbs-i0tgik", "dss10_1buiceq-7qvd50", "dss10_f69c5w-oyp9nw", "dss10_u84hpd-dby8y8", "dss10_1avj1fl-7qvd50", "dss10_153t8jg-7qvd50", "dss10_1eznbv4-6laobc", "dss10_1jgjtkn-1k19bls"],␊ 115 | foo: ["dss10_j9ctud-1t7uh5u"],␊ 116 | ..._styleSheet.StyleSheet.create({␊ 117 | notExtractable: props.foo␊ 118 | })␊ 119 | };␊ 120 | const styles2 = {␊ 121 | root: ["dss10_j9ctud-1kagvzm"]␊ 122 | };␊ 123 | var _ref = {␊ 124 | __styleProp: ["dss10_h28rbs-i0tgik"]␊ 125 | };␊ 126 | ␊ 127 | const ComponentStatic = () =>
;␊ 128 | ␊ 129 | var _ref2 = {␊ 130 | __styleProp: ["dss10_h28rbs-i0tgik"]␊ 131 | };␊ 132 | var _ref3 = {␊ 133 | __styleProp: ["dss10_1buiceq-7qvd50"]␊ 134 | };␊ 135 | var _ref4 = [_ref2, _ref3];␊ 136 | ␊ 137 | const ComponentStaticArray = () =>
;␊ 138 | ␊ 139 | var _ref5 = {␊ 140 | __styleProp: ["dss10_1buiceq-7qvd50"]␊ 141 | };␊ 142 | ␊ 143 | const ComponentConstant = () =>
;␊ 144 | ␊ 145 | var _ref6 = {␊ 146 | __styleProp: ["dss10_1buiceq-7qvd50"]␊ 147 | };␊ 148 | ␊ 149 | const ComponentConstantImported = () =>
;␊ 150 | ␊ 151 | const ComponentDynamic = ({␊ 152 | margin␊ 153 | }) =>
;␊ 159 | ␊ 160 | const ComponentDynamicArray = ({␊ 161 | margin,␊ 162 | padding␊ 163 | }) =>
;␊ 173 | ␊ 174 | var _ref7 = {␊ 175 | __styleProp: ["dss10_h28rbs-i0tgik"]␊ 176 | };␊ 177 | ␊ 178 | const ComponentMixedStaticDynamicArray = ({␊ 179 | margin␊ 180 | }) =>
;` 185 | 186 | > Snapshot 2 187 | 188 | `[style-sheet-group="10"]{}␊ 189 | .dss10_1jgjtkn-1k19bls{-webkit-filter:blur(10px);filter:blur(10px);}␊ 190 | .dss10_1eznbv4-6laobc{background-color:hotpink;}␊ 191 | .dss10_153t8jg-7qvd50{val:10px;}␊ 192 | .dss10_1avj1fl-7qvd50{font-size:10px;}␊ 193 | .dss10_u84hpd-dby8y8{padding-top:5px;}␊ 194 | .dss10_f69c5w-oyp9nw{margin-bottom:10px;}␊ 195 | .dss10_1buiceq-7qvd50{margin-top:10px;}␊ 196 | .dss10_h28rbs-i0tgik{color:red;}␊ 197 | .dss10_j9ctud-1t7uh5u{display:block;}␊ 198 | .dss10_j9ctud-1kagvzm{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;}` 199 | -------------------------------------------------------------------------------- /test/babel/snapshots/index.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppeg/style-sheet/e71c119ecaccdb2e50be0759b45820b8cbf0c6df/test/babel/snapshots/index.js.snap -------------------------------------------------------------------------------- /test/createElement.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import setup from '../src/createElement' 3 | 4 | const libMock = { 5 | StyleSheet: { 6 | create(rules) { 7 | return { 8 | rule: Object.entries(rules.rule).map(([key, val]) => { 9 | return `${key.substring(0, 3)}-${val.substring(0, 3)}` 10 | }), 11 | } 12 | }, 13 | }, 14 | StyleResolver: { 15 | resolve(styles) { 16 | return styles.reduce((acc, val) => acc.concat(val), []).join(' ') 17 | }, 18 | }, 19 | } 20 | const createElement = setup(libMock, 'css') 21 | 22 | test('works just with a tag', t => { 23 | t.snapshot(createElement('div')) 24 | }) 25 | 26 | test('works without props', t => { 27 | t.snapshot(createElement('div', null, [createElement('div')])) 28 | }) 29 | 30 | test('works with empty', t => { 31 | t.snapshot(createElement('div', {})) 32 | }) 33 | 34 | test('works with empty style prop', t => { 35 | t.snapshot(createElement('div', { css: {} })) 36 | }) 37 | 38 | test('works with simple style prop', t => { 39 | t.snapshot(createElement('div', { css: { color: 'red', display: 'block' } })) 40 | }) 41 | 42 | test('works with style prop as array', t => { 43 | t.snapshot(createElement('div', { css: [] })) 44 | t.snapshot(createElement('div', { css: [{ color: 'red' }] })) 45 | }) 46 | 47 | test('works with style prop as array with multiple rules', t => { 48 | t.snapshot( 49 | createElement('div', { css: [{ color: 'red' }, { display: 'block' }] }) 50 | ) 51 | }) 52 | 53 | test('removes falsy rules', t => { 54 | t.snapshot( 55 | createElement('div', { 56 | css: [{ color: 'red' }, false && { display: 'block' }], 57 | }) 58 | ) 59 | }) 60 | 61 | test('accepts an existing array of rules', t => { 62 | t.snapshot(createElement('div', { css: [{ color: 'red' }, ['dis-inl']] })) 63 | }) 64 | 65 | test('works with precompiled rules', t => { 66 | t.snapshot(createElement('div', { css: { __styleProp: ['dis-fle'] } })) 67 | t.snapshot(createElement('div', { css: [{ __styleProp: ['dis-inl'] }] })) 68 | }) 69 | 70 | test('works with mixed precompiled and normal rules', t => { 71 | t.snapshot( 72 | createElement('div', { 73 | css: [{ __styleProp: ['dis-inl'] }, { color: 'red' }], 74 | }) 75 | ) 76 | }) 77 | 78 | test('merges with className (put at the end)', t => { 79 | t.snapshot( 80 | createElement('div', { 81 | css: { color: 'red' }, 82 | className: 'mar-top dss10_pad-left', 83 | }) 84 | ) 85 | }) 86 | -------------------------------------------------------------------------------- /test/createOrderedCSSStyleSheet.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import create from '../src/createOrderedCSSStyleSheet' 3 | 4 | test('creates a sheet', t => { 5 | const sheet = create() 6 | t.truthy(sheet.insertRule) 7 | t.is(sheet.getTextContent(), '') 8 | }) 9 | 10 | test('inserts rules', t => { 11 | const sheet = create() 12 | sheet.insertRule('.test { color: red }', 0) 13 | t.snapshot(sheet.getTextContent()) 14 | }) 15 | 16 | test('does not insert duplicates', t => { 17 | const sheet = create() 18 | sheet.insertRule('.test { color: red }', 0) 19 | sheet.insertRule('.test { color: red }', 0) 20 | sheet.insertRule('.test1 { color: green }', 0) 21 | t.snapshot(sheet.getTextContent()) 22 | }) 23 | 24 | test('insert @media queries', t => { 25 | const sheet = create() 26 | sheet.insertRule('@media (min-width: 300px) { .test1 { color: red } }', 0) 27 | sheet.insertRule( 28 | '@media (min-width: 300px) { .test1:hover { color: red } }', 29 | 0 30 | ) 31 | sheet.insertRule( 32 | '@media (min-width: 300px) { .test1 > :hover { color: red } }', 33 | 0 34 | ) 35 | t.snapshot(sheet.getTextContent()) 36 | }) 37 | 38 | test('inserts groups in order', t => { 39 | const sheet = create() 40 | sheet.insertRule('.test1 { color: red }', 2) 41 | sheet.insertRule('.test2 { color: red }', 2.5) 42 | sheet.insertRule('.test3 { color: green }', 10) 43 | sheet.insertRule('.test4 { color: green }', 20) 44 | sheet.insertRule('.test5 { color: green }', 20.5) 45 | 46 | t.snapshot(sheet.getTextContent()) 47 | }) 48 | 49 | test('inserts at the end of the group when no index is provided', t => { 50 | const sheet = create() 51 | sheet.insertRule('.test1 { color: red }', 2) 52 | sheet.insertRule('.test2 { color: green }', 2) 53 | t.snapshot(sheet.getTextContent()) 54 | }) 55 | 56 | test('inserts at a specific index in the group', t => { 57 | const sheet = create() 58 | sheet.insertRule('.group3 { color: orange }', 3) 59 | sheet.insertRule('.group4 { display: block }', 4) 60 | sheet.insertRule('.test1 { color: red }', 2) 61 | sheet.insertRule('.test2 { color: green }', 2) 62 | sheet.insertRule('.test3 { color: yellow }', 2, 1) 63 | sheet.insertRule('.test4 { color: hotpink }', 2, 1) 64 | sheet.insertRule('.nextGroupStillWorks { color: papaya }', 3) 65 | t.snapshot(sheet.getTextContent()) 66 | }) 67 | 68 | test('throws when the index is out of bound', t => { 69 | const sheet = create() 70 | t.throws(() => { 71 | sheet.insertRule('.test1 { color: red }', 2, 1) 72 | }) 73 | }) 74 | 75 | test('not throws when the index is valid', t => { 76 | const sheet = create() 77 | t.notThrows(() => { 78 | sheet.insertRule('.test1 { color: red }', 2, 0) 79 | }) 80 | t.snapshot(sheet.getTextContent()) 81 | }) 82 | 83 | test('sorts media queries', t => { 84 | const sheet = create() 85 | sheet.insertRule('@media (min-width: 200px) { .test200 { color: red } }', 1) 86 | sheet.insertRule('@media (min-width: 300px) { .test300 { color: red } }', 0) 87 | sheet.insertRule('@media (min-width: 100px) { .test100 { color: red } }', 0) 88 | sheet.insertRule('@media (min-width: 100px) { .test100.1 { color: red } }', 1) 89 | t.snapshot(sheet.getTextContent()) 90 | }) 91 | -------------------------------------------------------------------------------- /test/e2e/_setup.js: -------------------------------------------------------------------------------- 1 | /* global global:readonly */ 2 | const puppeteer = require('puppeteer') 3 | 4 | global.debug = false 5 | 6 | global.testBefore = async () => { 7 | const browser = await puppeteer.launch({ headless: !global.debug }) 8 | const page = await browser.newPage() 9 | const gotoPage = async (fileName, { onLoad } = {}) => { 10 | await page.goto('http://localhost:5000/' + fileName, { waitUntil: 'load' }) 11 | if (onLoad) { 12 | await onLoad() 13 | } 14 | await page.addScriptTag({ 15 | url: 'http://localhost:5000/lib/_styleSheet.js', 16 | }) 17 | await page.addScriptTag({ 18 | url: 'http://localhost:5000/lib/_styleSheetFactory.js', 19 | }) 20 | } 21 | return { browser, page, gotoPage } 22 | } 23 | 24 | global.testAfter = async context => { 25 | await context.page.close() 26 | await context.browser.close() 27 | } 28 | -------------------------------------------------------------------------------- /test/e2e/fixtures/external.css: -------------------------------------------------------------------------------- 1 | .dss_h28rbs-i0tgik {color:red} 2 | -------------------------------------------------------------------------------- /test/e2e/test.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/test.js: -------------------------------------------------------------------------------- 1 | /* global testBefore:readonly testAfter:readonly styleSheet:readonly getComputedStyle:readonly setTimeout */ 2 | const test = require('ava') 3 | require('./_setup') 4 | 5 | test('minimal testcase', async t => { 6 | const context = await testBefore() 7 | const { gotoPage, page } = context 8 | 9 | await gotoPage('test.html') 10 | 11 | const color = await page.evaluate(() => { 12 | const { StyleSheet, StyleResolver } = styleSheet 13 | 14 | const styles = StyleSheet.create({ 15 | test: { 16 | color: 'green', 17 | }, 18 | }) 19 | const root = document.querySelector('#root') 20 | root.classList.add(StyleResolver.resolve(styles.test)) 21 | return getComputedStyle(root).getPropertyValue('color') 22 | }) 23 | 24 | t.is(color, 'rgb(0, 128, 0)') 25 | 26 | await testAfter(context) 27 | }) 28 | 29 | test('reconciles i18n values', async t => { 30 | const context = await testBefore() 31 | const { gotoPage, page } = context 32 | 33 | await gotoPage('test.html', { 34 | onLoad: async () => { 35 | await page.evaluate(() => { 36 | const preRendered = document.createElement('style') 37 | preRendered.id = '__style_sheet__' 38 | preRendered.textContent = ` 39 | [style-sheet-group="10"]{} 40 | .dss10_1idvwo2-oyp9nw{border-top-right-radius:10px;} 41 | .dss10_1qlnxpd-7qvd50{border-top-left-radius:10px;} 42 | .dss10_xjidwl-oyp9nw{right:10px;} 43 | .dss10_52pxm8-7qvd50{left:10px;} 44 | ` 45 | document.head.appendChild(preRendered) 46 | }) 47 | }, 48 | }) 49 | 50 | const values = await page.evaluate(() => { 51 | const { StyleSheet, StyleResolver, setI18nManager } = styleSheet 52 | 53 | setI18nManager({ 54 | isRTL: true, 55 | doLeftAndRightSwapInRTL: true, 56 | }) 57 | 58 | const styles = StyleSheet.create({ 59 | test: { 60 | borderTopLeftRadius: 10, 61 | left: 10, 62 | }, 63 | }) 64 | 65 | const root = document.querySelector('#root') 66 | root.className = StyleResolver.resolve(styles.test) 67 | 68 | const computed = getComputedStyle(root) 69 | return ( 70 | [ 71 | 'border-top-left-radius', 72 | 'left', 73 | 'border-top-right-radius', 74 | 'right', 75 | ].reduce((values, current) => { 76 | values += `${current}:${computed.getPropertyValue(current)};` 77 | return values 78 | }, '{') + '}' 79 | ) 80 | }) 81 | 82 | t.is( 83 | values, 84 | '{border-top-left-radius:0px;left:auto;border-top-right-radius:10px;right:10px;}' 85 | ) 86 | 87 | await testAfter(context) 88 | }) 89 | 90 | test('inserts only the resolved i18n rules', async t => { 91 | const context = await testBefore() 92 | const { gotoPage, page } = context 93 | 94 | await gotoPage('test.html') 95 | 96 | const styles = await page.evaluate(() => { 97 | const { StyleSheet, StyleResolver, setI18nManager } = styleSheet 98 | 99 | setI18nManager({ 100 | isRTL: true, 101 | doLeftAndRightSwapInRTL: true, 102 | }) 103 | 104 | const styles = StyleSheet.create({ 105 | test: { 106 | left: 10, 107 | }, 108 | }) 109 | 110 | StyleResolver.resolve(styles.test) 111 | const resolved = [] 112 | resolved.push(StyleResolver.getStyleSheet().getTextContent()) 113 | 114 | setI18nManager({ 115 | isRTL: false, 116 | doLeftAndRightSwapInRTL: false, 117 | }) 118 | StyleResolver.resolve(styles.test) 119 | resolved.push(StyleResolver.getStyleSheet().getTextContent()) 120 | return resolved 121 | }) 122 | 123 | t.deepEqual(styles, [ 124 | '[style-sheet-group="10"]{}\n.dss10_xjidwl-oyp9nw{right:10px;}', 125 | '[style-sheet-group="10"]{}\n.dss10_xjidwl-oyp9nw{right:10px;}\n.dss10_52pxm8-7qvd50{left:10px;}', 126 | ]) 127 | 128 | await testAfter(context) 129 | }) 130 | 131 | test('resolves shorthand properties', async t => { 132 | const context = await testBefore() 133 | const { gotoPage, page } = context 134 | 135 | await gotoPage('test.html') 136 | 137 | const margins = await page.evaluate(() => { 138 | const { StyleSheet, StyleResolver } = styleSheet 139 | 140 | const styles = StyleSheet.create({ 141 | test: { 142 | margin: 10, 143 | marginTop: 20, 144 | '@media (min-width: 0px)': { 145 | marginLeft: 30, 146 | }, 147 | }, 148 | }) 149 | const root = document.querySelector('#root') 150 | root.className = StyleResolver.resolve(styles.test) 151 | const computed = getComputedStyle(root) 152 | 153 | return [ 154 | computed.getPropertyValue('margin'), 155 | computed.getPropertyValue('margin-top'), 156 | ].join(', ') 157 | }) 158 | 159 | t.is(margins, '20px 10px 10px 30px, 20px') 160 | 161 | await testAfter(context) 162 | }) 163 | 164 | test('reconciles shorthand properties', async t => { 165 | const context = await testBefore() 166 | const { gotoPage, page } = context 167 | 168 | const preRenderedStyles = `[style-sheet-group="2"] { } 169 | .dss2_1nrzrej-7qvd50 { margin: 10px; } 170 | [style-sheet-group="10"] { } 171 | .dss10_1buiceq-13dvipr { margin-top: 20px; }` 172 | 173 | await gotoPage('test.html', { 174 | onLoad: async () => { 175 | await page.evaluate( 176 | preRenderedStyles => { 177 | const preRendered = document.createElement('style') 178 | preRendered.id = '__style_sheet__' 179 | preRendered.textContent = preRenderedStyles 180 | document.head.appendChild(preRendered) 181 | }, 182 | [preRenderedStyles] 183 | ) 184 | }, 185 | }) 186 | 187 | const before = await page.evaluate(() => { 188 | const { StyleResolver } = styleSheet 189 | return StyleResolver.getStyleSheet().getTextContent() 190 | }) 191 | 192 | t.is(before, preRenderedStyles) 193 | 194 | const after = await page.evaluate(() => { 195 | const { StyleSheet, StyleResolver } = styleSheet 196 | 197 | const styles = StyleSheet.create({ 198 | test: { 199 | margin: 10, 200 | marginTop: 20, 201 | '@media (max-width: 200px)': { 202 | marginLeft: 30, 203 | }, 204 | }, 205 | }) 206 | 207 | StyleResolver.resolve(styles.test) 208 | return StyleResolver.getStyleSheet().getTextContent() 209 | }) 210 | 211 | const beforeWithMedia = 212 | before + 213 | ` 214 | [style-sheet-group="11"]{} 215 | @media (max-width: 200px){.dss11_zi3on2-11pur1y{margin-left:30px;}}` 216 | t.is(beforeWithMedia, after) 217 | 218 | await testAfter(context) 219 | }) 220 | 221 | test('combinator selectors are more specific than states', async t => { 222 | const context = await testBefore() 223 | const { gotoPage, page } = context 224 | 225 | await gotoPage('test.html') 226 | 227 | await page.evaluate(() => { 228 | const { StyleSheet, StyleResolver } = styleSheet 229 | 230 | const styles = StyleSheet.create({ 231 | test: { 232 | height: 10, 233 | color: 'blue', 234 | '&:hover': { 235 | color: 'red', 236 | margin: 10, 237 | }, 238 | }, 239 | another: { 240 | ':hover > &': { 241 | color: 'green', 242 | margin: 20, 243 | marginTop: 30, 244 | }, 245 | }, 246 | }) 247 | 248 | // Assume sometime ago this was already resolved and injected. 249 | StyleResolver.resolve(styles.another) 250 | 251 | const root = document.querySelector('#root') 252 | root.className = StyleResolver.resolve([styles.test, styles.another]) 253 | }) 254 | 255 | await page.hover('body') 256 | 257 | const result = await page.evaluate(() => { 258 | const root = document.querySelector('#root') 259 | const computedStyle = getComputedStyle(root) 260 | return [ 261 | computedStyle.getPropertyValue('color'), 262 | computedStyle.getPropertyValue('margin'), 263 | ].join(',') 264 | }) 265 | 266 | t.is(result, 'rgb(0, 128, 0),30px 20px 20px') 267 | 268 | await testAfter(context) 269 | }) 270 | 271 | test('resolves pseudo classes deterministically', async t => { 272 | const context = await testBefore() 273 | const { gotoPage, page } = context 274 | 275 | await gotoPage('test.html') 276 | 277 | const colorIdle = await page.evaluate(() => { 278 | const { StyleSheet, StyleResolver } = styleSheet 279 | 280 | const styles = StyleSheet.create({ 281 | elsewhere: { 282 | '&:active': { 283 | color: 'white', 284 | }, 285 | }, 286 | test: { 287 | color: 'blue', 288 | '&:hover': { 289 | color: 'red', 290 | }, 291 | '&:active': { 292 | color: 'white', 293 | backgroundColor: 'green', 294 | }, 295 | }, 296 | }) 297 | 298 | // Somewhere in the app the active styles have been injected already. 299 | StyleResolver.resolve(styles.elsewhere) 300 | 301 | const root = document.querySelector('#root') 302 | const button = root.appendChild(document.createElement('button')) 303 | button.textContent = 'test' 304 | button.className = StyleResolver.resolve(styles.test) 305 | return getComputedStyle(button).getPropertyValue('color') 306 | }) 307 | 308 | t.is(colorIdle, 'rgb(0, 0, 255)') 309 | 310 | await page.hover('button') 311 | const colorHover = await page.evaluate(() => 312 | getComputedStyle(document.querySelector('button')).getPropertyValue('color') 313 | ) 314 | t.is(colorHover, 'rgb(255, 0, 0)') 315 | 316 | await active(page, 'button', async function() { 317 | const colorActive = await page.evaluate(() => 318 | getComputedStyle(document.querySelector('button')).getPropertyValue( 319 | 'color' 320 | ) 321 | ) 322 | t.is( 323 | colorActive, 324 | 'rgb(255, 255, 255)', 325 | 'the color should be white on :active' 326 | ) 327 | }) 328 | 329 | await testAfter(context) 330 | }) 331 | 332 | test('sorts media queries in a mobile-first fashion', async t => { 333 | const context = await testBefore() 334 | const { gotoPage, page } = context 335 | 336 | const preRenderedStyles = `[style-sheet-group="11"] { } 337 | @media (min-width: 200px) { 338 | .dss11_zi3on2-11pur1y { margin-left: 30px; } 339 | }` 340 | 341 | await gotoPage('test.html', { 342 | onLoad: async () => { 343 | await page.evaluate( 344 | preRenderedStyles => { 345 | const preRendered = document.createElement('style') 346 | preRendered.id = '__style_sheet__' 347 | preRendered.textContent = preRenderedStyles 348 | document.head.appendChild(preRendered) 349 | }, 350 | [preRenderedStyles] 351 | ) 352 | }, 353 | }) 354 | 355 | const before = await page.evaluate(() => { 356 | const { StyleResolver } = styleSheet 357 | return StyleResolver.getStyleSheet().getTextContent() 358 | }) 359 | 360 | t.is(before, preRenderedStyles) 361 | 362 | const after = await page.evaluate(() => { 363 | const { StyleSheet, StyleResolver } = styleSheet 364 | 365 | const styles = StyleSheet.create({ 366 | test: { 367 | '@media (max-width: 100px)': { 368 | padding: 10, 369 | }, 370 | '@media (min-width: 100px)': { 371 | padding: 10, 372 | marginLeft: 20, 373 | }, 374 | '@media (min-width: 200px)': { 375 | padding: 20, 376 | }, 377 | }, 378 | }) 379 | 380 | StyleResolver.resolve(styles.test) 381 | return StyleResolver.getStyleSheet().getTextContent() 382 | }) 383 | 384 | const sorted = `[style-sheet-group="3"]{} 385 | @media (min-width: 100px){.dss3_16y3i9f-7qvd50{padding:10px;}} 386 | @media (min-width: 200px){.dss3_epu5c8-13dvipr{padding:20px;}} 387 | @media (max-width: 100px){.dss3_oqkgbt-7qvd50{padding:10px;}} 388 | [style-sheet-group="11"] { } 389 | @media (min-width: 100px){.dss11_5om9wv-13dvipr{margin-left:20px;}} 390 | @media (min-width: 200px) { 391 | .dss11_zi3on2-11pur1y { margin-left: 30px; } 392 | }` 393 | t.is(sorted, after) 394 | 395 | await testAfter(context) 396 | }) 397 | 398 | async function active(page, selector, doSomething) { 399 | const el = await page.$(selector) 400 | const { top, left } = await page.evaluate(el => { 401 | el.scrollIntoViewIfNeeded() 402 | const { top, left } = el.getBoundingClientRect() 403 | return { top, left } 404 | }, el) 405 | await page.mouse.move(top + 1, left + 1) 406 | await page.mouse.down() 407 | await doSomething() 408 | await page.mouse.up() 409 | } 410 | 411 | // eslint-disable-next-line no-unused-vars 412 | function sleep(ms) { 413 | return new Promise(resolve => setTimeout(resolve, ms)) 414 | } 415 | -------------------------------------------------------------------------------- /test/i18n.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create as _create } from '../src/factory' 3 | import { i18n } from '../src/data' 4 | import { resolverToString } from './_utils' 5 | 6 | const create = opts => 7 | _create({ 8 | i18n: {}, 9 | ...opts, 10 | }) 11 | 12 | test('creates and resolves i18n styles', t => { 13 | const { StyleSheet, StyleResolver } = create({ 14 | i18n: { 15 | isRTL: true, 16 | doLeftAndRightSwapInRTL: true, 17 | }, 18 | }) 19 | 20 | const result = StyleSheet.create({ 21 | root: { 22 | marginLeft: 10, 23 | float: 'right', 24 | display: 'block', 25 | }, 26 | }) 27 | 28 | t.deepEqual(result, { 29 | root: [ 30 | 'dss10_1b1ksw2-7qvd50|dss10_107tc4v-oyp9nw', 31 | 'dss10_1a9sfb9-xjidwl|dss10_1a9sfb9-52pxm8', 32 | 'dss10_j9ctud-1t7uh5u', 33 | ], 34 | }) 35 | 36 | const resolved = StyleResolver.resolve(result.root) 37 | t.is( 38 | resolved, 39 | 'dss10_j9ctud-1t7uh5u dss10_1a9sfb9-52pxm8 dss10_107tc4v-oyp9nw' 40 | ) 41 | t.snapshot(resolverToString(StyleResolver)) 42 | }) 43 | 44 | test('resolves i18n styles based on the i18n manager values', t => { 45 | let doLeftAndRightSwapInRTL = true 46 | 47 | const { StyleSheet, StyleResolver } = create({ 48 | i18n: { 49 | isRTL: true, 50 | get doLeftAndRightSwapInRTL() { 51 | return doLeftAndRightSwapInRTL 52 | }, 53 | }, 54 | }) 55 | 56 | const result = StyleSheet.create({ 57 | root: { 58 | marginLeft: 10, 59 | float: 'right', 60 | display: 'block', 61 | }, 62 | }) 63 | 64 | let resolved = StyleResolver.resolve(result.root) 65 | t.is( 66 | resolved, 67 | 'dss10_j9ctud-1t7uh5u dss10_1a9sfb9-52pxm8 dss10_107tc4v-oyp9nw' 68 | ) 69 | 70 | doLeftAndRightSwapInRTL = false 71 | resolved = StyleResolver.resolve(result.root) 72 | t.is( 73 | resolved, 74 | 'dss10_j9ctud-1t7uh5u dss10_1a9sfb9-xjidwl dss10_1b1ksw2-7qvd50' 75 | ) 76 | }) 77 | 78 | test('resolves multiple rules', t => { 79 | let doLeftAndRightSwapInRTL = true 80 | 81 | const { StyleSheet, StyleResolver } = create({ 82 | i18n: { 83 | isRTL: true, 84 | get doLeftAndRightSwapInRTL() { 85 | return doLeftAndRightSwapInRTL 86 | }, 87 | }, 88 | }) 89 | 90 | const one = StyleSheet.create({ 91 | root: { 92 | borderTopLeftRadius: 0, 93 | left: 0, 94 | }, 95 | }).root 96 | 97 | const two = StyleSheet.create({ 98 | root: { 99 | borderTopLeftRadius: 10, 100 | left: 10, 101 | }, 102 | }).root 103 | 104 | let resolved = StyleResolver.resolve([one, two]) 105 | t.is(resolved, 'dss10_1idvwo2-oyp9nw dss10_xjidwl-oyp9nw') 106 | t.snapshot(resolverToString(StyleResolver)) 107 | 108 | doLeftAndRightSwapInRTL = false 109 | resolved = StyleResolver.resolve([one, two]) 110 | t.is(resolved, 'dss10_1qlnxpd-7qvd50 dss10_52pxm8-7qvd50') 111 | t.snapshot(resolverToString(StyleResolver)) 112 | }) 113 | 114 | test('flips properties', t => { 115 | const { StyleSheet, StyleResolver } = create({ 116 | i18n: { 117 | isRTL: true, 118 | doLeftAndRightSwapInRTL: true, 119 | }, 120 | }) 121 | 122 | const styles = StyleSheet.create({ 123 | root: Object.keys(i18n.properties).reduce((styles, prop) => { 124 | styles[prop] = 'test' 125 | return styles 126 | }, {}), 127 | }) 128 | 129 | const resolved = StyleResolver.resolve(styles.root) 130 | t.snapshot(resolved) 131 | t.snapshot(resolverToString(StyleResolver)) 132 | }) 133 | 134 | test('flips values', t => { 135 | const { StyleSheet, StyleResolver } = create({ 136 | i18n: { 137 | isRTL: true, 138 | doLeftAndRightSwapInRTL: true, 139 | }, 140 | }) 141 | 142 | const styles = StyleSheet.create({ 143 | root: Object.keys(i18n.values).reduce((styles, value, index) => { 144 | styles[`test${index}`] = value 145 | return styles 146 | }, {}), 147 | }) 148 | 149 | const resolved = StyleResolver.resolve(styles.root) 150 | t.snapshot(resolved) 151 | t.snapshot(resolverToString(StyleResolver)) 152 | }) 153 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../src/factory' 3 | import { resolverToString } from './_utils' 4 | 5 | test('works', t => { 6 | const { StyleSheet, StyleResolver } = create() 7 | const result = StyleSheet.create({ 8 | root: { 9 | color: 'red', 10 | }, 11 | }) 12 | 13 | t.deepEqual(result, { 14 | root: ['dss10_h28rbs-i0tgik'], 15 | }) 16 | t.is(StyleResolver.resolve(result.root), 'dss10_h28rbs-i0tgik') 17 | }) 18 | 19 | test('works with multiple rules', t => { 20 | const { StyleSheet } = create() 21 | const result = StyleSheet.create({ 22 | root: { 23 | color: 'red', 24 | }, 25 | another: { 26 | display: 'block', 27 | }, 28 | }) 29 | 30 | t.deepEqual(Object.keys(result), ['root', 'another']) 31 | }) 32 | 33 | test('resolves &', t => { 34 | const { StyleSheet, StyleResolver } = create() 35 | const result = StyleSheet.create({ 36 | root: { 37 | color: 'red', 38 | '&:hover': { 39 | color: 'green', 40 | }, 41 | ':hover > &': { 42 | color: 'yellow', 43 | }, 44 | '@media (min-width: 30px)': { 45 | '&:hover': { 46 | color: 'green', 47 | }, 48 | ':hover > &': { 49 | color: 'yellow', 50 | }, 51 | }, 52 | }, 53 | }) 54 | t.snapshot(result.root) 55 | StyleResolver.resolve(result.root) 56 | t.snapshot(resolverToString(StyleResolver)) 57 | }) 58 | 59 | test('can use :hover:active', t => { 60 | const { StyleSheet, StyleResolver } = create() 61 | const result = StyleSheet.create({ 62 | root: { 63 | display: 'block', 64 | '&:active': { 65 | color: 'white', 66 | }, 67 | '&:hover': { 68 | color: 'green', 69 | }, 70 | '&:hover:active': { 71 | color: 'green', 72 | }, 73 | }, 74 | }) 75 | t.snapshot(result.root) 76 | StyleResolver.resolve(result.root) 77 | t.snapshot(resolverToString(StyleResolver)) 78 | }) 79 | 80 | test('resolves non unitless numbers', t => { 81 | const { StyleSheet, StyleResolver } = create() 82 | const result = StyleSheet.create({ 83 | root: { 84 | marginTop: 10, 85 | marginBottom: '20px', 86 | paddingTop: 0, 87 | paddingBottom: [5, 30], 88 | zIndex: 10, 89 | }, 90 | }) 91 | 92 | StyleResolver.resolve(result.root) 93 | t.snapshot(resolverToString(StyleResolver)) 94 | }) 95 | 96 | // Hashing 97 | 98 | test('hashes selectors deterministically', t => { 99 | const { StyleSheet } = create() 100 | const result = StyleSheet.create({ 101 | root: { 102 | color: 'red', 103 | }, 104 | }) 105 | 106 | t.is(result.root[0], 'dss10_h28rbs-i0tgik') 107 | }) 108 | 109 | test('hashes media queries and descendant selectors', t => { 110 | const { StyleSheet, StyleResolver } = create() 111 | const result = StyleSheet.create({ 112 | root: { 113 | '@media (min-width: 30px)': { 114 | color: 'red', 115 | }, 116 | '&:hover': { 117 | color: 'red', 118 | }, 119 | }, 120 | }) 121 | StyleResolver.resolve(result.root) 122 | t.snapshot(resolverToString(StyleResolver)) 123 | t.is(result.root[0], 'dss11_3bdajn-i0tgik') 124 | t.is(result.root[1], 'dss10.3_41vss2-i0tgik') 125 | }) 126 | 127 | test('supports fallback values', t => { 128 | const { StyleSheet, StyleResolver } = create() 129 | const styles = StyleSheet.create({ 130 | root: { 131 | color: ['red', 'rgba(255, 0, 0, 1)'], 132 | }, 133 | }) 134 | t.deepEqual(styles.root, ['dss10_h28rbs-aulp3c']) 135 | StyleResolver.resolve(styles.root) 136 | t.snapshot(resolverToString(StyleResolver)) 137 | }) 138 | 139 | test('adds vendor prefixes', t => { 140 | const { StyleSheet, StyleResolver } = create() 141 | const styles = StyleSheet.create({ 142 | root: { 143 | filter: 'blur(10px)', 144 | }, 145 | }) 146 | 147 | t.deepEqual(styles, { 148 | root: ['dss10_1jgjtkn-1k19bls'], 149 | }) 150 | StyleResolver.resolve(styles.root) 151 | const css = resolverToString(StyleResolver) 152 | t.is( 153 | css, 154 | '[style-sheet-group="10"]{}\n' + 155 | '.dss10_1jgjtkn-1k19bls{-webkit-filter:blur(10px);filter:blur(10px);}' 156 | ) 157 | }) 158 | 159 | // test.skip('flush multiple times', t => { 160 | // const { StyleSheet, StyleResolver } = create() 161 | // let styles = StyleSheet.create({ 162 | // root: { 163 | // color: 'red', 164 | // }, 165 | // }) 166 | // StyleResolver.resolve(styles.root) 167 | // let sheet = StyleResolver.getStyleSheet() 168 | // t.is(sheet.cssRules.length, 1) 169 | // let result = flush(sheet) 170 | // t.is(sheet.cssRules.length, 0) 171 | // t.is(StyleResolver.getStyleSheet().sheet.cssRules.length, 0) 172 | // t.is(result, '.dssh_28rbs-i0tgik{color:red;}') 173 | // 174 | // styles = StyleSheet.create({ 175 | // root: { 176 | // color: 'red', 177 | // }, 178 | // }) 179 | // StyleResolver.resolve(styles.root) 180 | // sheet = StyleResolver.getStyleSheet().sheet 181 | // t.is(sheet.cssRules.length, 1) 182 | // result = flush(sheet) 183 | // t.is(sheet.cssRules.length, 0) 184 | // t.is(StyleResolver.getStyleSheet().sheet.cssRules.length, 0) 185 | // t.is(result, '.dssh_28rbs-i0tgik{color:red;}') 186 | // }) 187 | -------------------------------------------------------------------------------- /test/snapshots/createElement.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/createElement.js` 2 | 3 | The actual snapshot is saved in `createElement.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## accepts an existing array of rules 8 | 9 | > Snapshot 1 10 | 11 |
14 | 15 | ## removes falsy rules 16 | 17 | > Snapshot 1 18 | 19 |
22 | 23 | ## works just with a tag 24 | 25 | > Snapshot 1 26 | 27 |
28 | 29 | ## works with empty 30 | 31 | > Snapshot 1 32 | 33 |
34 | 35 | ## works with empty style prop 36 | 37 | > Snapshot 1 38 | 39 |
42 | 43 | ## works with precompiled rules 44 | 45 | > Snapshot 1 46 | 47 |
50 | 51 | > Snapshot 2 52 | 53 |
56 | 57 | ## works with simple style prop 58 | 59 | > Snapshot 1 60 | 61 |
64 | 65 | ## works with style prop as array 66 | 67 | > Snapshot 1 68 | 69 |
72 | 73 | > Snapshot 2 74 | 75 |
78 | 79 | ## works with style prop as array with multiple rules 80 | 81 | > Snapshot 1 82 | 83 |
86 | 87 | ## works without props 88 | 89 | > Snapshot 1 90 | 91 |
92 |
93 |
94 | 95 | ## works with mixed precompiled and normal rules 96 | 97 | > Snapshot 1 98 | 99 |
102 | 103 | ## merges with className (put at the end) 104 | 105 | > Snapshot 1 106 | 107 |
110 | -------------------------------------------------------------------------------- /test/snapshots/createElement.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppeg/style-sheet/e71c119ecaccdb2e50be0759b45820b8cbf0c6df/test/snapshots/createElement.js.snap -------------------------------------------------------------------------------- /test/snapshots/createOrderedCSSStyleSheet.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/createOrderedCSSStyleSheet.js` 2 | 3 | The actual snapshot is saved in `createOrderedCSSStyleSheet.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## does not insert duplicates 8 | 9 | > Snapshot 1 10 | 11 | `[style-sheet-group="0"]{}␊ 12 | .test { color: red }␊ 13 | .test1 { color: green }` 14 | 15 | ## insert @media queries 16 | 17 | > Snapshot 1 18 | 19 | `[style-sheet-group="0"]{}␊ 20 | @media (min-width: 300px) { .test1 > :hover { color: red } }␊ 21 | @media (min-width: 300px) { .test1:hover { color: red } }␊ 22 | @media (min-width: 300px) { .test1 { color: red } }` 23 | 24 | ## inserts at a specific index in the group 25 | 26 | > Snapshot 1 27 | 28 | `[style-sheet-group="2"]{}␊ 29 | .test1 { color: red }␊ 30 | .test4 { color: hotpink }␊ 31 | .test3 { color: yellow }␊ 32 | .test2 { color: green }␊ 33 | [style-sheet-group="3"]{}␊ 34 | .group3 { color: orange }␊ 35 | .nextGroupStillWorks { color: papaya }␊ 36 | [style-sheet-group="4"]{}␊ 37 | .group4 { display: block }` 38 | 39 | ## inserts at the end of the group when no index is provided 40 | 41 | > Snapshot 1 42 | 43 | `[style-sheet-group="2"]{}␊ 44 | .test1 { color: red }␊ 45 | .test2 { color: green }` 46 | 47 | ## inserts groups in order 48 | 49 | > Snapshot 1 50 | 51 | `[style-sheet-group="2"]{}␊ 52 | .test1 { color: red }␊ 53 | [style-sheet-group="2.5"]{}␊ 54 | .test2 { color: red }␊ 55 | [style-sheet-group="10"]{}␊ 56 | .test3 { color: green }␊ 57 | [style-sheet-group="20"]{}␊ 58 | .test4 { color: green }␊ 59 | [style-sheet-group="20.5"]{}␊ 60 | .test5 { color: green }` 61 | 62 | ## inserts rules 63 | 64 | > Snapshot 1 65 | 66 | `[style-sheet-group="0"]{}␊ 67 | .test { color: red }` 68 | 69 | ## not throws when the index is valid 70 | 71 | > Snapshot 1 72 | 73 | `[style-sheet-group="2"]{}␊ 74 | .test1 { color: red }` 75 | 76 | ## sorts media queries 77 | 78 | > Snapshot 1 79 | 80 | `[style-sheet-group="0"]{}␊ 81 | @media (min-width: 100px) { .test100 { color: red } }␊ 82 | @media (min-width: 300px) { .test300 { color: red } }␊ 83 | [style-sheet-group="1"]{}␊ 84 | @media (min-width: 100px) { .test100.1 { color: red } }␊ 85 | @media (min-width: 200px) { .test200 { color: red } }` 86 | -------------------------------------------------------------------------------- /test/snapshots/createOrderedCSSStyleSheet.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppeg/style-sheet/e71c119ecaccdb2e50be0759b45820b8cbf0c6df/test/snapshots/createOrderedCSSStyleSheet.js.snap -------------------------------------------------------------------------------- /test/snapshots/i18n.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/i18n.js` 2 | 3 | The actual snapshot is saved in `i18n.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## creates and resolves i18n styles 8 | 9 | > Snapshot 1 10 | 11 | `[style-sheet-group="10"]{}␊ 12 | .dss10_j9ctud-1t7uh5u{display:block;}␊ 13 | .dss10_1a9sfb9-52pxm8{float:left;}␊ 14 | .dss10_1a9sfb9-xjidwl{float:right;}␊ 15 | .dss10_107tc4v-oyp9nw{margin-right:10px;}␊ 16 | .dss10_1b1ksw2-7qvd50{margin-left:10px;}` 17 | 18 | ## flips properties 19 | 20 | > Snapshot 1 21 | 22 | 'dss10_52pxm8-1cs5qlh dss10_1b6vh77-1cs5qlh dss10_qulcgs-1cs5qlh dss10_1b1ksw2-1cs5qlh dss10_107tc4v-1cs5qlh dss10_xjidwl-1cs5qlh dss10_wh353u-1cs5qlh dss10_13j0uyb-1cs5qlh dss10_1i5ijdt-1cs5qlh dss10_7iuesl-1cs5qlh dss10_mgrnb8-1cs5qlh dss10_lo4bzm-1cs5qlh dss10_cm0rhj-1cs5qlh dss10_wtkis8-1cs5qlh dss10_1qlnxpd-1cs5qlh dss10_1idvwo2-1cs5qlh' 23 | 24 | > Snapshot 2 25 | 26 | `[style-sheet-group="10"]{}␊ 27 | .dss10_52pxm8-1cs5qlh{left:test;}␊ 28 | .dss10_xjidwl-1cs5qlh{right:test;}␊ 29 | .dss10_1b6vh77-1cs5qlh{padding-left:test;}␊ 30 | .dss10_qulcgs-1cs5qlh{padding-right:test;}␊ 31 | .dss10_1b1ksw2-1cs5qlh{margin-left:test;}␊ 32 | .dss10_107tc4v-1cs5qlh{margin-right:test;}␊ 33 | .dss10_wh353u-1cs5qlh{border-left-width:test;}␊ 34 | .dss10_7iuesl-1cs5qlh{border-right-width:test;}␊ 35 | .dss10_13j0uyb-1cs5qlh{border-left-style:test;}␊ 36 | .dss10_mgrnb8-1cs5qlh{border-right-style:test;}␊ 37 | .dss10_1i5ijdt-1cs5qlh{border-left-color:test;}␊ 38 | .dss10_lo4bzm-1cs5qlh{border-right-color:test;}␊ 39 | .dss10_cm0rhj-1cs5qlh{border-bottom-left-radius:test;}␊ 40 | .dss10_wtkis8-1cs5qlh{border-bottom-right-radius:test;}␊ 41 | .dss10_1qlnxpd-1cs5qlh{border-top-left-radius:test;}␊ 42 | .dss10_1idvwo2-1cs5qlh{border-top-right-radius:test;}` 43 | 44 | ## flips values 45 | 46 | > Snapshot 1 47 | 48 | 'dss10_18pk4lw-ntl2u dss10_18zjqav-1ptahzo dss10_15xo3o6-1rg1oxz dss10_167npd5-12p8k81 dss10_15dowa8-jd0kne dss10_15nohz7-5chwig dss10_171mig2-52pxm8 dss10_17bm451-xjidwl dss10_16hnb24-ngu9n1 dss10_16rmwr3-lv86j1' 49 | 50 | > Snapshot 2 51 | 52 | `[style-sheet-group="10"]{}␊ 53 | .dss10_18pk4lw-ntl2u{test9:nwResize;}␊ 54 | .dss10_18pk4lw-1ptahzo{test9:neResize;}␊ 55 | .dss10_18zjqav-1ptahzo{test8:neResize;}␊ 56 | .dss10_18zjqav-ntl2u{test8:nwResize;}␊ 57 | .dss10_15xo3o6-1rg1oxz{test7:swResize;}␊ 58 | .dss10_15xo3o6-12p8k81{test7:seResize;}␊ 59 | .dss10_167npd5-12p8k81{test6:seResize;}␊ 60 | .dss10_167npd5-1rg1oxz{test6:swResize;}␊ 61 | .dss10_15dowa8-jd0kne{test5:wResize;}␊ 62 | .dss10_15dowa8-5chwig{test5:eResize;}␊ 63 | .dss10_15nohz7-5chwig{test4:eResize;}␊ 64 | .dss10_15nohz7-jd0kne{test4:wResize;}␊ 65 | .dss10_171mig2-52pxm8{test3:left;}␊ 66 | .dss10_171mig2-xjidwl{test3:right;}␊ 67 | .dss10_17bm451-xjidwl{test2:right;}␊ 68 | .dss10_17bm451-52pxm8{test2:left;}␊ 69 | .dss10_16hnb24-ngu9n1{test1:ltr;}␊ 70 | .dss10_16hnb24-lv86j1{test1:rtl;}␊ 71 | .dss10_16rmwr3-lv86j1{test0:rtl;}␊ 72 | .dss10_16rmwr3-ngu9n1{test0:ltr;}` 73 | 74 | ## resolves multiple rules 75 | 76 | > Snapshot 1 77 | 78 | `[style-sheet-group="10"]{}␊ 79 | .dss10_1idvwo2-oyp9nw{border-top-right-radius:10px;}␊ 80 | .dss10_1qlnxpd-7qvd50{border-top-left-radius:10px;}␊ 81 | .dss10_xjidwl-oyp9nw{right:10px;}␊ 82 | .dss10_52pxm8-7qvd50{left:10px;}` 83 | 84 | > Snapshot 2 85 | 86 | `[style-sheet-group="10"]{}␊ 87 | .dss10_1idvwo2-oyp9nw{border-top-right-radius:10px;}␊ 88 | .dss10_1qlnxpd-7qvd50{border-top-left-radius:10px;}␊ 89 | .dss10_xjidwl-oyp9nw{right:10px;}␊ 90 | .dss10_52pxm8-7qvd50{left:10px;}` 91 | -------------------------------------------------------------------------------- /test/snapshots/i18n.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppeg/style-sheet/e71c119ecaccdb2e50be0759b45820b8cbf0c6df/test/snapshots/i18n.js.snap -------------------------------------------------------------------------------- /test/snapshots/index.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/index.js` 2 | 3 | The actual snapshot is saved in `index.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## can use :hover:active 8 | 9 | > Snapshot 1 10 | 11 | [ 12 | 'dss10_j9ctud-1t7uh5u', 13 | 'dss10.7_r6mk6i-1plkpk6', 14 | 'dss10.3_41vss2-b5mm4', 15 | 'dss10.7_2oxbv6-b5mm4', 16 | ] 17 | 18 | > Snapshot 2 19 | 20 | `[style-sheet-group="10"]{}␊ 21 | .dss10_j9ctud-1t7uh5u{display:block;}␊ 22 | [style-sheet-group="10.3"]{}␊ 23 | .dss10\\.3_41vss2-b5mm4:hover{color:green;}␊ 24 | [style-sheet-group="10.7"]{}␊ 25 | .dss10\\.7_2oxbv6-b5mm4:hover:active{color:green;}␊ 26 | .dss10\\.7_r6mk6i-1plkpk6:active{color:white;}` 27 | 28 | ## hashes media queries and descendant selectors 29 | 30 | > Snapshot 1 31 | 32 | `[style-sheet-group="10.3"]{}␊ 33 | .dss10\\.3_41vss2-i0tgik:hover{color:red;}␊ 34 | [style-sheet-group="11"]{}␊ 35 | @media (min-width: 30px){.dss11_3bdajn-i0tgik{color:red;}}` 36 | 37 | ## resolves & 38 | 39 | > Snapshot 1 40 | 41 | [ 42 | 'dss10_h28rbs-i0tgik', 43 | 'dss10.3_41vss2-b5mm4', 44 | 'dss12.3_19npt1s-1let6x', 45 | 'dss11.3_18ibe2l-b5mm4', 46 | 'dss13.3_vtexq3-1let6x', 47 | ] 48 | 49 | > Snapshot 2 50 | 51 | `[style-sheet-group="10"]{}␊ 52 | .dss10_h28rbs-i0tgik{color:red;}␊ 53 | [style-sheet-group="10.3"]{}␊ 54 | .dss10\\.3_41vss2-b5mm4:hover{color:green;}␊ 55 | [style-sheet-group="11.3"]{}␊ 56 | @media (min-width: 30px){.dss11\\.3_18ibe2l-b5mm4:hover{color:green;}}␊ 57 | [style-sheet-group="12.3"]{}␊ 58 | :hover > .dss12\\.3_19npt1s-1let6x{color:yellow;}␊ 59 | [style-sheet-group="13.3"]{}␊ 60 | @media (min-width: 30px){:hover > .dss13\\.3_vtexq3-1let6x{color:yellow;}}` 61 | 62 | ## resolves non unitless numbers 63 | 64 | > Snapshot 1 65 | 66 | `[style-sheet-group="10"]{}␊ 67 | .dss10_1jb7ahf-7qvd50{z-index:10;}␊ 68 | .dss10_aasunh-5m4d0v{padding-bottom:5px;padding-bottom:30px;}␊ 69 | .dss10_u84hpd-epw9f3{padding-top:0;}␊ 70 | .dss10_f69c5w-12635vj{margin-bottom:20px;}␊ 71 | .dss10_1buiceq-7qvd50{margin-top:10px;}` 72 | 73 | ## supports fallback values 74 | 75 | > Snapshot 1 76 | 77 | `[style-sheet-group="10"]{}␊ 78 | .dss10_h28rbs-aulp3c{color:red;color:rgba(255, 0, 0, 1);}` 79 | -------------------------------------------------------------------------------- /test/snapshots/index.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppeg/style-sheet/e71c119ecaccdb2e50be0759b45820b8cbf0c6df/test/snapshots/index.js.snap -------------------------------------------------------------------------------- /test/snapshots/server.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/server.js` 2 | 3 | The actual snapshot is saved in `server.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## does not insert duplicates 8 | 9 | > Snapshot 1 10 | 11 | `[style-sheet-group="0"]{}␊ 12 | .test { color: red }␊ 13 | .test1 { color: green }` 14 | 15 | ## insert @media queries 16 | 17 | > Snapshot 1 18 | 19 | `[style-sheet-group="0"]{}␊ 20 | @media (min-width: 300px) { .test1 { color: red } }␊ 21 | @media (min-width: 300px) { .test1:hover { color: red } }␊ 22 | @media (min-width: 300px) { .test1 > :hover { color: red } }` 23 | 24 | ## inserts rules 25 | 26 | > Snapshot 1 27 | 28 | `[style-sheet-group="0"]{}␊ 29 | .test { color: red }` 30 | -------------------------------------------------------------------------------- /test/snapshots/server.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppeg/style-sheet/e71c119ecaccdb2e50be0759b45820b8cbf0c6df/test/snapshots/server.js.snap -------------------------------------------------------------------------------- /test/validate.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import validate from '../src/validate' 3 | 4 | test('simple object pass validation', t => { 5 | t.notThrows(() => { 6 | validate({ 7 | color: 'red', 8 | }) 9 | }) 10 | }) 11 | 12 | test('throws when using important', t => { 13 | t.throws(() => { 14 | validate({ 15 | color: 'red !important', 16 | }) 17 | }, /important/) 18 | }) 19 | 20 | test('nested: throws when grouping selectors', t => { 21 | t.throws(() => { 22 | validate({ 23 | 'a, b': { 24 | color: 'red', 25 | }, 26 | }) 27 | }, /Selectors cannot be grouped/) 28 | }) 29 | 30 | test('nested: throws when using pseudo elements', t => { 31 | t.throws(() => { 32 | validate({ 33 | ':before': { 34 | color: 'red', 35 | }, 36 | }) 37 | }, /Detected pseudo-element/) 38 | 39 | t.throws(() => { 40 | validate({ 41 | '::after': { 42 | color: 'red', 43 | }, 44 | }) 45 | }, /Detected pseudo-element/) 46 | }) 47 | 48 | test('nested: throws when using an unsupported pseudo-class', t => { 49 | t.throws(() => { 50 | validate({ 51 | '&:matches(.foo)': { 52 | color: 'red', 53 | }, 54 | }) 55 | }, /Detected unsupported pseudo-class/) 56 | }) 57 | 58 | test('nested: throws when using a pseudo-class without &', t => { 59 | t.throws(() => { 60 | validate({ 61 | ':hover': { 62 | color: 'red', 63 | }, 64 | }) 65 | }, /pseudo-class selector should reference its parent/) 66 | }) 67 | 68 | test('nested: the left part of a combinator must be a pseudo-class', t => { 69 | t.throws(() => { 70 | validate({ 71 | 'foo > &': { 72 | color: 'red', 73 | }, 74 | }) 75 | }, /left part of a combinator selector must be a pseudo-class/) 76 | 77 | t.notThrows(() => { 78 | validate({ 79 | ':hover > &': { 80 | color: 'red', 81 | }, 82 | }) 83 | }) 84 | }) 85 | 86 | test('nested: the right part of a combinator must be &', t => { 87 | t.throws(() => { 88 | validate({ 89 | ':hover > foo': { 90 | color: 'red', 91 | }, 92 | }) 93 | }, /right part of a combinator selector must be `&`/) 94 | 95 | t.notThrows(() => { 96 | validate({ 97 | ':hover > &': { 98 | color: 'red', 99 | }, 100 | }) 101 | }) 102 | }) 103 | 104 | test('nested: does not allow nested selectors', t => { 105 | t.throws(() => { 106 | validate({ 107 | 'foo bar': { 108 | color: 'red', 109 | }, 110 | }) 111 | }, /Complex selectors are not supported/) 112 | }) 113 | 114 | test('nested: media queries work', t => { 115 | t.notThrows(() => { 116 | validate({ 117 | '@media (min-width: 30px)': { 118 | color: 'red', 119 | }, 120 | }) 121 | }) 122 | }) 123 | 124 | test('nested: throws with invalid nested inside of media queries', t => { 125 | t.throws(() => { 126 | validate({ 127 | '@media (min-width: 30px)': { 128 | ':hover > foo': { 129 | color: 'red', 130 | }, 131 | }, 132 | }) 133 | }, /right part of a combinator selector must be `&`/) 134 | }) 135 | 136 | test('works with array values', t => { 137 | t.notThrows(() => { 138 | validate({ 139 | color: ['red', 'rgba(255, 0, 0, 1)'], 140 | }) 141 | }) 142 | }) 143 | --------------------------------------------------------------------------------