assetgraph
Version:
An auto discovery dependency graph based optimization framework for web pages and applications
294 lines (283 loc) • 10.5 kB
JavaScript
/* jshint unused:false */
const _ = require('lodash');
const pathModule = require('path');
const urlTools = require('urltools');
const getTemporaryFilePath = require('gettemporaryfilepath');
const estraverse = require('estraverse-fb');
const esanimate = require('esanimate');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
function extractRequireJsConfigFragments(parseTree, htmlAsset) {
const requireJsConfigFragments = [];
estraverse.traverse(parseTree, {
enter: node => {
if (
node.type === 'ExpressionStatement' &&
node.expression.type === 'CallExpression' &&
node.expression.callee.type === 'MemberExpression' &&
!node.expression.callee.computed &&
node.expression.callee.property.name === 'config' &&
node.expression.callee.object.type === 'Identifier' &&
node.expression.arguments.length > 0 &&
node.expression.arguments[0].type === 'ObjectExpression' &&
(node.expression.callee.object.name === 'require' ||
node.expression.callee.object.name === 'requirejs')
) {
// require.config({})
requireJsConfigFragments.push(
esanimate.objectify(node.expression.arguments[0])
);
} else if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
if (
declarator.id.type === 'Identifier' &&
(declarator.id.name === 'require' ||
declarator.id.name === 'requirejs') &&
declarator.init &&
declarator.init.type === 'ObjectExpression'
) {
// var require = {}
// var requirejs = {}
requireJsConfigFragments.push(esanimate.objectify(declarator.init));
}
}
} else if (
node.type === 'AssignmentExpression' &&
node.left.type === 'Identifier' &&
node.operator === '=' &&
node.right.type === 'ObjectExpression' &&
(node.left.name === 'require' || node.left.name === 'requirejs')
) {
// require = {}
// requirejs = {}
requireJsConfigFragments.push(esanimate.objectify(node.right));
} else if (
node.type === 'AssignmentExpression' &&
node.left.type === 'MemberExpression' &&
!node.left.computed &&
node.operator === '=' &&
node.left.object.type === 'Identifier' &&
node.left.object.name === 'window' &&
(node.left.property.name === 'require' ||
node.left.property.name === 'requirejs') &&
node.right.type === 'ObjectExpression'
) {
// window.require = {}
// window.requirejs = {}
requireJsConfigFragments.push(esanimate.objectify(node.right));
} else if (
node.type === 'AssignmentExpression' &&
node.left.type === 'MemberExpression' &&
!node.left.computed &&
node.left.object.type === 'Identifier' &&
node.left.object.name === 'require' &&
node.left.property.name === 'baseUrl' &&
node.right.type === 'Literal' &&
typeof node.right.value === 'string'
) {
// require.config.baseUrl = '...'
requireJsConfigFragments.push({
baseUrl: htmlAsset.assetGraph.resolveUrl(
htmlAsset.url.replace(/[^/]+([?#].*)?$/, ''),
node.right.value.replace(/\/?$/, '/')
)
});
}
}
});
return requireJsConfigFragments;
}
module.exports = () => {
return async function bundleRequireJs(assetGraph) {
let requireJs;
const entryPoints = [];
for (const htmlAsset of assetGraph.findAssets({
type: 'Html',
isFragment: false,
isLoaded: true
})) {
const htmlScripts = assetGraph.findRelations({
from: htmlAsset,
type: 'HtmlScript'
});
for (const [i, htmlScript] of htmlScripts.entries()) {
const dataMain = htmlScript.node.getAttribute('data-main');
if (dataMain) {
const requireJsConfigFragments = [];
for (const preceedingHtmlScript of htmlScripts.slice(0, i)) {
if (preceedingHtmlScript.to && preceedingHtmlScript.to.isLoaded) {
requireJsConfigFragments.push(
...extractRequireJsConfigFragments(
preceedingHtmlScript.to.parseTree,
htmlAsset
)
);
}
}
entryPoints.push({
htmlScript,
requireJsConfig: _.merge({}, ...requireJsConfigFragments)
});
}
}
}
if (entryPoints.length > 0) {
const globalSnapshot = Object.assign({}, global);
try {
requireJs = require('requirejs');
} catch (e) {
throw new Error(
`The graph contains ${
entryPoints.length
} top-level data-main attribute ${
entryPoints.length === 1 ? '' : 's'
}` +
', but the requirejs package is not available. Please install requirejs in the the containing project.'
);
}
const potentiallyOrphanedAssets = new Set();
try {
for (const entryPoint of entryPoints) {
const { requireJsConfig, htmlScript } = entryPoint;
let dataMain = htmlScript.node.getAttribute('data-main');
htmlScript.node.removeAttribute('data-main');
let baseUrl = requireJsConfig.baseUrl;
if (baseUrl) {
baseUrl = assetGraph
.resolveUrl(htmlScript.from.nonInlineAncestor.url, baseUrl)
.replace(/^file:\/\//, '');
} else {
baseUrl = urlTools.fileUrlToFsPath(assetGraph.root);
const lastIndexOfSlash = dataMain.lastIndexOf('/');
if (lastIndexOfSlash !== -1) {
baseUrl = assetGraph.resolveUrl(
baseUrl,
dataMain.slice(0, lastIndexOfSlash)
);
dataMain = dataMain.slice(lastIndexOfSlash + 1, dataMain.length);
}
}
baseUrl = baseUrl.replace(/\/?$/, '/'); // Ensure trailing slash
const outBundleFile = getTemporaryFilePath({ suffix: '.js' });
const outCssFileName = outBundleFile.replace(/\.js$/, '.css');
const requireJsOptimizeOptions = _.defaults(
{
siteRoot: urlTools.fileUrlToFsPath(assetGraph.root), // https://github.com/guybedford/require-css#siteroot-configuration
baseUrl,
name: dataMain,
out: outBundleFile,
optimize: 'none',
generateSourceMaps: true,
preserveLicenseComments: true
},
requireJsConfig
);
const dataAlmond = htmlScript.node.getAttribute('data-almond');
if (dataAlmond) {
potentiallyOrphanedAssets.add(htmlScript.to);
htmlScript.href = dataAlmond;
htmlScript.to = assetGraph.addAsset(
{ url: dataAlmond },
htmlScript
);
htmlScript.node.removeAttribute('data-almond');
}
await new Promise(resolve =>
requireJs.optimize(requireJsOptimizeOptions, resolve)
); // Does not pass err as the first parameter
let contents = await fs.readFileAsync(outBundleFile, 'utf-8');
await fs.unlinkAsync(outBundleFile);
let sourceMapFileName;
contents = contents.replace(
/\/\/[@#]\s*sourceMappingURL=([\w-.]+)\s*$/,
($0, sourceMapUrl) => {
sourceMapFileName = pathModule.resolve(
pathModule.dirname(outBundleFile),
decodeURIComponent(sourceMapUrl)
);
return '';
}
);
let sourceMap;
const bundleAssetUrl = `file://${baseUrl}${
dataMain ? `${dataMain}-` : ''
}bundle.js`;
if (sourceMapFileName) {
const sourceMapContents = await fs.readFileAsync(
sourceMapFileName,
'utf-8'
);
await fs.unlinkAsync(sourceMapFileName);
sourceMap = JSON.parse(sourceMapContents);
sourceMap.file = `/${urlTools.buildRelativeUrl(
assetGraph.root,
bundleAssetUrl
)}`;
sourceMap.sources = sourceMap.sources.map(
sourceFileName =>
`/${urlTools.buildRelativeUrl(
assetGraph.root,
`file://${baseUrl}${sourceFileName}`
)}`
);
}
htmlScript.from.addRelation(
{
type: 'HtmlScript',
to: {
type: 'JavaScript',
text: contents,
url: bundleAssetUrl,
sourceMap
}
},
'after',
htmlScript
);
let stats;
try {
stats = await fs.statAsync(outCssFileName);
} catch (e) {}
if (stats && stats.isFile()) {
const cssContents = await fs.readFileAsync(outCssFileName, 'utf-8');
if (cssContents) {
const existingHtmlStyles = assetGraph.findRelations({
from: htmlScript.from,
type: 'HtmlStyle'
});
const lastExistingHtmlStyle =
existingHtmlStyles[existingHtmlStyles.length - 1];
htmlScript.from.addRelation(
{
type: 'HtmlStyle',
to: {
type: 'Css',
text: cssContents,
url: `file://${baseUrl}${
dataMain ? `${dataMain}-` : ''
}bundle.css`,
sourceMap: undefined
}
},
lastExistingHtmlStyle ? 'after' : 'first',
lastExistingHtmlStyle
);
}
await fs.unlinkAsync(outCssFileName);
}
}
for (const asset of potentiallyOrphanedAssets) {
if (assetGraph.findRelations({ to: asset }).length === 0) {
assetGraph.removeAsset(asset);
}
}
} finally {
for (const key of Object.keys(global)) {
if (!(key in globalSnapshot)) {
delete global[key];
}
}
}
}
};
};