1 | var waterfall = require('async-collection/waterfall')
|
2 | var mapLimit = require('async-collection/map-limit')
|
3 | var explain = require('explain-error')
|
4 | var concat = require('concat-stream')
|
5 | var resolve = require('resolve')
|
6 | var crypto = require('crypto')
|
7 | var pump = require('pump')
|
8 | var path = require('path')
|
9 |
|
10 | var critical = require('inline-critical-css')
|
11 | var documentify = require('documentify')
|
12 | var hyperstream = require('hstream')
|
13 |
|
14 | var ttyError = require('./tty-error')
|
15 | var ServerRender = require('../ssr')
|
16 | var utils = require('./utils')
|
17 |
|
18 | var WRITE_CONCURRENCY = 3
|
19 |
|
20 | module.exports = node
|
21 |
|
22 | function node (state, createEdge) {
|
23 | var entry = utils.basefile(state.metadata.entry)
|
24 | var self = this
|
25 |
|
26 | var ssr = new ServerRender(entry)
|
27 | if (ssr.error && ssr.error.isSsr) {
|
28 | self.emit('ssr', { success: false, error: ssr.error })
|
29 | } else if (ssr.error) {
|
30 | ssr.error = ttyError('documents', entry, ssr.error)
|
31 | return self.emit('error', 'documents', entry, ssr.error)
|
32 | }
|
33 |
|
34 |
|
35 |
|
36 | self.emit('ssr', { success: true, renderRoute: documentifyRoute })
|
37 |
|
38 | var fonts = extractFonts(state.assets)
|
39 | var list = ssr.routes
|
40 |
|
41 | mapLimit(list, WRITE_CONCURRENCY, documentifyRoute, function (err) {
|
42 | if (err) return self.emit(err)
|
43 | createEdge('list', Buffer.from(list.join(',')))
|
44 | })
|
45 |
|
46 | function documentifyRoute (route, done) {
|
47 | waterfall([
|
48 | renderApp,
|
49 | documentifyApp,
|
50 | pushEdge
|
51 | ], done)
|
52 |
|
53 | function renderApp (done) {
|
54 | ssr.render(route, function (err, content) {
|
55 | if (err) {
|
56 | self.emit('ssr', {success: false, error: err})
|
57 | return done(err)
|
58 | }
|
59 | done(null, content)
|
60 | })
|
61 | }
|
62 |
|
63 | function pushEdge (buf, done) {
|
64 | var name = route
|
65 | if (name === '/') name = 'index'
|
66 | name = name + '.html'
|
67 | createEdge(name, buf, {
|
68 | mime: 'text/html'
|
69 | })
|
70 | done(null, buf)
|
71 | }
|
72 | }
|
73 |
|
74 | function getTemplate (content, done) {
|
75 |
|
76 | var name = 'index'
|
77 |
|
78 | var dir = path.join(path.dirname(entry), name)
|
79 | resolve('.', { basedir: dir, extensions: ['.html'] }, function (err, filename) {
|
80 | if (err) {
|
81 | done(dir, head(content.language))
|
82 | } else {
|
83 |
|
84 | done(filename, null)
|
85 | }
|
86 | })
|
87 | }
|
88 |
|
89 | function documentifyApp (content, done) {
|
90 | var base = state.metadata.opts.base
|
91 | var route = content.route
|
92 | var title = content.title
|
93 | var body = content.body
|
94 | var selector = content.selector
|
95 |
|
96 | var hasDynamicScripts = state.scripts.bundle.dynamicBundles.length > 0
|
97 |
|
98 | getTemplate(content, ontemplate)
|
99 |
|
100 | function ontemplate (filename, html) {
|
101 | var d = documentify(filename, html)
|
102 | var header = [
|
103 | viewportTag(),
|
104 | scriptTag({ hash: state.scripts.bundle.hash, base: base }),
|
105 | hasDynamicScripts && dynamicScriptsTag({
|
106 | bundleNames: state.scripts.bundle.dynamicBundles,
|
107 | scripts: state.scripts,
|
108 | base: base
|
109 | }),
|
110 | preloadTag(),
|
111 | loadFontsTag({ fonts: fonts, base: base }),
|
112 | manifestTag({ base: base }),
|
113 | descriptionTag({ description: state.manifest.bundle.description }),
|
114 | themeColorTag({ color: state.manifest.bundle.color }),
|
115 | titleTag({ title: title })
|
116 | ].filter(Boolean)
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 | if (state.metadata.reload) {
|
123 | header.push(reloadTag({ bundle: state.reload.bundle.buffer }))
|
124 | }
|
125 |
|
126 | d.transform(addToHead, header.join(''))
|
127 |
|
128 | d.transform(insertApp, {
|
129 | selector: selector,
|
130 | body: body
|
131 | })
|
132 |
|
133 | if (state.styles.bundle.buffer.length) {
|
134 | d.transform(criticalTransform, { css: state.styles.bundle.buffer })
|
135 | }
|
136 |
|
137 | d.transform(addToHead, styleTag({ hash: state.styles.bundle.hash, base: base }))
|
138 |
|
139 | function complete (buf) { done(null, buf) }
|
140 |
|
141 | pump(d.bundle(), concat({ encoding: 'buffer' }, complete), function (err) {
|
142 | if (err) return done(explain(err, 'Error in documentify while operating on ' + route))
|
143 | })
|
144 | }
|
145 | }
|
146 | }
|
147 |
|
148 | function head (lang) {
|
149 | var dir = 'ltr'
|
150 | return `<!DOCTYPE html><html lang="${lang}" dir="${dir}"><head></head><body></body></html>`
|
151 | }
|
152 |
|
153 |
|
154 | function preloadTag () {
|
155 | var content = ';(function(a){"use strict";var b=function(b,c,d){function e(a){return h.body?a():void setTimeout(function(){e(a)})}function f(){i.addEventListener&&i.removeEventListener("load",f),i.media=d||"all"}var g,h=a.document,i=h.createElement("link");if(c)g=c;else{var j=(h.body||h.getElementsByTagName("head")[0]).childNodes;g=j[j.length-1]}var k=h.styleSheets;i.rel="stylesheet",i.href=b,i.media="only x",e(function(){g.parentNode.insertBefore(i,c?g:g.nextSibling)});var l=function(a){for(var b=i.href,c=k.length;c--;)if(k[c].href===b)return a();setTimeout(function(){l(a)})};return i.addEventListener&&i.addEventListener("load",f),i.onloadcssdefined=l,l(f),i};"undefined"!=typeof exports?exports.loadCSS=b:a.loadCSS=b})("undefined"!=typeof global?global:this);'
|
156 | content += ';(function(a){if(a.loadCSS){var b=loadCSS.relpreload={};if(b.support=function(){try{return a.document.createElement("link").relList.supports("preload")}catch(b){return!1}},b.poly=function(){for(var b=a.document.getElementsByTagName("link"),c=0;c<b.length;c++){var d=b[c];"preload"===d.rel&&"style"===d.getAttribute("as")&&(a.loadCSS(d.href,d,d.getAttribute("media")),d.rel=null)}},!b.support()){b.poly();var c=a.setInterval(b.poly,300);a.addEventListener&&a.addEventListener("load",function(){b.poly(),a.clearInterval(c)}),a.attachEvent&&a.attachEvent("onload",function(){a.clearInterval(c)})}}})(this);'
|
157 | return `<script>${content}</script>`
|
158 | }
|
159 |
|
160 | function scriptTag (opts) {
|
161 | var hex = opts.hash.toString('hex').slice(0, 16)
|
162 | var base64 = 'sha512-' + opts.hash.toString('base64')
|
163 | var link = `${opts.base || ''}/${hex}/bundle.js`
|
164 | return `<script src="${link}" defer integrity="${base64}"></script>`
|
165 | }
|
166 |
|
167 | function dynamicScriptsTag (opts) {
|
168 | return opts.bundleNames.map(function (name) {
|
169 | name = name.replace(/\.js$/, '')
|
170 | var hash = opts.scripts[name].hash
|
171 | var hex = hash.toString('hex').slice(0, 16)
|
172 | var base64 = 'sha512-' + hash.toString('base64')
|
173 | var link = `${opts.base || ''}/${hex}/${name}.js`
|
174 | return `<link rel="prefetch" href="${link}" integrity="${base64}">`
|
175 | }).join('')
|
176 | }
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 | function styleTag (opts) {
|
183 | var hex = opts.hash.toString('hex').slice(0, 16)
|
184 | var link = `${opts.base || ''}/${hex}/bundle.css`
|
185 | return `<link rel="preload" as="style" href="${link}" onload="this.rel='stylesheet'">`
|
186 | }
|
187 |
|
188 | function loadFontsTag (opts) {
|
189 | return opts.fonts.reduce(function (html, font) {
|
190 | if (!path.isAbsolute(font)) font = (opts.base || '') + '/' + font
|
191 | return html + `<link rel="preload" as="font" crossorigin href="${font}">`
|
192 | }, '')
|
193 | }
|
194 |
|
195 | function manifestTag (opts) {
|
196 | return `
|
197 | <link rel="manifest" href="${opts.base || ''}/manifest.json">
|
198 | `.replace(/\n +/g, '')
|
199 | }
|
200 |
|
201 | function viewportTag () {
|
202 | return `
|
203 | <meta charset="utf-8">
|
204 | <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
205 | `.replace(/\n +/g, '')
|
206 | }
|
207 |
|
208 | function descriptionTag (opts) {
|
209 | return `
|
210 | <meta name="description" content="${opts.description}">
|
211 | `.replace(/\n +/g, '')
|
212 | }
|
213 |
|
214 | function themeColorTag (opts) {
|
215 | return `
|
216 | <meta name="theme-color" content=${opts.color}>
|
217 | `.replace(/\n +/g, '')
|
218 | }
|
219 |
|
220 | function titleTag (opts) {
|
221 | return `
|
222 | <title>${opts.title}</title>
|
223 | `.replace(/\n +/g, '')
|
224 | }
|
225 |
|
226 | function criticalTransform (opts) {
|
227 | return critical(String(opts.css))
|
228 | }
|
229 |
|
230 | function reloadTag (opts) {
|
231 | var bundle = opts.bundle
|
232 | var base64 = sha512(bundle)
|
233 | return `<script integrity="${base64}">${bundle}</script>`
|
234 | }
|
235 |
|
236 | function addToHead (str) {
|
237 | return hyperstream({
|
238 | head: {
|
239 | _appendHtml: str
|
240 | }
|
241 | })
|
242 | }
|
243 |
|
244 | function insertApp (opts) {
|
245 | return hyperstream({
|
246 | [opts.selector]: {
|
247 | _replaceHtml: opts.body
|
248 | }
|
249 | })
|
250 | }
|
251 |
|
252 |
|
253 | function extractFonts (state) {
|
254 | var list = String(state.list.buffer).split(',')
|
255 |
|
256 | var res = list.filter(function (font) {
|
257 | var extname = path.extname(font)
|
258 |
|
259 | return extname === '.woff' ||
|
260 | extname === '.woff2' ||
|
261 | extname === '.eot' ||
|
262 | extname === '.ttf'
|
263 | })
|
264 |
|
265 | return res
|
266 | }
|
267 |
|
268 | function sha512 (buf) {
|
269 | return 'sha512-' + crypto.createHash('sha512')
|
270 | .update(buf)
|
271 | .digest('base64')
|
272 | }
|