diff --git a/index.js b/index.js new file mode 100644 index 00000000..2d2f3c0b --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./dist') diff --git a/package.json b/package.json index c1ff298d..a3e2692e 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "build": "babel src --out-dir dist", "test": "ava", "lint": "xo", - "prepublishOnly": "yarn build && yarn test && yarn lint --quiet" + "prepublishOnly": "rm -rf dist && yarn build" }, "husky": { "hooks": { diff --git a/readme.md b/readme.md index cd5f80ff..524943e6 100644 --- a/readme.md +++ b/readme.md @@ -11,38 +11,37 @@ Code and docs are for v3 which we highly recommend you to try. Looking for style - [Getting started](#getting-started) - [Configuration options](#configuration-options) - * [`optimizeForSpeed`](#optimizeforspeed) - * [`sourceMaps`](#sourcemaps) - * [`styleModule`](#stylemodule) - * [`vendorPrefixes`](#vendorprefixes) + - [`optimizeForSpeed`](#optimizeforspeed) + - [`sourceMaps`](#sourcemaps) + - [`styleModule`](#stylemodule) + - [`vendorPrefixes`](#vendorprefixes) - [Features](#features) - [How It Works](#how-it-works) - * [Why It Works Like This](#why-it-works-like-this) + - [Why It Works Like This](#why-it-works-like-this) - [Targeting The Root](#targeting-the-root) - [Global styles](#global-styles) - * [One-off global selectors](#one-off-global-selectors) + - [One-off global selectors](#one-off-global-selectors) - [Dynamic styles](#dynamic-styles) - * [Via interpolated dynamic props](#via-interpolated-dynamic-props) - * [Via `className` toggling](#via-classname-toggling) - * [Via inline `style`](#via-inline-style) + - [Via interpolated dynamic props](#via-interpolated-dynamic-props) + - [Via `className` toggling](#via-classname-toggling) + - [Via inline `style`](#via-inline-style) - [Constants](#constants) - [Server-Side Rendering](#server-side-rendering) - * [`styled-jsx/server`](#styled-jsxserver) - [External CSS and styles outside of the component](#external-css-and-styles-outside-of-the-component) - * [External styles](#external-styles) - * [Styles outside of components](#styles-outside-of-components) - * [The `resolve` tag](#the-resolve-tag) - * [Styles in regular CSS files](#styles-in-regular-css-files) + - [External styles](#external-styles) + - [Styles outside of components](#styles-outside-of-components) + - [The `resolve` tag](#the-resolve-tag) + - [Styles in regular CSS files](#styles-in-regular-css-files) - [CSS Preprocessing via Plugins](#css-preprocessing-via-plugins) - * [Plugin options](#plugin-options) - * [Example plugins](#example-plugins) + - [Plugin options](#plugin-options) + - [Example plugins](#example-plugins) - [Rendering in tests](#rendering-in-tests) - [FAQ](#faq) - * [Warning: unknown `jsx` prop on <style> tag](#warning-unknown-jsx-prop-on-style-tag) - * [Can I return an array of components when using React 16?](#can-i-return-an-array-of-components-when-using-react-16) - * [Styling third parties / child components from the parent](#styling-third-parties--child-components-from-the-parent) - * [Some styles are missing in production](https://github.com/zeit/styled-jsx/issues/319#issuecomment-349239326) - * [Build a component library with styled-jsx](#build-a-component-library-with-styled-jsx) + - [Warning: unknown `jsx` prop on <style> tag](#warning-unknown-jsx-prop-on-style-tag) + - [Can I return an array of components when using React 16?](#can-i-return-an-array-of-components-when-using-react-16) + - [Styling third parties / child components from the parent](#styling-third-parties--child-components-from-the-parent) + - [Some styles are missing in production](https://github.com/zeit/styled-jsx/issues/319#issuecomment-349239326) + - [Build a component library with styled-jsx](#build-a-component-library-with-styled-jsx) - [Syntax Highlighting](#syntax-highlighting) ## Getting started @@ -57,9 +56,7 @@ Next, add `styled-jsx/babel` to `plugins` in your babel configuration: ```json { - "plugins": [ - "styled-jsx/babel" - ] + "plugins": ["styled-jsx/babel"] } ``` @@ -70,8 +67,8 @@ export default () => (

only this paragraph will get the style :)

- { /* you can include s here that include - other

s that don't get unexpected styles! */ } + {/* you can include s here that include + other

s that don't get unexpected styles! */}

@@ -198,7 +193,7 @@ the global styles being inserted multiple times. Sometimes it's useful to skip selectors scoping. In order to get a one-off global selector we support `:global()`, inspired by [css-modules](https://github.com/css-modules/css-modules). -This is very useful in order to, for example, generate a *global class* that +This is very useful in order to, for example, generate a _global class_ that you can pass to 3rd-party components. For example, to style `react-select` which supports passing a custom class via `optionClassName`: @@ -212,7 +207,7 @@ export default () => ( /* "div" will be prefixed, but ".react-select" won't */ div :global(.react-select) { - color: red + color: red; } `} @@ -228,18 +223,18 @@ To make a component's visual representation customizable from the outside world Any value that comes from the component's `render` method scope is treated as dynamic. This makes it possible to use `props` and `state` for example. ```jsx -const Button = (props) => ( +const Button = props => ( ) ``` @@ -249,22 +244,22 @@ New styles' injection is optimized to perform well at runtime. That said when your CSS is mostly static we recommend to split it up in static and dynamic styles and use two separate `style` tags so that, when changing, only the dynamic parts are recomputed/rendered. ```jsx -const Button = (props) => ( +const Button = props => ( ) ``` @@ -274,19 +269,19 @@ const Button = (props) => ( The second option is to pass properties that toggle class names. ```jsx -const Button = (props) => ( - ) ``` @@ -302,14 +297,14 @@ Imagine that you wanted to make the padding in the button above completely custo ```jsx const Button = ({ padding, children }) => ( ) ``` @@ -326,14 +321,14 @@ import { invertColor } from '../theme/utils' const Button = ({ children }) => ( ) ``` @@ -342,48 +337,31 @@ Please keep in mind that constants defined outside of the component scope are tr ## Server-Side Rendering -### `styled-jsx/server` - -The main export flushes your styles to an array of `React.Element`: - ```jsx import React from 'react' import ReactDOM from 'react-dom/server' -import flush from 'styled-jsx/server' +import { StyleRegistry, useStyles } from 'styled-jsx' import App from './app' -export default (req, res) => { - const app = ReactDOM.renderToString() - const styles = flush() - const html = ReactDOM.renderToStaticMarkup( - { styles } - -
- - ) - res.end('' + html) +function Head() { + const registry = useStyleRegistry() + const styles = registry.styles() + return {styles} } -``` - -We also expose `flushToHTML` to return generated HTML: - -```jsx -import React from 'react' -import ReactDOM from 'react-dom/server' -import { flushToHTML } from 'styled-jsx/server' -import App from './app' export default (req, res) => { const app = ReactDOM.renderToString() - const styles = flushToHTML() - const html = ` - - ${styles} - -
${app}
- - ` - res.end(html) + const html = ReactDOM.renderToStaticMarkup( + + + + +
+ + + + ) + res.end('' + html) } ``` @@ -396,6 +374,7 @@ duplicate styles are avoided. Strict [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) is supported. You should generate a nonce **per request**. + ```js import nanoid from 'nanoid' @@ -411,9 +390,9 @@ Your CSP policy must share the same nonce as well (the header nonce needs to mat In styled-jsx styles can be defined outside of the component's render method or in separate JavaScript modules using the `styled-jsx/css` library. `styled-jsx/css` exports three tags that can be used to tag your styles: -* `css`, the default export, to define scoped styles. -* `css.global` to define global styles. -* `css.resolve` to define scoped styles that resolve to the scoped `className` and a `styles` element. +- `css`, the default export, to define scoped styles. +- `css.global` to define global styles. +- `css.resolve` to define scoped styles that resolve to the scoped `className` and a `styles` element. #### External styles @@ -424,7 +403,11 @@ In an external file: import css from 'styled-jsx/css' // Scoped styles -export const button = css`button { color: hotpink; }` +export const button = css` + button { + color: hotpink; + } +` // Global styles export const body = css.global`body { margin: 0; }` @@ -435,7 +418,11 @@ export const link = css.resolve`a { color: green; }` // link.styles -> styles element to render inside of your component // Works also with default exports -export default css`div { color: green; }` +export default css` + div { + color: green; + } +` ``` You can then import and use those styles: @@ -448,7 +435,9 @@ export default () => ( - +
) ``` @@ -478,7 +467,11 @@ export default () => (
) -const button = css`button { color: hotpink; }` +const button = css` + button { + color: hotpink; + } +` ``` Like in externals styles `css` doesn't work with dynamic styles. If you have dynamic parts you might want to place them inline inside of your component using a regular ` + {alt} + ) ``` @@ -900,7 +906,7 @@ export default () => ( /* "div" will be prefixed, but ".nested-element" won't */ div > :global(.nested-element) { - color: red + color: red; } `} @@ -917,7 +923,7 @@ There's an [article](https://medium.com/@tomaszmularczyk89/guide-to-building-a-r When working with template literals a common drawback is missing syntax highlighting. The following editors currently have support for highlighting CSS inside ` + {children} + + {/*language=CSS*/} + ) ``` ### Emmet - If you're using Emmet you can add the following snippet to `~/emmet/snippets-styledjsx.json` This will allow you to expand `style-jsx` to a styled-jsx block. +If you're using Emmet you can add the following snippet to `~/emmet/snippets-styledjsx.json` This will allow you to expand `style-jsx` to a styled-jsx block. - ```json - { +```json +{ "html": { "snippets": { "style-jsx": "" @@ -974,18 +980,23 @@ const Button = ({ children }) => ( ``` ### Syntax Highlighting [Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=Divlo.vscode-styled-jsx-syntax) + Launch VS Code Quick Open (⌘+P), paste the following command, and press enter. + ``` ext install Divlo.vscode-styled-jsx-syntax ``` If you use Stylus instead of plain CSS, install [vscode-styled-jsx-stylus](https://marketplace.visualstudio.com/items?itemName=samuelroy.vscode-styled-jsx-stylus) or paste the command below. + ``` ext install vscode-styled-jsx-stylus ``` ### Autocomplete [Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=Divlo.vscode-styled-jsx-languageserver) + Launch VS Code Quick Open (⌘+P), paste the following command, and press enter. + ``` ext install Divlo.vscode-styled-jsx-languageserver ``` @@ -995,6 +1006,7 @@ ext install Divlo.vscode-styled-jsx-languageserver Install [vim-styled-jsx](https://github.com/alampros/vim-styled-jsx) with your plugin manager of choice. ## ESLint + If you're using `eslint-plugin-import`, the `css` import will generate errors, being that it's a "magic" import (not listed in package.json). To avoid these, simply add the following line to your eslint configuration: ``` diff --git a/server.js b/server.js deleted file mode 100644 index 3b4b9b0e..00000000 --- a/server.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/server') diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..cb4fe737 --- /dev/null +++ b/src/index.js @@ -0,0 +1 @@ +export { StyleRegistry, useStyleRegistry } from './stylesheet-registry' diff --git a/src/server.js b/src/server.js deleted file mode 100644 index 19e833f2..00000000 --- a/src/server.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react' -import { flush } from './style' - -export default function flushToReact(options = {}) { - return flush().map(args => { - const id = args[0] - const css = args[1] - return React.createElement('style', { - id: `__${id}`, - // Avoid warnings upon render with a key - key: `__${id}`, - nonce: options.nonce ? options.nonce : undefined, - dangerouslySetInnerHTML: { - __html: css - } - }) - }) -} - -export function flushToHTML(options = {}) { - return flush().reduce((html, args) => { - const id = args[0] - const css = args[1] - html += `` - return html - }, '') -} diff --git a/src/style.js b/src/style.js index 3a08f433..2d0c6bc2 100644 --- a/src/style.js +++ b/src/style.js @@ -1,17 +1,16 @@ -import { useLayoutEffect } from 'react' -import StyleSheetRegistry from './stylesheet-registry' - -const styleSheetRegistry = new StyleSheetRegistry() +import { useLayoutEffect, useContext } from 'react' +import { StyleSheetContext } from './stylesheet-registry' export default function JSXStyle(props) { + const registry = useContext(StyleSheetContext) if (typeof window === 'undefined') { - styleSheetRegistry.add(props) + registry.add(props) return null } useLayoutEffect(() => { - styleSheetRegistry.add(props) + registry.add(props) return () => { - styleSheetRegistry.remove(props) + registry.remove(props) } // props.children can be string[], will be striped since id is identical }, [props.id, String(props.dynamic)]) @@ -19,17 +18,12 @@ export default function JSXStyle(props) { } JSXStyle.dynamic = info => { + const registry = useContext(StyleSheetContext) return info .map(tagInfo => { const baseId = tagInfo[0] const props = tagInfo[1] - return styleSheetRegistry.computeId(baseId, props) + return registry.computeId(baseId, props) }) .join(' ') } - -export function flush() { - const cssRules = styleSheetRegistry.cssRules() - styleSheetRegistry.flush() - return cssRules -} diff --git a/src/stylesheet-registry.js b/src/stylesheet-registry.js index a3e2593f..cbbaff97 100644 --- a/src/stylesheet-registry.js +++ b/src/stylesheet-registry.js @@ -1,8 +1,10 @@ +import React, { useState, useContext, createContext, useMemo } from 'react' import hashString from 'string-hash' import DefaultStyleSheet from './lib/stylesheet' const sanitize = rule => rule.replace(/\/style/gi, '\\/style') -export default class StyleSheetRegistry { + +export class StyleSheetRegistry { constructor({ styleSheet = null, optimizeForSpeed = false, @@ -217,3 +219,50 @@ function invariant(condition, message) { throw new Error(`StyleSheetRegistry: ${message}.`) } } + +export const StyleSheetContext = createContext(new StyleSheetRegistry()) + +export function StyleRegistry({ children }) { + const rootRegistry = useContext(StyleSheetContext) + const registry = useState(() => rootRegistry || new StyleSheetRegistry()) + + return React.createElement( + StyleSheetContext.Provider, + { value: registry }, + children + ) +} + +export function useStyleRegistry() { + const registry = useContext(StyleSheetContext) + + return useMemo( + () => ({ + styles() { + return mapRulesToStyle(registry.cssRules()) + }, + flush() { + registry.flush() + } + }), + [registry] + ) +} + +function mapRulesToStyle(options = {}) { + const registry = useStyleRegistry() + const cssRules = registry.styles() + return cssRules.map(args => { + const id = args[0] + const css = args[1] + return React.createElement('style', { + id: `__${id}`, + // Avoid warnings upon render with a key + key: `__${id}`, + nonce: options.nonce ? options.nonce : undefined, + dangerouslySetInnerHTML: { + __html: css + } + }) + }) +} diff --git a/test/index.js b/test/index.js index d0dfc89c..06969e07 100644 --- a/test/index.js +++ b/test/index.js @@ -5,8 +5,44 @@ import ReactDOM from 'react-dom/server' // Ours import plugin from '../src/babel' +import JSXStyle from '../src/style' +import { + StyleSheetRegistry, + StyleSheetContext +} from '../src/stylesheet-registry' import _transform, { transformSource as _transformSource } from './_transform' +const flushToHTML = (registry, options = {}) => { + const cssRules = registry.cssRules() + registry.flush() + return cssRules.reduce((html, args) => { + const id = args[0] + const css = args[1] + html += `` + return html + }, '') +} + +function flushToReact(registry, options = {}) { + const cssRules = registry.cssRules() + registry.flush() + return cssRules.map(args => { + const id = args[0] + const css = args[1] + return React.createElement('style', { + id: `__${id}`, + // Avoid warnings upon render with a key + key: `__${id}`, + nonce: options.nonce ? options.nonce : undefined, + dangerouslySetInnerHTML: { + __html: css + } + }) + }) +} + const transform = (file, opts = {}) => _transform(file, { plugins: [plugin], @@ -143,18 +179,39 @@ test('works with exported non-jsx style (CommonJS modules)', async t => { t.snapshot(code) }) -function clearModulesCache() { - ;['../src/lib/stylesheet', '../src/style', '../src/server'].forEach( - moduleName => { - delete require.cache[require.resolve(moduleName)] - } +test('sever rendering with hook api', t => { + function App() { + const color = 'green' + return React.createElement( + 'div', + null, + React.createElement(JSXStyle, { id: 2 }, 'div { color: blue }'), + React.createElement(JSXStyle, { id: 3 }, `div { color: ${color} }`) + ) + } + + // Expected CSS + const expected = + '' + + '' + + const registry = new StyleSheetRegistry() + const createApp = () => + React.createElement( + StyleSheetContext.Provider, + { value: registry }, + React.createElement(App) + ) + + // Render using react + ReactDOM.renderToString(createApp()) + const html = ReactDOM.renderToStaticMarkup( + React.createElement('head', null, flushToReact(registry)) ) -} + t.is(html, `${expected}`) +}) test('server rendering', t => { - clearModulesCache() - const JSXStyle = require('../src/style').default - const { default: flush, flushToHTML } = require('../src/server') function App() { const color = 'green' return React.createElement( @@ -190,31 +247,36 @@ test('server rendering', t => { '' + '' + const registry = new StyleSheetRegistry() + const createApp = () => + React.createElement( + StyleSheetContext.Provider, + { value: registry }, + React.createElement(App) + ) + // Render using react - ReactDOM.renderToString(React.createElement(App)) + ReactDOM.renderToString(createApp()) const html = ReactDOM.renderToStaticMarkup( - React.createElement('head', null, flush()) + React.createElement('head', null, flushToReact(registry)) ) t.is(html, `${expected}`) // Assert that memory is empty - t.is(0, flush().length) - t.is('', flushToHTML()) + t.is(0, registry.cssRules().length) + t.is('', flushToHTML(registry)) // Render to html again - ReactDOM.renderToString(React.createElement(App)) - t.is(expected, flushToHTML()) + ReactDOM.renderToString(createApp()) + t.is(expected, flushToHTML(registry)) // Assert that memory is empty - t.is(0, flush().length) - t.is('', flushToHTML()) + t.is(0, flushToReact(registry).length) + t.is('', flushToHTML(registry)) }) test('server rendering with nonce', t => { - clearModulesCache() - const JSXStyle = require('../src/style').default - const { default: flush, flushToHTML } = require('../src/server') function App() { const color = 'green' return React.createElement( @@ -244,6 +306,14 @@ test('server rendering with nonce', t => { ) } + const registry = new StyleSheetRegistry() + const createApp = () => + React.createElement( + StyleSheetContext.Provider, + { value: registry }, + React.createElement(App) + ) + // Expected CSS const expected = '' + @@ -251,30 +321,31 @@ test('server rendering with nonce', t => { '' // Render using react - ReactDOM.renderToString(React.createElement(App)) + ReactDOM.renderToString(createApp()) const html = ReactDOM.renderToStaticMarkup( - React.createElement('head', null, flush({ nonce: 'test-nonce' })) + React.createElement( + 'head', + null, + flushToReact(registry, { nonce: 'test-nonce' }) + ) ) t.is(html, `${expected}`) // Assert that memory is empty - t.is(0, flush({ nonce: 'test-nonce' }).length) - t.is('', flushToHTML({ nonce: 'test-nonce' })) + t.is(0, flushToReact(registry, { nonce: 'test-nonce' }).length) + t.is('', flushToHTML(registry, { nonce: 'test-nonce' })) // Render to html again - ReactDOM.renderToString(React.createElement(App)) - t.is(expected, flushToHTML({ nonce: 'test-nonce' })) + ReactDOM.renderToString(createApp()) + t.is(expected, flushToHTML(registry, { nonce: 'test-nonce' })) // Assert that memory is empty - t.is(0, flush({ nonce: 'test-nonce' }).length) - t.is('', flushToHTML({ nonce: 'test-nonce' })) + t.is(0, flushToReact(registry, { nonce: 'test-nonce' }).length) + t.is('', flushToHTML(registry, { nonce: 'test-nonce' })) }) test('optimized styles do not contain new lines', t => { - clearModulesCache() - const JSXStyle = require('../src/style').default - const { default: flush } = require('../src/server') function App() { return React.createElement( 'div', @@ -289,9 +360,17 @@ test('optimized styles do not contain new lines', t => { ) } - ReactDOM.renderToString(React.createElement(App)) + const registry = new StyleSheetRegistry() + const createApp = () => + React.createElement( + StyleSheetContext.Provider, + { value: registry }, + React.createElement(App) + ) + + ReactDOM.renderToString(createApp()) const html = ReactDOM.renderToStaticMarkup( - React.createElement('head', null, flush()) + React.createElement('head', null, flushToReact(registry)) ) const expected = '' diff --git a/test/snapshots/attribute.js.snap b/test/snapshots/attribute.js.snap index 61300916..b6a89e61 100644 Binary files a/test/snapshots/attribute.js.snap and b/test/snapshots/attribute.js.snap differ diff --git a/test/snapshots/external.js.snap b/test/snapshots/external.js.snap index 83c58757..4ef41ea0 100644 Binary files a/test/snapshots/external.js.snap and b/test/snapshots/external.js.snap differ diff --git a/test/snapshots/index.js.snap b/test/snapshots/index.js.snap index ff80a432..aa6e036c 100644 Binary files a/test/snapshots/index.js.snap and b/test/snapshots/index.js.snap differ diff --git a/test/snapshots/plugins.js.snap b/test/snapshots/plugins.js.snap index 0ecb63e0..3ddf9d22 100644 Binary files a/test/snapshots/plugins.js.snap and b/test/snapshots/plugins.js.snap differ diff --git a/test/stylesheet-registry.js b/test/stylesheet-registry.js index 12c06840..2dca45b7 100644 --- a/test/stylesheet-registry.js +++ b/test/stylesheet-registry.js @@ -2,7 +2,7 @@ import test from 'ava' // Ours -import StyleSheetRegistry from '../src/stylesheet-registry' +import { StyleSheetRegistry } from '../src/stylesheet-registry' import makeSheet, { invalidRules } from './stylesheet' import withMock, { withMockDocument } from './helpers/with-mock'