UNPKG

7 kBJavaScriptView Raw
1'use strict';
2const AsyncStream = require('./streams/async-stream');
3const LinkHeader = require('http-link-header');
4const ContentLengthStream = require('./streams/content-length-stream');
5const FRAGMENT_EVENTS = [
6 'start',
7 'response',
8 'end',
9 'error',
10 'timeout',
11 'fallback',
12 'warn'
13];
14const { TEMPLATE_NOT_FOUND } = require('./fetch-template');
15
16const processTemplate = require('./process-template');
17
18const getCrossOriginHeader = (fragmentUrl, host) => {
19 if (host && fragmentUrl.indexOf(`://${host}`) < 0) {
20 return 'crossorigin';
21 }
22 return '';
23};
24
25// Early preloading of primary fragments assets to improve Performance
26const getAssetsToPreload = ({ link }, { headers = {} }) => {
27 let assetsToPreload = [];
28
29 const { refs = [] } = LinkHeader.parse(link);
30 const scriptRefs = refs
31 .filter(ref => ref.rel === 'fragment-script')
32 .map(ref => ref.uri);
33 const styleRefs = refs
34 .filter(ref => ref.rel === 'stylesheet')
35 .map(ref => ref.uri);
36
37 // Handle Server rendered fragments without depending on assets
38 if (!scriptRefs[0] && !styleRefs[0]) {
39 return '';
40 }
41 styleRefs.forEach(uri => {
42 assetsToPreload.push(`<${uri}>; rel="preload"; as="style"; nopush`);
43 });
44 scriptRefs.forEach(uri => {
45 const crossOrigin = getCrossOriginHeader(uri, headers.host);
46 assetsToPreload.push(
47 `<${uri}>; rel="preload"; as="script"; nopush; ${crossOrigin}`
48 );
49 });
50 return assetsToPreload.join(',');
51};
52
53function nextIndexGenerator(initialIndex, step) {
54 let index = initialIndex;
55
56 return () => {
57 let pastIndex = index;
58 index += step;
59 return pastIndex;
60 };
61}
62
63/**
64 * Process the HTTP Request to the Tailor Middleware
65 *
66 * @param {Object} options - Options object passed to Tailor
67 * @param {Object} request - HTTP request stream of Middleware
68 * @param {Object} response - HTTP response stream of middleware
69 */
70module.exports = function processRequest(options, request, response) {
71 this.emit('start', request);
72 const {
73 fetchContext,
74 fetchTemplate,
75 parseTemplate,
76 filterResponseHeaders,
77 maxAssetLinks
78 } = options;
79
80 const asyncStream = new AsyncStream();
81 asyncStream.once('plugged', () => {
82 asyncStream.end();
83 });
84
85 const contextPromise = fetchContext(request).catch(err => {
86 this.emit('context:error', request, err);
87 return {};
88 });
89 const templatePromise = fetchTemplate(request, parseTemplate);
90 const responseHeaders = {
91 // Disable cache in browsers and proxies
92 'Cache-Control': 'no-cache, no-store, must-revalidate',
93 Pragma: 'no-cache',
94 'Content-Type': 'text/html'
95 };
96
97 let shouldWriteHead = true;
98
99 const contentLengthStream = new ContentLengthStream(contentLength => {
100 this.emit('end', request, contentLength);
101 });
102
103 const handleError = err => {
104 this.emit('error', request, err);
105 if (shouldWriteHead) {
106 shouldWriteHead = false;
107 let statusCode = 500;
108 if (err.code === TEMPLATE_NOT_FOUND) {
109 statusCode = 404;
110 }
111
112 response.writeHead(statusCode, responseHeaders);
113 // To render with custom error template
114 if (typeof err.presentable === 'string') {
115 response.end(`${err.presentable}`);
116 } else {
117 response.end();
118 }
119 } else {
120 contentLengthStream.end();
121 }
122 };
123
124 const handlePrimaryFragment = (fragment, resultStream) => {
125 if (!shouldWriteHead) {
126 return;
127 }
128
129 shouldWriteHead = false;
130
131 fragment.once('response', (statusCode, headers) => {
132 // Map response headers
133 if (typeof filterResponseHeaders === 'function') {
134 Object.assign(
135 responseHeaders,
136 filterResponseHeaders(fragment.attributes, headers)
137 );
138 }
139
140 if (headers.location) {
141 responseHeaders.location = headers.location;
142 }
143
144 // Make resources early discoverable while processing HTML
145 const preloadAssets = headers.link
146 ? getAssetsToPreload(headers, request)
147 : '';
148 if (preloadAssets !== '') {
149 responseHeaders.link = preloadAssets;
150 }
151 this.emit('response', request, statusCode, responseHeaders);
152
153 response.writeHead(statusCode, responseHeaders);
154 resultStream.pipe(contentLengthStream).pipe(response);
155 });
156
157 fragment.once('fallback', err => {
158 this.emit('error', request, err);
159 response.writeHead(500, responseHeaders);
160 resultStream.pipe(contentLengthStream).pipe(response);
161 });
162
163 fragment.once('error', err => {
164 this.emit('error', request, err);
165 response.writeHead(500, responseHeaders);
166 response.end();
167 });
168 };
169
170 Promise.all([templatePromise, contextPromise])
171 .then(([parsedTemplate, context]) => {
172 // extendedOptions are mutated inside processTemplate
173 const extendedOptions = Object.assign({}, options, {
174 nextIndex: nextIndexGenerator(0, maxAssetLinks),
175 asyncStream
176 });
177
178 const resultStream = processTemplate(
179 request,
180 extendedOptions,
181 context
182 );
183
184 resultStream.on('fragment:found', fragment => {
185 FRAGMENT_EVENTS.forEach(eventName => {
186 fragment.once(eventName, (...args) => {
187 const prefixedName = 'fragment:' + eventName;
188 this.emit(
189 prefixedName,
190 request,
191 fragment.attributes,
192 ...args
193 );
194 });
195 });
196
197 const { primary } = fragment.attributes;
198 primary && handlePrimaryFragment(fragment, resultStream);
199 });
200
201 resultStream.once('finish', () => {
202 const statusCode = response.statusCode || 200;
203 if (shouldWriteHead) {
204 shouldWriteHead = false;
205 this.emit('response', request, statusCode, responseHeaders);
206 response.writeHead(statusCode, responseHeaders);
207 resultStream.pipe(contentLengthStream).pipe(response);
208 }
209 });
210
211 resultStream.once('error', handleError);
212
213 parsedTemplate.forEach(item => {
214 resultStream.write(item);
215 });
216 resultStream.end();
217 })
218 .catch(err => {
219 handleError(err);
220 });
221};