From f15dc2dd8f3dc75b3628072a8ea957514d3f6bda Mon Sep 17 00:00:00 2001 From: Davis <11590024+dav-is@users.noreply.github.com> Date: Tue, 28 Aug 2018 11:00:15 -0500 Subject: [PATCH 1/8] Support setting a nonce --- readme.md | 14 +++++++++++ src/lib/stylesheet.js | 18 ++++++++------ src/server.js | 9 ++++--- test/index.js | 47 +++++++++++++++++++++++++++++++++++++ test/stylesheet-registry.js | 20 ++++++++++++++++ 5 files changed, 98 insertions(+), 10 deletions(-) diff --git a/readme.md b/readme.md index 5ae1fae3..7bb65bdf 100644 --- a/readme.md +++ b/readme.md @@ -385,6 +385,20 @@ It's **paramount** that you use one of these two functions so that the generated styles can be diffed when the client loads and duplicate styles are avoided. +### Content Security Policy + +Strict CSP is supported. You must pass a nonce as a parameter to either `flush(nonce)` or `flushToHTML(nonce)` **and** set a `` tag. + +You should generate a nonce per request. +```jsx +import uuidv4 from 'uuid/v4' + +const nonce = new Buffer(uuidv4()).toString('base64') //ex: N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3 +``` + +Your CSP policy must have this same nonce as well. +`Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3';` + ### External CSS and styles outside of the component 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: diff --git a/src/lib/stylesheet.js b/src/lib/stylesheet.js index 39b60963..001def73 100644 --- a/src/lib/stylesheet.js +++ b/src/lib/stylesheet.js @@ -7,13 +7,11 @@ const isProd = process.env && process.env.NODE_ENV === 'production' const isString = o => Object.prototype.toString.call(o) === '[object String]' export default class StyleSheet { - constructor( - { - name = 'stylesheet', - optimizeForSpeed = isProd, - isBrowser = typeof window !== 'undefined' - } = {} - ) { + constructor({ + name = 'stylesheet', + optimizeForSpeed = isProd, + isBrowser = typeof window !== 'undefined' + } = {}) { invariant(isString(name), '`name` must be a string') this._name = name this._deletedRulePlaceholder = `#${name}-deleted-rule____{}` @@ -29,6 +27,11 @@ export default class StyleSheet { this._tags = [] this._injected = false this._rulesCount = 0 + + const node = + typeof document === 'object' && + document.querySelector('meta[property="csp-nonce"]') + this._nonce = node ? node.getAttribute('content') : null } setOptimizeForSpeed(bool) { @@ -225,6 +228,7 @@ export default class StyleSheet { ) } const tag = document.createElement('style') + if (this._nonce) tag.setAttribute('nonce', this._nonce) tag.type = 'text/css' tag.setAttribute(`data-${name}`, '') if (cssString) { diff --git a/src/server.js b/src/server.js index f650169a..3db50ce9 100644 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,7 @@ import React from 'react' import { flush } from './style' -export default function flushToReact() { +export default function flushToReact(nonce) { return flush().map(args => { const id = args[0] const css = args[1] @@ -9,6 +9,7 @@ export default function flushToReact() { id: `__${id}`, // Avoid warnings upon render with a key key: `__${id}`, + nonce: nonce ? nonce : undefined, dangerouslySetInnerHTML: { __html: css } @@ -16,11 +17,13 @@ export default function flushToReact() { }) } -export function flushToHTML() { +export function flushToHTML(nonce) { return flush().reduce((html, args) => { const id = args[0] const css = args[1] - html += `` + html += `` return html }, '') } diff --git a/test/index.js b/test/index.js index d27f3130..f51363e8 100644 --- a/test/index.js +++ b/test/index.js @@ -145,3 +145,50 @@ test('server rendering', t => { t.is(0, flush().length) t.is('', flushToHTML()) }) + +test('server rendering with nonce', t => { + function App() { + const color = 'green' + return React.createElement( + 'div', + null, + React.createElement(JSXStyle, { + css: 'p { color: red }', + styleId: 1 + }), + React.createElement(JSXStyle, { + css: 'div { color: blue }', + styleId: 2 + }), + React.createElement(JSXStyle, { + css: `div { color: ${color} }`, + styleId: 3 + }) + ) + } + // Expected CSS + const expected = + '' + + '' + + '' + + // Render using react + ReactDOM.renderToString(React.createElement(App)) + const html = ReactDOM.renderToStaticMarkup( + React.createElement('head', null, flush('test-nonce')) + ) + + t.is(html, `${expected}`) + + // Assert that memory is empty + t.is(0, flush('test-nonce').length) + t.is('', flushToHTML('test-nonce')) + + // Render to html again + ReactDOM.renderToString(React.createElement(App)) + t.is(expected, flushToHTML('test-nonce')) + + // Assert that memory is empty + t.is(0, flush('test-nonce').length) + t.is('', flushToHTML('test-nonce')) +}) diff --git a/test/stylesheet-registry.js b/test/stylesheet-registry.js index 874ece75..2cffcbbb 100644 --- a/test/stylesheet-registry.js +++ b/test/stylesheet-registry.js @@ -104,6 +104,26 @@ test('add - sanitizes dynamic CSS on the server', t => { ]) }) +test('add - nonce is properly fetched from meta tag', t => { + // We need to stub a document in order to simulate the meta tag + global.document = { + querySelector(query) { + t.is(query, 'meta[property="csp-nonce"]') + return { + getAttribute(attr) { + t.is(attr, 'content') + return 'test-nonce' + } + } + } + } + + const registry = makeRegistry() + registry.add({ styleId: '123', css: [cssRule] }) + + t.is(registry._sheet._nonce, 'test-nonce') +}) + // registry.remove test('remove', t => { From bb98185aec9c9bdac8220601dbed752247ada1bf Mon Sep 17 00:00:00 2001 From: Davis <11590024+dav-is@users.noreply.github.com> Date: Tue, 28 Aug 2018 17:23:04 -0500 Subject: [PATCH 2/8] Parameter is not required --- src/server.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server.js b/src/server.js index 3db50ce9..70e1d234 100644 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,7 @@ import React from 'react' import { flush } from './style' -export default function flushToReact(nonce) { +export default function flushToReact(nonce = undefined) { return flush().map(args => { const id = args[0] const css = args[1] @@ -9,7 +9,7 @@ export default function flushToReact(nonce) { id: `__${id}`, // Avoid warnings upon render with a key key: `__${id}`, - nonce: nonce ? nonce : undefined, + nonce, dangerouslySetInnerHTML: { __html: css } @@ -17,7 +17,7 @@ export default function flushToReact(nonce) { }) } -export function flushToHTML(nonce) { +export function flushToHTML(nonce = undefined) { return flush().reduce((html, args) => { const id = args[0] const css = args[1] From fe01906f1c9051dfe8de2d0e9417e4e96db2da83 Mon Sep 17 00:00:00 2001 From: Davis <11590024+dav-is@users.noreply.github.com> Date: Tue, 28 Aug 2018 17:41:40 -0500 Subject: [PATCH 3/8] Update README.md --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 7bb65bdf..8800a1f8 100644 --- a/readme.md +++ b/readme.md @@ -391,9 +391,9 @@ Strict CSP is supported. You must pass a nonce as a parameter to either `flush(n You should generate a nonce per request. ```jsx -import uuidv4 from 'uuid/v4' +import nanoid from 'nanoid' -const nonce = new Buffer(uuidv4()).toString('base64') //ex: N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3 +const nonce = Buffer.from(nanoid()).toString('base64') //ex: N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3 ``` Your CSP policy must have this same nonce as well. From 278e5d589bc6c00b08c06d9aa4e6df741c73b2ea Mon Sep 17 00:00:00 2001 From: Davis <11590024+dav-is@users.noreply.github.com> Date: Wed, 29 Aug 2018 17:31:16 -0500 Subject: [PATCH 4/8] Requested Fixes --- readme.md | 10 ++++++---- src/server.js | 8 ++++---- test/index.js | 12 ++++++------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index 8800a1f8..c709894a 100644 --- a/readme.md +++ b/readme.md @@ -387,16 +387,18 @@ duplicate styles are avoided. ### Content Security Policy -Strict CSP is supported. You must pass a nonce as a parameter to either `flush(nonce)` or `flushToHTML(nonce)` **and** set a `` tag. +Strict [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) is supported. -You should generate a nonce per request. -```jsx +You should generate a nonce **per request**. +```js import nanoid from 'nanoid' const nonce = Buffer.from(nanoid()).toString('base64') //ex: N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3 ``` -Your CSP policy must have this same nonce as well. +You must then pass a nonce to either `flush({ nonce })` or `flushToHTML({ nonce })` **and** set a `` tag. + +Your CSP policy must share the same nonce as well (the header nonce needs to match the html nonce and remain unpredictable). `Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3';` ### External CSS and styles outside of the component diff --git a/src/server.js b/src/server.js index 70e1d234..19e833f2 100644 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,7 @@ import React from 'react' import { flush } from './style' -export default function flushToReact(nonce = undefined) { +export default function flushToReact(options = {}) { return flush().map(args => { const id = args[0] const css = args[1] @@ -9,7 +9,7 @@ export default function flushToReact(nonce = undefined) { id: `__${id}`, // Avoid warnings upon render with a key key: `__${id}`, - nonce, + nonce: options.nonce ? options.nonce : undefined, dangerouslySetInnerHTML: { __html: css } @@ -17,12 +17,12 @@ export default function flushToReact(nonce = undefined) { }) } -export function flushToHTML(nonce = undefined) { +export function flushToHTML(options = {}) { return flush().reduce((html, args) => { const id = args[0] const css = args[1] html += `` return html }, '') diff --git a/test/index.js b/test/index.js index f51363e8..90c9b266 100644 --- a/test/index.js +++ b/test/index.js @@ -175,20 +175,20 @@ test('server rendering with nonce', t => { // Render using react ReactDOM.renderToString(React.createElement(App)) const html = ReactDOM.renderToStaticMarkup( - React.createElement('head', null, flush('test-nonce')) + React.createElement('head', null, flush({ nonce: 'test-nonce' })) ) t.is(html, `${expected}`) // Assert that memory is empty - t.is(0, flush('test-nonce').length) - t.is('', flushToHTML('test-nonce')) + t.is(0, flush({ nonce: 'test-nonce' }).length) + t.is('', flushToHTML({ nonce: 'test-nonce' })) // Render to html again ReactDOM.renderToString(React.createElement(App)) - t.is(expected, flushToHTML('test-nonce')) + t.is(expected, flushToHTML({ nonce: 'test-nonce' })) // Assert that memory is empty - t.is(0, flush('test-nonce').length) - t.is('', flushToHTML('test-nonce')) + t.is(0, flush({ nonce: 'test-nonce' }).length) + t.is('', flushToHTML({ nonce: 'test-nonce' })) }) From 6f5f14f6c8dbcbd2942eab0b6138836953615f6c Mon Sep 17 00:00:00 2001 From: Davis <11590024+dav-is@users.noreply.github.com> Date: Thu, 30 Aug 2018 11:44:50 -0500 Subject: [PATCH 5/8] Minor fixes --- src/lib/stylesheet.js | 8 ++++---- src/stylesheet-registry.js | 4 ++-- test/stylesheet-registry.js | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lib/stylesheet.js b/src/lib/stylesheet.js index 001def73..f69100c9 100644 --- a/src/lib/stylesheet.js +++ b/src/lib/stylesheet.js @@ -34,7 +34,7 @@ export default class StyleSheet { this._nonce = node ? node.getAttribute('content') : null } - setOptimizeForSpeed(bool) { + setOptimizeForSpeed(bool, options = { flush: {} }) { invariant( typeof bool === 'boolean', '`setOptimizeForSpeed` accepts a boolean' @@ -44,7 +44,7 @@ export default class StyleSheet { this._rulesCount === 0, 'optimizeForSpeed cannot be when rules have already been inserted' ) - this.flush() + this.flush(options.flush) this._optimizeForSpeed = bool this.inject() } @@ -53,7 +53,7 @@ export default class StyleSheet { return this._optimizeForSpeed } - inject() { + inject(options = { flush: {} }) { invariant(!this._injected, 'sheet already injected') this._injected = true if (this._isBrowser && this._optimizeForSpeed) { @@ -65,7 +65,7 @@ export default class StyleSheet { 'StyleSheet: optimizeForSpeed mode not supported falling back to standard mode.' ) // eslint-disable-line no-console } - this.flush() + this.flush(options.flush) this._injected = true } return diff --git a/src/stylesheet-registry.js b/src/stylesheet-registry.js index 781d3891..58f92787 100644 --- a/src/stylesheet-registry.js +++ b/src/stylesheet-registry.js @@ -94,8 +94,8 @@ export default class StyleSheetRegistry { this.remove(props) } - flush() { - this._sheet.flush() + flush(options = {}) { + this._sheet.flush(options) this._sheet.inject() this._fromServer = undefined this._indices = {} diff --git a/test/stylesheet-registry.js b/test/stylesheet-registry.js index 2cffcbbb..efd0215e 100644 --- a/test/stylesheet-registry.js +++ b/test/stylesheet-registry.js @@ -105,6 +105,7 @@ test('add - sanitizes dynamic CSS on the server', t => { }) test('add - nonce is properly fetched from meta tag', t => { + const originalDocument = global.document // We need to stub a document in order to simulate the meta tag global.document = { querySelector(query) { @@ -122,6 +123,8 @@ test('add - nonce is properly fetched from meta tag', t => { registry.add({ styleId: '123', css: [cssRule] }) t.is(registry._sheet._nonce, 'test-nonce') + + global.document = originalDocument }) // registry.remove From 368a6e4e3e1211e84b6bc4bb4db44dcd426a0bd9 Mon Sep 17 00:00:00 2001 From: Davis <11590024+dav-is@users.noreply.github.com> Date: Thu, 30 Aug 2018 12:49:32 -0500 Subject: [PATCH 6/8] use isBrowser :( --- src/lib/stylesheet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/stylesheet.js b/src/lib/stylesheet.js index f69100c9..72665aef 100644 --- a/src/lib/stylesheet.js +++ b/src/lib/stylesheet.js @@ -29,7 +29,7 @@ export default class StyleSheet { this._rulesCount = 0 const node = - typeof document === 'object' && + this._isBrowser && document.querySelector('meta[property="csp-nonce"]') this._nonce = node ? node.getAttribute('content') : null } From bb588e931792bd0fb9fe6fff07d3913ba51b6a73 Mon Sep 17 00:00:00 2001 From: Davis <11590024+dav-is@users.noreply.github.com> Date: Fri, 31 Aug 2018 14:12:22 -0500 Subject: [PATCH 7/8] Fix tests, remove useless parameters --- src/lib/stylesheet.js | 11 +- src/stylesheet-registry.js | 4 +- test/helpers/with-mock.js | 30 +++ test/stylesheet-registry.js | 421 +++++++++++++++++++----------------- test/stylesheet.js | 252 +++++++++++---------- 5 files changed, 398 insertions(+), 320 deletions(-) create mode 100644 test/helpers/with-mock.js diff --git a/src/lib/stylesheet.js b/src/lib/stylesheet.js index 72665aef..438a7321 100644 --- a/src/lib/stylesheet.js +++ b/src/lib/stylesheet.js @@ -29,12 +29,11 @@ export default class StyleSheet { this._rulesCount = 0 const node = - this._isBrowser && - document.querySelector('meta[property="csp-nonce"]') + this._isBrowser && document.querySelector('meta[property="csp-nonce"]') this._nonce = node ? node.getAttribute('content') : null } - setOptimizeForSpeed(bool, options = { flush: {} }) { + setOptimizeForSpeed(bool) { invariant( typeof bool === 'boolean', '`setOptimizeForSpeed` accepts a boolean' @@ -44,7 +43,7 @@ export default class StyleSheet { this._rulesCount === 0, 'optimizeForSpeed cannot be when rules have already been inserted' ) - this.flush(options.flush) + this.flush() this._optimizeForSpeed = bool this.inject() } @@ -53,7 +52,7 @@ export default class StyleSheet { return this._optimizeForSpeed } - inject(options = { flush: {} }) { + inject() { invariant(!this._injected, 'sheet already injected') this._injected = true if (this._isBrowser && this._optimizeForSpeed) { @@ -65,7 +64,7 @@ export default class StyleSheet { 'StyleSheet: optimizeForSpeed mode not supported falling back to standard mode.' ) // eslint-disable-line no-console } - this.flush(options.flush) + this.flush() this._injected = true } return diff --git a/src/stylesheet-registry.js b/src/stylesheet-registry.js index 58f92787..781d3891 100644 --- a/src/stylesheet-registry.js +++ b/src/stylesheet-registry.js @@ -94,8 +94,8 @@ export default class StyleSheetRegistry { this.remove(props) } - flush(options = {}) { - this._sheet.flush(options) + flush() { + this._sheet.flush() this._sheet.inject() this._fromServer = undefined this._indices = {} diff --git a/test/helpers/with-mock.js b/test/helpers/with-mock.js new file mode 100644 index 00000000..604937e5 --- /dev/null +++ b/test/helpers/with-mock.js @@ -0,0 +1,30 @@ +export default function withMock(mockFn, testFn) { + return t => { + const cleanUp = mockFn(t) + if (typeof cleanUp !== 'function') { + throw new TypeError('mockFn should return a cleanup function') + } + testFn(t) + cleanUp(t) + } +} + +export function withMockDocument(t) { + const originalDocument = global.document + // We need to stub a document in order to simulate the meta tag + global.document = { + querySelector(query) { + t.is(query, 'meta[property="csp-nonce"]') + return { + getAttribute(attr) { + t.is(attr, 'content') + return 'test-nonce' + } + } + } + } + + return () => { + global.document = originalDocument + } +} diff --git a/test/stylesheet-registry.js b/test/stylesheet-registry.js index efd0215e..03f5766d 100644 --- a/test/stylesheet-registry.js +++ b/test/stylesheet-registry.js @@ -4,6 +4,7 @@ import test from 'ava' // Ours import StyleSheetRegistry from '../src/stylesheet-registry' import makeSheet, { invalidRules } from './stylesheet' +import withMock, { withMockDocument } from './helpers/with-mock' function makeRegistry(options = { optimizeForSpeed: true, isBrowser: true }) { const registry = new StyleSheetRegistry({ @@ -19,71 +20,77 @@ const cssRuleAlt = 'p { color: red }' // registry.add -test('add', t => { - const options = [ - { optimizeForSpeed: true, isBrowser: true }, - { optimizeForSpeed: false, isBrowser: true }, - { optimizeForSpeed: true, isBrowser: false }, - { optimizeForSpeed: false, isBrowser: false } - ] - - options.forEach(options => { - const registry = makeRegistry(options) - registry.add({ - styleId: '123', - css: options.optimizeForSpeed ? [cssRule] : cssRule - }) +test( + 'add', + withMock(withMockDocument, t => { + const options = [ + { optimizeForSpeed: true, isBrowser: true }, + { optimizeForSpeed: false, isBrowser: true }, + { optimizeForSpeed: true, isBrowser: false }, + { optimizeForSpeed: false, isBrowser: false } + ] - t.deepEqual(registry.cssRules(), [['jsx-123', cssRule]]) + options.forEach(options => { + const registry = makeRegistry(options) + registry.add({ + styleId: '123', + css: options.optimizeForSpeed ? [cssRule] : cssRule + }) - // Dedupe + t.deepEqual(registry.cssRules(), [['jsx-123', cssRule]]) - registry.add({ - styleId: '123', - css: options.optimizeForSpeed ? [cssRule] : cssRule - }) + // Dedupe - t.deepEqual(registry.cssRules(), [['jsx-123', cssRule]]) - - registry.add({ - styleId: '345', - css: options.optimizeForSpeed ? [cssRule] : cssRule - }) + registry.add({ + styleId: '123', + css: options.optimizeForSpeed ? [cssRule] : cssRule + }) - t.deepEqual(registry.cssRules(), [ - ['jsx-123', cssRule], - ['jsx-345', cssRule] - ]) + t.deepEqual(registry.cssRules(), [['jsx-123', cssRule]]) - if (options.optimizeForSpeed) { - registry.add({ styleId: '456', css: [cssRule, cssRuleAlt] }) + registry.add({ + styleId: '345', + css: options.optimizeForSpeed ? [cssRule] : cssRule + }) t.deepEqual(registry.cssRules(), [ ['jsx-123', cssRule], - ['jsx-345', cssRule], - ['jsx-456', 'div { color: red }\np { color: red }'] + ['jsx-345', cssRule] ]) - } + + if (options.optimizeForSpeed) { + registry.add({ styleId: '456', css: [cssRule, cssRuleAlt] }) + + t.deepEqual(registry.cssRules(), [ + ['jsx-123', cssRule], + ['jsx-345', cssRule], + ['jsx-456', 'div { color: red }\np { color: red }'] + ]) + } + }) }) -}) +) -test('add - filters out invalid rules (index `-1`)', t => { - const registry = makeRegistry() +test( + 'add - filters out invalid rules (index `-1`)', + withMock(withMockDocument, t => { + const registry = makeRegistry() - // Insert a valid rule - registry.add({ styleId: '123', css: [cssRule] }) + // Insert a valid rule + registry.add({ styleId: '123', css: [cssRule] }) - // Insert an invalid rule - registry.add({ styleId: '456', css: [invalidRules[0]] }) + // Insert an invalid rule + registry.add({ styleId: '456', css: [invalidRules[0]] }) - // Insert another valid rule - registry.add({ styleId: '678', css: [cssRule] }) + // Insert another valid rule + registry.add({ styleId: '678', css: [cssRule] }) - t.deepEqual(registry.cssRules(), [ - ['jsx-123', 'div { color: red }'], - ['jsx-678', 'div { color: red }'] - ]) -}) + t.deepEqual(registry.cssRules(), [ + ['jsx-123', 'div { color: red }'], + ['jsx-678', 'div { color: red }'] + ]) + }) +) test('add - sanitizes dynamic CSS on the server', t => { const registry = makeRegistry({ optimizeForSpeed: false, isBrowser: false }) @@ -129,177 +136,191 @@ test('add - nonce is properly fetched from meta tag', t => { // registry.remove -test('remove', t => { - const options = [ - { optimizeForSpeed: true, isBrowser: true }, - { optimizeForSpeed: false, isBrowser: true }, - { optimizeForSpeed: true, isBrowser: false }, - { optimizeForSpeed: false, isBrowser: false } - ] - - options.forEach(options => { - const registry = makeRegistry(options) - registry.add({ - styleId: '123', - css: options.optimizeForSpeed ? [cssRule] : cssRule - }) +test( + 'remove', + withMock(withMockDocument, t => { + const options = [ + { optimizeForSpeed: true, isBrowser: true }, + { optimizeForSpeed: false, isBrowser: true }, + { optimizeForSpeed: true, isBrowser: false }, + { optimizeForSpeed: false, isBrowser: false } + ] - registry.add({ - styleId: '345', - css: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt - }) + options.forEach(options => { + const registry = makeRegistry(options) + registry.add({ + styleId: '123', + css: options.optimizeForSpeed ? [cssRule] : cssRule + }) - registry.remove({ styleId: '123' }) - t.deepEqual(registry.cssRules(), [['jsx-345', cssRuleAlt]]) + registry.add({ + styleId: '345', + css: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt + }) + + registry.remove({ styleId: '123' }) + t.deepEqual(registry.cssRules(), [['jsx-345', cssRuleAlt]]) - // Add a duplicate - registry.add({ - styleId: '345', - css: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt + // Add a duplicate + registry.add({ + styleId: '345', + css: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt + }) + // and remove it + registry.remove({ styleId: '345' }) + // Still in the registry + t.deepEqual(registry.cssRules(), [['jsx-345', cssRuleAlt]]) + // remove again + registry.remove({ styleId: '345' }) + // now the registry should be empty + t.deepEqual(registry.cssRules(), []) }) - // and remove it - registry.remove({ styleId: '345' }) - // Still in the registry - t.deepEqual(registry.cssRules(), [['jsx-345', cssRuleAlt]]) - // remove again - registry.remove({ styleId: '345' }) - // now the registry should be empty - t.deepEqual(registry.cssRules(), []) }) -}) +) // registry.update -test('update', t => { - const options = [ - { optimizeForSpeed: true, isBrowser: true }, - { optimizeForSpeed: false, isBrowser: true }, - { optimizeForSpeed: true, isBrowser: false }, - { optimizeForSpeed: false, isBrowser: false } - ] - - options.forEach(options => { - const registry = makeRegistry(options) - registry.add({ - styleId: '123', - css: options.optimizeForSpeed ? [cssRule] : cssRule - }) - - registry.add({ - styleId: '123', - css: options.optimizeForSpeed ? [cssRule] : cssRule - }) +test( + 'update', + withMock(withMockDocument, t => { + const options = [ + { optimizeForSpeed: true, isBrowser: true }, + { optimizeForSpeed: false, isBrowser: true }, + { optimizeForSpeed: true, isBrowser: false }, + { optimizeForSpeed: false, isBrowser: false } + ] - registry.update( - { styleId: '123' }, - { - styleId: '345', - css: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt - } - ) - // Doesn't remove when there are multiple instances of 123 - t.deepEqual(registry.cssRules(), [ - ['jsx-123', cssRule], - ['jsx-345', cssRuleAlt] - ]) + options.forEach(options => { + const registry = makeRegistry(options) + registry.add({ + styleId: '123', + css: options.optimizeForSpeed ? [cssRule] : cssRule + }) + + registry.add({ + styleId: '123', + css: options.optimizeForSpeed ? [cssRule] : cssRule + }) + + registry.update( + { styleId: '123' }, + { + styleId: '345', + css: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt + } + ) + // Doesn't remove when there are multiple instances of 123 + t.deepEqual(registry.cssRules(), [ + ['jsx-123', cssRule], + ['jsx-345', cssRuleAlt] + ]) - registry.remove({ styleId: '345' }) - t.deepEqual(registry.cssRules(), [['jsx-123', cssRule]]) + registry.remove({ styleId: '345' }) + t.deepEqual(registry.cssRules(), [['jsx-123', cssRule]]) - // Update again - registry.update( - { styleId: '123' }, - { - styleId: '345', - css: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt - } - ) - // 123 replaced with 345 - t.deepEqual(registry.cssRules(), [['jsx-345', cssRuleAlt]]) + // Update again + registry.update( + { styleId: '123' }, + { + styleId: '345', + css: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt + } + ) + // 123 replaced with 345 + t.deepEqual(registry.cssRules(), [['jsx-345', cssRuleAlt]]) + }) }) -}) - -// Utils - -const utilRegistry = makeRegistry() +) // createComputeId -test('createComputeId', t => { - const computeId = utilRegistry.createComputeId() +test( + 'createComputeId', + withMock(withMockDocument, t => { + const utilRegistry = makeRegistry() + const computeId = utilRegistry.createComputeId() - // without props - t.is(computeId('123'), 'jsx-123') + // without props + t.is(computeId('123'), 'jsx-123') - // with props - t.is(computeId('123', ['test', 3, 'test']), 'jsx-1172888331') -}) + // with props + t.is(computeId('123', ['test', 3, 'test']), 'jsx-1172888331') + }) +) // createComputeSelector -test('createComputeSelector', t => { - const computeSelector = utilRegistry - .createComputeSelector() - .bind(utilRegistry) - - t.is( - computeSelector( - 'jsx-123', - '.test {} .__jsx-style-dynamic-selector { color: red } .__jsx-style-dynamic-selector { color: red }' - ), - '.test {} .jsx-123 { color: red } .jsx-123 { color: red }' - ) -}) +test( + 'createComputeSelector', + withMock(withMockDocument, t => { + const utilRegistry = makeRegistry() + const computeSelector = utilRegistry + .createComputeSelector() + .bind(utilRegistry) + + t.is( + computeSelector( + 'jsx-123', + '.test {} .__jsx-style-dynamic-selector { color: red } .__jsx-style-dynamic-selector { color: red }' + ), + '.test {} .jsx-123 { color: red } .jsx-123 { color: red }' + ) + }) +) // getIdAndRules -test('getIdAndRules', t => { - // simple - t.deepEqual( - utilRegistry.getIdAndRules({ - styleId: '123', - css: '.test {} .jsx-123 { color: red } .jsx-123 { color: red }' - }), - { - styleId: 'jsx-123', - rules: ['.test {} .jsx-123 { color: red } .jsx-123 { color: red }'] - } - ) - - // dynamic - t.deepEqual( - utilRegistry.getIdAndRules({ - styleId: '123', - css: - '.test {} .__jsx-style-dynamic-selector { color: red } .__jsx-style-dynamic-selector { color: red }', - dynamic: ['test', 3, 'test'] - }), - { - styleId: 'jsx-1172888331', - rules: [ - '.test {} .jsx-1172888331 { color: red } .jsx-1172888331 { color: red }' - ] - } - ) - - // dynamic, css array - t.deepEqual( - utilRegistry.getIdAndRules({ - styleId: '123', - css: [ - '.test {}', - '.__jsx-style-dynamic-selector { color: red }', - '.__jsx-style-dynamic-selector { color: red }' - ], - dynamic: ['test', 3, 'test'] - }), - { - styleId: 'jsx-1172888331', - rules: [ - '.test {}', - '.jsx-1172888331 { color: red }', - '.jsx-1172888331 { color: red }' - ] - } - ) -}) +test( + 'getIdAndRules', + withMock(withMockDocument, t => { + const utilRegistry = makeRegistry() + // simple + t.deepEqual( + utilRegistry.getIdAndRules({ + styleId: '123', + css: '.test {} .jsx-123 { color: red } .jsx-123 { color: red }' + }), + { + styleId: 'jsx-123', + rules: ['.test {} .jsx-123 { color: red } .jsx-123 { color: red }'] + } + ) + + // dynamic + t.deepEqual( + utilRegistry.getIdAndRules({ + styleId: '123', + css: + '.test {} .__jsx-style-dynamic-selector { color: red } .__jsx-style-dynamic-selector { color: red }', + dynamic: ['test', 3, 'test'] + }), + { + styleId: 'jsx-1172888331', + rules: [ + '.test {} .jsx-1172888331 { color: red } .jsx-1172888331 { color: red }' + ] + } + ) + + // dynamic, css array + t.deepEqual( + utilRegistry.getIdAndRules({ + styleId: '123', + css: [ + '.test {}', + '.__jsx-style-dynamic-selector { color: red }', + '.__jsx-style-dynamic-selector { color: red }' + ], + dynamic: ['test', 3, 'test'] + }), + { + styleId: 'jsx-1172888331', + rules: [ + '.test {}', + '.jsx-1172888331 { color: red }', + '.jsx-1172888331 { color: red }' + ] + } + ) + }) +) diff --git a/test/stylesheet.js b/test/stylesheet.js index 8f2bcd78..c0150c8b 100644 --- a/test/stylesheet.js +++ b/test/stylesheet.js @@ -3,6 +3,7 @@ import test from 'ava' // Ours import StyleSheet from '../src/lib/stylesheet' +import withMock, { withMockDocument } from './helpers/with-mock' export const invalidRules = ['invalid rule'] @@ -62,148 +63,175 @@ export default function makeSheet( // sheet.setOptimizeForSpeed -test('can change optimizeForSpeed only when the stylesheet is empty', t => { - const sheet = makeSheet() +test( + 'can change optimizeForSpeed only when the stylesheet is empty', + withMock(withMockDocument, t => { + const sheet = makeSheet() - sheet.inject() - t.notThrows(() => { - sheet.setOptimizeForSpeed(true) - }) + sheet.inject() + t.notThrows(() => { + sheet.setOptimizeForSpeed(true) + }) - sheet.insertRule('div { color: red }') - t.throws(() => { - sheet.setOptimizeForSpeed(false) - }) + sheet.insertRule('div { color: red }') + t.throws(() => { + sheet.setOptimizeForSpeed(false) + }) - sheet.flush() - t.notThrows(() => { - sheet.setOptimizeForSpeed(false) + sheet.flush() + t.notThrows(() => { + sheet.setOptimizeForSpeed(false) + }) }) -}) +) // sheet.insertRule -test('insertRule', t => { - const options = [ - { optimizeForSpeed: true, isBrowser: true }, - { optimizeForSpeed: false, isBrowser: true }, - { optimizeForSpeed: true, isBrowser: false } - ] +test( + 'insertRule', + withMock(withMockDocument, t => { + const options = [ + { optimizeForSpeed: true, isBrowser: true }, + { optimizeForSpeed: false, isBrowser: true }, + { optimizeForSpeed: true, isBrowser: false } + ] + + options.forEach(options => { + const sheet = makeSheet(options) + sheet.inject() + + sheet.insertRule('div { color: red }') + t.deepEqual(sheet.cssRules(), [{ cssText: 'div { color: red }' }]) + + sheet.insertRule('div { color: green }') + t.deepEqual(sheet.cssRules(), [ + { cssText: 'div { color: red }' }, + { cssText: 'div { color: green }' } + ]) + }) + }) +) - options.forEach(options => { - const sheet = makeSheet(options) +test( + 'insertRule - returns the rule index', + withMock(withMockDocument, t => { + const sheet = makeSheet() sheet.inject() - sheet.insertRule('div { color: red }') - t.deepEqual(sheet.cssRules(), [{ cssText: 'div { color: red }' }]) + let i = sheet.insertRule('div { color: red }') + t.is(i, 0) - sheet.insertRule('div { color: green }') - t.deepEqual(sheet.cssRules(), [ - { cssText: 'div { color: red }' }, - { cssText: 'div { color: green }' } - ]) + i = sheet.insertRule('div { color: red }') + t.is(i, 1) }) -}) - -test('insertRule - returns the rule index', t => { - const sheet = makeSheet() - sheet.inject() - - let i = sheet.insertRule('div { color: red }') - t.is(i, 0) - - i = sheet.insertRule('div { color: red }') - t.is(i, 1) -}) +) -test('insertRule - handles invalid rules and returns -1 as index', t => { - const sheet = makeSheet() - sheet.inject() +test( + 'insertRule - handles invalid rules and returns -1 as index', + withMock(withMockDocument, t => { + const sheet = makeSheet() + sheet.inject() - const i = sheet.insertRule(invalidRules[0]) - t.is(i, -1) -}) + const i = sheet.insertRule(invalidRules[0]) + t.is(i, -1) + }) +) -test('insertRule - does not fail when the css is a String object', t => { - const sheet = makeSheet() - sheet.inject() +test( + 'insertRule - does not fail when the css is a String object', + withMock(withMockDocument, t => { + const sheet = makeSheet() + sheet.inject() - // eslint-disable-next-line unicorn/new-for-builtins - sheet.insertRule(new String('div { color: red }')) // eslint-disable-line no-new-wrappers - t.deepEqual(sheet.cssRules(), [{ cssText: 'div { color: red }' }]) -}) + // eslint-disable-next-line unicorn/new-for-builtins + sheet.insertRule(new String('div { color: red }')) // eslint-disable-line no-new-wrappers + t.deepEqual(sheet.cssRules(), [{ cssText: 'div { color: red }' }]) + }) +) // sheet.deleteRule -test('deleteRule', t => { - const options = [ - { optimizeForSpeed: true, isBrowser: true }, - { optimizeForSpeed: false, isBrowser: true }, - { optimizeForSpeed: true, isBrowser: false }, - { optimizeForSpeed: false, isBrowser: false } - ] +test( + 'deleteRule', + withMock(withMockDocument, t => { + const options = [ + { optimizeForSpeed: true, isBrowser: true }, + { optimizeForSpeed: false, isBrowser: true }, + { optimizeForSpeed: true, isBrowser: false }, + { optimizeForSpeed: false, isBrowser: false } + ] + + options.forEach(options => { + const sheet = makeSheet(options) + sheet.inject() + + sheet.insertRule('div { color: red }') + sheet.insertRule('div { color: green }') + const rulesCount = sheet.length + + sheet.deleteRule(1) + // When deleting we replace rules with placeholders to keep the indices stable. + t.is(sheet.length, rulesCount) + + t.deepEqual(sheet.cssRules(), [{ cssText: 'div { color: red }' }, null]) + }) + }) +) - options.forEach(options => { - const sheet = makeSheet(options) +test( + 'deleteRule - does not throw when the rule at index does not exist', + withMock(withMockDocument, t => { + const sheet = makeSheet() sheet.inject() - sheet.insertRule('div { color: red }') - sheet.insertRule('div { color: green }') - const rulesCount = sheet.length - - sheet.deleteRule(1) - // When deleting we replace rules with placeholders to keep the indices stable. - t.is(sheet.length, rulesCount) - - t.deepEqual(sheet.cssRules(), [{ cssText: 'div { color: red }' }, null]) + t.notThrows(() => { + sheet.deleteRule(sheet.length + 1) + }) }) -}) +) -test('deleteRule - does not throw when the rule at index does not exist', t => { - const sheet = makeSheet() - sheet.inject() +// sheet.replaceRule - t.notThrows(() => { - sheet.deleteRule(sheet.length + 1) - }) -}) +test( + 'replaceRule', + withMock(withMockDocument, t => { + const options = [ + { optimizeForSpeed: true, isBrowser: true }, + { optimizeForSpeed: false, isBrowser: true }, + { optimizeForSpeed: true, isBrowser: false }, + { optimizeForSpeed: false, isBrowser: false } + ] -// sheet.replaceRule + options.forEach(options => { + const sheet = makeSheet(options) + sheet.inject() -test('replaceRule', t => { - const options = [ - { optimizeForSpeed: true, isBrowser: true }, - { optimizeForSpeed: false, isBrowser: true }, - { optimizeForSpeed: true, isBrowser: false }, - { optimizeForSpeed: false, isBrowser: false } - ] + const index = sheet.insertRule('div { color: red }') + sheet.replaceRule(index, 'p { color: hotpink }') + + t.deepEqual(sheet.cssRules(), [{ cssText: 'p { color: hotpink }' }]) + }) + }) +) - options.forEach(options => { - const sheet = makeSheet(options) +test( + 'replaceRule - handles invalid rules gracefully', + withMock(withMockDocument, t => { + const sheet = makeSheet() sheet.inject() + // Insert two rules + sheet.insertRule('div { color: red }') const index = sheet.insertRule('div { color: red }') - sheet.replaceRule(index, 'p { color: hotpink }') - t.deepEqual(sheet.cssRules(), [{ cssText: 'p { color: hotpink }' }]) + // Replace the latter with an invalid rule + const i = sheet.replaceRule(index, invalidRules[0]) + t.is(i, index) + t.is(sheet.length, 2) + + // Even though replacement (insertion) failed deletion succeeded + // therefore the lib must insert a delete placeholder which resolves to `null` + // when `cssRules()` is called. + t.deepEqual(sheet.cssRules(), [{ cssText: 'div { color: red }' }, null]) }) -}) - -test('replaceRule - handles invalid rules gracefully', t => { - const sheet = makeSheet() - sheet.inject() - - // Insert two rules - sheet.insertRule('div { color: red }') - const index = sheet.insertRule('div { color: red }') - - // Replace the latter with an invalid rule - const i = sheet.replaceRule(index, invalidRules[0]) - t.is(i, index) - t.is(sheet.length, 2) - - // Even though replacement (insertion) failed deletion succeeded - // therefore the lib must insert a delete placeholder which resolves to `null` - // when `cssRules()` is called. - t.deepEqual(sheet.cssRules(), [{ cssText: 'div { color: red }' }, null]) -}) +) From 0cdda13d4960eac41472fc1d916e1783b614ca96 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Wed, 5 Sep 2018 09:17:38 +0200 Subject: [PATCH 8/8] Fix docs --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index c709894a..338080c2 100644 --- a/readme.md +++ b/readme.md @@ -396,7 +396,7 @@ import nanoid from 'nanoid' const nonce = Buffer.from(nanoid()).toString('base64') //ex: N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3 ``` -You must then pass a nonce to either `flush({ nonce })` or `flushToHTML({ nonce })` **and** set a `` tag. +You must then pass a nonce to either `flushToReact({ nonce })` or `flushToHTML({ nonce })` **and** set a `` tag. Your CSP policy must share the same nonce as well (the header nonce needs to match the html nonce and remain unpredictable). `Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3';`