Browserify: External Hash ID Keeps Changing - javascript

I'm using Grunt-Browserify to load a library (jQuery) in one bundle and reference that library as external in other bundles.
Browserify assigns a unique hash id to the external library, and everything works fine for a single developer.
However, when a second developer runs the same Grunt task, the unique id for jQuery changes -- breaking any bundles that are still looking for it at the old "address".
Does anyone know how to control the id assigned to an external library in Browserify -- or how to prevent Browserify from using a hash id for external dependencies?
Here's my current configuration:
browserify: {
main: {
files: {
'./dist/main.js': ['./dev/js/main.js']
},
options: {
require: ['jquery'],
fullPaths: true,
watch: true
}
},
bundles: {
files: {
'./dist/bundle-1.js': ['./dev/bundle-1.js'],
// ...
},
options: {
external: ['jquery'],
fullPaths: true,
watch: true
}
}
}

Related

RrequireJS optimizer and loading dynamic paths after optimization

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.

How to create a vendor bundle with grunt-browserify

I'm working on a single page app that requires several third-party libs. To reduce build time I'm trying to create two separate bundles: one for the libs code and one for the app code. My build process uses grunt-browserify to generate the bundles. Here's my browserify task (I'm using grunt-load-tasks to modularize my Grunt tasks):
var libs = [
'backbone',
'backbone-relational',
'backbone.babysitter',
'backbone.wreqr',
'bootstrap',
'bootstrap-growl',
'handlebars',
'jquery',
'marionette',
'underscore'
];
module.exports = {
options: {
external: libs
},
libs: {
src: [],
dest: './build/js/libs.js',
options: {
external: null,
require: libs
}
},
app: {
src: ['./frontend/js/app.coffee'],
dest: './build/js/app.js',
options: {
browserifyOptions: {
debug: true,
extensions: ['.coffee'],
},
watch: true
}
}
}
This successfully creates the two separate bundles and my app is functional after including the bundles on the page. However I'm noticing that Backbone and Handlebars are being included in both the libs.js bundle and the app.js bundle when I would have expected them to be included only in the libs.js bundle. Any idea what I'm doing wrong?

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

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.

r.js, almond: Is it possible for two .js files containing almond to share dependencies?

Is it possible to use almond with a multipage setup as follows:
common.js is loaded on all pages & contains almond, bootstrap & jquery
main1.js is loaded only on page 1 & contains almond, and app/main1.js which requires jquery.
When i run the build for main1.js i am excluding bootstrap & jquery since it is in common.
on page1 common.js & main1.js are loaded but, i get an error: Uncaught Error: app/main1 missing jquery.
Is it possible to do this with almond or am I doing something wrong?
UPDATE:
I am useing django-require which converts python objects to command line entries for r.js, further more it renames the supplied modules to 'almond' and adds the named module to the include (this may be what is causing my error?). Also note, django-require does not permit include/exclude for REQUIRE_STANDALONE_MODULES, i added this functionality:
REQUIRE_STANDALONE_MODULES = {
"common": {
"out": "common.js",
"include": ["bootstrap", "jquery"],
"build_profile": "module.build.js"
},
"main1": {
"out": "main1.js",
"exclude": ["bootstrap", "jquery"],
"build_profile": "module.build.js"
}
}
Main1.js
require(['app/main1']);
This translates to a build file entry like this:
modules = {
"almond": {
"out": "common.js",
"include": ["common", "bootstrap", "jquery"],
"build_profile": "module.build.js"
},
"almond": {
"out": "main1.js",
"include:"main1",
"exclude": ["bootstrap", "jquery"],
"build_profile": "module.build.js"
}
}
It is possible. You just need to be clear about your inclusions and exclusions. In the following setup all the modules are stored in the js subdirectory and the output of optimization goes out to build. For the sake of simplicity jQuery is stored as js/jquery.js so there's no need to call require.config.
The files in js are: almond.js, jquery.js, main1.js and main2.js.
Here is the build configuration:
({
baseUrl: "js",
optimize: "none", // So that we can see what is going on in the bundles.
dir: "build",
removeCombined: true,
skipDirOptimize: true,
modules: [
{
name: "common",
create: true,
include: ["almond", "jquery"]
},
{
name: "main1",
exclude: ["jquery"],
insertRequire: ["main1"]
},
{
name: "main2",
exclude: ["jquery"],
insertRequire: ["main2"]
}
]
})
The create: true option for the common module is required so that the optimizer creates it. Presumably, a call to require.config would be put in js/common.js and then you'd remove this option.
The results of this optimization are loaded on page 1 with:
<script type="text/javascript" src="build/common.js"></script>
<script type="text/javascript" src="build/main1.js"></script>
Page 2 would load build/main2.js instead.
Loading Bootstrap requires a RequireJS configuration which is the same as the general case and is otherwise treated exactly like jQuery in the code above.

Categories

Resources