0

I'm trying to modularize my existing project by breaking out functionality into separate applications that share a lot of common code. It's a Backbone/Marionette app, and everything is working fine in development mode, but I'm having trouble getting optimization to work. I currently have two pages, with 2 main files and 2 application files. The main files both contain requirejs.config blocks which are almost identical, except the second one uses the map config option to map the app module to loginApp. The reason for this is that most of the other modules depend on the app module for some application-wide functionality, including messaging and some global state variables.

main.js:

requirejs.config({
    shim: { ... },
    paths: { ... }
});

define(['vendor'], function() {
    // This loads app.js
    require(['app'], function(Application) {
        Application.start();
    });
});

main-login.js:

requirejs.config({
    shim: { ... },
    paths: { ... },
    map: {
        "*": { "app": "loginApp" }
    }
});

define(['vendor'], function() {
    // This loads loginApp.js because of the mapping above
    require(['app'], function(Application) {
        Application.start();
    });
});

This works great until I optimize. I'm getting an error about a missing file, but having worked with requirejs long enough, I know that really has nothing to do with the problem. :)

From the docs:

Note: when doing builds with map config, the map config needs to be fed to the optimizer, and the build output must still contain a requirejs config call that sets up the map config. The optimizer does not do ID renaming during the build, because some dependency references in a project could depend on runtime variable state. So the optimizer does not invalidate the need for a map config after the build.

My build.js file looks like this:

({
    baseUrl: "js",
    dir: "build",
    mainConfigFile: "js/main.js",
    removeCombined: true,
    findNestedDependencies: true,
    skipDirOptimize: true,
    inlineText: true,
    useStrict: true,
    wrap: true,
    keepBuildDir: false,
    optimize: "uglify2",
    modules: [
        {
            name: "vendor"
        },
        {
            name: "main",
            exclude: ["vendor"]
        },
        {
            name: "main-login",
            exclude: ["vendor"],
            override: {
                mainConfigFile: "js/main-login.js",
                map: {
                    "*": {
                        "app": "loginApp"
                    }
                }
            }
        }
    ]
});

I'd like to avoid having 2 separate build files, if possible, and I'm working on breaking out the requirejs.config block into a single, shared file and having the 2 main files load that and then load the app files (this is similar to how the multipage example works) but I need that map config to work in the optimizer in order for this to work. Any ideas what I'm missing here?

Update

I've split out the config into its own file, config.js, which gets included by the main-* files. In the main-login.js file, I include the map config above the define and everything works in development mode.

require.config({
    map: {
        "*": {
            "app": "loginApp"
        }
    }
});

define(['module', 'config'], function(module, config) {
    ...

The build.js file is the same as above, except with the second mainConfigFile removed. Optimization still fails, though. What I think is happening is, since this is a Marionette app, it's common practice to pass the Application object as a dependency to other parts of the app, including views, controllers and models. When I optimize, I run into two different problems. If I leave removeCombined as true, the optimizer will build in the dependencies from the first app, then remove those files, so when it sees them in the second app, it will fail because it can't find the source files anymore. Setting this to false seems reasonable, but the problem is this then gives me the following error:

Error: RangeError: Maximum call stack size exceeded

I can't find any consistent information on this particular error. It might have something to do with the hbs plugin (similar to text but for pre-compiling Handlebars templates) but I'm not positive that's the case. Since there's no stack trace, I'm not sure where to start looking. My gut feeling is it's a circular dependency somewhere, though. So, my updated question is, how should a multi-page Marionette app be decoupled so as to make sharing code (not just 3rd party code, but custom code such as data models and views) possible? Do I need to remove any dependencies on the core Application object? (That would require an awful lot of refactoring.) Since it works just fine in development mode, is there some trick to r.js's config I'm overlooking? I've tried adding app to the exclude lists as well as stubModules but nothing seems to work. I'm thinking of just creating 2 build files and being done with it, but I'd really like to know how to solve this the "right" way.

ken.dunnington
  • 865
  • 7
  • 20
  • + using only **one config file** (only one require.config({}) is recommended + you can use more than one build file to build different js file + dependencies are **case sensitive** (all references to the same file have to be exactly the same) – Nima Shahri Feb 24 '15 at 05:51
  • I ended up using two build files (and one config file) and everything's working perfectly now. There's some unnecessary duplication going on (the 'vendor' file gets compiled twice, in case the files get run out of order) but it's a small price to pay. – ken.dunnington Feb 24 '15 at 16:58

2 Answers2

1

Your build file can be like this:

({
    ...
    include: [ './config/main.js' ],
    pragmasOnSave: {
        excludeBuildConfig: true
    }
})

You can use pragmasOnSave to tell optimizer to exclude a section in a file in optimized result, so Requirejs config file can be like following code

requirejs.config({
//>>excludeStart('excludeBuildConfig', pragmas.excludeBuildConfig)
    shim: { ... },
    paths: { ... },
//>>excludeEnd('excludeBuildConfig')
    map: {
          "*": { "app": "loginApp" }
    }
});
Nima Shahri
  • 167
  • 6
  • That's a very cool idea, thanks for the info. I was able to use `override` in the build script and include the map there (I created a shared config file, similar to the multipage-site example), but there are other issues preventing me from compiling. I'm going to spend more time refactoring and searching and will update this question with my results. – ken.dunnington Feb 23 '15 at 19:31
0

The final solution used was to incorporate Grunt into the build workflow. Inside Grunt, I'm dynamically creating task targets to the requirejs task. I refactored my multiple applications to all use the same folder structure, so it was easy to reuse the same build config for each. There's still the minor inconvenience of compiling the vendor file multiple times, but that's a small price to pay.

Here's the function I use to create the config inside my dev task, in case anyone's interested:

var buildRequireTargets = function(appList) {
    var requireTargets = {},
        buildConfig = {
            baseUrl: "<%= sourceDir %>/js",
            dir: "<%= buildDir %>/js",
            mainConfigFile: "<%= sourceDir %>/js/config.js",
            removeCombined: true,
            findNestedDependencies: true,
            skipDirOptimize: true,
            inlineText: true,
            useStrict: true,
            wrap: true,
            keepBuildDir: true,
            optimize: "none",
            pragmasOnSave: {
                excludeHbs: true
            }
        };

    _.each(appList, function (app) {
        requireTargets[app] = {
            options: _.extend({
                map: {
                    "*": {
                        "app": app + "/app"
                    }
                },
                modules: [
                    {
                        name: "vendor"
                    },
                    {
                        name: app + "/main",
                        exclude: ["vendor"]
                    }
                ]
            }, buildConfig)
        };
    });

    grunt.config("requirejs", requireTargets);
};
ken.dunnington
  • 865
  • 7
  • 20