Skip to content

Commit 4fb0bdd

Browse files
committed
Merge pull request #63 from angularity/feature/nonembedded-css-assets
non-embedded css assets
2 parents 2a3291c + 40becc0 commit 4fb0bdd

File tree

5 files changed

+192
-129
lines changed

5 files changed

+192
-129
lines changed

lib/build/node-sass.js

Lines changed: 155 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ 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'),
15+
defaults = require('lodash.defaults');
1416

1517
/**
1618
* Search for the relative file reference from the <code>startPath</code> up to the process
1719
* working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>.
1820
* @param {string} startPath The location of the uri declaration and the place to start the search from
1921
* @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
22+
* @returns {string} the full file path of the file where found or <code>null</code> otherwise
2123
*/
22-
function encodeRelativeURL(startPath, uri) {
24+
function findFile(startPath, uri) {
2325

2426
/**
2527
* Test whether the given directory is the root of its own package
@@ -66,7 +68,7 @@ function encodeRelativeURL(startPath, uri) {
6668
var isWorking;
6769
do {
6870
pathToRoot.push(absoluteStart);
69-
isWorking = (absoluteStart !== process.cwd()) && notPackage(absoluteStart);
71+
isWorking = (absoluteStart !== process.cwd()) && notPackage(absoluteStart);
7072
absoluteStart = path.resolve(absoluteStart, '..');
7173
} while (isWorking);
7274

@@ -83,16 +85,36 @@ function encodeRelativeURL(startPath, uri) {
8385

8486
// file exists so convert to a dataURI and end
8587
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;
88+
return fullPath;
9089
}
9190
// enqueue subdirectories that are not packages and are not in the root path
9291
else {
9392
enqueue(queue, basePath);
9493
}
9594
}
95+
96+
// not found
97+
return null;
98+
}
99+
}
100+
101+
/**
102+
* Search for the relative file reference from the <code>startPath</code> up to the process
103+
* working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>,
104+
* and encode as base64 data URI.
105+
* @param {string} startPath The location of the uri declaration and the place to start the search from
106+
* @param {string} uri The content of the url() statement, expected to be a relative file path
107+
* @returns {string} data URI of the file where found or <code>null</code> otherwise
108+
*/
109+
function embedRelativeURL(startPath, uri) {
110+
var fullPath = findFile(startPath, uri);
111+
if (fullPath) {
112+
var type = mime.lookup(fullPath),
113+
contents = fs.readFileSync(fullPath),
114+
base64 = new Buffer(contents).toString('base64');
115+
return 'data:' + type + ';base64,' + base64;
116+
} else {
117+
return null;
96118
}
97119
}
98120

@@ -103,107 +125,129 @@ function encodeRelativeURL(startPath, uri) {
103125
* @param {Array.<string>} [libraryPaths] Any number of library path strings
104126
* @returns {stream.Through} A through stream that performs the operation of a gulp stream
105127
*/
106-
module.exports = function (bannerWidth, libraryPaths) {
107-
var output = [ ];
108-
var libList = (libraryPaths || [ ]).filter(function isString(value) {
109-
return (typeof value === 'string');
128+
module.exports = function (options) {
129+
defaults(options, {
130+
libraryPaths: [],
131+
embedAssets : false
110132
});
133+
134+
var output = [],
135+
libList = options.libraryPaths.filter(function isString(value) {
136+
return (typeof value === 'string');
137+
});
138+
111139
return through.obj(function (file, encoding, done) {
112140
var stream = this;
113141

114142
// 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;
143+
var sourcePath = path.dirname(file.path),
144+
compiledName = path.basename(file.path, path.extname(file.path)) + '.css',
145+
mapName = compiledName + '.map',
146+
sourceMapConsumer;
119147

120148
/**
121149
* 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
150+
* @param {string} filename The filename of the file, including extension
151+
* @param {Buffer|string|object} [contents] Optional contents for the file or fields to assign to it
124152
* @return {vinyl.File} The file that has been pushed to the stream
125153
*/
126-
function pushResult(ext, contents) {
154+
function pushResult(filename, contents) {
127155
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
156+
cwd : file.cwd,
157+
base : file.base,
158+
path : path.join(sourcePath, filename),
159+
contents: Buffer.isBuffer(contents) ? contents : (typeof contents === 'string') ? new Buffer(contents) : null
132160
});
133-
if (typeof contents === 'object') {
134-
for (var key in contents) {
135-
pending[key] = contents[key];
136-
}
137-
}
138161
stream.push(pending);
139162
return pending;
140163
}
141164

142165
/**
143-
* Plugin for css rework that follows SASS transpilation
144-
* @param {object} stylesheet AST for the CSS output from SASS
166+
* Create a plugin for css rework that performs rewriting of url() sources
167+
* @param {function({string}, {string}):{string}} uriRewriter A method that rewrites uris
145168
*/
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
169+
function rewriteUriPlugin(uriRewriter) {
170+
return function reworkPlugin(stylesheet) {
171+
172+
// visit each node (selector) in the stylesheet recursively using the official utility method
173+
// each node may have multiple declarations
174+
visit(stylesheet, function visitor(declarations) {
175+
declarations
176+
.forEach(eachDeclaration);
167177
});
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('');
181178

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

@@ -232,12 +276,13 @@ module.exports = function (bannerWidth, libraryPaths) {
232276
);
233277

234278
// rework css
235-
var reworked = rework(cssWithMap, '')
236-
.use(reworkPlugin)
237-
.toString({
238-
sourcemap : true,
239-
sourcemapAsObject: true
240-
});
279+
var plugin = rewriteUriPlugin(options.embedAssets ? embedRelativeURL : pushAssetToOutput),
280+
reworked = rework(cssWithMap, '')
281+
.use(plugin)
282+
.toString({
283+
sourcemap : true,
284+
sourcemapAsObject: true
285+
});
241286

242287
// adjust overall sourcemap
243288
delete reworked.map.file;
@@ -247,8 +292,8 @@ module.exports = function (bannerWidth, libraryPaths) {
247292
});
248293

249294
// write stream output
250-
pushResult('.css', reworked.code + '\n/*# sourceMappingURL=' + mapName + ' */');
251-
pushResult('.css.map', JSON.stringify(reworked.map, null, 2));
295+
pushResult(compiledName, reworked.code + '\n/*# sourceMappingURL=' + mapName + ' */');
296+
pushResult(mapName, JSON.stringify(reworked.map, null, 2));
252297
done();
253298
}
254299

@@ -257,19 +302,19 @@ module.exports = function (bannerWidth, libraryPaths) {
257302
* @param {string} error The error text from node-sass
258303
*/
259304
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');
305+
var analysis = /(.*)\:(\d+)\:\s*error\:\s*(.*)/.exec(error),
306+
resolved = path.resolve(analysis[1]),
307+
filename = ['.scss', '.css']
308+
.map(function (ext) {
309+
return resolved + ext;
310+
})
311+
.filter(function (fullname) {
312+
return fs.existsSync(fullname);
313+
})
314+
.pop(),
315+
message = analysis ?
316+
((filename || resolved) + ':' + analysis[2] + ':0: ' + analysis[3] + '\n') :
317+
('TODO parse this error\n' + error + '\n');
273318
if (output.indexOf(message) < 0) {
274319
output.push(message);
275320
}
@@ -291,7 +336,7 @@ module.exports = function (bannerWidth, libraryPaths) {
291336
error : error,
292337
includePaths: libList,
293338
outputStyle : 'compressed',
294-
stats : { },
339+
stats : {},
295340
sourceMap : map
296341
});
297342
}
@@ -305,10 +350,10 @@ module.exports = function (bannerWidth, libraryPaths) {
305350

306351
// display the output buffer with padding before and after and between each item
307352
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') : '';
353+
var WIDTH = 80,
354+
hr = new Array(WIDTH + 1), // this is a good trick to repeat a character N times
355+
start = (WIDTH > 0) ? (hr.join('\u25BC') + '\n') : '',
356+
stop = (WIDTH > 0) ? (hr.join('\u25B2') + '\n') : '';
312357
process.stdout.write(start + '\n' + output.join('\n') + '\n' + stop);
313358
}
314359
done();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
},
112112
"devDependencies": {
113113
"angularity-helloworld-es5": "angularity/angularity-helloworld-es5#ci-build-0.2.0-E",
114-
"angularity-todo-es5": "angularity/angularity-todo-es5#ci-build-0.2.0-F",
114+
"angularity-todo-es5": "angularity/angularity-todo-es5#ci-build-0.3.1-A",
115115
"autodocs": "^0.6.8",
116116
"jasmine-diff-matchers": "~2.0.0",
117117
"jasmine-node": "2.0.0-beta4",

0 commit comments

Comments
 (0)