Skip to content

Commit

Permalink
feat: support inline CSS (#190)
Browse files Browse the repository at this point in the history
  • Loading branch information
Will Howlett authored and gregberge committed Jan 11, 2019
1 parent cf0bb8d commit 2caf676
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 17 deletions.
39 changes: 39 additions & 0 deletions packages/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,17 @@ Get style links as a string of `<link>` tags.
const head = `<head>${chunkExtractor.getStyleTags()}</head>`
```

### chunkExtractor.getInlineStyleTags

Get inline style links as a string of `<link>` tags (returns a promise).

```js
chunkExtractor.getInlineStyleTags()
.then((styleTags) => {
const head = `<head>${styleTags}</head>`
}
```
### chunkExtractor.getStyleElements
Get style links as an array of React `<link>` elements.
Expand All @@ -284,6 +295,34 @@ Get style links as an array of React `<link>` elements.
const head = renderToString(<head>{chunkExtractor.getStyleElements()}</head>)
```
### chunkExtractor.getInlineStyleElements
Get inline style links as an array of React `<link>` elements (returns a promise).
```js
chunkExtractor.getInlineStyleElements()
.then((styleElements) => {
const head = renderToString(<head>{styleElements}</head>)
}
```
### chunkExtractor.getCssString
Get css as a raw string for using directly within app (e.g. in custom AMP style tag)
```js
chunkExtractor.getCssString()
.then((cssString) => {
const head = renderToString(
<head>
<style
dangerouslySetInnerHTML={{ __html: cssString }}
/>
</head>
)
}
```
### ChunkExtractorManager
Used to inject a `ChunkExtractor` in the context of your application.
Expand Down
3 changes: 3 additions & 0 deletions packages/server/__fixtures__/letters-A.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
background: pink;
}
3 changes: 3 additions & 0 deletions packages/server/__fixtures__/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
h1 {
color: cyan;
}
66 changes: 66 additions & 0 deletions packages/server/src/ChunkExtractor.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable react/no-danger */
import path from 'path'
import fs from 'fs'
import _ from 'lodash'
import React from 'react'
import { invariant, LOADABLE_REQUIRED_CHUNKS_KEY } from './sharedInternals'
Expand Down Expand Up @@ -31,12 +32,41 @@ function assetToScriptElement(asset) {
)
}

function assetToStyleString(asset) {
return new Promise((resolve, reject) => {
fs.readFile(asset.path, 'utf8', (err, data) => {
if (err) {
reject(err);
return;
}
resolve(data);
})
})
}

function assetToStyleTag(asset) {
return `<link data-chunk="${asset.chunk}" rel="stylesheet" href="${
asset.url
}">`
}

function assetToStyleTagInline(asset) {
return new Promise((resolve, reject) => {
fs.readFile(asset.path, 'utf8', (err, data) => {
if (err) {
reject(err);
return;
}
resolve(
`<style data-chunk="${asset.chunk}">
${data}
</style>
`
);
})
})
}

function assetToStyleElement(asset) {
return (
<link
Expand All @@ -48,6 +78,24 @@ function assetToStyleElement(asset) {
)
}

function assetToStyleElementInline(asset) {
return new Promise((resolve, reject) => {
fs.readFile(asset.path, 'utf8', (err, data) => {
if (err) {
reject(err);
return;
}
resolve(
<style
key={asset.url}
data-chunk={asset.chunk}
dangerouslySetInnerHTML={{ __html: data }}
/>
);
})
})
}

const LINK_ASSET_HINTS = {
mainAsset: 'data-chunk',
childAsset: 'data-parent-chunk',
Expand Down Expand Up @@ -244,15 +292,33 @@ class ChunkExtractor {
return [requiredScriptElement, ...assetsScriptElements]
}

getCssString() {
const mainAssets = this.getMainAssets('style')
const promises = mainAssets.map((asset) => assetToStyleString(asset).then(data => data))
return Promise.all(promises).then(results => joinTags(results))
}

getStyleTags() {
const mainAssets = this.getMainAssets('style')
return joinTags(mainAssets.map(asset => assetToStyleTag(asset)))
}

getInlineStyleTags() {
const mainAssets = this.getMainAssets('style')
const promises = mainAssets.map((asset) => assetToStyleTagInline(asset).then(data => data))
return Promise.all(promises).then(results => joinTags(results))
}

getStyleElements() {
const mainAssets = this.getMainAssets('style')
return mainAssets.map(asset => assetToStyleElement(asset))
}

getInlineStyleElements() {
const mainAssets = this.getMainAssets('style')
const promises = mainAssets.map((asset) => assetToStyleElementInline(asset).then(data => data))
return Promise.all(promises).then(results => results)
}

// Pre assets

Expand Down
105 changes: 88 additions & 17 deletions packages/server/src/ChunkExtractor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,36 @@ import stats from '../__fixtures__/stats.json'
import ChunkExtractor from './ChunkExtractor'

describe('ChunkExtrator', () => {
let extractor

beforeEach(() => {
extractor = new ChunkExtractor({
stats,
outputPath: path.resolve(__dirname, '../__fixtures__'),
})
})

describe('#stats', () => {
it('should load stats from file', () => {
const extractor = new ChunkExtractor({
extractor = new ChunkExtractor({
statsFile: path.resolve(__dirname, '../__fixtures__/stats.json'),
})

expect(extractor.stats).toBe(stats)
})

it('should load stats from stats', () => {
const extractor = new ChunkExtractor({ stats })
expect(extractor.stats).toBe(stats)
})
})

describe('#addChunk', () => {
it('should reference chunk', () => {
const extractor = new ChunkExtractor({ stats })
extractor.addChunk('foo')
expect(extractor.chunks).toEqual(['foo'])
})

it('should be uniq', () => {
const extractor = new ChunkExtractor({ stats })
extractor.addChunk('a')
extractor.addChunk('b')
extractor.addChunk('b')
Expand All @@ -36,15 +42,13 @@ describe('ChunkExtrator', () => {

describe('#getScriptTags', () => {
it('should return main script tag without chunk', () => {
const extractor = new ChunkExtractor({ stats })
expect(extractor.getScriptTags()).toMatchInlineSnapshot(`
"<script>window.__LOADABLE_REQUIRED_CHUNKS__ = [];</script>
<script async data-chunk=\\"main\\" src=\\"/dist/node/main.js\\"></script>"
`)
})

it('should return other chunks if referenced', () => {
const extractor = new ChunkExtractor({ stats })
extractor.addChunk('letters-A')
expect(extractor.getScriptTags()).toMatchInlineSnapshot(`
"<script>window.__LOADABLE_REQUIRED_CHUNKS__ = [\\"letters-A\\"];</script>
Expand All @@ -56,7 +60,6 @@ describe('ChunkExtrator', () => {

describe('#getScriptElements', () => {
it('should return main script tag without chunk', () => {
const extractor = new ChunkExtractor({ stats })
expect(extractor.getScriptElements()).toMatchInlineSnapshot(`
Array [
<script
Expand All @@ -76,7 +79,6 @@ Array [
})

it('should return other chunks if referenced', () => {
const extractor = new ChunkExtractor({ stats })
extractor.addChunk('letters-A')
expect(extractor.getScriptElements()).toMatchInlineSnapshot(`
Array [
Expand Down Expand Up @@ -104,25 +106,47 @@ Array [

describe('#getStyleTags', () => {
it('should return main style tag without chunk', () => {
const extractor = new ChunkExtractor({ stats })
expect(extractor.getStyleTags()).toMatchInlineSnapshot(
`"<link data-chunk=\\"main\\" rel=\\"stylesheet\\" href=\\"/dist/node/main.css\\">"`,
)
})

it('should return other chunks if referenced', () => {
const extractor = new ChunkExtractor({ stats })
extractor.addChunk('letters-A')
expect(extractor.getStyleTags()).toMatchInlineSnapshot(`
"<link data-chunk=\\"letters-A\\" rel=\\"stylesheet\\" href=\\"/dist/node/letters-A.css\\">
<link data-chunk=\\"main\\" rel=\\"stylesheet\\" href=\\"/dist/node/main.css\\">"
`)
})

})

describe('#getInlineStyleTags', () => {
it('should return inline style tags as a promise', () => {
extractor.addChunk('letters-A')
expect.assertions(1)
return extractor.getInlineStyleTags().then(data => expect(data).toMatchInlineSnapshot(`
"<style data-chunk=\\"letters-A\\">
body {
background: pink;
}
</style>
<style data-chunk=\\"main\\">
h1 {
color: cyan;
}
</style>
"
`),
)
})

})

describe('#getStyleElements', () => {
it('should return main style tag without chunk', () => {
const extractor = new ChunkExtractor({ stats })
expect(extractor.getStyleElements()).toMatchInlineSnapshot(`
Array [
<link
Expand All @@ -135,7 +159,6 @@ Array [
})

it('should return other chunks if referenced', () => {
const extractor = new ChunkExtractor({ stats })
extractor.addChunk('letters-A')
expect(extractor.getStyleElements()).toMatchInlineSnapshot(`
Array [
Expand All @@ -152,11 +175,63 @@ Array [
]
`)
})

})

describe('#getInlineStyleElements', () => {
it('should return inline style elements as a promise', () => {
extractor.addChunk('letters-A')
expect.assertions(1)
return extractor.getInlineStyleElements().then(data => expect(data).toMatchInlineSnapshot(`
Array [
<style
dangerouslySetInnerHTML={
Object {
"__html": "body {
background: pink;
}
",
}
}
data-chunk="letters-A"
/>,
<style
dangerouslySetInnerHTML={
Object {
"__html": "h1 {
color: cyan;
}",
}
}
data-chunk="main"
/>,
]
`),
)
})

})

describe('#getCssString', () => {
it('should return a string of the referenced css files as a promise', () => {
extractor.addChunk('letters-A')
expect.assertions(1)
return extractor.getCssString().then(data => expect(data).toMatchInlineSnapshot(`
"body {
background: pink;
}
h1 {
color: cyan;
}"
`),
)
})

})

describe('#getLinkTags', () => {
it('should return main script tag without chunk', () => {
const extractor = new ChunkExtractor({ stats })
expect(extractor.getLinkTags()).toMatchInlineSnapshot(`
"<link data-chunk=\\"main\\" rel=\\"preload\\" as=\\"style\\" href=\\"/dist/node/main.css\\">
<link data-chunk=\\"main\\" rel=\\"preload\\" as=\\"script\\" href=\\"/dist/node/main.js\\">
Expand All @@ -166,7 +241,6 @@ Array [
})

it('should return other chunks if referenced', () => {
const extractor = new ChunkExtractor({ stats })
extractor.addChunk('letters-A')
expect(extractor.getLinkTags()).toMatchInlineSnapshot(`
"<link data-chunk=\\"letters-A\\" rel=\\"preload\\" as=\\"style\\" href=\\"/dist/node/letters-A.css\\">
Expand All @@ -181,7 +255,6 @@ Array [

describe('#getLinkElements', () => {
it('should return main script tag without chunk', () => {
const extractor = new ChunkExtractor({ stats })
expect(extractor.getLinkElements()).toMatchInlineSnapshot(`
Array [
<link
Expand Down Expand Up @@ -213,7 +286,6 @@ Array [
})

it('should return other chunks if referenced', () => {
const extractor = new ChunkExtractor({ stats })
extractor.addChunk('letters-A')
expect(extractor.getLinkElements()).toMatchInlineSnapshot(`
Array [
Expand Down Expand Up @@ -260,7 +332,6 @@ Array [

describe('#requireEntryPoint', () => {
it('should load the first entrypoint', () => {
const extractor = new ChunkExtractor({ stats })
const x = extractor.requireEntrypoint()
expect(x).toBe('hello')
})
Expand Down

0 comments on commit 2caf676

Please sign in to comment.