Skip to content

Commit 0f5c86c

Browse files
committedMay 26, 2011
adding support for optimizing all plugin resources. Still a bit experimental.
1 parent 5acd1bd commit 0f5c86c

File tree

8 files changed

+234
-51
lines changed

8 files changed

+234
-51
lines changed
 

‎build/example.build.js

+8
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@
124124
//together.
125125
skipModuleInsertion: false,
126126

127+
//If it is not a one file optimization, scan through all .js files in the
128+
//output directory for any plugin resource dependencies, and if the plugin
129+
//supports optimizing them as separate files, optimize them. Can be a
130+
//slower optimization. Only use if there are some plugins that use things
131+
//like XMLHttpRequest that do not work across domains, but the built code
132+
//will be placed on another domain.
133+
optimizeAllPluginResources: false,
134+
127135
//List the modules that will be optimized. All their immediate and deep
128136
//dependencies will be included in the module's file when the build is
129137
//done. If that module or any of its dependencies includes i18n bundles,

‎build/jslib/build.js

+73-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ function (lang, logger, file, parse, optimize, pragma,
2222
optimize: "uglify",
2323
optimizeCss: "standard.keepLines",
2424
inlineText: true,
25-
isBuild: true
25+
isBuild: true,
26+
optimizeAllPluginResources: false
2627
};
2728

2829
/**
@@ -47,6 +48,13 @@ function (lang, logger, file, parse, optimize, pragma,
4748
return dirName;
4849
}
4950

51+
//Method used by plugin writeFile calls, defined up here to avoid
52+
//jslint warning about "making a function in a loop".
53+
function writeFile(name, contents) {
54+
logger.trace('Saving plugin-optimized file: ' + name);
55+
file.saveUtf8File(name, contents);
56+
}
57+
5058
/**
5159
* Main API entry point into the build. The args argument can either be
5260
* an array of arguments (like the onese passed on a command-line),
@@ -105,11 +113,13 @@ function (lang, logger, file, parse, optimize, pragma,
105113

106114
build._run = function (cmdConfig) {
107115
var buildFileContents = "",
116+
pluginCollector = {},
108117
buildPaths, fileName, fileNames,
109118
prop, paths, i,
110119
baseConfig, config,
111120
modules, builtModule, srcPath, buildContext,
112-
destPath;
121+
destPath, moduleName, moduleMap, parentModuleMap, context,
122+
resources, resource, pluginProcessed = {}, plugin;
113123

114124
//Can now run the patches to require.js to allow it to be used for
115125
//build generation. Do it here instead of at the top of the module
@@ -294,9 +304,69 @@ function (lang, logger, file, parse, optimize, pragma,
294304
//JS optimizations.
295305
fileNames = file.getFilteredFileList(config.dir, /\.js$/, true);
296306
for (i = 0; (fileName = fileNames[i]); i++) {
297-
optimize.jsFile(fileName, fileName, config);
307+
//Generate the module name from the config.dir root.
308+
moduleName = fileName.replace(config.dir, '');
309+
//Get rid of the extension
310+
moduleName = moduleName.substring(0, moduleName.length - 3);
311+
optimize.jsFile(fileName, fileName, config, moduleName, pluginCollector);
298312
}
299313

314+
//Normalize all the plugin resources.
315+
context = require.s.contexts._;
316+
317+
for (moduleName in pluginCollector) {
318+
if (pluginCollector.hasOwnProperty(moduleName)) {
319+
parentModuleMap = context.makeModuleMap(moduleName);
320+
resources = pluginCollector[moduleName];
321+
for (i = 0; (resource = resources[i]); i++) {
322+
moduleMap = context.makeModuleMap(resource, parentModuleMap);
323+
if (!context.plugins[moduleMap.prefix]) {
324+
//Set the value in context.plugins so it
325+
//will be evaluated as a full plugin.
326+
context.plugins[moduleMap.prefix] = true;
327+
328+
//Do not bother if the plugin is not available.
329+
if (!file.exists(require.toUrl(moduleMap.prefix + '.js'))) {
330+
continue;
331+
}
332+
333+
//Rely on the require in the build environment
334+
//to be synchronous
335+
context.require([moduleMap.prefix]);
336+
337+
//Now that the plugin is loaded, redo the moduleMap
338+
//since the plugin will need to normalize part of the path.
339+
moduleMap = context.makeModuleMap(resource, parentModuleMap);
340+
}
341+
342+
//Only bother with plugin resources that can be handled
343+
//processed by the plugin, via support of the writeFile
344+
//method.
345+
if (!pluginProcessed[moduleMap.fullName]) {
346+
//Only do the work if the plugin was really loaded.
347+
//Using an internal access because the file may
348+
//not really be loaded.
349+
plugin = context.defined[moduleMap.prefix];
350+
if (plugin && plugin.writeFile) {
351+
plugin.writeFile(
352+
moduleMap.prefix,
353+
moduleMap.name,
354+
require,
355+
writeFile,
356+
context.config
357+
);
358+
}
359+
360+
pluginProcessed[moduleMap.fullName] = true;
361+
}
362+
}
363+
364+
}
365+
}
366+
367+
//console.log('PLUGIN COLLECTOR: ' + JSON.stringify(pluginCollector, null, " "));
368+
369+
300370
//All module layers are done, write out the build.txt file.
301371
file.saveUtf8File(config.dir + "build.txt", buildFileContents);
302372
}

‎build/jslib/optimize.js

+31-7
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
/*jslint plusplus: false, nomen: false, regexp: false, strict: false */
88
/*global define: false */
99

10-
define([ 'lang', 'logger', 'env!env/optimize', 'env!env/file', 'uglifyjs/index'],
11-
function (lang, logger, envOptimize, file, uglify) {
10+
define([ 'lang', 'logger', 'env!env/optimize', 'env!env/file', 'parse',
11+
'uglifyjs/index'],
12+
function (lang, logger, envOptimize, file, parse,
13+
uglify) {
1214

1315
var optimize,
1416
cssImportRegExp = /\@import\s+(url\()?\s*([^);]+)\s*(\))?([\w, ]*)(;)?/g,
@@ -127,23 +129,45 @@ function (lang, logger, envOptimize, file, uglify) {
127129

128130
optimize = {
129131
/**
130-
* Optimizes a file that contains JavaScript content. It will inline
131-
* text plugin files and run it through Google Closure Compiler
132-
* minification, if the config options specify it.
132+
* Optimizes a file that contains JavaScript content. Optionally collects
133+
* plugin resources mentioned in a file, and then passes the content
134+
* through an minifier if one is specified via config.optimize.
133135
*
134136
* @param {String} fileName the name of the file to optimize
135137
* @param {String} outFileName the name of the file to use for the
136138
* saved optimized content.
137139
* @param {Object} config the build config object.
140+
* @param {String} [moduleName] the module name to use for the file.
141+
* Used for plugin resource collection.
142+
* @param {Array} [pluginCollector] storage for any plugin resources
143+
* found.
138144
*/
139-
jsFile: function (fileName, outFileName, config) {
145+
jsFile: function (fileName, outFileName, config, moduleName, pluginCollector) {
140146
var parts = (config.optimize + "").split('.'),
141147
optimizerName = parts[0],
142148
keepLines = parts[1] === 'keepLines',
143-
fileContents, optFunc;
149+
fileContents, optFunc, deps, i, dep;
144150

145151
fileContents = file.readFile(fileName);
146152

153+
//If there is a plugin collector, scan the file for plugin resources.
154+
if (config.optimizeAllPluginResources && pluginCollector) {
155+
try {
156+
deps = parse.findDependencies(fileName, fileContents);
157+
if (deps.length) {
158+
for (i = 0; (dep = deps[i]); i++) {
159+
if (dep.indexOf('!') !== -1) {
160+
(pluginCollector[moduleName] ||
161+
(pluginCollector[moduleName] = [])).push(dep);
162+
}
163+
}
164+
}
165+
} catch (e) {
166+
logger.error('Parse error looking for plugin resources in ' +
167+
fileName + ', skipping.');
168+
}
169+
}
170+
147171
//Optimize the JS files if asked.
148172
if (optimizerName && optimizerName !== 'none') {
149173
optFunc = envOptimize[optimizerName] || optimize.optimizers[optimizerName];

‎build/jslib/parse.js

+77-21
Original file line numberDiff line numberDiff line change
@@ -37,31 +37,46 @@ define(['uglifyjs/index'], function (uglify) {
3737

3838
/**
3939
* Validates a node as being an object literal (like for i18n bundles)
40-
* or an array literal with just string members.
40+
* or an array literal with just string members. If an array literal,
41+
* only return array members that are full strings. So the caller of
42+
* this function should use the return value as the new value for the
43+
* node.
44+
*
4145
* This function does not need to worry about comments, they are not
4246
* present in this AST.
47+
*
48+
* @param {Node} node an AST node.
49+
*
50+
* @returns {Node} an AST node to use for the valid dependencies.
51+
* If null is returned, then it means the input node was not a valid
52+
* dependency.
4353
*/
4454
function validateDeps(node) {
45-
var arrayArgs, i, dep;
55+
var newDeps = ['array', []],
56+
arrayArgs, i, dep;
57+
58+
if (!node) {
59+
return null;
60+
}
4661

4762
if (isObjectLiteral(node) || node[0] === 'function') {
48-
return true;
63+
return node;
4964
}
5065

5166
//Dependencies can be an object literal or an array.
5267
if (!isArrayLiteral(node)) {
53-
return false;
68+
return null;
5469
}
5570

5671
arrayArgs = node[1];
5772

5873
for (i = 0; i < arrayArgs.length; i++) {
5974
dep = arrayArgs[i];
60-
if (dep[0] !== 'string') {
61-
return false;
75+
if (dep[0] === 'string') {
76+
newDeps[1].push(dep);
6277
}
6378
}
64-
return true;
79+
return newDeps[1].length ? newDeps : null;
6580
}
6681

6782
/**
@@ -77,7 +92,12 @@ define(['uglifyjs/index'], function (uglify) {
7792
var matches = [], result = null,
7893
astRoot = parser.parse(fileContents);
7994

80-
parse.recurse(astRoot, matches);
95+
parse.recurse(astRoot, function () {
96+
var parsed = parse.callToString.apply(parse, arguments);
97+
if (parsed) {
98+
matches.push(parsed);
99+
}
100+
});
81101

82102
if (matches.length) {
83103
result = matches.join("\n");
@@ -94,19 +114,16 @@ define(['uglifyjs/index'], function (uglify) {
94114
/**
95115
* Handles parsing a file recursively for require calls.
96116
* @param {Array} parentNode the AST node to start with.
97-
* @param {Array} matches where to store the string matches
117+
* @param {Function} onMatch function to call on a parse match.
98118
*/
99-
parse.recurse = function (parentNode, matches) {
100-
var i, node, parsed;
119+
parse.recurse = function (parentNode, onMatch) {
120+
var i, node;
101121
if (isArray(parentNode)) {
102122
for (i = 0; i < parentNode.length; i++) {
103123
node = parentNode[i];
104124
if (isArray(node)) {
105-
parsed = this.parseNode(node);
106-
if (parsed) {
107-
matches.push(parsed);
108-
}
109-
this.recurse(node, matches);
125+
this.parseNode(node, onMatch);
126+
this.recurse(node, onMatch);
110127
}
111128
}
112129
}
@@ -192,6 +209,42 @@ define(['uglifyjs/index'], function (uglify) {
192209
return null;
193210
};
194211

212+
/**
213+
* Finds all dependencies specified in dependency arrays and inside
214+
* simplified commonjs wrappers.
215+
* @param {String} fileName
216+
* @param {String} fileContents
217+
*
218+
* @returns {Array} an array of dependency strings. The dependencies
219+
* have not been normalized, they may be relative IDs.
220+
*/
221+
parse.findDependencies = function (fileName, fileContents) {
222+
//This is a litle bit inefficient, it ends up with two uglifyjs parser
223+
//calls. Can revisit later, but trying to build out larger functional
224+
//pieces first.
225+
var dependencies = parse.getAnonDeps(fileName, fileContents),
226+
astRoot = parser.parse(fileContents),
227+
i, dep;
228+
229+
parse.recurse(astRoot, function (callName, config, name, deps) {
230+
//Normalize the input args.
231+
if (name && isArrayLiteral(name)) {
232+
deps = name;
233+
name = null;
234+
}
235+
236+
if (!(deps = validateDeps(deps)) || !isArrayLiteral(deps)) {
237+
return;
238+
}
239+
240+
for (i = 0; (dep = deps[1][i]); i++) {
241+
dependencies.push(dep[1]);
242+
}
243+
});
244+
245+
return dependencies;
246+
};
247+
195248
parse.findRequireDepNames = function (node, deps) {
196249
var moduleName, i, n, call, args;
197250

@@ -291,7 +344,7 @@ define(['uglifyjs/index'], function (uglify) {
291344
name = null;
292345
}
293346

294-
if (deps && !validateDeps(deps)) {
347+
if (!(deps = validateDeps(deps))) {
295348
return null;
296349
}
297350

@@ -311,11 +364,14 @@ define(['uglifyjs/index'], function (uglify) {
311364
/**
312365
* Determines if a specific node is a valid require or define/require.def call.
313366
* @param {Array} node
367+
* @param {Function} onMatch a function to call when a match is found.
368+
* It is passed the match name, and the config, name, deps possible args.
369+
* The config, name and deps args are not normalized.
314370
*
315371
* @returns {String} a JS source string with the valid require/define call.
316372
* Otherwise null.
317373
*/
318-
parse.parseNode = function (node) {
374+
parse.parseNode = function (node, onMatch) {
319375
var call, name, config, deps, args;
320376

321377
if (!isArray(node)) {
@@ -337,11 +393,11 @@ define(['uglifyjs/index'], function (uglify) {
337393
config = null;
338394
}
339395

340-
if (!deps || !validateDeps(deps)) {
396+
if (!(deps = validateDeps(deps))) {
341397
return null;
342398
}
343399

344-
return this.callToString("require", null, null, deps);
400+
return onMatch("require", null, null, deps);
345401

346402
} else if ((call[0] === 'name' && call[1] === 'define') ||
347403
(call[0] === 'dot' && call[1][1] === 'require' &&
@@ -364,7 +420,7 @@ define(['uglifyjs/index'], function (uglify) {
364420
name[0] === 'function' || isObjectLiteral(name))) &&
365421
(!deps || isArrayLiteral(deps) ||
366422
deps[0] === 'function' || isObjectLiteral(deps))) {
367-
return this.callToString("define", null, name, deps);
423+
return onMatch("define", null, name, deps);
368424
}
369425
}
370426
}

‎build/tests/parse.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ define(['parse'], function (parse) {
1313

1414
t.is('require(["one","two"]);', parse("good1", good1));
1515
t.is('require(["one","two"]);', parse("good2", good2));
16-
t.is(null, parse("bad1", bad1));
16+
t.is('require(["me"]);', parse("bad1", bad1));
1717
t.is(null, parse("bad2", bad2));
1818
}
1919
]
@@ -38,7 +38,7 @@ define(['parse'], function (parse) {
3838
t.is('define("one",function(){});', parse("good2", good2));
3939
t.is('define("one",["two"]);', parse("good3", good3));
4040
t.is('define("one",function(){});', parse("good4", good4));
41-
t.is(null, parse("bad1", bad1));
41+
t.is('define("one",["me"]);', parse("bad1", bad1));
4242
t.is(null, parse("bad2", bad2));
4343
t.is(['require', 'foo'], parse.getAnonDeps("goodAnon1", goodAnon1));
4444
t.is(['require', 'exports', 'module', 'bar'], parse.getAnonDeps("goodAnon2", goodAnon2));
@@ -70,8 +70,8 @@ define(['parse'], function (parse) {
7070
t.is('define("one",function(){});', parse("good2", good2));
7171
t.is('define("one",["two"]);', parse("good3", good3));
7272
t.is('define("one",function(){});', parse("good4", good4));
73-
t.is('define("foo",[]);', parse("nested1", nested1));
74-
t.is(null, parse("bad1", bad1));
73+
t.is(null, parse("nested1", nested1));
74+
t.is('define("one",["me"]);', parse("bad1", bad1));
7575
t.is(null, parse("bad2", bad2));
7676
t.is(null, parse("bad3", bad3));
7777
t.is(null, parse("bad4", bad4));

‎build/tests/text.build.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
//A simple build file using the tests directory for requirejs
22
{
3-
baseUrl: "../../tests/text",
3+
baseUrl: "../../../requirejs/tests/text",
44
paths: {
55
text: "../../text"
66
},
77
dir: "builds/text",
88
optimize: "none",
9+
optimizeAllPluginResources: true,
910

1011
modules: [
1112
{

‎require.js

+38-15
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,22 @@ var require, define;
112112
}
113113
}
114114

115+
/**
116+
* jQuery 1.4.3-1.5.x use a readyWait/ready() pairing to hold DOM
117+
* ready callbacks, but jQuery 1.6 supports a holdReady() API instead.
118+
* At some point remove the readyWait/ready() support and just stick
119+
* with using holdReady.
120+
*/
121+
function jQueryHoldReady($, shouldHold) {
122+
if ($.holdReady) {
123+
$.holdReady(shouldHold);
124+
} else if (shouldHold) {
125+
$.readyWait += 1;
126+
} else {
127+
$.ready(true);
128+
}
129+
}
130+
115131
//Check for an existing version of require. If so, then exit out. Only allow
116132
//one version of require to be active in a page. However, allow for a require
117133
//config object, just exit quickly if require is an actual function.
@@ -690,9 +706,9 @@ var require, define;
690706
}
691707

692708
/**
693-
* As of jQuery 1.4.3, it supports a readyWait property that will hold off
709+
* jQuery 1.4.3+ supports ways to hold off calling
694710
* calling jQuery ready callbacks until all scripts are loaded. Be sure
695-
* to track it if readyWait is available. Also, since jQuery 1.4.3 does
711+
* to track it if the capability exists.. Also, since jQuery 1.4.3 does
696712
* not register as a module, need to do some global inference checking.
697713
* Even if it does register as a module, not guaranteed to be the precise
698714
* name of the global. If a jQuery is tracked for this context, then go
@@ -709,7 +725,7 @@ var require, define;
709725
return;
710726
}
711727

712-
if ("readyWait" in $) {
728+
if ("holdReady" in $ || "readyWait" in $) {
713729
context.jQuery = $;
714730

715731
//Manually create a "jquery" module entry if not one already
@@ -720,9 +736,9 @@ var require, define;
720736
return jQuery;
721737
}]);
722738

723-
//Increment jQuery readyWait if ncecessary.
739+
//Ask jQuery to hold DOM ready callbacks.
724740
if (context.scriptCount) {
725-
$.readyWait += 1;
741+
jQueryHoldReady($, true);
726742
context.jQueryIncremented = true;
727743
}
728744
}
@@ -1160,7 +1176,7 @@ var require, define;
11601176
},
11611177

11621178
require: function (deps, callback, relModuleMap) {
1163-
var moduleName, ret, moduleMap;
1179+
var moduleName, fullName, moduleMap;
11641180
if (typeof deps === "string") {
11651181
//Synchronous access to one module. If require.get is
11661182
//available (as in the Node adapter), prefer that.
@@ -1177,15 +1193,15 @@ var require, define;
11771193

11781194
//Normalize module name, if it contains . or ..
11791195
moduleMap = makeModuleMap(moduleName, relModuleMap);
1196+
fullName = moduleMap.fullName;
11801197

1181-
ret = defined[moduleMap.fullName];
1182-
if (ret === undefined) {
1198+
if (!(fullName in defined)) {
11831199
return req.onError(makeError("notloaded", "Module name '" +
11841200
moduleMap.fullName +
11851201
"' has not been loaded yet for context: " +
11861202
contextName));
11871203
}
1188-
return ret;
1204+
return defined[fullName];
11891205
}
11901206

11911207
main(null, deps, callback, relModuleMap);
@@ -1428,6 +1444,14 @@ var require, define;
14281444
return context.require(deps, callback);
14291445
};
14301446

1447+
/**
1448+
* Global require.toUrl(), to match global require, mostly useful
1449+
* for debugging/work in the global space.
1450+
*/
1451+
req.toUrl = function (moduleNamePlusExt) {
1452+
return contexts[defContextName].toUrl(moduleNamePlusExt);
1453+
};
1454+
14311455
req.version = version;
14321456
req.isArray = isArray;
14331457
req.isFunction = isFunction;
@@ -1485,11 +1509,11 @@ var require, define;
14851509
context.scriptCount += 1;
14861510
req.attach(url, contextName, moduleName);
14871511

1488-
//If tracking a jQuery, then make sure its readyWait
1489-
//is incremented to prevent its ready callbacks from
1512+
//If tracking a jQuery, then make sure its ready callbacks
1513+
//are put on hold to prevent its ready callbacks from
14901514
//triggering too soon.
14911515
if (context.jQuery && !context.jQueryIncremented) {
1492-
context.jQuery.readyWait += 1;
1516+
jQueryHoldReady(context.jQuery, true);
14931517
context.jQueryIncremented = true;
14941518
}
14951519
};
@@ -1820,14 +1844,13 @@ var require, define;
18201844
}
18211845
}
18221846

1823-
//If jQuery with readyWait is being tracked, updated its
1824-
//readyWait count.
1847+
//If jQuery with DOM ready delayed, release it now.
18251848
contexts = s.contexts;
18261849
for (prop in contexts) {
18271850
if (!(prop in empty)) {
18281851
context = contexts[prop];
18291852
if (context.jQueryIncremented) {
1830-
context.jQuery.ready(true);
1853+
jQueryHoldReady(context.jQuery, false);
18311854
context.jQueryIncremented = false;
18321855
}
18331856
}

‎tasks.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
* Need more docs on optimizeAllPluginResources?
12
* allow for npm install

0 commit comments

Comments
 (0)
Please sign in to comment.