├── .codeclimate.yml ├── src ├── utils │ ├── namespace.js │ ├── isDOMReady.js │ └── appendStyle.js ├── index.js └── components │ ├── StyleCacheProvider.js │ ├── ClientStyle.js │ ├── __tests__ │ ├── __snapshots__ │ │ └── UniversalStyle-test.js.snap │ ├── ClientStyle-test.js │ └── UniversalStyle-test.js │ └── UniversalStyle.js ├── .prettierrc ├── .travis.yml ├── .gitignore ├── .babelrc ├── LICENSE ├── .eslintrc ├── .vscode └── settings.json ├── package.json ├── README.md └── yarn.lock /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | Ruby: true 3 | JavaScript: true 4 | PHP: true 5 | Python: true 6 | exclude_paths: 7 | - "src/__tests__/**/*" -------------------------------------------------------------------------------- /src/utils/namespace.js: -------------------------------------------------------------------------------- 1 | export const CONTEXT_NAMESPACE = '__react_css_component_cache' 2 | export const ID_NAMESPACE = '__react_css_component_id-' 3 | -------------------------------------------------------------------------------- /src/utils/isDOMReady.js: -------------------------------------------------------------------------------- 1 | export default function isDOMReady() { 2 | return ( 3 | typeof window !== 'undefined' && 4 | typeof document !== 'undefined' && 5 | document.head 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | trailingComma: "es5", 3 | singleQuote: true, 4 | bracketSpacing: true, 5 | jsxBracketSameLine: true, 6 | parser: "babylon", 7 | printWidth: 80, 8 | tabWidth: 2, 9 | useTabs: false, 10 | semi: false 11 | } 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import StyleCacheProvider from './components/StyleCacheProvider' 2 | import UniversalStyle from './components/UniversalStyle' 3 | import ClientStyle from './components/ClientStyle' 4 | 5 | export { StyleCacheProvider, UniversalStyle, ClientStyle } 6 | -------------------------------------------------------------------------------- /src/utils/appendStyle.js: -------------------------------------------------------------------------------- 1 | export default function appendStyle(id, css) { 2 | if (!document.head.querySelector('#' + id)) { 3 | const node = document.createElement('style') 4 | node.textContent = css 5 | node.type = 'text/css' 6 | node.id = id 7 | 8 | document.head.appendChild(node) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | script: 5 | - npm run check 6 | addons: 7 | code_climate: 8 | repo_token: b9b1d75d35a4a0abd6cefa9934564d22166b2ff346af0730d20dc70c92af5da8 9 | before_script: 10 | - npm run setup 11 | after_script: 12 | - codeclimate-test-reporter < coverage/lcov.info 13 | notifications: 14 | email: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS or Editor files 2 | ._* 3 | .DS_Store 4 | Thumbs.db 5 | 6 | # Files that might appear on external disks 7 | .Spotlight-V100 8 | .Trashes 9 | 10 | # Always-ignore extensions 11 | *~ 12 | *.diff 13 | *.err 14 | *.log 15 | *.orig 16 | *.pyc 17 | *.rej 18 | *.sass-cache 19 | *.sw? 20 | *.vi 21 | 22 | node_modules 23 | es 24 | coverage 25 | lib 26 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "stage-0", 10 | "react" 11 | ], 12 | "plugins": [ 13 | "transform-class-properties" 14 | ], 15 | "env": { 16 | "commonjs": { 17 | "plugins": [ 18 | "transform-es2015-modules-commonjs" 19 | ] 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/components/StyleCacheProvider.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { CONTEXT_NAMESPACE } from '../utils/namespace' 5 | 6 | export default class StyleCacheProvider extends Component { 7 | constructor(props, context) { 8 | super(props, context) 9 | this.cache = {} 10 | } 11 | 12 | getChildContext() { 13 | return { 14 | [CONTEXT_NAMESPACE]: this.cache, 15 | } 16 | } 17 | 18 | render() { 19 | return this.props.children 20 | } 21 | } 22 | 23 | StyleCacheProvider.childContextTypes = { 24 | [CONTEXT_NAMESPACE]: PropTypes.object, 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ClientStyle.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | 3 | import { ID_NAMESPACE } from '../utils/namespace' 4 | import appendStyle from '../utils/appendStyle' 5 | import isDOMReady from '../utils/isDOMReady' 6 | 7 | const idCache = {} 8 | 9 | export default class ClientStyle extends Component { 10 | constructor(props, context) { 11 | super(props, context) 12 | 13 | if (!idCache[props.css]) { 14 | // generating a unique style id to prevent duplicate nodes 15 | // within client-sides document.head 16 | const uniqueId = Object.keys(idCache).length 17 | idCache[props.css] = ID_NAMESPACE + uniqueId 18 | } 19 | 20 | if (isDOMReady()) { 21 | appendStyle(idCache[props.css], props.css) 22 | this.isReady = true 23 | } 24 | } 25 | 26 | componentDidMount() { 27 | if (!this.isReady && isDOMReady()) { 28 | appendStyle(idCache[this.props.css], this.props.css) 29 | } 30 | } 31 | 32 | render() { 33 | return null 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/UniversalStyle-test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`UniversalComponent Component should move each different style to the document.head if the component unmounts 1`] = `""`; 4 | 5 | exports[`UniversalComponent Component should move the style to the document.head if the component unmounts 1`] = `""`; 6 | 7 | exports[`UniversalComponent Component should only move the style once to the document.head 1`] = `""`; 8 | 9 | exports[`UniversalComponent Component should render a style element on the client 1`] = `""`; 10 | 11 | exports[`UniversalComponent Component should render a style element on the server 1`] = `""`; 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Robin Frischmann 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 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "airbnb" ], 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "plugins": [ 10 | "flowtype" 11 | ], 12 | "rules": { 13 | "semi": [ 2, "never" ], 14 | "object-curly-newline": [ 0 ], 15 | "object-property-newline": [ 0 ], 16 | "comma-dangle": [ 0 ], 17 | "react/jsx-filename-extension": [2, { 18 | "extensions": [".js", ".jsx"] 19 | }], 20 | "import/no-extraneous-dependencies": [ 0 ], 21 | "react/prop-types": [ 0 ], 22 | "no-confusing-arrow": [ 0 ], 23 | "no-underscore-dangle": [ 0 ], 24 | "no-param-reassign": [ 0 ], 25 | "react/forbid-prop-types": [ 0 ], 26 | "no-plusplus": [ 0 ], 27 | "guard-for-in": [ 0 ], 28 | "no-restricted-syntax": [ 0 ], 29 | "no-continue": [ 1 ], 30 | "no-prototype-builtins": [ 0 ], 31 | "max-len": [ 0, 80 ], 32 | "no-mixed-operators": [ 0 ], 33 | "no-lonely-if": [ 1 ], 34 | "no-bitwise": [ 0 ], 35 | "arrow-parens": [ 0 ], 36 | "function-paren-newline": [ 0 ], 37 | "prefer-destructuring": [ 0 ], 38 | "prefer-template": [ 0 ], 39 | "radix": [ 0 ] 40 | } 41 | } -------------------------------------------------------------------------------- /src/components/__tests__/ClientStyle-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { renderToString } from 'react-dom/server' 4 | import { JSDOM } from 'jsdom' 5 | 6 | import Style from '../ClientStyle' 7 | 8 | afterEach(() => { 9 | global.window = undefined 10 | global.document = undefined 11 | }) 12 | 13 | describe('ClientStyle Component', () => { 14 | it('should render nothing, but apply a style node to the document.head', () => { 15 | const { window } = new JSDOM('') 16 | 17 | global.window = window 18 | global.document = window.document 19 | 20 | const div = document.createElement('div') 21 | const css = '.class1 { color: red } .class2 { color: blue }' 22 | 23 | render(