RrequireJS optimizer and loading dynamic paths after optimization - javascript

I have a grunt task with requirejs and am running the optimizer.
I load a lot of external files which are not always needed at run time, usually I only need a handful of core files. Then based on user decisions I load certain files during run time.
Ex:
define(["backbone", 'text!data/filePaths.json'],
function(Backbone, filePaths) {
'use strict';
return Backbone.Model.extend({
initialize: function(){
// parse the file paths, there could be a hundred here
this.filePaths = JSON.parse(filePaths);
},
// dynamically add a file via this function call
addFile: function(id){
var self = this;
return new Promise(function(resolve, reject){
// load files dynamically based on the id passed in
require([self.filePaths[id].path], function(View){
resolve(new View());
});
});
}
});
}
);
the file paths json might look like this:
"ONE": {
"name": "BlueBox",
"path": "views/box/Blue"
},
"TWO": {
"name": "RedBox",
"path": "views/box/Red"
},
etc...
The issue is that this does not work with the optimizer for me.
When I run my app with the optimized file I get:
Uncaught Error: undefined missing views/box/Red
Update:
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
requirejs: {
desktopJS: {
options: {
baseUrl: "public/js/app",
wrap: true,
// Cannot use almond since it does not currently appear to support requireJS's config-map
name: "../libs/almond",
preserveLicenseComments: false,
optimize: "uglify2",
uglify2: {
// mangle: false,
// no_mangle: true,
// stats: true,
// "mangle-props": false,
"max-line-len": 1000,
max_line_length: 1000
},
uglify: {
mangle: false,
no_mangle: true,
stats: true,
"mangle-props": false,
max_line_length: 1000,
beautify: true
},
mainConfigFile: "public/js/app/config/config.js",
include: ["init/DesktopInit"],
out: "public/js/app/init/DesktopInit.min.js"
}
},
desktopCSS: {
options: {
optimizeCss: "standard",
cssIn: "./public/css/desktop.css",
out: "./public/css/desktop.min.css"
}
}
},
Note: if I use the unoptimized version, everything works just fine.

The optimizer is unable to trace dependencies for require calls that do not have dependencies as an array of string literals. Your call is an example of a require call that the optimizer cannot process because the list of dependencies is computed at run-time:
require([self.filePaths[id].path], function(View){
The reason for this is simple: the optimizer does not evaluate your modules as it optimizes them. Anyway, the number of possible values for self.filePaths[id].path is potentially infinite so there's no way the optimizer could handle all cases. So when the optimizer optimizes your code, the modules that should be loaded by this require call are not included in the bundle. One solution, which you've touched upon in your own answer is to use include to include all possible modules that could be loaded by that require call.
As you point out, if you can have hundred of modules, this means including them all in the bundle produced by the optimizer. Is there an alternative? Yes, there is.
You can produce a bundle that includes only the other modules of your application and leave the modules that are to be loaded by the require call above to be loaded individually rather than as part of the bundle. Ah, but there's a problem with the specific configuration you show in the question. You have a comment that says you cannot use Almond. Yet, in fact you do use it, right there on the next line. (And you also have it in your answer.) The problem is that one of Almond's restrictions is that it does not do dynamic loading. That's the very first restriction in the list of restrictions. You'd have to use a full-featured AMD loader for this, like RequireJS itself.

This is not really the answer, just a way "around" my application not working.
I believe the only way I can circumvent this is with the include configuration
Ex:
requirejs: {
desktopJS: {
options: {
baseUrl: "public/js/app",
wrap: true,
name: "../libs/almond",
optimize: "uglify2",
uglify2: {
"max-line-len": 1000,
max_line_length: 1000
},
mainConfigFile: "public/js/app/config/config.js",
include: ["init/DesktopInit", "views/box/Blue"], // include the file here, all of a sudden the error goes away for that file.
Though this becomes cumbersome for when I have a hundred files. I don't want to build the whole thing with all my files included, I would like a set of optimized files, and a bunch of other ones which can be required dynamically.

Related

Ordering the output using gulp and main-bower-files (gulp-order doesn't work)

Java engineer here, new to the Javascript world, so apologies if I'm not providing enough/correct info!
I have a gulp build script and use bower to manage the dependencies for my JS frontend app. I am struggling to get the (bower controlled) vendor javascript libraries concatenated in the right order so that jQuery and angular come first.
If they aren't first then I get angular is undefined or jQuery is undefined when I load my application, as their corresponding javascript code is buried somewhere further down in the concatenated library.
I've attempted to use gulp-order to order the output from the resulting main-bower-files call, but it appears to have no effect.
My gulp task is as follows:
gulp.task('lib', function () {
var files =
gulp.src(mainBowerFiles({
paths: {
bowerrc: './.bowerrc',
bowerJson: './bower.json'
}, env: 'lib'}))
.pipe(order(["vendor/bower/jquery/**/*.js", "vendor/bower/angular/**/*.js", "vendor/bower/**/*.js" ]))
return processJsFiles('lib', files);
});
processJsFiles does the uglify work and looks pretty much like:
.pipe(uglify(name + '.js', {
outSourceMap: true,
screwIe8: true,
compress: {
hoist_funs: false,
if_return: false
}
}))
.pipe(gulp.dest(build_dir))
.pipe(gzip())
.pipe(gulp.dest(build_dir));
Any clues or better ways I should be doing this?
After much spelunking through the docs I figured out that you need to specify the base of where the order plugin needs to start from, e.g.:
gulp.task('lib', function () {
var files =
gulp.src(mainBowerFiles({
paths: {
bowerrc: './.bowerrc',
bowerJson: './bower.json'
}, env: 'lib'}))
.pipe(order([
"vendor/bower/jquery/**/*.js",
"vendor/bower/angular/**/*.js",
"vendor/bower/**/*.js"
], { base: './' })
);
return processJsFiles('lib', files);
});

RequireJS optimize multi-page app using map config

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.
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" }
}
});
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);
};

Stop Grunt Contrib Uglify from removing unused Javascript

There is a function within my Javascript source file that I need to keep within my distribution for posterity (specifically for a sniffer to sniff out some information). It is not being called, but it needs to stay.
The Grunt task, grunt-contrib-uglify is removing this method because it is uncalled in my application.
How can I utilize the compression provided by grunt-contrib-uglify, without removing any code that is deemed unusable by this Grunt library?
Thanks.
Set options: unused to false
grunt.initConfig({
uglify: {
options: {
compress: {
unused: false
}
},
my_target: {
files: {
'dest/output.min.js': ['src/input.js']
}
}
}
});
Source: UglifyJS global definitions documentation
unused : true, // drop unused variables/functions

Downloading Script Dependencies in Parallel RequireJS

Following this RequireJS example I'm trying to have a single file for all vendor js assets like jquery and foundation, whilst loading page specific code in other modules.
While I can build and copy the js successfully (using grunt requirejs optimiser) into a build folder, the baseUrl in the require.config is obviously now wrong.
baseUrl: 'js' points to public/js instead of public/build/js and so all paths are incorrect.
Is there a way of dynamically updating the baseUrl when running the optimiser? So it points to public/build/js?
Here's my grunt task:
requirejs: {
dist: {
options: {
baseUrl: '<%=pkg.paths.js%>',
dir:'project/public/build/js',
mainConfigFile: '<%=pkg.paths.js%>main.js',
optimize: 'none',
removeCombined: true,
uglify2: {
no_mangle: true,
compress: {
drop_console: true
}
},
modules: [
{
name: 'vendorCommon'
},
{
name: 'dashboard',
exclude: ['vendorCommon']
}
]
}
}
}
Vendor Common
define(['jquery', 'foundation'],
function () {
//Just an empty function, this is a place holder
//module that will be optimized to include the
//common depenendencies listed in this module's dependency array.
});
Require Config
require.config({
baseUrl: '/js',
priority: ['vendorCommon'],
paths: {
'vendorCommon':'vendor/vendor-common',
'jquery':'../bower_components/jquery/dist/jquery.min',
'foundation':'../bower_components/foundation/js/foundation/foundation',
'dashboard':'views/dashboard'
},
shim: {
'foundation': ['jquery']
}
});
I've used the optimizer's onBuildWrite setting to modify some modules when they are optimized. If your configuration is included in your optimized bundle then you could use onBuildWrite to patch it:
onBuildWrite: function (moduleName, path, contents) {
if (moduleName === '<%=pkg.paths.js%>main') {
return contents.replace("baseUrl: '/js'", "baseUrl: '/build/js'");
}
return contents;
}
Disclaimer: I'm writing this off the top of my head. Beware of typos.
Another possibility would be to override baseUrl at runtime. RequireJS is able to combine multiple configurations into a single configuration. A new value of baseUrl in a later call would override the earlier value. So if you have a way to set up the optimized version of your app (for instance, through different HTML served by your server) to call require.config({baseUrl: '/build/js'}); after the call you show in your question but before any code that needs the correct baseUrl, this could also be an option.

How to add a plugin to the requirejs r.js optimizer, that only runs during build?

I'm trying to write a custom script/plugin to be included in the requireJS r.js optimizer (for fingerprinting static files and one for bundling JSON resources).
What I have not figured out is how to trigger my custom-optimizer during the build process. I can write the scripts allright, but how would I trigger a bundle-build.js module during optimization.
Thanks for hints!
EDIT:
I know I can use 'onBuildRead/Write', but this is not the place to add a optimizer plugin. Something like require-css is closer, but a file prefix, which triggers the plugin, which has a pluginBuilder specified to use during build. Not quite what I'm looking for.
Optimizer should pickup all files that are specified as dependencies. If you need to include something extra then you can add extra logic using onBuildRead or onBuildWrite callback, where you can add/remove extra stuff:
({
name: 'main',
baseUrl: '../',
// optimize: 'none',
optimize: 'uglify2',
exclude: ['jquery'],
mainConfigFile: '../main.js',
out: '../main.min.js',
// A function that if defined will be called for every file read in the
// build that is done to trace JS dependencies.
// Remove references to console.log(...)
onBuildRead: function (moduleName, path, contents) {
return contents;
// return contents.replace(/console.log(.*);/g, '');
},
onBuildWrite: function (moduleName, path, contents) {
// Add extra stufff;
return contents;
}
})

Categories

Resources