1 | 'use strict';
|
2 | const AsyncStream = require('./streams/async-stream');
|
3 | const LinkHeader = require('http-link-header');
|
4 | const ContentLengthStream = require('./streams/content-length-stream');
|
5 | const FRAGMENT_EVENTS = [
|
6 | 'start',
|
7 | 'response',
|
8 | 'end',
|
9 | 'error',
|
10 | 'timeout',
|
11 | 'fallback',
|
12 | 'warn'
|
13 | ];
|
14 | const { TEMPLATE_NOT_FOUND } = require('./fetch-template');
|
15 |
|
16 | const processTemplate = require('./process-template');
|
17 |
|
18 | const getCrossOriginHeader = (fragmentUrl, host) => {
|
19 | if (host && fragmentUrl.indexOf(`://${host}`) < 0) {
|
20 | return 'crossorigin';
|
21 | }
|
22 | return '';
|
23 | };
|
24 |
|
25 |
|
26 | const 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 |
|
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 |
|
53 | function 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 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 | module.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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 | };
|