Skip to content

Commit 8b57356

Browse files
author
benholloway
committed
assets are now outputted from sass as distinct files and are no-longer base64 embedded
1 parent ff46296 commit 8b57356

File tree

3 files changed

+158
-121
lines changed

3 files changed

+158
-121
lines changed

lib/build/node-sass.js

Lines changed: 143 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@ var path = require('path'),
1010
visit = require('rework-visit'),
1111
convert = require('convert-source-map'),
1212
SourceMapConsumer = require('source-map').SourceMapConsumer,
13-
mime = require('mime');
13+
mime = require('mime'),
14+
crypto = require('crypto');
1415

1516
/**
1617
* Search for the relative file reference from the <code>startPath</code> up to the process
1718
* working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>.
1819
* @param {string} startPath The location of the uri declaration and the place to start the search from
1920
* @param {string} uri The content of the url() statement, expected to be a relative file path
20-
* @returns {string} dataURI of the file where found or <code>undefined</code> otherwise
21+
* @returns {string} the full file path of the file where found or <code>null</code> otherwise
2122
*/
22-
function encodeRelativeURL(startPath, uri) {
23+
function findFile(startPath, uri) {
2324

2425
/**
2526
* Test whether the given directory is the root of its own package
@@ -66,7 +67,7 @@ function encodeRelativeURL(startPath, uri) {
6667
var isWorking;
6768
do {
6869
pathToRoot.push(absoluteStart);
69-
isWorking = (absoluteStart !== process.cwd()) && notPackage(absoluteStart);
70+
isWorking = (absoluteStart !== process.cwd()) && notPackage(absoluteStart);
7071
absoluteStart = path.resolve(absoluteStart, '..');
7172
} while (isWorking);
7273

@@ -83,16 +84,36 @@ function encodeRelativeURL(startPath, uri) {
8384

8485
// file exists so convert to a dataURI and end
8586
if (fs.existsSync(fullPath)) {
86-
var type = mime.lookup(fullPath);
87-
var contents = fs.readFileSync(fullPath);
88-
var base64 = new Buffer(contents).toString('base64');
89-
return 'data:' + type + ';base64,' + base64;
87+
return fullPath;
9088
}
9189
// enqueue subdirectories that are not packages and are not in the root path
9290
else {
9391
enqueue(queue, basePath);
9492
}
9593
}
94+
95+
// not found
96+
return null;
97+
}
98+
}
99+
100+
/**
101+
* Search for the relative file reference from the <code>startPath</code> up to the process
102+
* working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>,
103+
* and encode as base64 data URI.
104+
* @param {string} startPath The location of the uri declaration and the place to start the search from
105+
* @param {string} uri The content of the url() statement, expected to be a relative file path
106+
* @returns {string} data URI of the file where found or <code>null</code> otherwise
107+
*/
108+
function embedRelativeURL(startPath, uri) {
109+
var fullPath = findFile(startPath, uri);
110+
if (fullPath) {
111+
var type = mime.lookup(fullPath),
112+
contents = fs.readFileSync(fullPath),
113+
base64 = new Buffer(contents).toString('base64');
114+
return 'data:' + type + ';base64,' + base64;
115+
} else {
116+
return null;
96117
}
97118
}
98119

@@ -103,107 +124,123 @@ function encodeRelativeURL(startPath, uri) {
103124
* @param {Array.<string>} [libraryPaths] Any number of library path strings
104125
* @returns {stream.Through} A through stream that performs the operation of a gulp stream
105126
*/
106-
module.exports = function (bannerWidth, libraryPaths) {
107-
var output = [ ];
108-
var libList = (libraryPaths || [ ]).filter(function isString(value) {
109-
return (typeof value === 'string');
110-
});
127+
module.exports = function (libraryPaths) {
128+
var output = [],
129+
libList = (libraryPaths || []).filter(function isString(value) {
130+
return (typeof value === 'string');
131+
});
111132
return through.obj(function (file, encoding, done) {
112133
var stream = this;
113134

114135
// setup parameters
115-
var sourcePath = file.path.replace(path.basename(file.path), '');
116-
var sourceName = path.basename(file.path, path.extname(file.path));
117-
var mapName = sourceName + '.css.map';
118-
var sourceMapConsumer;
136+
var sourcePath = path.dirname(file.path),
137+
compiledName = path.basename(file.path, path.extname(file.path)) + '.css',
138+
mapName = compiledName + '.map',
139+
sourceMapConsumer;
119140

120141
/**
121142
* Push file contents to the output stream.
122-
* @param {string} ext The extension for the file, including dot
123-
* @param {string|object?} contents The contents for the file or fields to assign to it
143+
* @param {string} filename The filename of the file, including extension
144+
* @param {Buffer|string|object} [contents] Optional contents for the file or fields to assign to it
124145
* @return {vinyl.File} The file that has been pushed to the stream
125146
*/
126-
function pushResult(ext, contents) {
147+
function pushResult(filename, contents) {
127148
var pending = new gutil.File({
128-
cwd: file.cwd,
129-
base: file.base,
130-
path: sourcePath + sourceName + ext,
131-
contents: (typeof contents === 'string') ? new Buffer(contents) : null
149+
cwd : file.cwd,
150+
base : file.base,
151+
path : path.join(sourcePath, filename),
152+
contents: Buffer.isBuffer(contents) ? contents : (typeof contents === 'string') ? new Buffer(contents) : null
132153
});
133-
if (typeof contents === 'object') {
134-
for (var key in contents) {
135-
pending[key] = contents[key];
136-
}
137-
}
138154
stream.push(pending);
139155
return pending;
140156
}
141157

142158
/**
143-
* Plugin for css rework that follows SASS transpilation
144-
* @param {object} stylesheet AST for the CSS output from SASS
159+
* Create a plugin for css rework that performs rewriting of url() sources
160+
* @param {function({string}, {string}):{string}} uriRewriter A method that rewrites uris
145161
*/
146-
function reworkPlugin(stylesheet) {
147-
148-
// visit each node (selector) in the stylesheet recursively using the official utility method
149-
// each node may have multiple declarations
150-
visit(stylesheet, function visitor(declarations) {
151-
declarations
152-
.forEach(eachDeclaration);
153-
});
154-
155-
/**
156-
* Process a declaration from the syntax tree.
157-
* @param declaration
158-
*/
159-
function eachDeclaration(declaration) {
160-
var URL_STATEMENT_REGEX = /(url\s*\()\s*(?:(['"])((?:(?!\2).)*)(\2)|([^'"](?:(?!\)).)*[^'"]))\s*(\))/g;
161-
162-
// reverse the original source-map to find the original sass file
163-
var cssStart = declaration.position.start;
164-
var sassStart = sourceMapConsumer.originalPositionFor({
165-
line : cssStart.line,
166-
column: cssStart.column
162+
function rewriteUriPlugin(uriRewriter) {
163+
return function reworkPlugin(stylesheet) {
164+
165+
// visit each node (selector) in the stylesheet recursively using the official utility method
166+
// each node may have multiple declarations
167+
visit(stylesheet, function visitor(declarations) {
168+
declarations
169+
.forEach(eachDeclaration);
167170
});
168-
if (!sassStart.source) {
169-
throw new Error('failed to decode node-sass source map'); // this can occur with regressions in libsass
170-
}
171-
var sassDir = path.dirname(sassStart.source);
172-
173-
// allow multiple url() values in the declaration
174-
// split by url statements and process the content
175-
// additional capture groups are needed to match quotations correctly
176-
// escaped quotations are not considered
177-
declaration.value = declaration.value
178-
.split(URL_STATEMENT_REGEX)
179-
.map(eachSplitOrGroup)
180-
.join('');
181171

182172
/**
183-
* Encode the content portion of <code>url()</code> statements.
184-
* There are 4 capture groups in the split making every 5th unmatched.
185-
* @param {string} token A single split item
186-
* @param i The index of the item in the split
187-
* @returns {string} Every 3 or 5 items is an encoded url everything else is as is
173+
* Process a declaration from the syntax tree.
174+
* @param declaration
188175
*/
189-
function eachSplitOrGroup(token, i) {
190-
191-
// we can get groups as undefined under certain match circumstances
192-
var initialised = token || '';
193-
194-
// the content of the url() statement is either in group 3 or group 5
195-
var mod = i % 7;
196-
if ((mod === 3) || (mod === 5)) {
197-
198-
// remove query string or hash suffix
199-
var uri = initialised.split(/[?#]/g).shift();
200-
return uri && encodeRelativeURL(sassDir, uri) || initialised;
176+
function eachDeclaration(declaration) {
177+
var URL_STATEMENT_REGEX = /(url\s*\()\s*(?:(['"])((?:(?!\2).)*)(\2)|([^'"](?:(?!\)).)*[^'"]))\s*(\))/g;
178+
179+
// reverse the original source-map to find the original sass file
180+
var cssStart = declaration.position.start;
181+
var sassStart = sourceMapConsumer.originalPositionFor({
182+
line : cssStart.line,
183+
column: cssStart.column
184+
});
185+
if (!sassStart.source) {
186+
throw new Error('failed to decode node-sass source map'); // this can occur with regressions in libsass
201187
}
202-
// everything else, including parentheses and quotation (where present) and media statements
203-
else {
204-
return initialised;
188+
var sassDir = path.dirname(sassStart.source);
189+
190+
// allow multiple url() values in the declaration
191+
// split by url statements and process the content
192+
// additional capture groups are needed to match quotations correctly
193+
// escaped quotations are not considered
194+
declaration.value = declaration.value
195+
.split(URL_STATEMENT_REGEX)
196+
.map(eachSplitOrGroup)
197+
.join('');
198+
199+
/**
200+
* Encode the content portion of <code>url()</code> statements.
201+
* There are 4 capture groups in the split making every 5th unmatched.
202+
* @param {string} token A single split item
203+
* @param i The index of the item in the split
204+
* @returns {string} Every 3 or 5 items is an encoded url everything else is as is
205+
*/
206+
function eachSplitOrGroup(token, i) {
207+
208+
// we can get groups as undefined under certain match circumstances
209+
var initialised = token || '';
210+
211+
// the content of the url() statement is either in group 3 or group 5
212+
var mod = i % 7;
213+
if ((mod === 3) || (mod === 5)) {
214+
215+
// remove query string or hash suffix
216+
var uri = initialised.split(/[?#]/g).shift();
217+
return uri && uriRewriter(sassDir, uri) || initialised;
218+
}
219+
// everything else, including parentheses and quotation (where present) and media statements
220+
else {
221+
return initialised;
222+
}
205223
}
206224
}
225+
};
226+
}
227+
228+
/**
229+
* A URI re-writer function that pushes the file to the output stream and rewrites the URI accordingly.
230+
* @param {string} startPath The location of the uri declaration and the place to start the search from
231+
* @param {string} uri The content of the url() statement, expected to be a relative file path
232+
* @returns {string} the new URL of the output file where found or <code>null</code> otherwise
233+
*/
234+
function pushAssetToOutput(startPath, uri) {
235+
var fullPath = findFile(startPath, uri);
236+
if (fullPath) {
237+
var contents = fs.readFileSync(fullPath),
238+
hash = crypto.createHash('md5').update(contents).digest('hex'),
239+
filename = ['.', compiledName + '.assets', hash + path.extname(fullPath)].join('/');
240+
pushResult(filename, contents);
241+
return filename;
242+
} else {
243+
return null;
207244
}
208245
}
209246

@@ -233,7 +270,7 @@ module.exports = function (bannerWidth, libraryPaths) {
233270

234271
// rework css
235272
var reworked = rework(cssWithMap, '')
236-
.use(reworkPlugin)
273+
.use(rewriteUriPlugin(pushAssetToOutput))
237274
.toString({
238275
sourcemap : true,
239276
sourcemapAsObject: true
@@ -247,8 +284,8 @@ module.exports = function (bannerWidth, libraryPaths) {
247284
});
248285

249286
// write stream output
250-
pushResult('.css', reworked.code + '\n/*# sourceMappingURL=' + mapName + ' */');
251-
pushResult('.css.map', JSON.stringify(reworked.map, null, 2));
287+
pushResult(compiledName, reworked.code + '\n/*# sourceMappingURL=' + mapName + ' */');
288+
pushResult(mapName, JSON.stringify(reworked.map, null, 2));
252289
done();
253290
}
254291

@@ -257,19 +294,19 @@ module.exports = function (bannerWidth, libraryPaths) {
257294
* @param {string} error The error text from node-sass
258295
*/
259296
function errorHandler(error) {
260-
var analysis = /(.*)\:(\d+)\:\s*error\:\s*(.*)/.exec(error);
261-
var resolved = path.resolve(analysis[1]);
262-
var filename = [ '.scss', '.css']
263-
.map(function (ext) {
264-
return resolved + ext;
265-
})
266-
.filter(function (fullname) {
267-
return fs.existsSync(fullname);
268-
})
269-
.pop();
270-
var message = analysis ?
271-
((filename || resolved) + ':' + analysis[2] + ':0: ' + analysis[3] + '\n') :
272-
('TODO parse this error\n' + error + '\n');
297+
var analysis = /(.*)\:(\d+)\:\s*error\:\s*(.*)/.exec(error),
298+
resolved = path.resolve(analysis[1]),
299+
filename = ['.scss', '.css']
300+
.map(function (ext) {
301+
return resolved + ext;
302+
})
303+
.filter(function (fullname) {
304+
return fs.existsSync(fullname);
305+
})
306+
.pop(),
307+
message = analysis ?
308+
((filename || resolved) + ':' + analysis[2] + ':0: ' + analysis[3] + '\n') :
309+
('TODO parse this error\n' + error + '\n');
273310
if (output.indexOf(message) < 0) {
274311
output.push(message);
275312
}
@@ -291,7 +328,7 @@ module.exports = function (bannerWidth, libraryPaths) {
291328
error : error,
292329
includePaths: libList,
293330
outputStyle : 'compressed',
294-
stats : { },
331+
stats : {},
295332
sourceMap : map
296333
});
297334
}
@@ -305,10 +342,10 @@ module.exports = function (bannerWidth, libraryPaths) {
305342

306343
// display the output buffer with padding before and after and between each item
307344
if (output.length) {
308-
var width = Number(bannerWidth) || 0;
309-
var hr = new Array(width + 1); // this is a good trick to repeat a character N times
310-
var start = (width > 0) ? (hr.join('\u25BC') + '\n') : '';
311-
var stop = (width > 0) ? (hr.join('\u25B2') + '\n') : '';
345+
var WIDTH = 80,
346+
hr = new Array(WIDTH + 1), // this is a good trick to repeat a character N times
347+
start = (WIDTH > 0) ? (hr.join('\u25BC') + '\n') : '',
348+
stop = (WIDTH > 0) ? (hr.join('\u25B2') + '\n') : '';
312349
process.stdout.write(start + '\n' + output.join('\n') + '\n' + stop);
313350
}
314351
done();

tasks/css.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ function setUpTaskCss(context) {
99
}
1010

1111
var taskDefinition = {
12-
name: 'css',
13-
description: 'The "css" task performs a one time build of the SASS composition root(s).',
12+
name : 'css',
13+
description : 'The "css" task performs a one time build of the SASS composition root(s).',
1414
prerequisiteTasks: ['help'],
15-
checks: [],
16-
options: [],
17-
onInit: function onInitCssTask() {
18-
var gulp = context.gulp,
19-
runSequence = context.runSequence,
20-
rimraf = require('gulp-rimraf');
15+
checks : [],
16+
options : [],
17+
onInit : function onInitCssTask() {
18+
var gulp = context.gulp,
19+
runSequence = context.runSequence,
20+
rimraf = require('gulp-rimraf');
2121

22-
var nodeSass = require('../lib/build/node-sass'),
23-
hr = require('../lib/util/hr'),
24-
streams = require('../lib/config/streams');
22+
var nodeSass = require('../lib/build/node-sass'),
23+
hr = require('../lib/util/hr'),
24+
streams = require('../lib/config/streams');
2525

2626
gulp.task('css', function (done) {
2727
console.log(hr('-', 80, 'css'));
@@ -41,12 +41,12 @@ function setUpTaskCss(context) {
4141
// compile sass with the previously discovered lib paths
4242
gulp.task('css:build', function () {
4343
return streams.scssApp()
44-
.pipe(nodeSass(80, [streams.BOWER, streams.NODE]))
44+
.pipe(nodeSass([streams.BOWER, streams.NODE]))
4545
.pipe(gulp.dest(streams.BUILD));
4646
});
4747
},
48-
onRun: function onRunCssTask() {
49-
var gulp = context.gulp;
48+
onRun : function onRunCssTask() {
49+
var gulp = context.gulp;
5050
gulp.start(taskDefinition.name);
5151
}
5252
};

0 commit comments

Comments
 (0)