assetgraph-builder-esprima
Version:
Build system for web sites and applications
328 lines (314 loc) • 18.4 kB
JavaScript
var urlTools = require('urltools'),
_ = require('lodash'),
browsersList = require('browserslist');
module.exports = function (options) {
options = options || {};
var minify = typeof options.minify === 'undefined' ? true : options.minify;
var javaScriptSerializationOptions = _.extend({}, options.javaScriptSerializationOptions);
// Default to try to support all browsers
var browsers = {
has: function () { return true; },
minimum: function () { return 1; }
};
if (options.browsers) {
browsers = {
selected: browsersList(options.browsers),
has: function (browserOrBrowsers) {
try {
return browsersList(browserOrBrowsers).some(function (browser) {
return browsers.selected.some(function (selectedBrowser) {
return selectedBrowser === browser || selectedBrowser.indexOf(browser + ' ') === 0;
});
});
} catch (e) {
// Parse error, try to match it as a browser name (any version) so that
// browsers.has('ie') does the expected.
return browsers.selected.some(function (selectedBrowser) {
return selectedBrowser.indexOf(browserOrBrowsers + ' ') === 0;
});
}
},
// Find the minimum version of a given browser that is to be supported.
// For example: browsers.minimum('ie'); // 10
minimum: function (browser) {
var minimumVersion = null;
browsers.selected.forEach(function (selectedBrowser) {
if (selectedBrowser.indexOf(browser + ' ') === 0) {
var version = parseFloat(selectedBrowser.substr(browser.length + 1));
if (minimumVersion === null || version < minimumVersion) {
minimumVersion = version;
}
}
});
return minimumVersion;
}
};
if (browsers.minimum('ie') > 8) {
javaScriptSerializationOptions.screw_ie8 = true;
}
}
var bundleStrategyName = options.sharedBundles ? 'sharedBundles' : 'oneBundlePerIncludingAsset',
inlineByRelationType = options.inlineByRelationType || {HtmlScript: 4096, HtmlStyle: 4096};
return function buildProduction(assetGraph, cb) {
var query = assetGraph.query;
assetGraph.javaScriptSerializationOptions = javaScriptSerializationOptions;
if (!assetGraph.followRelations) {
if (options.recursive) {
assetGraph.followRelations =
query.or({
to: {type: 'I18n'}
},
{
type: ['HtmlAnchor', 'HtmlMetaRefresh'],
to: /^file:/
},
{
type: query.not(['JavaScriptInclude', 'JavaScriptExtJsRequire', 'JavaScriptCommonJsRequire', 'SvgAnchor', 'JavaScriptSourceMappingUrl', 'JavaScriptSourceUrl']),
to: {url: query.not(/^https?:/)}
});
} else {
assetGraph.followRelations =
query.or({
to: {type: 'I18n'}
},
{
type: query.not(['JavaScriptInclude', 'JavaScriptExtJsRequire', 'JavaScriptCommonJsRequire', 'SvgAnchor', 'JavaScriptSourceMappingUrl', 'JavaScriptSourceUrl', 'HtmlAnchor', 'HtmlMetaRefresh']),
to: {url: query.not(/^https?:/)}
});
}
}
assetGraph
.populate({from: {type: 'Html'}, followRelations: {type: 'HtmlScript', to: {url: /^file:/}}})
.assumeRequireJsConfigHasBeenFound()
.populate()
.fixBaseAssetsOfUnresolvedOutgoingRelationsFromHtmlFragments({isInitial: true})
.assumeThatAllHtmlFragmentAssetsWithoutIncomingRelationsAreNotTemplates()
.populate({startAssets: {type: 'Html'}})
// Remove bootstrapper scripts injected by buildDevelopment:
.removeRelations({type: 'HtmlScript', node: {id: 'bootstrapper'}, from: {type: 'Html'}}, {detach: true, removeOrphan: true})
.addDataVersionAttributeToHtmlElement({type: 'Html', isInitial: true}, options.version)
.replaceDartWithJavaScript()
.populate({startAssets: {type: 'JavaScript', url: /\.dart\.js$/}})
.if(options.localeIds)
.checkLanguageKeys({
supportedLocaleIds: options.localeIds,
defaultLocaleId: options.defaultLocaleId,
ignoreMessageTypes: 'unused'
})
.endif()
.if(options.less)
// Replace Less assets with their Css counterparts:
.compileLessToCss({type: 'Less', isLoaded: true})
// Remove the in-browser less compiler and its incoming relations,
// even if it's included from a CDN and thus hasn't been populated.
// FIXME: Turns out there's a bunch of less.js files (eg. in the ACE editor)
// that we don't want to be remove, so this is probably a bit too magical.
.removeRelations({type: 'HtmlScript', to: {url: /\/less(?:-\d+\.\d+\.\d+)?(?:\.min)?\.js$/}}, {unresolved: true, detach: true, removeOrphan: true})
.endif()
.if(options.scss)
.compileScssToCss({type: 'Scss', isLoaded: true})
.endif()
.compileJsxToJs()
.if(options.stripDebug)
.stripDebug({type: 'JavaScript', isLoaded: true})
.endif()
.removeRelations({type: 'JavaScriptInclude', to: {type: ['Css', 'JavaScript', 'Html', 'Less']}}, {detach: true, unresolved: true})
.externalizeRelations({from: {type: query.not('Htc')}, type: ['HtmlStyle', 'HtmlScript'], to: {isLoaded: true}, node: function (node) {return !node.hasAttribute('nobundle');}})
.mergeIdenticalAssets({
isImage: true,
isLoaded: true,
url: /^[^\?]$/ // Skip images with a query string in the url (might contain processImage instructions)
})
// First execute explicit instructions in the query strings for images that are to be sprited:
.processImages({isImage: true, isLoaded: true, url: /\?(?:|.*&)sprite(?:[&=#]|$)/})
.spriteBackgroundImages()
// Execute explicit and automatic optimizations for all images, including the generated sprite images:
.processImages({isImage: true, isLoaded: true}, {autoLossless: options.optimizeImages})
.minifySvgAssetsWithSvgo({isLoaded: true})
.inlineKnockoutJsTemplates()
.liftUpJavaScriptRequireJsCommonJsCompatibilityRequire()
.queue(function checkForRequireJsConfig(assetGraph, done) {
if (assetGraph.requireJsConfig) {
assetGraph
.flattenRequireJs({type: 'Html', isFragment: false})
.run(done);
} else {
done();
}
})
// Remove orphan assets, except I18n which might originally have been referenced from the no longer existing bootstrapper:
.removeUnreferencedAssets({type: query.not('I18n'), isInitial: query.not(true)})
.convertCssImportsToHtmlStyles()
.removeDuplicateHtmlStyles({type: 'Html', isInitial: true})
.mergeIdenticalAssets({isLoaded: true, isInline: false, type: ['JavaScript', 'Css']})
.if(options.browsers)
.autoprefixer(options.browsers)
.endif()
.bundleRelations({type: 'HtmlStyle', to: {type: 'Css', isLoaded: true}, node: function (node) {return !node.hasAttribute('nobundle');}}, {strategyName: bundleStrategyName})
.splitCssIfIeLimitIsReached({type: 'Css'}, {minimumIeVersion: browsers.minimum('ie')})
.replaceRequireJsWithAlmond()
.bundleRelations({type: 'HtmlScript', to: {type: 'JavaScript', isLoaded: true}, node: function (node) {return !node.hasAttribute('nobundle');}}, {strategyName: bundleStrategyName})
.mergeIdenticalAssets({isLoaded: true, isInline: false, type: ['JavaScript', 'Css']}) // The bundling might produce several identical files, especially the 'oneBundlePerIncludingAsset' strategy.
.if(options.angular)
.angularAnnotations()
.endif()
.removeNobundleAttribute({type: ['HtmlScript', 'HtmlStyle']})
.if(inlineByRelationType.CssImage)
.inlineCssImagesWithLegacyFallback({
type: 'Html',
isInline: false,
isFragment: false
}, {
sizeThreshold: typeof inlineByRelationType.CssImage === 'boolean' ? (inlineByRelationType.CssImage ? Infinity : 0) : inlineByRelationType.CssImage,
minimumIeVersion: browsers.minimum('ie')
})
.mergeIdenticalAssets({isLoaded: true, isInline: false, type: 'Css'})
.endif()
.if(minify)
.minifyAssets({isLoaded: true})
.endif()
.if(options.addInitialHtmlExtension)
.queue(function addInitialHtmlExtension() {
assetGraph.findAssets({isInitial: true, type: 'Html'}).forEach(function (asset) {
asset.fileName = asset.fileName.replace(/(?=\.|$)/, '.' + String(options.addInitialHtmlExtension).replace(/^\./, ''));
});
})
.endif()
.if(options.localeIds)
.cloneForEachLocale({type: 'Html', isInitial: true}, {
localeIds: options.localeIds,
supportedLocaleIds: options.localeIds,
localeCookieName: options.localeCookieName,
defaultLocaleId: options.defaultLocaleId
})
.runJavaScriptConditionalBlocks({isInitial: true}, 'LOCALIZE', true)
.removeRelations({type: query.not('JavaScriptGetText'), to: {type: 'I18n'}}, {detach: true})
.removeUnreferencedAssets({type: 'I18n'})
.endif()
.if(options.defines)
.replaceSymbolsInJavaScript({type: 'JavaScript', isLoaded: true}, options.defines)
.endif()
.if(options.removeDeadIfs)
.removeDeadIfsInJavaScript({isLoaded: true})
.endif()
.if(!options.noCompress)
.compressJavaScript({type: 'JavaScript', isLoaded: true}, 'uglifyJs', {mangleOptions: {except: options.reservedNames || []}})
.endif()
.removeEmptyJavaScripts()
.removeEmptyStylesheets()
.queue(function inlineRelations(assetGraph) {
return assetGraph.findRelations({
to: {isLoaded: true, isInline: false},
from: {isInline: false} // Excludes relations occurring in conditional comments
}).filter(function (relation) {
if (relation.type === 'CssImage' && !options.noInlineCssImagesWithLegacyFallback) {
return false; // Already handled by the inlineCssImagesWithLegacyFallback transform above
}
var sizeThreshold = inlineByRelationType[relation.type],
isStarRule = false;
if (typeof sizeThreshold === 'undefined') {
sizeThreshold = inlineByRelationType['*'];
isStarRule = true;
}
if (typeof sizeThreshold !== 'undefined' && sizeThreshold === true || relation.to.lastKnownByteLength < sizeThreshold) {
if (isStarRule) {
// Some relation types don't support inlining, and the inline method will be missing or throw an exception.
// Wrap it in a try...catch so the * rule means "all relation types that support inlining".
try {
relation.inline();
} catch (e) {}
} else {
relation.inline();
}
}
});
})
.if(options.noCompress)
.prettyPrintAssets({type: 'Css', isLoaded: true})
.prettyPrintAssets(function (asset) {
return asset.isLoaded && asset.type === 'JavaScript' && (!asset.isInline || asset.incomingRelations.every(function (incomingRelation) {
return incomingRelation.type === 'HtmlScript';
}));
})
.endif()
.if(options.angular)
.inlineAngularJsTemplates()
.endif()
.duplicateFavicon()
.setAsyncOrDeferOnHtmlScripts({to: {isInline: false, url: /^file:/}}, options.asyncScripts, options.deferScripts)
.omitFunctionCall({type: ['JavaScriptGetStaticUrl', 'JavaScriptTrHtml'], to: {isLoaded: true}})
.inlineRelations({type: ['JavaScriptGetText', 'JavaScriptTrHtml'], to: {isLoaded: true}})
.if(options.manifest)
.addCacheManifest({isInitial: true})
.if(options.localeIds && options.negotiateManifest)
.queue(function stripLocaleIdFromHtmlCacheManifestRelations(assetGraph) {
// This would be much less fragile if an asset could have a canonical url as well as an url (under consideration):
assetGraph.findRelations({type: 'HtmlCacheManifest'}).forEach(function (htmlCacheManifest) {
htmlCacheManifest.href = htmlCacheManifest.href.replace(/\.\w+\.appcache$/, '.appcache');
});
})
.endif()
.endif()
.if(options.canonicalUrl)
// Maybe this should be the effect of updating assetGraph.root?
.moveAssets({isLoaded: true, isInline: false}, function (asset, assetGraph) {
if (asset.url.indexOf(assetGraph.root) === 0) {
return assetGraph.resolveUrl(options.canonicalUrl, urlTools.buildRelativeUrl(assetGraph.root, asset.url));
}
})
.updateRelations({from: {type: 'Html', isFragment: true, nonInlineAncestor: {type: ['Rss', 'Atom']}}}, {hrefType: 'absolute'})
.endif()
.if(!options.noFileRev)
.moveAssetsInOrder(query.and(
{
isLoaded: true,
isInline: false,
type: query.not(['CacheManifest', 'Rss', 'Atom']),
fileName: query.not(['.htaccess', 'humans.txt', 'robots.txt'])
},
{
url: query.not(assetGraph.root + 'favicon.ico')
},
query.not({
type: 'Html',
incomingRelations: function (relations) {
return relations.some(function (rel) {
return rel.type === 'HtmlAnchor' || rel.type === 'HtmlMetaRefresh';
});
}
}),
query.or(
query.not({
isInitial: true
}),
{
type: 'Html',
isFragment: true
}
)), function (asset, assetGraph) {
var targetUrl = (options.canonicalUrl || '/') + 'static/';
// Conservatively assume that all GETSTATICURL relations pointing at non-images are intended to be fetched via XHR
// and thus cannot be put on a CDN because of same origin restrictions:
if (options.cdnRoot && asset.type !== 'Htc' && asset.extension !== '.jar' && (asset.type !== 'Html' || options.cdnHtml) && (asset.isImage || assetGraph.findRelations({to: asset, type: 'StaticUrlMapEntry'}).length === 0) || (options.cdnRoot && options.cdnFlash && asset.type === 'Flash')) {
targetUrl = options.cdnRoot;
if (/^\/\//.test(options.cdnRoot)) {
assetGraph.findRelations({to: asset}).forEach(function (incomingRelation) {
incomingRelation.hrefType = 'protocolRelative';
// Set crossorigin=anonymous on <script> tags pointing at CDN JavaScript.
// See http://blog.errorception.com/2012/12/catching-cross-domain-js-errors.html'
if (asset.type === 'JavaScript' && incomingRelation.type === 'HtmlScript') {
incomingRelation.node.setAttribute('crossorigin', 'anonymous');
incomingRelation.from.markDirty();
}
});
}
}
return targetUrl + asset.fileName.split('.').shift() + '.' + asset.md5Hex.substr(0, 10) + asset.extension + asset.url.replace(/^[^#\?]*(?:)/, ''); // Preserve query string and fragment identifier
})
.endif()
.if(options.gzip)
.gzip({isInline: false, isText: true, isLoaded: true, url: query.not(/\.t?gz([\?#]|$)/)})
.endif()
.run(cb);
};
};