webpack/lib/buildDeps.js

589 lines
16 KiB
JavaScript
Raw Normal View History

2012-03-12 04:50:55 +08:00
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
2012-03-10 20:11:23 +08:00
var parse = require("./parse");
var resolve = require("./resolve");
var execLoaders = require("./execLoaders");
2012-03-10 20:11:23 +08:00
var fs = require("fs");
var path = require("path");
2012-05-02 20:06:42 +08:00
var assert = require("assert");
2012-03-10 20:11:23 +08:00
/**
2012-05-12 22:43:37 +08:00
* @param context: current directory
* @param mainModule: the entrance module
* @param options: options
* @param callback: function(err, result)
2012-03-10 20:11:23 +08:00
*/
module.exports = function buildDeps(context, mainModule, options, callback) {
if(!callback) {
callback = options;
options = {};
}
2012-05-12 22:43:37 +08:00
// options.events mock for tests
2012-03-10 20:11:23 +08:00
if(!options) options = {};
2012-05-02 03:33:59 +08:00
if(!options.events) options.events = { emit: function() {} };
2012-03-10 20:11:23 +08:00
2012-05-12 22:43:37 +08:00
// create data structure
2012-03-10 20:11:23 +08:00
var depTree = {
warnings: [],
2012-04-03 22:26:08 +08:00
errors: [],
2012-03-10 20:11:23 +08:00
modules: {},
modulesById: {},
chunks: {},
nextModuleId: 0,
2012-05-17 18:45:48 +08:00
nextChunkId: 1,
2012-03-10 20:11:23 +08:00
chunkModules: {} // used by checkObsolete
}
2012-05-12 22:43:37 +08:00
// some progress info
2012-05-02 20:06:42 +08:00
options.events.emit("task", "build modules");
options.events.emit("task", "build chunks");
options.events.emit("task", "optimize");
options.events.emit("task", "cleanup");
2012-05-12 22:43:37 +08:00
// add the entrance file as module
// all other stuff is added recursivly
2012-03-15 07:05:29 +08:00
addModule(depTree, context, mainModule, options, {type: "main"}, function(err, id) {
2012-03-10 20:11:23 +08:00
if(err) {
callback(err);
return;
}
2012-05-12 22:43:37 +08:00
buildTree(id);
2012-03-10 20:11:23 +08:00
});
2012-05-12 22:43:37 +08:00
// enhance the tree
function buildTree(mainModuleId) {
2012-05-02 20:06:42 +08:00
options.events.emit("task-end", "build modules");
2012-05-02 03:33:59 +08:00
2012-05-12 22:43:37 +08:00
// split the modules into chunks
2012-05-17 18:45:48 +08:00
depTree.modulesById[mainModuleId].name = "main";
2012-03-10 20:11:23 +08:00
addChunk(depTree, depTree.modulesById[mainModuleId], options);
2012-05-12 22:43:37 +08:00
// rename the module ids after a defined sheme
createRealIds(depTree, options);
2012-05-02 20:06:42 +08:00
options.events.emit("task-end", "build chunks");
2012-05-02 03:33:59 +08:00
2012-03-10 20:11:23 +08:00
for(var chunkId in depTree.chunks) {
2012-05-12 22:43:37 +08:00
// remove modules which are included in parent chunk
2012-03-10 20:11:23 +08:00
removeParentsModules(depTree, depTree.chunks[chunkId]);
2012-05-12 22:43:37 +08:00
// remove the chunk if it is empty
2012-03-10 20:11:23 +08:00
removeChunkIfEmpty(depTree, depTree.chunks[chunkId]);
2012-05-12 22:43:37 +08:00
// remove duplicate chunks
2012-03-10 20:11:23 +08:00
checkObsolete(depTree, depTree.chunks[chunkId]);
}
2012-05-13 22:19:11 +08:00
createRealChunkIds(depTree, options);
2012-05-02 20:06:42 +08:00
options.events.emit("task-end", "optimize");
2012-05-02 03:33:59 +08:00
2012-05-12 22:43:37 +08:00
// cleanup temporary stuff
2012-03-15 07:05:29 +08:00
delete depTree.chunkModules;
depTree.modulesByFile = depTree.modules;
depTree.modules = depTree.modulesById;
delete depTree.modulesById;
delete depTree.nextModuleId;
delete depTree.nextChunkId;
2012-05-12 22:43:37 +08:00
2012-03-15 07:05:29 +08:00
// return
2012-05-02 20:06:42 +08:00
options.events.emit("task-end", "cleanup");
2012-03-10 20:11:23 +08:00
callback(null, depTree);
}
}
2012-05-02 03:33:59 +08:00
function addModule(depTree, context, modu, options, reason, finalCallback) {
options.events.emit("task");
function callback(err, result) {
options.events.emit("task-end");
finalCallback(err, result);
}
2012-05-12 22:43:37 +08:00
// resolve the filename of the required module
2012-05-18 05:34:43 +08:00
resolve(context = context || path.dirname(modu), modu, options.resolve, resolved);
2012-03-12 04:37:18 +08:00
function resolved(err, filename) {
2012-03-10 20:11:23 +08:00
if(err) {
callback(err);
return;
}
2012-05-12 22:43:37 +08:00
// check if the module is already included
2012-03-10 20:11:23 +08:00
if(depTree.modules[filename]) {
2012-03-15 07:05:29 +08:00
depTree.modules[filename].reasons.push(reason);
2012-03-10 20:11:23 +08:00
callback(null, depTree.modules[filename].id);
} else {
2012-05-12 22:43:37 +08:00
// create a new module
2012-04-03 22:26:08 +08:00
var modu = depTree.modules[filename] = {
2012-03-10 20:11:23 +08:00
id: depTree.nextModuleId++,
2012-03-15 07:05:29 +08:00
filename: filename,
reasons: [reason]
2012-03-10 20:11:23 +08:00
};
2012-04-03 22:26:08 +08:00
depTree.modulesById[modu.id] = modu;
2012-05-12 22:43:37 +08:00
// split the loaders from the require
2012-03-27 06:00:32 +08:00
var filenameWithLoaders = filename;
var loaders = filename.split(/!/g);
filename = loaders.pop();
2012-05-02 03:33:59 +08:00
options.events.emit("module", modu, filename);
2012-05-12 22:43:37 +08:00
2012-05-21 06:09:30 +08:00
if(options.cache) {
options.cache.get(filenameWithLoaders, function(err, source) {
if(err) return readFile();
processJs(null, source);
});
} else
readFile();
2012-05-12 22:43:37 +08:00
2012-05-21 06:09:30 +08:00
// Read the file and process it with loaders
function readFile() {
var cacheEntry = options.cache && options.cache.create(filenameWithLoaders);
2012-05-12 22:43:37 +08:00
2012-05-21 06:09:30 +08:00
// read file content
if(cacheEntry) cacheEntry.add(filename);
fs.readFile(filename, function(err, content) {
2012-04-03 22:26:08 +08:00
if(err) {
callback(err);
2012-03-27 06:00:32 +08:00
return;
}
2012-03-10 20:11:23 +08:00
2012-05-21 06:09:30 +08:00
// exec the loaders
execLoaders(context, filenameWithLoaders, loaders, [filename], [content], cacheEntry, options, processJs);
});
}
2012-05-12 22:43:37 +08:00
2012-05-21 06:09:30 +08:00
// process the finished javascript code
// for inclusion into the result
// (static code analysis for requires and other stuff)
function processJs(err, source) {
if(err) {
callback(err);
return;
}
var deps;
try {
deps = parse(source, options.parse);
} catch(e) {
callback("File \"" + filenameWithLoaders + "\" parsing failed: " + e);
return;
}
modu.requires = deps.requires || [];
modu.asyncs = deps.asyncs || [];
modu.contexts = deps.contexts || [];
modu.source = source;
var requires = {}, directRequire = {};
var contexts = [], directContexts = {};
function add(r) {
requires[r.name] = requires[r.name] || [];
requires[r.name].push(r);
}
function addContext(m) {
return function(c) {
contexts.push({context: c, module: m});
}
}
if(modu.requires) {
modu.requires.forEach(add);
modu.requires.forEach(function(r) {
directRequire[r.name] = true;
});
}
if(modu.contexts) {
modu.contexts.forEach(addContext(modu));
modu.contexts.forEach(function(c) {
directContexts[c.name] = true;
});
}
if(modu.asyncs)
modu.asyncs.forEach(function addAsync(c) {
if(c.requires)
c.requires.forEach(add);
if(c.asyncs)
c.asyncs.forEach(addAsync);
if(c.contexts)
c.contexts.forEach(addContext(c));
});
var requiresNames = Object.keys(requires);
2012-05-21 06:09:30 +08:00
var count = requiresNames.length + contexts.length + 1;
var errors = [];
if(requiresNames.length)
requiresNames.forEach(function(moduleName) {
var reason = {
type: directRequire[moduleName] ? "require" : "async require",
count: requires[moduleName].length,
filename: filename
};
2012-05-12 22:43:37 +08:00
2012-05-21 06:09:30 +08:00
// create or get the module for each require
addModule(depTree, path.dirname(filename), moduleName, options, reason, function(err, moduleId) {
if(err) {
depTree.errors.push("Cannot find module '" + moduleName + "'\n " + err +
"\n @ " + filename + " (line " + requires[moduleName][0].line + ", column " + requires[moduleName][0].column + ")");
} else {
requires[moduleName].forEach(function(requireItem) {
requireItem.id = moduleId;
});
2012-03-10 20:11:23 +08:00
}
2012-05-21 06:09:30 +08:00
endOne();
2012-03-12 04:37:18 +08:00
});
2012-05-21 06:09:30 +08:00
});
if(contexts) {
contexts.forEach(function(contextObj) {
var context = contextObj.context;
var module = contextObj.module;
var reason = {
type: directContexts[context.name] ? "context" : "async context",
filename: filename
};
// create of get the context module for each require.context
addContextModule(depTree, path.dirname(filename), context.name, options, reason, function(err, contextModuleId) {
if(err) {
depTree.errors.push("Cannot find context '"+context.name+"'\n " + err +
"\n @ " + filename + " (line " + context.line + ", column " + context.column + ")");
2012-03-12 04:37:18 +08:00
} else {
2012-05-21 06:09:30 +08:00
context.id = contextModuleId;
module.requires.push({id: context.id});
2012-03-12 04:37:18 +08:00
}
2012-05-21 06:09:30 +08:00
endOne();
});
if(context.warn) {
depTree.warnings.push(filename + " (line " + context.line + ", column " + context.column + "): " +
"implicit use of require.context(\".\") is not recommended.");
}
});
}
endOne();
function endOne() {
count--;
assert(count >= 0);
if(count === 0) {
if(errors.length) {
callback(errors.join("\n"));
} else {
callback(null, modu.id);
2012-03-12 04:37:18 +08:00
}
}
}
2012-05-21 06:09:30 +08:00
}
2012-03-12 04:37:18 +08:00
}
}
}
2012-05-02 03:33:59 +08:00
function addContextModule(depTree, context, contextModuleName, options, reason, finalCallback) {
options.events.emit("task");
function callback(err, result) {
options.events.emit("task-end");
finalCallback(err, result);
}
2012-05-12 22:43:37 +08:00
// resolve the filename of the required context
2012-03-12 04:37:18 +08:00
resolve.context(context, contextModuleName, options.resolve, resolved);
function resolved(err, dirname) {
if(err) {
callback(err);
return;
}
2012-05-12 22:43:37 +08:00
// check if the context is already included
2012-03-12 04:37:18 +08:00
if(depTree.modules[dirname]) {
2012-03-15 07:05:29 +08:00
depTree.modules[dirname].reasons.push(reason);
2012-03-12 04:37:18 +08:00
callback(null, depTree.modules[dirname].id);
} else {
2012-05-12 22:43:37 +08:00
// create a new context
2012-03-12 04:37:18 +08:00
var contextModule = depTree.modules[dirname] = {
name: contextModuleName,
2012-03-15 07:05:29 +08:00
dirname: dirname,
2012-03-12 04:37:18 +08:00
id: depTree.nextModuleId++,
requireMap: {},
2012-03-15 07:05:29 +08:00
requires: [],
reasons: [reason]
2012-03-12 04:37:18 +08:00
};
depTree.modulesById[contextModule.id] = contextModule;
2012-05-12 22:43:37 +08:00
// split the loaders from the require
2012-04-03 22:26:08 +08:00
var contextModuleNameWithLoaders = dirname;
var loaders = dirname.split(/!/g);
dirname = loaders.pop();
2012-05-02 03:33:59 +08:00
options.events.emit("context", contextModule, dirname);
2012-04-03 22:26:08 +08:00
var prependLoaders = loaders.length === 0 ? "" : loaders.join("!") + "!";
var extensions = (options.resolve && options.resolve.extensions) || [".web.js", ".js"];
2012-05-13 22:19:11 +08:00
2012-05-12 22:43:37 +08:00
// iterate all files in directory
2012-03-12 04:37:18 +08:00
function doDir(dirname, moduleName, done) {
fs.readdir(dirname, function(err, list) {
if(err) {
done(err);
} else {
var count = list.length + 1;
var errors = [];
function endOne(err) {
if(err) {
errors.push(err);
}
2012-03-10 20:11:23 +08:00
count--;
2012-05-02 20:06:42 +08:00
assert(count >= 0);
2012-03-12 04:37:18 +08:00
if(count == 0) {
if(errors.length > 0)
done(errors.join("\n"));
else
done();
}
}
list.forEach(function(file) {
var filename = path.join(dirname, file);
fs.stat(filename, function(err, stat) {
if(err) {
errors.push(err);
endOne();
2012-03-10 20:11:23 +08:00
} else {
2012-03-12 04:37:18 +08:00
if(stat.isDirectory()) {
2012-05-12 22:43:37 +08:00
// node_modules and web_modules directories are excluded
2012-03-15 07:05:29 +08:00
if(file === "node_modules" || file === "web_modules")
endOne();
else
doDir(filename, moduleName + "/" + file, endOne);
2012-03-12 04:37:18 +08:00
} else {
2012-04-03 22:26:08 +08:00
var match = false;
if(loaders.length === 0)
extensions.forEach(function(ext) {
if(file.substr(file.length - ext.length) === ext)
match = true;
if(options.resolve && options.resolve.loaders)
options.resolve.loaders.forEach(function(loader) {
if(loader.test.test(filename))
match = true;
});
});
if(!match && loaders.length === 0) {
endOne();
return;
}
2012-03-15 07:05:29 +08:00
var modulereason = {
type: "context",
filename: reason.filename
};
2012-05-12 22:43:37 +08:00
// add each file as module
2012-04-03 22:26:08 +08:00
addModule(depTree, dirname, prependLoaders + filename, options, reason, function(err, moduleId) {
2012-03-12 04:37:18 +08:00
if(err) {
depTree.warnings.push("A file in context was excluded because of error: " + err);
endOne();
2012-03-12 04:37:18 +08:00
} else {
contextModule.requires.push({id: moduleId});
2012-05-13 22:19:11 +08:00
2012-05-12 22:43:37 +08:00
// store the module id in a require map
// when generating the source it is included
2012-03-12 04:37:18 +08:00
contextModule.requireMap[moduleName + "/" + file] = moduleId;
endOne();
}
});
}
2012-03-10 20:11:23 +08:00
}
2012-03-12 04:37:18 +08:00
});
2012-03-10 20:11:23 +08:00
});
2012-03-12 04:37:18 +08:00
endOne();
}
});
}
doDir(dirname, ".", function(err) {
if(err) {
callback(err);
return;
2012-03-10 20:11:23 +08:00
}
2012-03-12 04:37:18 +08:00
callback(null, contextModule.id);
2012-03-10 20:11:23 +08:00
});
}
2012-03-12 04:37:18 +08:00
}
2012-03-10 20:11:23 +08:00
}
2012-05-12 22:43:37 +08:00
// rename the module ids after a defined sheme
function createRealIds(depTree, options) {
var sortedModules = [];
for(var id in depTree.modulesById) {
if(""+id === "0") continue;
var modu = depTree.modulesById[id];
var usages = 1;
modu.reasons.forEach(function(reason) {
usages += reason.count ? reason.count : 1;
});
modu.usages = usages;
sortedModules.push(modu);
}
depTree.modulesById[0].realId = 0;
sortedModules.sort(function(a, b) {
if(a.chunks && b.chunks &&
2012-05-18 07:15:53 +08:00
(a.chunks.indexOf("main") !== -1 || b.chunks.indexOf("main") !== -1)) {
if(a.chunks.indexOf("main") === -1)
return 1;
2012-05-18 07:15:53 +08:00
if(b.chunks.indexOf("main") === -1)
return -1;
}
var diff = b.usages - a.usages;
if(diff !== 0) return diff;
if(typeof a.filename === "string" || typeof b.filename === "string") {
if(typeof a.filename !== "string")
return -1;
if(typeof b.filename !== "string")
return 1;
if(a.filename === b.filename)
return 0;
return (a.filename < b.filename) ? -1 : 1;
}
if(a.dirname === b.dirname)
return 0;
return (a.dirname < b.dirname) ? -1 : 1;
});
sortedModules.forEach(function(modu, idx) {
modu.realId = idx + 1;
});
}
2012-05-12 22:43:37 +08:00
// add a chunk
2012-03-10 20:11:23 +08:00
function addChunk(depTree, chunkStartpoint, options) {
2012-05-17 18:45:48 +08:00
var chunk;
if(chunkStartpoint && chunkStartpoint.name) {
chunk = depTree.chunks[chunkStartpoint.name];
if(chunk) {
chunk.usages++;
chunk.contexts.push(chunkStartpoint);
}
}
if(!chunk) {
chunk = {
id: (chunkStartpoint && chunkStartpoint.name) || depTree.nextChunkId++,
modules: {},
contexts: chunkStartpoint ? [chunkStartpoint] : [],
usages: 1
};
depTree.chunks[chunk.id] = chunk;
}
2012-03-10 20:11:23 +08:00
if(chunkStartpoint) {
chunkStartpoint.chunkId = chunk.id;
addModuleToChunk(depTree, chunkStartpoint, chunk.id, options);
}
return chunk;
}
function addModuleToChunk(depTree, context, chunkId, options) {
context.chunks = context.chunks || [];
if(context.chunks.indexOf(chunkId) === -1) {
context.chunks.push(chunkId);
if(context.id !== undefined)
depTree.chunks[chunkId].modules[context.id] = "include";
if(context.requires) {
context.requires.forEach(function(requireItem) {
if(requireItem.id)
addModuleToChunk(depTree, depTree.modulesById[requireItem.id], chunkId, options);
2012-03-10 20:11:23 +08:00
});
}
if(context.asyncs) {
context.asyncs.forEach(function(context) {
var subChunk
if(context.chunkId) {
subChunk = depTree.chunks[context.chunkId];
2012-05-13 22:19:11 +08:00
subChunk.usages++;
2012-03-10 20:11:23 +08:00
} else {
subChunk = addChunk(depTree, context, options);
}
subChunk.parents = subChunk.parents || [];
subChunk.parents.push(chunkId);
});
}
}
}
function removeParentsModules(depTree, chunk) {
if(!chunk.parents) return;
for(var moduleId in chunk.modules) {
2012-03-12 04:37:18 +08:00
var inParent = true;
2012-05-01 21:42:01 +08:00
var checkedParents = {};
chunk.parents.forEach(function checkParent(parentId) {
2012-05-01 22:09:33 +08:00
if(!inParent) return;
2012-05-01 21:42:01 +08:00
if(checkedParents[parentId]) return;
checkedParents[parentId] = true;
2012-05-01 22:09:33 +08:00
if(!depTree.chunks[parentId].modules[moduleId]) {
var parents = depTree.chunks[parentId].parents;
if(parents && parents.length > 0)
parents.forEach(checkParent);
else
inParent = false;
}
2012-03-10 20:11:23 +08:00
});
if(inParent) {
chunk.modules[moduleId] = "in-parent";
}
}
}
function removeChunkIfEmpty(depTree, chunk) {
var hasModules = false;
for(var moduleId in chunk.modules) {
if(chunk.modules[moduleId] === "include") {
hasModules = true;
break;
}
}
if(!hasModules) {
2012-05-17 18:45:48 +08:00
chunk.contexts.forEach(function(c) { c.chunkId = null; });
2012-03-10 20:11:23 +08:00
chunk.empty = true;
}
}
function checkObsolete(depTree, chunk) {
var modules = [];
for(var moduleId in chunk.modules) {
if(chunk.modules[moduleId] === "include") {
modules.push(moduleId);
}
}
if(modules.length === 0) return;
modules.sort();
var moduleString = modules.join(" ");
if(depTree.chunkModules[moduleString]) {
chunk.equals = depTree.chunkModules[moduleString];
2012-05-17 18:45:48 +08:00
chunk.contexts.forEach(function(c) {
c.chunkId = chunk.equals;
});
2012-03-10 20:11:23 +08:00
} else
depTree.chunkModules[moduleString] = chunk.id;
2012-05-13 22:19:11 +08:00
}
// rename the chunk ids after a defined sheme
function createRealChunkIds(depTree, options) {
var sortedChunks = [];
for(var id in depTree.chunks) {
2012-05-17 18:45:48 +08:00
if(id === "main") continue;
2012-05-13 22:19:11 +08:00
var chunk = depTree.chunks[id];
if(chunk.empty) continue;
if(chunk.equals !== undefined) continue;
sortedChunks.push(chunk);
}
2012-05-17 18:45:48 +08:00
depTree.chunks["main"].realId = 0;
2012-05-13 22:19:11 +08:00
sortedChunks.sort(function(a, b) {
if(a.usages < b.usages)
return -1;
if(a.usages > b.usages)
return 1;
var aCount = Object.keys(a.modules).length;
var bCount = Object.keys(b.modules).length;
2012-05-13 22:26:58 +08:00
if(aCount != bCount)
return aCount - bCount;
function genModulesString(modules) {
2012-05-13 22:37:35 +08:00
var moduleIds = [];
for(var id in modules) {
if(modules[id] === "include") {
var m = depTree.modulesById[id];
moduleIds.push(m.realId);
}
}
return moduleIds.sort().join("-");
2012-05-13 22:26:58 +08:00
}
var aModules = genModulesString(a.modules);
var bModules = genModulesString(b.modules);
if(aModules == bModules)
return 0;
return aModules < bModules ? -1 : 1;
2012-05-13 22:19:11 +08:00
});
sortedChunks.forEach(function(chunk, idx) {
chunk.realId = idx + 1;
});
2012-03-10 20:11:23 +08:00
}