Fix bug where bundler stops watching files when an error occurs.

This commit is contained in:
Alexandre Gigliotti
2015-10-07 11:54:47 -07:00
parent f28f0e718a
commit c7ca7bfc75
13 changed files with 342 additions and 134 deletions

View File

@@ -1,13 +1,21 @@
var co = require('co'); var co = require('co');
var cheerio = require('cheerio');
var fs = require('mz/fs'); var fs = require('mz/fs');
var inlineStyle = require('./inline-style'); var inlineStyle = require('./inline-style');
var inlineImg = require('./inline-img'); var inlineImg = require('./inline-img');
var inlineLinkLess = require('./inline-link-less'); var inlineLess = require('./inline-less');
var R = require('ramda'); var R = require('ramda');
var Ru = require('@panosoft/ramda-utils'); var Ru = require('@panosoft/ramda-utils');
var inline = co.wrap(function * (html, options) { /**
* Embed referenced local assets within and HTML file.
*
* @param {String} html
* Filename or html string.
* @param {Object} options
*
* @return {Promise}
*/
var inlineHtml = co.wrap(function * (html, options) {
options = Ru.defaults({ options = Ru.defaults({
filename: null, filename: null,
less: {}, less: {},
@@ -19,39 +27,37 @@ var inline = co.wrap(function * (html, options) {
html = yield fs.readFile(filename, 'utf8'); html = yield fs.readFile(filename, 'utf8');
} }
catch (error) { catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') filename = options.filename;
filename = options.filename; else throw error;
}
else {
throw error;
}
} }
// Embed assets
var files = [filename]; var files = [filename];
try {
var lessResult = yield inlineLess(html, filename, options.less);
html = lessResult.html;
files = R.concat(files, lessResult.files);
// Inline links var styleResult = inlineStyle(html, filename);
var lessResult = yield inlineLinkLess(html, filename, options.less); html = styleResult.html;
html = lessResult.html; files = R.concat(files, styleResult.files);
files.push(lessResult.files);
// TODO inline links: css var imgResult = inlineImg(html, filename);
html = imgResult.html;
// TODO inline scripts files = R.concat(files, imgResult.files);
// browserify js? => scriptify }
catch (error) {
// Inline paths -> datauris if (!error.filename) error.filename = filename;
var styleResult = inlineStyle(html, filename); // Inline styles error.files = R.uniq(R.concat(files, error.files || []));
html = styleResult.html; throw error;
files.push(styleResult.files); }
var imgResult = inlineImg(html, filename); // Inline images
html = imgResult.html;
files.push(imgResult.files);
files = R.uniq(files);
var result = { var result = {
html: html, html,
files: R.uniq(R.flatten(files, true)) files
}; };
return (options.verbose ? result : result.html); return (options.verbose ? result : result.html);
}); });
module.exports = inline; module.exports = inlineHtml;

View File

@@ -28,26 +28,34 @@ var clean = function (path) {
* @param filename * @param filename
* @returns {{css: (css|any), files: Array}} * @returns {{css: (css|any), files: Array}}
*/ */
var inline = function (css, filename) { var inlineUrl = function (css, filename) {
var files = []; var files = [];
var basePath = path.dirname(filename); var basePath = path.dirname(filename);
var result = postcss() var result = postcss()
.use(postcssUrl({ .use(postcssUrl({
url: function (urlPath) { url: function (urlPath) {
if (isLocalPath(urlPath) && !isTemplateExpression(urlPath)) { if (isLocalPath(urlPath) && !isTemplateExpression(urlPath)) {
urlPath = clean(urlPath); try {
urlPath = path.resolve(basePath, urlPath); urlPath = clean(urlPath);
files.push(urlPath); urlPath = path.resolve(basePath, urlPath);
urlPath = datauri(urlPath); files = R.append(urlPath, files);
urlPath = datauri(urlPath);
}
catch (error) {
error.filename = filename;
error.files = R.uniq(files);
throw error;
}
} }
return urlPath; return urlPath;
} }
})) }))
.process(css); .process(css);
files = R.uniq(files);
return { return {
css: result.css, css: result.css,
files: files files
}; };
}; };
module.exports = inline; module.exports = inlineUrl;

View File

@@ -3,25 +3,34 @@ var datauri = require('datauri');
var isLocalPath = require('is-local-path'); var isLocalPath = require('is-local-path');
var isTemplateExpression = require('./is-template-expression'); var isTemplateExpression = require('./is-template-expression');
var path = require('path'); var path = require('path');
var R = require('ramda');
var inline = function (html, filename) { var inline = function (html, filename) {
var files = []; var files = [];
var basedir = path.dirname(filename); var basedir = path.dirname(filename);
var $ = cheerio.load(html, {decodeEntities: false}); var $ = cheerio.load(html, {decodeEntities: false});
var images = $('img').filter(function (index, element) { var $images = $('img').filter((index, element) => {
var path = $(element).attr('src'); var path = $(element).attr('src');
return isLocalPath(path) && !isTemplateExpression(path); return isLocalPath(path) && !isTemplateExpression(path);
}); });
images.each(function (index, element) { try {
var src = $(element).attr('src'); $images.each((index, element) => {
var filename = path.resolve(basedir, src); var source = $(element).attr('src');
files.push(filename); var filename = path.resolve(basedir, source);
src = datauri(filename); files = R.append(filename, files);
$(element).attr('src', src); var uri = datauri(filename);
}); $(element).attr('src', uri);
});
}
catch (error) {
error.filename = filename;
error.files = R.uniq(files);
throw error;
}
files = R.uniq(files);
return { return {
html: $.xml(), html: $.xml(),
files: files files
}; };
}; };

67
lib/inline-less.js Normal file
View File

@@ -0,0 +1,67 @@
var co = require('co');
var cheerio = require('cheerio');
var fs = require('mz/fs');
var isLocalPath = require('is-local-path');
var less = require('less');
var path = require('path');
var R = require('ramda');
var Ru = require('@panosoft/ramda-utils');
var render = co.wrap(function * (filename, options) {
options = R.merge(options || {}, { filename });
var contents = yield fs.readFile(filename, 'utf8');
return yield less.render(contents, options);
});
/**
* @param {String} html
* HTML source to inline
* @param {String} filename
* Filename to apply to the HTML source being inlined
* @param {Object} options
* LESS compiler options
*/
var inlineLess = co.wrap(function * (html, filename, options) {
options = Ru.defaults({
relativeUrls: true
}, options || {});
var basedir = path.dirname(filename);
// get links
var $ = cheerio.load(html, {decodeEntities: false});
var $links = $('link[rel="stylesheet/less"]')
.filter((index, element) => isLocalPath($(element).attr('href')));
// render LESS stylesheets
var files = [];
var outputs = [];
try {
$links.each((index, element) => {
var href = $(element).attr('href');
var filename = path.resolve(basedir, href);
files = R.append(filename, files);
outputs = R.append(render(filename, options), outputs);
});
outputs = yield outputs;
}
catch (error) {
if (!error.filename) error.filename = filename;
error.files = R.uniq(files);
throw error;
}
// include imported filenames in files array
files = R.concat(files, R.flatten(R.map(output => output.imports, outputs)));
files = R.uniq(files);
// replace links
$links.each((index, element) => {
var style = $('<style>').html(outputs[index].css);
$(element).replaceWith(style);
});
return {
html: $.xml(),
files
};
});
module.exports = inlineLess;

View File

@@ -1,65 +0,0 @@
var co = require('co');
var cheerio = require('cheerio');
var fs = require('mz/fs');
var isLocalPath = require('is-local-path');
var less = require('less');
var path = require('path');
var R = require('ramda');
var Ru = require('@panosoft/ramda-utils');
var url = require('url');
var render = co.wrap(function * (filename, options) {
options = R.merge(options || {}, {
filename: filename
});
var contents = yield fs.readFile(filename, 'utf8');
return yield less.render(contents, options);
});
/**
* @params html
* HTML to inline
* @params options
* LESS compiler options
*/
var inline = co.wrap(function * (html, filename, options) {
var files = [];
var basedir = path.dirname(filename);
options = Ru.defaults({
relativeUrls: true
}, options || {});
// TODO Import less links
// get links
var $ = cheerio.load(html, {decodeEntities: false});
var links = $('link[rel="stylesheet/less"]')
.filter(function (index, element) {
return isLocalPath($(element).attr('href'));
});
// render stylesheets
var outputs = [];
links.each(function (index, element) {
var href = $(element).attr('href');
var filename = path.resolve(basedir, href);
files.push(filename);
outputs.push(render(filename, options));
});
outputs = yield outputs;
// replace links
links.each(function (index, element) {
var style = $('<style>').html(outputs[index].css);
$(element).replaceWith(style);
});
// create list of imported files from all outputs, unique listing
files.push(R.map(function (output) {
return output.imports;
}, outputs));
return {
html: $.xml(),
files: files
};
});
module.exports = inline;

View File

@@ -1,5 +1,6 @@
var cheerio = require('cheerio'); var cheerio = require('cheerio');
var inlineUrl = require('./inline-css-url'); var inlineUrl = require('./inline-css-url');
var R = require('ramda');
var string = require('string'); var string = require('string');
var prefix = 'element {'; var prefix = 'element {';
@@ -11,34 +12,43 @@ var unwrap = function (value) {
var regexp = new RegExp('^' + prefix + '\\s*(.*)\\s*' + suffix + '$'); var regexp = new RegExp('^' + prefix + '\\s*(.*)\\s*' + suffix + '$');
return value.replace(regexp, '$1'); return value.replace(regexp, '$1');
}; };
var inline = function (html, filename) { var inlineStyle = function (html, filename) {
var files = []; var files = [];
var $ = cheerio.load(html, {decodeEntities: false}); var $ = cheerio.load(html, {decodeEntities: false});
// style elements
var styles = $('style');
styles.each(function (index, element) {
var css = $(element).html();
var result = inlineUrl(css, filename);
files.push(result.files);
$(element).html(result.css);
});
// style attributes try {
var attributes = $('*').filter('[style]'); // style elements
attributes.each(function (index, element) { var $styles = $('style');
var css = $(element).attr('style'); $styles.each((index, element) => {
css = wrap(css); var css = $(element).html();
var result = inlineUrl(css, filename); var result = inlineUrl(css, filename);
files.push(result.files); files = R.concat(files, result.files);
css = string(result.css).collapseWhitespace().toString(); $(element).html(result.css);
css = unwrap(css); });
$(element).attr('style', css);
});
// style attributes
var $attributes = $('*').filter('[style]');
$attributes.each((index, element) => {
var css = $(element).attr('style');
css = wrap(css);
var result = inlineUrl(css, filename);
files = R.concat(files, result.files);
css = string(result.css).collapseWhitespace().toString();
css = unwrap(css);
$(element).attr('style', css);
});
}
catch (error) {
if (!error.filename) error.filename = filename;
error.files = R.uniq(R.concat(files, error.files || []));
throw error;
}
files = R.uniq(files);
return { return {
html: $.xml(), html: $.xml(),
files: files files
}; };
}; };
module.exports = inline; module.exports = inlineStyle;

View File

@@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet/less" href="main.less" />
</head>
<body></body>
</html>

View File

@@ -0,0 +1 @@
@import (less) 'missing.css';

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet/less" href="main.less" />
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1 @@
div {

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet/less" href="main.less" />
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,3 @@
div {
background-image: url('missing.png');
}

View File

@@ -1,9 +1,10 @@
var inline = require('../lib'); var co = require('co');
var datauri = require('datauri');
var expect = require('chai') var expect = require('chai')
.use(require('chai-as-promised')) .use(require('chai-as-promised'))
.expect; .expect;
var datauri = require('datauri');
var fs = require('fs'); var fs = require('fs');
var inline = require('../lib');
var path = require('path'); var path = require('path');
describe('inlineHtml', function () { describe('inlineHtml', function () {
@@ -173,4 +174,146 @@ describe('inlineHtml', function () {
var html = (source) => `<style> div { background-image: url('${source}'); }</style>`; var html = (source) => `<style> div { background-image: url('${source}'); }</style>`;
return expect(inline(html(filename))).to.eventually.equal(html(uri)); return expect(inline(html(filename))).to.eventually.equal(html(uri));
}); });
// Error handling
// inline-img
it('throw error when html image source invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var source = 'missing.png';
var html = `<img src="${source}" >`;
var resolvedSource = path.resolve(path.dirname(filename), source);
try {
yield inline(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedSource);
}
});
});
// inline-style
it('throw error when html style attribute syntax invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var html = `<div style="background url()"></div>`;
try {
yield inline(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(filename);
}
});
});
it('throw error when html style attribute url invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var url = 'missing.png';
var resolvedUrl = path.resolve(path.dirname(filename), url);
var html = `<div style="background-image: url('${url}')"></div>`;
try {
yield inline(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedUrl);
}
});
});
it('throw error when html style syntax invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var html = `<style>div {</style>`;
try {
yield inline(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(filename);
}
});
});
it('throw error when html style url invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var url = 'missing.png';
var resolvedUrl = path.resolve(path.dirname(filename), url);
var html = `<style>div { background-image: url('${url}'); }</style>`;
try {
yield inline(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedUrl);
}
});
});
// inline-link-less
it('throw error when link href invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var href = 'missing.less';
var resolvedHref = path.resolve(path.dirname(filename), href);
var html = `<link rel="stylesheet/less" href="${href}">`;
try {
yield inline(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedHref);
}
});
});
it('throw error when less import invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'fixtures/errors/lessImport/index.html');
var lessFilename = path.resolve(path.dirname(filename), 'main.less');
try {
yield inline(filename);
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(lessFilename);
expect(error).to.have.property('files').that.contains(lessFilename);
}
});
});
it('throw error when less syntax invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'fixtures/errors/lessSyntax/index.html');
var lessFilename = path.resolve(path.dirname(filename), 'main.less');
try {
yield inline(filename);
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(lessFilename);
expect(error).to.have.property('files').that.contains(lessFilename);
}
});
});
it('throw error when less url invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'fixtures/errors/lessUrl/index.html');
var lessFilename = path.resolve(path.dirname(filename), 'main.less');
var badUrl = path.resolve(path.dirname(filename), 'missing.png');
try {
yield inline(filename);
throw new Error('No error thrown');
}
catch (error) {
// expect error.filename to be html file, not less file, since images
// aren't inlined until after the compiled less has been inlined into the html.
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(badUrl);
}
});
});
}); });