├── .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 |
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 |
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 |
--------------------------------------------------------------------------------