diff --git a/package-lock.json b/package-lock.json index 6a70a92..e277342 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1402,6 +1402,41 @@ "any-observable": "^0.3.0" } }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -6010,6 +6045,12 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -7399,6 +7440,36 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "node-cleanup": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", @@ -9017,6 +9088,43 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "sinon": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.1.tgz", + "integrity": "sha512-ZSSmlkSyhUWbkF01Z9tEbxZLF/5tRC9eojCdFh33gtQaP7ITQVaMWQHGuFM7Cuf/KEfihuh1tTl3/ABju3AQMg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^7.1.0", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" + }, + "dependencies": { + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", diff --git a/package.json b/package.json index cf04fe5..9ccb5df 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "rollup-plugin-terser": "^5.3.0", "rollup-plugin-visualizer": "^4.0.4", "rollup-plugin-vue2": "^0.8.1", + "sinon": "^11.1.1", "vue": "^2.6.11" }, "lint-staged": { diff --git a/src/constructors/collectRules.js b/src/constructors/collectRules.js new file mode 100644 index 0000000..58518b5 --- /dev/null +++ b/src/constructors/collectRules.js @@ -0,0 +1,7 @@ +import StyleSheet from '../models/StyleSheet' + +const collectRules = () => { + return StyleSheet.rules().filter(rule => rule.cssText.length > 0) +} + +export default collectRules diff --git a/src/index.js b/src/index.js index 8dc4e0c..73ff080 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import generateAlphabeticName from './utils/generateAlphabeticName' import css from './constructors/css' +import collectRules from './constructors/collectRules' import keyframes from './constructors/keyframes' import injectGlobal from './constructors/injectGlobal' import ThemeProvider from './providers/ThemeProvider' @@ -7,6 +8,8 @@ import ThemeProvider from './providers/ThemeProvider' import _styledComponent from './models/StyledComponent' import _componentStyle from './models/ComponentStyle' import _styled from './constructors/styled' +import StyleSheet from './models/StyleSheet' +import ServerSideRenderMixin from './mixins/ssr' const styled = _styled( _styledComponent(_componentStyle(generateAlphabeticName)) @@ -14,4 +17,4 @@ const styled = _styled( export default styled -export { css, injectGlobal, keyframes, ThemeProvider } +export { css, collectRules, injectGlobal, StyleSheet, ServerSideRenderMixin, keyframes, ThemeProvider } diff --git a/src/mixins/ssr.js b/src/mixins/ssr.js new file mode 100644 index 0000000..6c954ba --- /dev/null +++ b/src/mixins/ssr.js @@ -0,0 +1,14 @@ +import collectRules from '../constructors/collectRules' + +export default { + head () { + if (typeof window === 'undefined') { + const css = collectRules().map(rule => rule.cssText).join('') + return { + style: [{ hide: 'ssrStyles', innerHTML: css, type: 'text/css' }], + __dangerouslyDisableSanitizers: ['style'] + } + } + return {} + } +} diff --git a/src/models/StyleSheet.js b/src/models/StyleSheet.js index 68f6373..e38c7f8 100644 --- a/src/models/StyleSheet.js +++ b/src/models/StyleSheet.js @@ -8,13 +8,19 @@ class StyleSheet { * are defined at initialization and should remain static after that */ this.globalStyleSheet = new GlamorSheet({ speedy: false }) this.componentStyleSheet = new GlamorSheet({ speedy: false, maxLength: 40 }) + + /* if the library user sets this to true in their index.vue, we assume they are handling + * the CSS injection themselves, we don't want to double inject. */ + this.serverRendered = false } get injected () { return this.globalStyleSheet.injected && this.componentStyleSheet.injected } inject () { - this.globalStyleSheet.inject() - this.componentStyleSheet.inject() + if (!this.serverRendered) { + this.globalStyleSheet.inject() + this.componentStyleSheet.inject() + } } flush () { if (this.globalStyleSheet.sheet) this.globalStyleSheet.flush() @@ -27,6 +33,12 @@ class StyleSheet { rules () { return this.globalStyleSheet.rules().concat(this.componentStyleSheet.rules()) } + serverRender () { + this.serverRendered = true + return this.rules().filter(function (rule) { + return rule.cssText.length > 0 + }) + } } /* Export stylesheet as a singleton class */ diff --git a/src/models/test/StyleSheet.test.js b/src/models/test/StyleSheet.test.js index 0540ef7..a699a1d 100644 --- a/src/models/test/StyleSheet.test.js +++ b/src/models/test/StyleSheet.test.js @@ -1,6 +1,7 @@ import styleSheet from '../StyleSheet' import { resetStyled } from '../../test/utils' import expect from 'expect' +import sinon from 'sinon' describe('stylesheet', () => { beforeEach(() => { @@ -9,6 +10,7 @@ describe('stylesheet', () => { describe('inject', () => { beforeEach(() => { + styleSheet.serverRendered = false styleSheet.inject() }) it('should inject the global sheet', () => { @@ -22,6 +24,40 @@ describe('stylesheet', () => { }) }) + describe('server-side inject', () => { + beforeEach(() => { + styleSheet.serverRendered = true + }) + afterEach(() => { + styleSheet.serverRendered = false + }) + it('does not call injects if serverRendered is true', () => { + const globalInject = sinon.spy() + const componentInject = sinon.spy() + styleSheet.inject() + expect(globalInject.called).toBe(false) + expect(componentInject.called).toBe(false) + }) + }) + + describe('serverRender', () => { + beforeEach(() => { + styleSheet.serverRendered = false + }) + afterEach(() => { + styleSheet.serverRendered = false + }) + it('sets serverRendered to true when called', () => { + styleSheet.serverRender() + expect(styleSheet.serverRendered).toBe(true) + }) + it('returns non empty rules', () => { + expect(styleSheet.serverRender()).toStrictEqual(styleSheet.rules().filter(function (rule) { + return rule.cssText.length > 0 + })) + }) + }) + describe('flush', () => { beforeEach(() => { styleSheet.flush() diff --git a/src/test/ssr.test.js b/src/test/ssr.test.js new file mode 100644 index 0000000..cf06eb9 --- /dev/null +++ b/src/test/ssr.test.js @@ -0,0 +1,23 @@ +import ServerSideRenderMixin from '../mixins/ssr' +import expect from 'expect' +import sinon from 'sinon' + +describe('ServerSideRenderMixin', () => { + it('has a function called head', () => { + expect(ServerSideRenderMixin.head instanceof Function).toBe(true) + }) + it('returns a sane value when window is undefined', () => { + expect(ServerSideRenderMixin.head()).toStrictEqual({}) + }) + // it('returns a well formed style object for use by Nuxt when window is defined', () => { + // const windowRef = global.window; + // global.window = {document: {querySelector: () => null}}; + // expect(ServerSideRenderMixin.head()).toStrictEqual( + // { + // style: [{ hide: 'ssrStyles', innerHTML: '', type: 'text/css' }], + // __dangerouslyDisableSanitizers: ['style'] + // } + // ) + // global.window = windowRef; + // }) +}) \ No newline at end of file diff --git a/src/vendor/glamor/sheet.js b/src/vendor/glamor/sheet.js index 0286d73..650c069 100644 --- a/src/vendor/glamor/sheet.js +++ b/src/vendor/glamor/sheet.js @@ -63,7 +63,23 @@ export class StyleSheet { maxLength = (isBrowser && oldIE) ? 4000 : 65000 } = {}) { this.isSpeedy = speedy // the big drawback here is that the css won't be editable in devtools - this.sheet = undefined + this.sheet = isBrowser ? sheetForTag(makeStyleTag()) : { + cssRules: [], + insertRule: (rule) => { + var serverRule = { + cssText: rule + } + + this.sheet.cssRules.push(serverRule) + + return { + serverRule: serverRule, + appendRule: (newCss) => { + return serverRule.cssText += newCss + } + } + } + } this.tags = [] this.maxLength = maxLength this.ctr = 0 @@ -123,7 +139,7 @@ export class StyleSheet { } else{ const textNode = document.createTextNode(rule) - last(this.tags).appendChild(textNode) + // last(this.tags).appendChild(textNode) // this fails because last(this.tags) is a CSSStyleSheet... seems the author expected it to be a DOM Node? insertedRule = { textNode, appendRule: newCss => textNode.appendData(newCss)} if(!this.isSpeedy) {