Skip to content

Commit 2caf676

Browse files
Will Howlettgregberge
authored andcommitted
feat: support inline CSS (#190)
1 parent cf0bb8d commit 2caf676

File tree

5 files changed

+199
-17
lines changed

5 files changed

+199
-17
lines changed

packages/server/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,17 @@ Get style links as a string of `<link>` tags.
276276
const head = `<head>${chunkExtractor.getStyleTags()}</head>`
277277
```
278278

279+
### chunkExtractor.getInlineStyleTags
280+
281+
Get inline style links as a string of `<link>` tags (returns a promise).
282+
283+
```js
284+
chunkExtractor.getInlineStyleTags()
285+
.then((styleTags) => {
286+
const head = `<head>${styleTags}</head>`
287+
}
288+
```
289+
279290
### chunkExtractor.getStyleElements
280291
281292
Get style links as an array of React `<link>` elements.
@@ -284,6 +295,34 @@ Get style links as an array of React `<link>` elements.
284295
const head = renderToString(<head>{chunkExtractor.getStyleElements()}</head>)
285296
```
286297
298+
### chunkExtractor.getInlineStyleElements
299+
300+
Get inline style links as an array of React `<link>` elements (returns a promise).
301+
302+
```js
303+
chunkExtractor.getInlineStyleElements()
304+
.then((styleElements) => {
305+
const head = renderToString(<head>{styleElements}</head>)
306+
}
307+
```
308+
309+
### chunkExtractor.getCssString
310+
311+
Get css as a raw string for using directly within app (e.g. in custom AMP style tag)
312+
313+
```js
314+
chunkExtractor.getCssString()
315+
.then((cssString) => {
316+
const head = renderToString(
317+
<head>
318+
<style
319+
dangerouslySetInnerHTML={{ __html: cssString }}
320+
/>
321+
</head>
322+
)
323+
}
324+
```
325+
287326
### ChunkExtractorManager
288327
289328
Used to inject a `ChunkExtractor` in the context of your application.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
body {
2+
background: pink;
3+
}

packages/server/__fixtures__/main.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
h1 {
2+
color: cyan;
3+
}

packages/server/src/ChunkExtractor.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable react/no-danger */
22
import path from 'path'
3+
import fs from 'fs'
34
import _ from 'lodash'
45
import React from 'react'
56
import { invariant, LOADABLE_REQUIRED_CHUNKS_KEY } from './sharedInternals'
@@ -31,12 +32,41 @@ function assetToScriptElement(asset) {
3132
)
3233
}
3334

35+
function assetToStyleString(asset) {
36+
return new Promise((resolve, reject) => {
37+
fs.readFile(asset.path, 'utf8', (err, data) => {
38+
if (err) {
39+
reject(err);
40+
return;
41+
}
42+
resolve(data);
43+
})
44+
})
45+
}
46+
3447
function assetToStyleTag(asset) {
3548
return `<link data-chunk="${asset.chunk}" rel="stylesheet" href="${
3649
asset.url
3750
}">`
3851
}
3952

53+
function assetToStyleTagInline(asset) {
54+
return new Promise((resolve, reject) => {
55+
fs.readFile(asset.path, 'utf8', (err, data) => {
56+
if (err) {
57+
reject(err);
58+
return;
59+
}
60+
resolve(
61+
`<style data-chunk="${asset.chunk}">
62+
${data}
63+
</style>
64+
`
65+
);
66+
})
67+
})
68+
}
69+
4070
function assetToStyleElement(asset) {
4171
return (
4272
<link
@@ -48,6 +78,24 @@ function assetToStyleElement(asset) {
4878
)
4979
}
5080

81+
function assetToStyleElementInline(asset) {
82+
return new Promise((resolve, reject) => {
83+
fs.readFile(asset.path, 'utf8', (err, data) => {
84+
if (err) {
85+
reject(err);
86+
return;
87+
}
88+
resolve(
89+
<style
90+
key={asset.url}
91+
data-chunk={asset.chunk}
92+
dangerouslySetInnerHTML={{ __html: data }}
93+
/>
94+
);
95+
})
96+
})
97+
}
98+
5199
const LINK_ASSET_HINTS = {
52100
mainAsset: 'data-chunk',
53101
childAsset: 'data-parent-chunk',
@@ -244,15 +292,33 @@ class ChunkExtractor {
244292
return [requiredScriptElement, ...assetsScriptElements]
245293
}
246294

295+
getCssString() {
296+
const mainAssets = this.getMainAssets('style')
297+
const promises = mainAssets.map((asset) => assetToStyleString(asset).then(data => data))
298+
return Promise.all(promises).then(results => joinTags(results))
299+
}
300+
247301
getStyleTags() {
248302
const mainAssets = this.getMainAssets('style')
249303
return joinTags(mainAssets.map(asset => assetToStyleTag(asset)))
250304
}
251305

306+
getInlineStyleTags() {
307+
const mainAssets = this.getMainAssets('style')
308+
const promises = mainAssets.map((asset) => assetToStyleTagInline(asset).then(data => data))
309+
return Promise.all(promises).then(results => joinTags(results))
310+
}
311+
252312
getStyleElements() {
253313
const mainAssets = this.getMainAssets('style')
254314
return mainAssets.map(asset => assetToStyleElement(asset))
255315
}
316+
317+
getInlineStyleElements() {
318+
const mainAssets = this.getMainAssets('style')
319+
const promises = mainAssets.map((asset) => assetToStyleElementInline(asset).then(data => data))
320+
return Promise.all(promises).then(results => results)
321+
}
256322

257323
// Pre assets
258324

packages/server/src/ChunkExtractor.test.js

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,36 @@ import stats from '../__fixtures__/stats.json'
33
import ChunkExtractor from './ChunkExtractor'
44

55
describe('ChunkExtrator', () => {
6+
let extractor
7+
8+
beforeEach(() => {
9+
extractor = new ChunkExtractor({
10+
stats,
11+
outputPath: path.resolve(__dirname, '../__fixtures__'),
12+
})
13+
})
14+
615
describe('#stats', () => {
716
it('should load stats from file', () => {
8-
const extractor = new ChunkExtractor({
17+
extractor = new ChunkExtractor({
918
statsFile: path.resolve(__dirname, '../__fixtures__/stats.json'),
1019
})
1120

1221
expect(extractor.stats).toBe(stats)
1322
})
1423

1524
it('should load stats from stats', () => {
16-
const extractor = new ChunkExtractor({ stats })
1725
expect(extractor.stats).toBe(stats)
1826
})
1927
})
2028

2129
describe('#addChunk', () => {
2230
it('should reference chunk', () => {
23-
const extractor = new ChunkExtractor({ stats })
2431
extractor.addChunk('foo')
2532
expect(extractor.chunks).toEqual(['foo'])
2633
})
2734

2835
it('should be uniq', () => {
29-
const extractor = new ChunkExtractor({ stats })
3036
extractor.addChunk('a')
3137
extractor.addChunk('b')
3238
extractor.addChunk('b')
@@ -36,15 +42,13 @@ describe('ChunkExtrator', () => {
3642

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

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

5761
describe('#getScriptElements', () => {
5862
it('should return main script tag without chunk', () => {
59-
const extractor = new ChunkExtractor({ stats })
6063
expect(extractor.getScriptElements()).toMatchInlineSnapshot(`
6164
Array [
6265
<script
@@ -76,7 +79,6 @@ Array [
7679
})
7780

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

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

113114
it('should return other chunks if referenced', () => {
114-
const extractor = new ChunkExtractor({ stats })
115115
extractor.addChunk('letters-A')
116116
expect(extractor.getStyleTags()).toMatchInlineSnapshot(`
117117
"<link data-chunk=\\"letters-A\\" rel=\\"stylesheet\\" href=\\"/dist/node/letters-A.css\\">
118118
<link data-chunk=\\"main\\" rel=\\"stylesheet\\" href=\\"/dist/node/main.css\\">"
119119
`)
120120
})
121+
122+
})
123+
124+
describe('#getInlineStyleTags', () => {
125+
it('should return inline style tags as a promise', () => {
126+
extractor.addChunk('letters-A')
127+
expect.assertions(1)
128+
return extractor.getInlineStyleTags().then(data => expect(data).toMatchInlineSnapshot(`
129+
"<style data-chunk=\\"letters-A\\">
130+
body {
131+
background: pink;
132+
}
133+
134+
</style>
135+
136+
<style data-chunk=\\"main\\">
137+
h1 {
138+
color: cyan;
139+
}
140+
</style>
141+
"
142+
`),
143+
)
144+
})
145+
121146
})
122147

123148
describe('#getStyleElements', () => {
124149
it('should return main style tag without chunk', () => {
125-
const extractor = new ChunkExtractor({ stats })
126150
expect(extractor.getStyleElements()).toMatchInlineSnapshot(`
127151
Array [
128152
<link
@@ -135,7 +159,6 @@ Array [
135159
})
136160

137161
it('should return other chunks if referenced', () => {
138-
const extractor = new ChunkExtractor({ stats })
139162
extractor.addChunk('letters-A')
140163
expect(extractor.getStyleElements()).toMatchInlineSnapshot(`
141164
Array [
@@ -152,11 +175,63 @@ Array [
152175
]
153176
`)
154177
})
178+
179+
})
180+
181+
describe('#getInlineStyleElements', () => {
182+
it('should return inline style elements as a promise', () => {
183+
extractor.addChunk('letters-A')
184+
expect.assertions(1)
185+
return extractor.getInlineStyleElements().then(data => expect(data).toMatchInlineSnapshot(`
186+
Array [
187+
<style
188+
dangerouslySetInnerHTML={
189+
Object {
190+
"__html": "body {
191+
background: pink;
192+
}
193+
",
194+
}
195+
}
196+
data-chunk="letters-A"
197+
/>,
198+
<style
199+
dangerouslySetInnerHTML={
200+
Object {
201+
"__html": "h1 {
202+
color: cyan;
203+
}",
204+
}
205+
}
206+
data-chunk="main"
207+
/>,
208+
]
209+
`),
210+
)
211+
})
212+
213+
})
214+
215+
describe('#getCssString', () => {
216+
it('should return a string of the referenced css files as a promise', () => {
217+
extractor.addChunk('letters-A')
218+
expect.assertions(1)
219+
return extractor.getCssString().then(data => expect(data).toMatchInlineSnapshot(`
220+
"body {
221+
background: pink;
222+
}
223+
224+
h1 {
225+
color: cyan;
226+
}"
227+
`),
228+
)
229+
})
230+
155231
})
156232

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

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

182256
describe('#getLinkElements', () => {
183257
it('should return main script tag without chunk', () => {
184-
const extractor = new ChunkExtractor({ stats })
185258
expect(extractor.getLinkElements()).toMatchInlineSnapshot(`
186259
Array [
187260
<link
@@ -213,7 +286,6 @@ Array [
213286
})
214287

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

261333
describe('#requireEntryPoint', () => {
262334
it('should load the first entrypoint', () => {
263-
const extractor = new ChunkExtractor({ stats })
264335
const x = extractor.requireEntrypoint()
265336
expect(x).toBe('hello')
266337
})

0 commit comments

Comments
 (0)