Skip to content

Commit 6e055a5

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

12 files changed

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

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

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)