posthtml-inline-assets
Version:
Inline external scripts, styles, and images in HTML
140 lines (109 loc) • 4.17 kB
JavaScript
;
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var filetype = _interopDefault(require('file-type'));
var fs = _interopDefault(require('fse'));
var path = _interopDefault(require('path'));
var defaultTransforms = {
image: {
resolve(node) {
return node.tag === 'img' && node.attrs && node.attrs.src;
},
transform(node, data) {
if (node.attrs.src.endsWith('.svg')) data.mime = 'image/svg+xml';
node.attrs.src = `data:${data.mime};base64,${data.buffer.toString('base64')}`;
}
},
script: {
resolve(node) {
return node.tag === 'script' && node.attrs && node.attrs.src;
},
transform(node, data) {
delete node.attrs.src;
node.content = [data.buffer.toString('utf8')];
}
},
style: {
resolve(node) {
return node.tag === 'link' && node.attrs && node.attrs.rel === 'stylesheet' && node.attrs.href;
},
transform(node, data) {
delete node.attrs.href;
delete node.attrs.rel;
node.tag = 'style';
node.content = [data.buffer.toString('utf8')];
}
},
favicon: {
resolve(node) {
return node.tag === 'link' && node.attrs && node.attrs.rel === 'icon' && node.attrs.href;
},
transform(node, data) {
node.attrs.href = `data:${data.mime};base64,${data.buffer.toString('base64')}`;
}
}
};
function errorHandler(resolution, message, messages) {
if (resolution === 'throw') {
throw new Error(message);
} else if (resolution === 'warn') {
messages.push(message);
}
}
// tooling
var index = (opts => {
// initialize root from options
const root = Object(opts).root; // initialize transform from options
const transforms = Object(opts).transforms === false ? {} : assign(defaultTransforms, Object(opts).transforms || {});
return function posthtmlInlineAssets(tree) {
// initialize current working directory from options
const cwd = Object(opts).cwd ? path.resolve(opts.cwd) : Object(tree.options).from ? path.dirname(path.resolve(tree.options.from)) : process.cwd(); // initialize error handling from options
const resolution = Object(opts).errors; // file promises
const promises = []; // walk html nodes
tree.walk(node => {
// walk transforms
Object.keys(transforms).forEach(type => {
// transform
const transform = Object(transforms[type]); // resolved asset file path
const assetpath = typeof transform.resolve === 'function' && typeof transform.transform === 'function' && transform.resolve(node);
if (typeof assetpath === 'string') {
// absolute asset file path (used as "from")
const from = root && path.isAbsolute(assetpath) ? path.join(root, assetpath) : path.resolve(cwd, assetpath); // add promise of asset contents
promises.push(fs.readFile(from).then(buffer => {
// file type (used as "mime")
const mime = (filetype(buffer) || {}).mime; // transform the node
return transform.transform(node, {
from,
buffer,
mime
});
}).catch(error => {
// otherwise, handle any issues
errorHandler(resolution, error, tree.messages);
}));
}
});
return node;
}); // resolve all inlined assets
return Promise.all(promises).then(() => {
// filter errors from messages as warnings
const warnings = tree.messages.filter(message => message instanceof Error);
if (warnings.length) {
// conditionally warn the user about any issues
console.warn(`\nWarnings (${warnings.length}):\n${warnings.map(message => ` ${message.message}`).join('\n')}\n`);
} // return the ast
return tree;
});
};
});
function assign(objectA, objectB) {
if (typeof objectA === 'object' && typeof objectB === 'object') {
const objectC = Object.assign({}, objectA);
for (let key in objectB) {
objectC[key] = assign(objectA[key], objectB[key]);
}
return objectC;
} else {
return objectB;
}
}
module.exports = index;