This repository has been archived by the owner on May 17, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 45
/
ssr.js
151 lines (135 loc) · 4.95 KB
/
ssr.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import path from 'path';
import {escape, consumeSanitizedHTML} from '../sanitization';
export default function createSSRPlugin({element}) {
return async function ssrPlugin(ctx, next) {
if (!isSSR(ctx)) return next();
const template = {
htmlAttrs: {},
title: '',
head: [],
body: [],
};
ctx.element = element;
ctx.rendered = '';
ctx.template = template;
ctx.type = 'text/html';
if (!ctx.chunkUrlMap) ctx.chunkUrlMap = new Map();
if (!ctx.syncChunks) ctx.syncChunks = [];
if (!ctx.preloadChunks) ctx.preloadChunks = [];
await next();
const {htmlAttrs, title, head, body} = ctx.template;
const safeAttrs = Object.keys(htmlAttrs)
.map(attrKey => {
return ` ${escape(attrKey)}="${escape(htmlAttrs[attrKey])}"`;
})
.join('');
const safeTitle = escape(title);
const safeHead = head.map(consumeSanitizedHTML).join('');
const safeBody = body.map(consumeSanitizedHTML).join('');
const preloadHintLinks = getPreloadHintLinks(ctx);
const coreGlobals = getCoreGlobals(ctx);
const chunkScripts = getChunkScripts(ctx);
const bundleSplittingBootstrap = [
preloadHintLinks,
coreGlobals,
chunkScripts,
].join('');
const chunkPreloaderScript = getChunkPreloaderScript(ctx);
ctx.body = [
'<!doctype html>',
`<html${safeAttrs}>`,
`<head>`,
`<title>${safeTitle}</title>`,
`${bundleSplittingBootstrap}${safeHead}`,
`</head>`,
`<body>${ctx.rendered}${safeBody}${chunkPreloaderScript}</body>`,
'</html>',
].join('');
};
}
function isSSR(ctx) {
// If the request has one of these extensions, we assume it's not something that requires server-side rendering of virtual dom
// TODO(#46): this check should probably look at the asset manifest to ensure asset 404s are handled correctly
if (ctx.path.match(/\.js$/)) return false;
// The Accept header is a good proxy for whether SSR should happen
// Requesting an HTML page via the browser url bar generates a request with `text/html` in its Accept headers
// XHR/fetch requests do not have `text/html` in the Accept headers
if (!ctx.headers.accept) return false;
if (!ctx.headers.accept.includes('text/html')) return false;
//TODO(#45): Investigate alternatives to checking accept header
return true;
}
function getCoreGlobals(ctx) {
const {chunkUrlMap, webpackPublicPath, nonce} = ctx;
const chunkManifest = {};
Array.from(chunkUrlMap.entries()).forEach(([id, variant]) => {
if (variant) {
const filepath = /*variant.get(ctx.esVersion) || */ variant.get('es5');
chunkManifest[id] = path.basename(filepath);
}
}, {});
const serializedManifest = JSON.stringify(chunkManifest);
const hasManifest = Object.keys(chunkManifest).length > 0;
const manifest = hasManifest ? `__MANIFEST__ = ${serializedManifest};` : ''; // consumed by webpack
return [
`<script nonce="${nonce}">`,
`__ROUTE_PREFIX__ = ${JSON.stringify(ctx.prefix)};`, // consumed by ./client
`__WEBPACK_PUBLIC_PATH__ = ${JSON.stringify(webpackPublicPath)};`, // consumed by fusion-clientries/client-entry
manifest,
`</script>`,
].join('');
}
function getUrls({chunkUrlMap, webpackPublicPath}, chunks) {
return chunks.map(id => {
let url = chunkUrlMap.get(id).get('es5');
if (webpackPublicPath.endsWith('/')) {
url = webpackPublicPath + url;
} else {
url = webpackPublicPath + '/' + url;
}
return {id, url};
});
}
function getChunkScripts(ctx) {
const sync = getUrls(ctx, ctx.syncChunks).map(({url}) => {
return `<script defer src="${url}"></script>`;
});
const preloaded = getUrls(ctx, ctx.preloadChunks).map(({id, url}) => {
return `<script defer src="${url}" data-webpack-preload="${id}"></script>`;
});
return [...sync, ...preloaded].join('');
}
function getPreloadHintLinks(ctx) {
const chunks = [...ctx.syncChunks, ...ctx.preloadChunks];
const hints = getUrls(ctx, chunks).map(({url}) => {
return `<link rel="preload" href="${url}" as="script" />`;
});
return hints.join('');
}
function getChunkPreloaderScript({nonce = '', preloadChunks}) {
// NOTE: the event listeners below are not needed if inline onerror event handlers are allowed by CSP.
// However, this is disallowed currently.
return trim(`
<script nonce="${nonce}">
(function(){
__PRELOADED_CHUNKS__ = ${JSON.stringify(preloadChunks)};
function onError(e) {
var el = e.target;
if (el.nodeName !== "SCRIPT") return;
var val = el.getAttribute("data-webpack-preload");
if (val === null) return;
var id = parseInt(val, 10);
if (__HANDLE_ERROR) return __HANDLE_ERROR(id);
if (!__UNHANDLED_ERRORS__) __UNHANDLED_ERRORS__ = [];
__UNHANDLED_ERRORS__.push(id);
}
addEventListener("error", onError, true);
addEventListener("load", function() {
removeEventListener("error", onError);
});
})();
</script>`);
}
function trim(str) {
return str.replace(/^\s+/gm, '');
}