lib/transforms/bundleRequireJs.js

lib/ AssetGraph.js errors.js index.js query.js
assets/ Asset.js Atom.js CacheManifest.js CoffeeScript.js Css.js Flash.js Gif.js Htc.js Html.js I18n.js Ico.js Image.js JavaScript.js Jpeg.js Json.js KnockoutJsTemplate.js Less.js Png.js Rss.js StaticUrlMap.js Stylus.js Text.js Xml.js index.js
relations/ CacheManifestEntry.js CssAlphaImageLoader.js CssBehavior.js CssFontFaceSrc.js CssImage.js CssImport.js CssUrlTokenRelation.js HtmlAlternateLink.js HtmlAnchor.js HtmlAppleTouchStartupImage.js HtmlApplet.js HtmlAudio.js HtmlCacheManifest.js HtmlConditionalComment.js HtmlDataBindAttribute.js HtmlEdgeSideInclude.js HtmlEmbed.js HtmlFrame.js HtmlIFrame.js HtmlIFrameSrcDoc.js HtmlImage.js HtmlInlineScriptTemplate.js HtmlKnockoutContainerless.js HtmlObject.js HtmlRelation.js HtmlRequireJsMain.js HtmlScript.js HtmlShortcutIcon.js HtmlStyle.js HtmlStyleAttribute.js HtmlVideo.js HtmlVideoPoster.js JavaScriptAmdDefine.js JavaScriptAmdRequire.js JavaScriptCommonJsRequire.js JavaScriptExtJsRequire.js JavaScriptGetStaticUrl.js JavaScriptGetText.js JavaScriptInclude.js JavaScriptShimRequire.js JavaScriptTrHtml.js Relation.js StaticUrlMapEntry.js index.js
resolvers/ data.js extJs4Dir.js file.js fixedDirectory.js http.js index.js javascript.js
transforms/ addCacheManifest.js bundleRelations.js bundleRequireJs.js compileCoffeeScriptToJavaScript.js compileLessToCss.js compileStylusToCss.js compressJavaScript.js convertCssImportsToHtmlStyles.js convertHtmlStylesToInlineCssImports.js convertStylesheetsToInlineStyles.js drawGraph.js executeJavaScriptInOrder.js externalizeRelations.js flattenStaticIncludes.js inlineCssImagesWithLegacyFallback.js inlineRelations.js loadAssets.js mergeIdenticalAssets.js minifyAssets.js moveAssets.js moveAssetsInOrder.js populate.js prettyPrintAssets.js pullGlobalsIntoVariables.js registerRequireJsConfig.js removeAssets.js removeRelations.js setAssetContentType.js setAssetEncoding.js setAssetExtension.js setHtmlImageDimensions.js startOverIfAssetSourceFilesChange.js writeAssetsToDisc.js writeAssetsToStdout.js writeStatsToStderr.js
util/ deepCopy.js extendWithGettersAndSetters.js fsTools.js getImageInfoFromBuffers.js memoizeAsyncAccessor.js uniqueId.js urlTools.js
var _ = require('underscore'),
    urlTools = require('../util/urlTools'),
    vm = require('vm'),
    uglifyJs = require('uglify-js-papandreou'),
    uglifyAst = require('uglifyast');

module.exports = function (queryObj) {
    return function bundleRequireJs(assetGraph) {
        var requireJsConfig = assetGraph.requireJsConfig,
            assetsToCleanUp = [];
        if (!requireJsConfig) {
            throw new Error('transforms.bundleRequireJs: No assetGraph.requireJsConfig found, please run transforms.registerRequireJsConfig first');
        }
        assetGraph.findAssets(_.extend({type: 'Html'}, queryObj)).forEach(function (htmlAsset) {
            var existingHtmlStyleRelations = assetGraph.findRelations({type: 'HtmlStyle', from: htmlAsset}, true), // includeUnpopulated
                seenKnockoutJsTemplateNames = {},
                htmlStyleInsertionPoint;
            if (existingHtmlStyleRelations.length > 0) {
                htmlStyleInsertionPoint = existingHtmlStyleRelations[existingHtmlStyleRelations.length - 1];
            }

            assetGraph.findRelations({from: htmlAsset, type: ['HtmlRequireJsMain', 'HtmlScript']}).forEach(function (startRelation) {

                // Don't do anything for regular scripts that don't use require:
                if (startRelation.type === 'HtmlScript' && assetGraph.findRelations({from: startRelation.to, type: 'JavaScriptAmdRequire'}).length === 0) {
                    return;
                }

                var fallbackBaseUrl = (startRelation.to.url || htmlAsset.url).replace(/[^\/]+$/, ''),
                    outgoingRelations = [],
                    assetsToBundle = [],
                    bundleTopLevelStatements = [];

                function getModuleName(asset) {
                    return requireJsConfig.getModuleName(asset, fallbackBaseUrl);
                }

                assetGraph.eachAssetPostOrder(startRelation, {type: ['JavaScriptAmdRequire', 'JavaScriptAmdDefine', 'JavaScriptShimRequire'], to: {type: 'JavaScript'}}, function (asset) {
                    var clonedAsset = asset.clone();
                    assetsToCleanUp.push(asset);
                    assetsToBundle.push(clonedAsset);
                    assetGraph.findRelations({from: clonedAsset}, true).forEach(function (outgoingRelation) {
                        var injectDefineStatementValueAst;
                        if (/^JavaScript(?:ShimRequire|Amd(?:Define|Require))$/.test(outgoingRelation.type)) {
                            if (outgoingRelation.to.type === 'JavaScript') {
                                var targetModuleName = getModuleName(outgoingRelation.to);
                                if (outgoingRelation.to._notAmd && targetModuleName !== 'jquery' && // require-jquery.js special cases 'jquery'
                                    (!requireJsConfig.shim[targetModuleName] || !requireJsConfig.shim[targetModuleName].exports)) {

                                    outgoingRelation.detach();
                                } else {
                                    assetGraph.removeRelation(outgoingRelation);
                                }
                            } else if (outgoingRelation.to.type === 'Css' || outgoingRelation.to.type === 'Less') {
                                var newHtmlStyle = new assetGraph.constructor.relations.HtmlStyle({to: outgoingRelation.to});
                                if (htmlStyleInsertionPoint) {
                                    newHtmlStyle.attach(htmlAsset, 'after', htmlStyleInsertionPoint);
                                } else {
                                    newHtmlStyle.attach(htmlAsset, 'first');
                                }
                                htmlStyleInsertionPoint = newHtmlStyle;
                                outgoingRelation.detach();
                            } else if (outgoingRelation.to.isText) {
                                injectDefineStatementValueAst = ['call', ['name', 'GETTEXT'], [['string', '']]];
                                var oneGetTextRelation = new assetGraph.constructor.relations.JavaScriptGetText({
                                    node: injectDefineStatementValueAst,
                                    from: clonedAsset,
                                    to: outgoingRelation.to
                                });
                                outgoingRelations.push(oneGetTextRelation);

                                if (outgoingRelation.to.type === 'KnockoutJsTemplate') {
                                    var templateName = getModuleName(outgoingRelation.to).replace(/^.*\/|\.ko$/g, '');
                                    if (Object.prototype.hasOwnProperty.call(seenKnockoutJsTemplateNames, templateName)) {
                                        assetGraph.emit('error', new Error('bundleRequireJs: More than one of the loaded Knockout.js templates have the file name ' + templateName + '.ko, so they will overwrite each other in the ko.externalTemplateEngine.templates hash. Please disambiguate by changing at least one of the file names.'));
                                    } else {
                                        seenKnockoutJsTemplateNames[templateName] = true;
                                    }
                                    injectDefineStatementValueAst = ['assign', true, ['sub', ['dot', ['dot', ['name', 'ko'], 'externalTemplateEngine'], 'templates'], ['string', templateName]], injectDefineStatementValueAst];
                                    outgoingRelation.node[1] = outgoingRelation.node[1].replace(/^tpl!/, '');
                                } else {
                                    outgoingRelation.node[1] = outgoingRelation.node[1].replace(/^text!/, "");
                                }
                            } else {
                                assetGraph.emit('warn', new Error("bundleRequireJs: Skipping unsupported outgoing relation: " + outgoingRelation.toString()));
                            }
                            if (injectDefineStatementValueAst) {
                                bundleTopLevelStatements.push(['stat', ['call', ['name', 'define'], [['string', getModuleName(outgoingRelation.to)], injectDefineStatementValueAst]]]);
                            }
                        } else {
                            outgoingRelations.push(outgoingRelation);
                        }
                    });

                    // Check for existing define() statements:
                    var hasFoundPlausibleDefineStatementValueAst = false,
                        topLevelStatements = clonedAsset.parseTree[1],
                        injectDefineStatementValueAst;

                    if (topLevelStatements.length === 1 && topLevelStatements[0][0] === 'stat' &&
                        topLevelStatements[0][1][0] === 'call' &&
                        topLevelStatements[0][1][1][0] === 'function' &&
                        topLevelStatements[0][1][1][3].length === 1 &&
                        topLevelStatements[0][1][1][3][0][0] === 'if') {

                        var numDefineStatements = 0,
                            walker = uglifyJs.uglify.ast_walker();
                        walker.with_walkers({
                            call: function (expr, args) {
                                if (expr[0] === 'name' && expr[1] === 'define') {
                                    numDefineStatements += 1;
                                }
                            }
                        }, function () {
                            walker.walk(topLevelStatements[0][1][1]);
                        });
                        if (numDefineStatements === 1) {
                            var context = vm.createContext(),
                                factory;
                            context.define = function () {
                                if (arguments.length > 0 && typeof arguments[arguments.length - 1] === 'function') {
                                    factory = arguments[arguments.length - 1];
                                }
                            };
                            context.define.amd = {jQuery: true, multiversion: true, plugins: true};

                            try {
                                vm.runInContext(uglifyJs.uglify.gen_code(topLevelStatements[0]), context);
                            } catch (e) {
                                factory = false;
                            }
                            if (factory) {
                                injectDefineStatementValueAst = uglifyAst.objToAst(factory);
                                topLevelStatements = clonedAsset.parseTree[1] = [];
                                hasFoundPlausibleDefineStatementValueAst = true;
                            }
                        }
                    }
                    if (!hasFoundPlausibleDefineStatementValueAst) {
                        var callDefineAsts = [],
                            walker = uglifyJs.uglify.ast_walker();
                        walker.with_walkers({
                            call: function (expr) {
                                if (expr[0] === 'name' && expr[1] === 'define') {
                                    var stack = walker.stack();
                                    callDefineAsts.push(stack[stack.length - 1]);
                                }
                            }
                        }, function () {
                            walker.walk(clonedAsset.parseTree);
                        });
                        if (callDefineAsts.length === 1) {
                            var moduleName = getModuleName(asset);
                            if (moduleName) {
                                if (asset === startRelation.to) {
                                    // Strip "scripts/" prefix of the main module (hmm, not sure?)
                                    moduleName = moduleName.replace(/^.*\//, "");
                                }
                                var firstArgumentIsString = callDefineAsts[0][2].length > 0 && callDefineAsts[0][2][0][0] === 'string';
                                callDefineAsts[0][2].splice(0, firstArgumentIsString ? 1 : 0, ['string', moduleName]);
                            }
                            injectDefineStatementValueAst = null;
                            hasFoundPlausibleDefineStatementValueAst = true;
                        }
                    }
                    if (injectDefineStatementValueAst) {
                        bundleTopLevelStatements.push(['stat', ['call', ['name', 'define'], [['string', getModuleName(asset)], injectDefineStatementValueAst]]]);
                    }
                    if (!hasFoundPlausibleDefineStatementValueAst) {
                        asset._notAmd = true;
                    }
                    Array.prototype.push.apply(bundleTopLevelStatements, clonedAsset.parseTree[1]);
                });
                startRelation.to.replaceWith(new assetGraph.constructor.assets.JavaScript({
                    url: startRelation.to.url,
                    parseTree: ['toplevel', bundleTopLevelStatements],
                    outgoingRelations: outgoingRelations
                }));
                outgoingRelations.forEach(function (outgoingRelation) {
                    outgoingRelation.refreshHref();
                });
                assetsToBundle.forEach(function (asset) {
                    assetGraph.removeAsset(asset);
                });
            });
        });
        assetsToCleanUp.forEach(function (asset) {
            if (asset.assetGraph && asset.incomingRelations.every(function (incomingRelation) {return assetsToCleanUp.indexOf(incomingRelation.from) !== -1;})) {
                assetGraph.removeAsset(asset);
            }
        });
        assetGraph.recomputeBaseAssets(true);
    };
};