Skip to content

Commit 3e12781

Browse files
committed
Split broccoli-middleware into two different middlewares
Implementation of ember-cli RFC # 80.
1 parent c016122 commit 3e12781

12 files changed

+602
-115
lines changed

lib/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
3+
var watcherServerMiddleware = require('./middleware');
4+
var serveAssetMiddleware = require('./serve-asset-middleware');
5+
var watcherMiddleware = require('./watcher-middleware');
6+
var setFileContentResponseHeaders = require('./utils/response-header').setFileContentResponseHeaders;
7+
8+
module.exports = {
9+
'watcherServerMiddleware': watcherServerMiddleware,
10+
'serveAssetMiddleware': serveAssetMiddleware,
11+
'watcherMiddleware': watcherMiddleware,
12+
'setFileContentResponseHeaders': setFileContentResponseHeaders
13+
}

lib/middleware.js

Lines changed: 25 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
'use strict';
22

33
var path = require('path');
4-
var fs = require('fs');
5-
6-
var handlebars = require('handlebars');
74
var url = require('url');
8-
var mime = require('mime');
95

10-
var errorTemplate = handlebars.compile(fs.readFileSync(path.resolve(__dirname, 'templates/error.html')).toString());
11-
var dirTemplate = handlebars.compile(fs.readFileSync(path.resolve(__dirname, 'templates/dir.html')).toString());
6+
var setResponseHeaders = require('./utils/response-header').setResponseHeaders;
7+
var serveAsset = require('./utils/serve-asset');
8+
var errorHandler = require('./utils/error-handler');
129

1310
// You must call watcher.watch() before you call `getMiddleware`
1411
//
@@ -29,107 +26,32 @@ module.exports = function getMiddleware(watcher, options) {
2926
return function broccoliMiddleware(request, response, next) {
3027
watcher.then(function(hash) {
3128
var outputPath = path.normalize(hash.directory);
32-
var urlObj = url.parse(request.url);
29+
var incomingUrl = request.url;
30+
var urlObj = url.parse(incomingUrl);
3331
var filename = path.join(outputPath, decodeURIComponent(urlObj.pathname));
34-
var stat, lastModified, type, charset, buffer;
35-
36-
// contains null byte or escapes directory
37-
if (filename.indexOf('\0') !== -1 || filename.indexOf(outputPath) !== 0) {
38-
response.writeHead(400);
39-
response.end();
40-
return;
41-
}
4232

43-
try {
44-
stat = fs.statSync(filename);
45-
} catch (e) {
46-
// asset not found
47-
next(e);
48-
return;
33+
var updatedFileName = setResponseHeaders(request, response, {
34+
'url': incomingUrl,
35+
'filename': filename,
36+
'outputPath': outputPath,
37+
'autoIndex': options.autoIndex
38+
});
39+
40+
if (updatedFileName) {
41+
serveAsset(response, {
42+
'filename': updatedFileName
43+
});
44+
} else {
45+
// bypassing serving assets and call the next middleware
46+
next();
4947
}
50-
51-
if (stat.isDirectory()) {
52-
var hasIndex = fs.existsSync(path.join(filename, 'index.html'));
53-
54-
if (!hasIndex && !options.autoIndex) {
55-
// if index.html not present and autoIndex is not turned on, move to the next
56-
// middleware (if present) to find the asset.
57-
next();
58-
return;
59-
}
60-
61-
// If no trailing slash, redirect. We use path.sep because filename
62-
// has backslashes on Windows.
63-
if (filename[filename.length - 1] !== path.sep) {
64-
urlObj.pathname += '/';
65-
response.setHeader('Location', url.format(urlObj));
66-
response.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
67-
response.writeHead(301);
68-
response.end();
69-
return;
70-
}
71-
72-
if (!hasIndex) { // implied: options.autoIndex is true
73-
var context = {
74-
url: request.url,
75-
files: fs.readdirSync(filename).sort().map(function (child) {
76-
var stat = fs.statSync(path.join(filename,child)),
77-
isDir = stat.isDirectory();
78-
return {
79-
href: child + (isDir ? '/' : ''),
80-
type: isDir ? 'dir' : path.extname(child).replace('.', '').toLowerCase()
81-
};
82-
}),
83-
liveReloadPath: options.liveReloadPath
84-
};
85-
response.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
86-
response.writeHead(200);
87-
response.end(dirTemplate(context));
88-
return;
89-
}
90-
91-
// otherwise serve index.html
92-
filename += 'index.html';
93-
stat = fs.statSync(filename);
94-
}
95-
96-
lastModified = stat.mtime.toUTCString();
97-
response.setHeader('Last-Modified', lastModified);
98-
99-
if (request.headers['if-modified-since'] === lastModified) {
100-
// nginx style treat last-modified as a tag since browsers echo it back
101-
response.writeHead(304);
102-
response.end();
103-
return;
104-
}
105-
106-
type = mime.lookup(filename);
107-
charset = mime.charsets.lookup(type);
108-
if (charset) {
109-
type += '; charset=' + charset;
110-
}
111-
response.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
112-
response.setHeader('Content-Length', stat.size);
113-
response.setHeader('Content-Type', type);
114-
115-
// read file sync so we don't hold open the file creating a race with
116-
// the builder (Windows does not allow us to delete while the file is open).
117-
buffer = fs.readFileSync(filename);
118-
response.writeHead(200);
119-
response.end(buffer);
12048
}, function(buildError) {
121-
// All errors thrown from builder.build() are guaranteed to be
122-
// Builder.BuildError instances.
123-
var context = {
124-
stack: buildError.stack,
125-
liveReloadPath: options.liveReloadPath,
126-
payload: buildError.broccoliPayload
127-
};
128-
response.setHeader('Content-Type', 'text/html');
129-
response.writeHead(500);
130-
response.end(errorTemplate(context));
131-
}).
132-
catch(function(err) {
49+
errorHandler(response, {
50+
'buildError': buildError,
51+
'liveReloadPath': options.liveReloadPath
52+
});
53+
})
54+
.catch(function(err) {
13355
console.log(err.stack);
13456
});
13557
};

lib/serve-asset-middleware.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
var serveAsset = require('./utils/serve-asset');
3+
var setResponseHeaders = require('./utils/response-header').setResponseHeaders;
4+
5+
module.exports = function serveAssetsMiddleware(request, response, next) {
6+
var broccoliHeader = request.headers['x-broccoli'];
7+
8+
if (broccoliHeader && broccoliHeader.filename) {
9+
// set response headers for assets (files) being served from disk
10+
var updatedFileName = setResponseHeaders(request, response, {
11+
'url': broccoliHeader.url,
12+
'filename': broccoliHeader.filename,
13+
'outputPath': broccoliHeader.outputPath,
14+
'autoIndex': broccoliHeader.autoIndex
15+
});
16+
17+
if (updatedFileName) {
18+
// serve the file from disk and end the response
19+
serveAsset(response, {
20+
'filename': updatedFileName
21+
});
22+
} else if (!response.finished) {
23+
// only if request has not finished processing, call the next middleware (for cases where file is not found)
24+
next();
25+
}
26+
} else {
27+
// bypass this middleware if the broccoli header or invalid file name is not defined
28+
next();
29+
}
30+
}

lib/utils/error-handler.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
3+
var fs = require('fs');
4+
var path = require('path');
5+
6+
var handlebars = require('handlebars');
7+
var errorTemplate = handlebars.compile(fs.readFileSync(path.resolve(__dirname, '..', 'templates/error.html')).toString());
8+
9+
module.exports = function errorHandler(response, options) {
10+
// All errors thrown from builder.build() are guaranteed to be
11+
// Builder.BuildError instances.
12+
var buildError = options.buildError;
13+
14+
var context = {
15+
stack: buildError.stack,
16+
liveReloadPath: options.liveReloadPath,
17+
payload: buildError.broccoliPayload
18+
};
19+
response.setHeader('Content-Type', 'text/html');
20+
response.writeHead(500);
21+
response.end(errorTemplate(context));
22+
}

lib/utils/response-header.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
'use strict';
2+
3+
var mime = require('mime');
4+
var url = require('url');
5+
var fs = require('fs');
6+
var path = require('path');
7+
8+
var handlebars = require('handlebars');
9+
var dirTemplate = handlebars.compile(fs.readFileSync(path.resolve(__dirname, '..', 'templates/dir.html')).toString());
10+
11+
12+
function updateFilenameHeader(req, filename) {
13+
var broccoliInfo = req.headers['x-broccoli'];
14+
if (broccoliInfo) {
15+
broccoliInfo.filename = filename;
16+
}
17+
}
18+
19+
/**
20+
* Function to update response headers for files served from disk.
21+
*
22+
* @param response {HTTP.Request} the incoming request object
23+
* @param response {HTTP.Response} the outgoing response object
24+
* @param options {Object} an object containing per request info
25+
* [options.filename] {String} absolute filename path
26+
* [options.lastModified] {Number} Last modified timestamp (in UTC) for the file
27+
* [options.fileSize] {Number} size of the file
28+
*
29+
*/
30+
var setFileContentResponseHeaders = function(request, response, options) {
31+
var filename = options.filename;
32+
var fileSize = options.fileSize;
33+
34+
var type = mime.lookup(filename);
35+
var charset = mime.charsets.lookup(type);
36+
if (charset) {
37+
type += '; charset=' + charset;
38+
}
39+
response.setHeader('Content-Length', fileSize);
40+
response.setHeader('Content-Type', type);
41+
}
42+
43+
/**
44+
* Function that sets the outgoing header values (ie the response headers).
45+
*
46+
* @param response {HTTP.Request} the incoming request object
47+
* @param response {HTTP.Response} the outgoing response object
48+
* @param options {Object} an object containing per request info
49+
*
50+
* @return filename {String} the updated file path that is currently being served
51+
*/
52+
var setResponseHeaders = function (request, response, options) {
53+
var incomingUrl = options.url;
54+
var urlObj = url.parse(incomingUrl);
55+
var filename = options.filename;
56+
var outputPath = options.outputPath;
57+
var stat;
58+
59+
// contains null byte or escapes directory
60+
if (filename.indexOf('\0') !== -1 || filename.indexOf(outputPath) !== 0) {
61+
response.writeHead(400);
62+
response.end();
63+
return;
64+
}
65+
66+
try {
67+
stat = fs.statSync(filename);
68+
} catch (e) {
69+
// asset not found
70+
updateFilenameHeader(request, '');
71+
return;
72+
}
73+
74+
if (stat.isDirectory()) {
75+
var hasIndex = fs.existsSync(path.join(filename, 'index.html'));
76+
77+
if (!hasIndex && !options.autoIndex) {
78+
// if index.html not present and autoIndex is not turned on, move to the next
79+
// middleware (if present) to find the asset.
80+
updateFilenameHeader(request, '');
81+
return;
82+
}
83+
84+
// If no trailing slash, redirect. We use path.sep because filename
85+
// has backslashes on Windows.
86+
if (filename[filename.length - 1] !== path.sep) {
87+
urlObj.pathname += '/';
88+
response.setHeader('Location', url.format(urlObj));
89+
response.writeHead(301);
90+
response.end();
91+
return;
92+
}
93+
94+
if (!hasIndex) { // implied: options.autoIndex is true
95+
var context = {
96+
url: incomingUrl,
97+
files: fs.readdirSync(filename).sort().map(function (child) {
98+
var stat = fs.statSync(path.join(filename,child)),
99+
isDir = stat.isDirectory();
100+
return {
101+
href: child + (isDir ? '/' : ''),
102+
type: isDir ? 'dir' : path.extname(child).replace('.', '').toLowerCase()
103+
};
104+
}),
105+
liveReloadPath: options.liveReloadPath
106+
};
107+
response.writeHead(200);
108+
response.end(dirTemplate(context));
109+
return;
110+
}
111+
112+
// otherwise serve index.html
113+
filename += 'index.html';
114+
stat = fs.statSync(filename);
115+
}
116+
117+
// set the response headers for files that are served from the disk
118+
var lastModified = stat.mtime.toUTCString();
119+
response.setHeader('Last-Modified', lastModified);
120+
121+
if (request.headers['if-modified-since'] === lastModified) {
122+
// nginx style treat last-modified as a tag since browsers echo it back
123+
response.writeHead(304);
124+
response.end();
125+
return;
126+
}
127+
128+
setFileContentResponseHeaders(request, response, {
129+
'filename': filename,
130+
'lastModified': lastModified,
131+
'fileSize': stat.size
132+
});
133+
134+
updateFilenameHeader(request, filename);
135+
return filename;
136+
}
137+
138+
module.exports = {
139+
'setResponseHeaders': setResponseHeaders,
140+
'setFileContentResponseHeaders': setFileContentResponseHeaders
141+
}

lib/utils/serve-asset.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
3+
var fs = require('fs');
4+
5+
/**
6+
* Function that serves the current request asset from disk.
7+
*
8+
* @param response {HTTP.Response} the outgoing response object
9+
* @param options {Object} an object containing per request info
10+
*
11+
*/
12+
module.exports = function serveAsset(response, options) {
13+
// read file sync so we don't hold open the file creating a race with
14+
// the builder (Windows does not allow us to delete while the file is open).
15+
var filename = options.filename;
16+
var buffer = fs.readFileSync(filename);
17+
response.writeHead(200);
18+
response.end(buffer);
19+
}

0 commit comments

Comments
 (0)