UNPKG

7.07 kBJavaScriptView Raw
1"use strict";
2
3const path = require("path");
4
5const mime = require("mime-types");
6
7const parseRange = require("range-parser");
8
9const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
10
11const {
12 getHeaderNames,
13 getHeaderFromRequest,
14 getHeaderFromResponse,
15 setHeaderForResponse,
16 setStatusCode,
17 send
18} = require("./utils/compatibleAPI");
19
20const ready = require("./utils/ready");
21/** @typedef {import("./index.js").NextFunction} NextFunction */
22
23/** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
24
25/** @typedef {import("./index.js").ServerResponse} ServerResponse */
26
27/**
28 * @param {string} type
29 * @param {number} size
30 * @param {import("range-parser").Range} [range]
31 * @returns {string}
32 */
33
34
35function getValueContentRangeHeader(type, size, range) {
36 return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
37}
38/**
39 * @param {string | number} title
40 * @param {string} body
41 * @returns {string}
42 */
43
44
45function createHtmlDocument(title, body) {
46 return `${"<!DOCTYPE html>\n" + '<html lang="en">\n' + "<head>\n" + '<meta charset="utf-8">\n' + "<title>"}${title}</title>\n` + `</head>\n` + `<body>\n` + `<pre>${body}</pre>\n` + `</body>\n` + `</html>\n`;
47}
48
49const BYTES_RANGE_REGEXP = /^ *bytes/i;
50/**
51 * @template {IncomingMessage} Request
52 * @template {ServerResponse} Response
53 * @param {import("./index.js").Context<Request, Response>} context
54 * @return {import("./index.js").Middleware<Request, Response>}
55 */
56
57function wrapper(context) {
58 return async function middleware(req, res, next) {
59 const acceptedMethods = context.options.methods || ["GET", "HEAD"]; // fixes #282. credit @cexoso. in certain edge situations res.locals is undefined.
60 // eslint-disable-next-line no-param-reassign
61
62 res.locals = res.locals || {};
63
64 if (req.method && !acceptedMethods.includes(req.method)) {
65 await goNext();
66 return;
67 }
68
69 ready(context, processRequest, req);
70
71 async function goNext() {
72 if (!context.options.serverSideRender) {
73 return next();
74 }
75
76 return new Promise(resolve => {
77 ready(context, () => {
78 /** @type {any} */
79 // eslint-disable-next-line no-param-reassign
80 res.locals.webpack = {
81 devMiddleware: context
82 };
83 resolve(next());
84 }, req);
85 });
86 }
87
88 async function processRequest() {
89 const filename = getFilenameFromUrl(context,
90 /** @type {string} */
91 req.url);
92
93 if (!filename) {
94 await goNext();
95 return;
96 }
97
98 let {
99 headers
100 } = context.options;
101
102 if (typeof headers === "function") {
103 // @ts-ignore
104 headers = headers(req, res, context);
105 }
106 /**
107 * @type {{key: string, value: string | number}[]}
108 */
109
110
111 const allHeaders = [];
112
113 if (typeof headers !== "undefined") {
114 if (!Array.isArray(headers)) {
115 // eslint-disable-next-line guard-for-in
116 for (const name in headers) {
117 // @ts-ignore
118 allHeaders.push({
119 key: name,
120 value: headers[name]
121 });
122 }
123
124 headers = allHeaders;
125 }
126
127 headers.forEach(
128 /**
129 * @param {{key: string, value: any}} header
130 */
131 header => {
132 setHeaderForResponse(res, header.key, header.value);
133 });
134 }
135
136 if (!getHeaderFromResponse(res, "Content-Type")) {
137 // content-type name(like application/javascript; charset=utf-8) or false
138 const contentType = mime.contentType(path.extname(filename)); // Only set content-type header if media type is known
139 // https://tools.ietf.org/html/rfc7231#section-3.1.1.5
140
141 if (contentType) {
142 setHeaderForResponse(res, "Content-Type", contentType);
143 }
144 }
145
146 if (!getHeaderFromResponse(res, "Accept-Ranges")) {
147 setHeaderForResponse(res, "Accept-Ranges", "bytes");
148 }
149
150 const rangeHeader = getHeaderFromRequest(req, "range");
151 let start;
152 let end;
153
154 if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) {
155 const size = await new Promise(resolve => {
156 /** @type {import("fs").lstat} */
157 context.outputFileSystem.lstat(filename, (error, stats) => {
158 if (error) {
159 context.logger.error(error);
160 return;
161 }
162
163 resolve(stats.size);
164 });
165 });
166 const parsedRanges = parseRange(size, rangeHeader, {
167 combine: true
168 });
169
170 if (parsedRanges === -1) {
171 const message = "Unsatisfiable range for 'Range' header.";
172 context.logger.error(message);
173 const existingHeaders = getHeaderNames(res);
174
175 for (let i = 0; i < existingHeaders.length; i++) {
176 res.removeHeader(existingHeaders[i]);
177 }
178
179 setStatusCode(res, 416);
180 setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", size));
181 setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8");
182 const document = createHtmlDocument(416, `Error: ${message}`);
183 const byteLength = Buffer.byteLength(document);
184 setHeaderForResponse(res, "Content-Length", Buffer.byteLength(document));
185 send(req, res, document, byteLength);
186 return;
187 } else if (parsedRanges === -2) {
188 context.logger.error("A malformed 'Range' header was provided. A regular response will be sent for this request.");
189 } else if (parsedRanges.length > 1) {
190 context.logger.error("A 'Range' header with multiple ranges was provided. Multiple ranges are not supported, so a regular response will be sent for this request.");
191 }
192
193 if (parsedRanges !== -2 && parsedRanges.length === 1) {
194 // Content-Range
195 setStatusCode(res, 206);
196 setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", size,
197 /** @type {import("range-parser").Ranges} */
198 parsedRanges[0]));
199 [{
200 start,
201 end
202 }] = parsedRanges;
203 }
204 }
205
206 const isFsSupportsStream = typeof context.outputFileSystem.createReadStream === "function";
207 let bufferOtStream;
208 let byteLength;
209
210 try {
211 if (typeof start !== "undefined" && typeof end !== "undefined" && isFsSupportsStream) {
212 bufferOtStream =
213 /** @type {import("fs").createReadStream} */
214 context.outputFileSystem.createReadStream(filename, {
215 start,
216 end
217 });
218 byteLength = end - start + 1;
219 } else {
220 bufferOtStream =
221 /** @type {import("fs").readFileSync} */
222 context.outputFileSystem.readFileSync(filename);
223 ({
224 byteLength
225 } = bufferOtStream);
226 }
227 } catch (_ignoreError) {
228 await goNext();
229 return;
230 }
231
232 send(req, res, bufferOtStream, byteLength);
233 }
234 };
235}
236
237module.exports = wrapper;
\No newline at end of file