Downloading Script Dependencies in Parallel RequireJS - javascript

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.

Related

Using the RequireJS text plugin

I am using Karma to run tests against some code.
Both the tests and the code are transpiled (ES6 => ES5 using babel) before being run by Karma.
This works great and the tests run fine.
But if I try and use the text! plugin from any of the files being tested...
import template from 'text!./template.html';
...I get:
There is no timestamp for /base/src/text.js!
Uncaught Error: Script error for "text", needed by: text!app/template.html_unnormalized2
http://requirejs.org/docs/errors.html#scripterror
Uncaught Error: Load timeout for modules: text!app/template.html_unnormalized2
Does anyone have any ideas why this might be?
The built artifact in the dist folder (i.e. the item under test) contains the successfully encoded text RequireJS items eg:
define('text!app/template.html',[],function () { return '<div>foo</div>';});
Additional Info
test-main.js
var TEST_REGEXP = /(spec|test)\.js$/i;
var allTestFiles = [];
Object.keys(window.__karma__.files).forEach(function(file) {
if (TEST_REGEXP.test(file)) {
var normalizedTestModule = file.replace(/^\/base\/|\.js$/g, '');
allTestFiles.push(normalizedTestModule);
}
});
require.config({
baseUrl: '/base/src',
paths: {},
shim: {},
deps: allTestFiles,
callback: window.__karma__.start
});
karma.conf.js
module.exports = function(config) {
'use strict';
var path = require('path');
var cdn = 'http://localhost:55635/modules/';
var basePath = path.dirname(__filename);
config.set({
basePath: '../../..',
frameworks: [
'requirejs',
'jasmine'
],
files: [
{
pattern: path.join(basePath, 'test-transpiled', '*-spec.js'),
included: false
},
path.join(basePath, 'dist', 'artifacts', 'app.js'),
path.join(basePath, 'test', 'unit', 'test-main.js')
],
proxies: {
'/cdn/': cdn
},
exclude: [],
preprocessors: {},
reporters: ['dots'],
colors: true,
autoWatch: false,
singleRun: false,
browsers: ['Chrome'],
});
};
Edit:
I have added the following to karma.conf.js:
files: [
{
pattern: path.join(basePath, 'node_modules/require-plugins/text/text.js'),
included: false
},
// ...
],
I continue to get an error when running the tests:
There is no timestamp for /base/src/text.js!
Presumably because I need to add "text" to the paths section of test-main.js?
require.config({
baseUrl: '/base/src',
paths: {
'text': '../node_modules/require-plugins/text/text'
},
// ...
});
But I have tried various combinations of baseUrl and the path in the text path and I cannot get it to stop 404-ing.
Your files option in karma.conf.js does not include the text plugin, which is why you get the error that there's no timestamp for it.
Add an item to your files list that hits the text plugin on your file system, and make sure that you have included: false for it. RequireJS plugins are like other modules: RequireJS must be able to load them to use them.
You may need to also set paths in test-main.js depending on where you put your plugin. RequireJS already is looking for it at /base/src/text.js. If you locate it so that the plugin is served at this URL, then there's no need to set paths. If you put it somewhere else, then you do need to set paths. Something like:
paths: {
text: 'path/to/text',
}
Remember that the paths in paths are interpreted relative to your baseUrl setting.
I tried using the require.js text! plugin, and also found that it conflicts with the baseUrl defined for the rest of the project.
The requirejs.config sets the baseUrl to the parent directory of the JS files, while my templates are defined in a sibling directory to the js.
There was no way to tell requirejs to load templates from /base/templates and js from base/js.
My solution was to change the text.js plugin, and add a hook to change the url just before the ajax call is made to fetch the HTML file. You can take my version of text.js from here.

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.

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

Browserify: External Hash ID Keeps Changing

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

Possible to skip inlined, named modules with r.js build?

In my application, I have a named module defined inline in index.html:
<script>
define('config', {
mode: '...',
environment: '...',
apiUrl: '...',
rootPath: '...',
baseImageUrl: '...'
});
</script>
I need to define this in my web page because I pull some stuff in using environment variables. So, in my modules, I can then include 'config':
define(['config', 'jquery'], function(config, $) { ... });
When I run a RequireJS build using Grunt (with grunt-require, which uses r.js), it's complaining that it's not finding config.js. My build configuration looks like this:
{
baseUrl: 'js',
main: 'app/Main',
dir: 'js/build',
out: null,
modules: { ... },
paths: { ... },
shim: { ... }
}
When I build, I get:
Error: Error: ENOENT, no such file or directory '/projects/projectname/js/build/config.js
How can I allow for use of named, inline-defined modules in conjunction with an r.js build?
Setting
paths: {
config : 'empty:'
}
in your build configuration file should exclude this module out of optimization process and, as a result, stop raising this error.
But this is a bit tricky way, because this option is commonly used to exclude third-party dependencies (for example, when developing some js library based on Backbone, it would be better to separate your lib from Backbone)

Categories

Resources