added loader support

This commit is contained in:
Tobias Koppers 2012-03-27 00:00:32 +02:00
parent be306cfaf6
commit a29a823305
19 changed files with 509 additions and 144 deletions

View File

@ -77,108 +77,147 @@ function addModule(depTree, context, module, options, reason, callback) {
reasons: [reason]
};
depTree.modulesById[module.id] = module;
fs.readFile(filename, "utf-8", function(err, source) {
var filenameWithLoaders = filename;
var loaders = filename.split(/!/g);
filename = loaders.pop();
fs.readFile(filename, "utf-8", function(err, content) {
if(err) {
callback(err);
return;
}
var deps;
try {
deps = parse(source, options.parse);
} catch(e) {
callback("File \"" + filename + "\" parsing failed: " + e);
return;
}
module.requires = deps.requires || [];
module.asyncs = deps.asyncs || [];
module.contexts = deps.contexts || [];
module.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(loaders.length === 0)
processJs(content);
else {
var loaderFunctions = [];
try {
loaders.forEach(function(name) {
var loader = require(name);
loaderFunctions.push(loader);
});
} catch(e) {
callback(e);
return;
}
}
if(module.requires) {
module.requires.forEach(add);
module.requires.forEach(function(r) {
directRequire[r.name] = true;
});
}
if(module.contexts) {
module.contexts.forEach(addContext(module));
module.contexts.forEach(function(c) {
directContexts[c.name] = true;
});
}
if(module.asyncs)
module.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));
});
requiresNames = Object.keys(requires);
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
};
addModule(depTree, path.dirname(filename), moduleName, options, reason, function(err, moduleId) {
if(err) {
depTree.warnings.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;
});
}
endOne();
});
});
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
};
addContextModule(depTree, path.dirname(filename), context.name, options, reason, function(err, contextModuleId) {
if(err) {
errors.push(err+"\n @ " + filename + " (line " + context.line + ", column " + context.column + ")");
} else {
context.id = contextModuleId;
module.requires.push({id: context.id});
}
endOne();
});
if(context.warn) {
depTree.warnings.push(filename + " (line " + context.line + ", column " + context.column + "): " +
"implicit use of require.context(\".\") is not recommended.");
function nextLoader(err, content) {
if(err) {
callback(err);
return;
}
});
}
endOne();
function endOne() {
count--;
if(count === 0) {
if(errors.length) {
callback(errors.join("\n"));
if(loaderFunctions.length > 0) {
try {
loaderFunctions.pop()([content], {
request: filenameWithLoaders,
filename: filename
}, nextLoader);
} catch(e) {
callback(err);
return;
}
} else {
callback(null, module.id);
processJs(content);
}
}
nextLoader(null, content);
}
function processJs(source) {
var deps;
try {
deps = parse(source, options.parse);
} catch(e) {
callback("File \"" + filenameWithLoaders + "\" parsing failed: " + e);
return;
}
module.requires = deps.requires || [];
module.asyncs = deps.asyncs || [];
module.contexts = deps.contexts || [];
module.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(module.requires) {
module.requires.forEach(add);
module.requires.forEach(function(r) {
directRequire[r.name] = true;
});
}
if(module.contexts) {
module.contexts.forEach(addContext(module));
module.contexts.forEach(function(c) {
directContexts[c.name] = true;
});
}
if(module.asyncs)
module.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));
});
requiresNames = Object.keys(requires);
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
};
addModule(depTree, path.dirname(filename), moduleName, options, reason, function(err, moduleId) {
if(err) {
depTree.warnings.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;
});
}
endOne();
});
});
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
};
addContextModule(depTree, path.dirname(filename), context.name, options, reason, function(err, contextModuleId) {
if(err) {
errors.push(err+"\n @ " + filename + " (line " + context.line + ", column " + context.column + ")");
} else {
context.id = contextModuleId;
module.requires.push({id: context.id});
}
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--;
if(count === 0) {
if(errors.length) {
callback(errors.join("\n"));
} else {
callback(null, module.id);
}
}
}
}

View File

@ -7,26 +7,8 @@ var fs = require("fs");
// http://nodejs.org/docs/v0.4.8/api/all.html#all_Together...
/**
* context: absolute filename of current file
* identifier: module to find
* options:
* paths: array of lookup paths
* callback: function(err, absoluteFilename)
*/
module.exports = function resolve(context, identifier, options, callback) {
if(!callback) {
callback = options;
options = {};
}
if(!options)
options = {};
if(!options.extensions)
options.extensions = [".web.js", ".js"];
if(!options.paths)
options.paths = [];
if(!options.alias)
options.alias = {};
function resolve(context, identifier, options, type, callback) {
function finalResult(err, absoluteFilename) {
if(err) {
callback("Module \"" + identifier + "\" not found in context \"" +
@ -46,18 +28,86 @@ module.exports = function resolve(context, identifier, options, callback) {
}
if(identArray[0] === "." || identArray[0] === ".." || identArray[0] === "" || identArray[0].match(/^[A-Z]:$/i)) {
var pathname = identArray[0][0] === "." ? join(contextArray, identArray) : path.join.apply(path, identArray);
loadAsFile(pathname, options, function(err, absoluteFilename) {
loadAsFile(pathname, options, type, function(err, absoluteFilename) {
if(err) {
loadAsDirectory(pathname, options, finalResult);
loadAsDirectory(pathname, options, type, finalResult);
return;
}
callback(null, absoluteFilename);
});
} else {
loadNodeModules(contextArray, identArray, options, finalResult);
loadNodeModules(contextArray, identArray, options, type, finalResult);
}
}
/**
* context: absolute filename of current file
* identifier: module to find
* options:
* paths: array of lookup paths
* callback: function(err, absoluteFilename)
*/
module.exports = function(context, identifier, options, callback) {
if(!callback) {
callback = options;
options = {};
}
if(!options)
options = {};
if(!options.extensions)
options.extensions = ["", ".webpack.js", ".web.js", ".js"];
if(!options.loaders)
options.loaders = [];
if(!options.postfixes)
options.postfixes = ["", "-webpack", "-web"];
if(!options.loaderExtensions)
options.loaderExtensions = [".webpack-web-loader.js", ".webpack-loader.js", ".web-loader.js", ".loader.js", ".js", ""];
if(!options.loaderPostfixes)
options.loaderPostfixes = ["-webpack-web-loader", "-webpack-loader", "-web-loader", "-loader", ""];
if(!options.paths)
options.paths = [];
if(!options.alias)
options.alias = {};
var identifiers = identifier.split(/!/g);
if(identifiers.length === 1) {
var resource = identifiers.pop();
for(var i = 0; i < options.loaders.length; i++) {
var line = options.loaders[i];
if(line.test.test(resource)) {
identifiers.push(line.loader);
break;
}
}
identifiers.push(resource);
}
var errors = [];
var count = identifiers.length;
function endOne() {
count--;
if(count === 0) {
if(errors.length > 0) {
callback(errors.join("\n"));
return;
}
callback(null, identifiers.join("!"));
}
}
identifiers.forEach(function(ident, index) {
resolve(context, ident, options, index === identifiers.length - 1 ? "normal" : "loader", function(err, filename) {
if(err) {
errors.push(err);
} else {
if(!filename) {
throw new Error(JSON.stringify({identifiers: identifiers, from: ident, to: filename}));
}
identifiers[index] = filename;
}
endOne()
});
});
}
module.exports.context = function(context, identifier, options, callback) {
if(!callback) {
callback = options;
@ -107,46 +157,66 @@ function join(a, b) {
return path.join.apply(path, c);
}
function loadAsFile(filename, options, callback) {
var pos = -1, result;
function loadAsFile(filename, options, type, callback) {
var pos = -1, result = "NOT SET";
var extensions = type === "loader" ? options.loaderExtensions : options.extensions;
function tryCb(err, stats) {
if(err || !stats || !stats.isFile()) {
pos++;
if(pos >= options.extensions.length) {
callback(err);
if(pos >= extensions.length) {
callback(err || "Isn't a file");
return;
}
fs.stat(result = filename + options.extensions[pos], tryCb);
fs.stat(result = filename + extensions[pos], tryCb);
return;
}
if(!result) throw new Error("no result");
callback(null, result);
}
fs.stat(result = filename, tryCb);
tryCb(true);
}
function loadAsDirectory(dirname, options, callback) {
function loadAsDirectory(dirname, options, type, callback) {
var packageJsonFile = join(split(dirname), ["package.json"]);
fs.stat(packageJsonFile, function(err, stats) {
var mainModule = "index";
if(!err && stats.isFile()) {
fs.readFile(packageJsonFile, "utf-8", function(err, content) {
if(err) {
callback(err);
return;
}
content = JSON.parse(content);
if(content.main)
if(content.webpackLoader && type === "loader")
mainModule = content.webpackLoader;
else if(content.webpack)
mainModule = content.webpack;
else if(content.browserify)
mainModule = content.browserify;
else if(content.main)
mainModule = content.main;
loadAsFile(join(split(dirname), [mainModule]), options, callback);
loadAsFile(join(split(dirname), [mainModule]), options, type, callback);
});
} else
loadAsFile(join(split(dirname), [mainModule]), options, callback);
loadAsFile(join(split(dirname), [mainModule]), options, type, callback);
});
}
function loadNodeModules(context, identifier, options, callback) {
nodeModulesPaths(context, options, function(err, dirs) {
function loadNodeModules(context, identifier, options, type, callback) {
var moduleName = identifier.shift();
var postfixes = type === "loader" ? options.loaderPostfixes : options.postfixes;
nodeModulesPaths(context, options, function(err, paths) {
var dirs = [];
paths.forEach(function(path) {
postfixes.forEach(function(postfix) {
dirs.push(join(split(path), [moduleName+postfix]));
});
});
function tryDir(dir) {
var pathname = join(split(dir), identifier);
loadAsFile(pathname, options, function(err, absoluteFilename) {
loadAsFile(pathname, options, type, function(err, absoluteFilename) {
if(err) {
loadAsDirectory(pathname, options, function(err, absoluteFilename) {
loadAsDirectory(pathname, options, type, function(err, absoluteFilename) {
if(err) {
if(dirs.length === 0) {
callback("no module in any path of paths");

View File

@ -39,9 +39,13 @@ var templateSingle = require("fs").readFileSync(path.join(__dirname, "templateSi
- resolve.alias (object)
replace a module. ex {"old-module": "new-module"}
- resolve.extensions (object)
possible extentions for files
possible extensions for files
- resolve.paths (array)
search paths
- resolve.loaders (array)
extension to loader mappings
{test: /\.extension$/, loader: "myloader"}
loads files that matches the RegExp to the loader if no other loader set
- parse.overwrites (object)
free module varables which are replaced with a module
ex. { "$": "jquery" }
@ -74,9 +78,14 @@ module.exports = function(context, moduleName, options, callback) {
options.resolve.paths.push(path.join(path.dirname(__dirname), "buildin"));
options.resolve.paths.push(path.join(path.dirname(__dirname), "buildin", "web_modules"));
options.resolve.paths.push(path.join(path.dirname(__dirname), "buildin", "node_modules"));
options.resolve.paths.push(path.join(path.dirname(__dirname), "node_modules"));
options.resolve.alias = options.resolve.alias || {};
options.resolve.alias.http = options.resolve.alias.http || path.join(path.dirname(__dirname), "node_modules", "http-browserify")
options.resolve.alias.vm = options.resolve.alias.vm || path.join(path.dirname(__dirname), "node_modules", "vm-browserify")
options.resolve.loaders = options.loaders || [];
options.resolve.loaders.push({test: /\.coffee$/, loader: "coffee"});
options.resolve.loaders.push({test: /\.json$/, loader: "json"});
options.resolve.loaders.push({test: /\.jade$/, loader: "jade"});
buildDeps(context, moduleName, options, function(err, depTree) {
if(err) {
callback(err);
@ -134,7 +143,12 @@ module.exports = function(context, moduleName, options, callback) {
buffer.push(writeChunk(depTree, chunk, options));
buffer.push("/******/})");
buffer = buffer.join("");
if(options.minimize) buffer = uglify(buffer, filename);
try {
if(options.minimize) buffer = uglify(buffer, filename);
} catch(e) {
callback(e);
return;
}
fs.writeFile(filename, buffer, "utf-8", function(err) {
if(err) throw err;
});
@ -184,8 +198,12 @@ module.exports = function(context, moduleName, options, callback) {
buffer.push(writeChunk(depTree, options));
buffer.push("/******/})");
buffer = buffer.join("");
if(options.minimize) buffer = uglify(buffer, "output");
callback(null, buffer);
try {
if(options.minimize) buffer = uglify(buffer, "output");
callback(null, buffer);
} catch(e) {
callback(e);
}
}
});
}
@ -198,7 +216,7 @@ function uglify(input, filename) {
source = uglify.uglify.ast_squeeze(source);
source = uglify.uglify.gen_code(source);
} catch(e) {
console.error(filename + " @ Line " + e.line + ", Col " + e.col + ", " + e.message);
throw new Error(filename + " @ Line " + e.line + ", Col " + e.col + ", " + e.message);
return input;
}
return source;

View File

@ -1,17 +1,21 @@
{
"name": "webpack",
"version": "0.2.8",
"version": "0.3.0",
"author": "Tobias Koppers @sokra",
"description": "Packs CommonJs Modules for the browser. Allows to split your codebase into multiple bundles, which can be loaded on demand.",
"dependencies": {
"esprima": "0.9.8",
"esprima": "0.9.x",
"optimist": "0.2.x",
"uglify-js": "1.2.5",
"uglify-js": "1.2.x",
"sprintf": "0.1.x"
},
"optionalDependencies": {
"http-browserify": "*",
"vm-browserify": "*"
"vm-browserify": "*",
"raw-loader": "0.1.x",
"json-loader": "0.1.x",
"jade-loader": "0.1.x",
"coffee-loader": "0.1.x"
},
"licenses": [
{
@ -31,5 +35,5 @@
"scripts": {
"test": "node node_modules/vows/bin/vows"
},
"licence": "MIT"
"license": "MIT"
}

View File

@ -9,7 +9,66 @@ module.exports = function(req) {
if(!req.webpackPolyfill) {
var oldReq = req;
req = function(name) {
return oldReq(name);
if(name.indexOf("!") !== -1) {
var items = name.split(/!/g);
var resource = oldReq.resolve(items.pop());
var resolved = [];
items.forEach(function(item, index) {
var relative = false;
if(item.length > 2 &&
item[0] === ".") {
if(item[1] === "/")
relative = true;
else if(item.length > 3 &&
item[1] === "." &&
item[2] === "/")
relative = true;
}
if(item.length > 3 &&
item[1] === ":" &&
item[2] === "\\")
relative = true;
var tries = [];
if(!relative) {
postfixes.forEach(function(postfix) {
if(item.indexOf("/") !== -1)
tries.push(item.replace("/", postfix+"/"));
else
tries.push(item + postfix);
});
}
tries.push(item);
for(var i = 0; i < tries.length; i++) {
for(var ext = 0; ext < extensions.length; ext++) {
try {
var file = oldReq.resolve(tries[i] + extensions[ext]);
} catch(e) {}
if(file) {
resolved.push(file);
break;
}
}
if(ext !== extensions.length)
break;
}
if(i === tries.length)
throw new Error("Cannot find loader module '"+item+"'");
});
resolved = resolved.reverse();
var cacheLine = resolved.join("!") + "!" + resource;
var cacheEntry = oldReq.cache[cacheLine];
if(cacheEntry)
return cacheEntry;
var content = [require("fs").readFileSync(resource, "utf-8")];
resolved.forEach(function(loader) {
content = oldReq(loader)(content, {
request: cacheLine,
filename: resource
});
});
return content;
} else
return oldReq(name);
};
req.__proto__ = oldReq;
req.webpackPolyfill = true;
@ -27,4 +86,6 @@ module.exports = function(req) {
}
}
return req;
}
}
var extensions = [".webpack-loader.js", ".loader.js", ".js", ""];
var postfixes = ["-webpack-loader", "-loader", ""]

View File

@ -105,3 +105,19 @@ setTimeout(function() {
window.test(sum === 2, "Multiple callbacks on code load finish");
window.test(sum2 === 2, "process.nextTick and process.emit/on should be polyfilled");
}, 3000);
// Loader tests
window.test(require("testloader!../resources/abc.txt") === "abcwebpack", "Loader in package.json");
window.test(require("testloader/lib/loader!../resources/abc.txt") === "abcwebpack", "Loader with .webpack-loader.js extention");
window.test(require("testloader/lib/loader.web-loader.js!../resources/abc.txt") === "abcweb", "Loader with .web-loader.js extention");
window.test(require("testloader/lib/loader.loader.js!../resources/abc.txt") === "abcloader", "Loader with .loader.js extention");
window.test(require("testloader/lib/loader-indirect!../resources/abc.txt") === "abcwebpack", "Loader with .js extention and requires in loader");
window.test(require("testloader!../loaders/reverseloader!../resources/abc.txt") === "cbawebpack", "Multiple loaders and relative paths");
window.test(require("raw!../resources/abc.txt") === "abc", "Buildin 'raw' loader");
window.test(require("jade!../resources/template.jade")({abc: "abc"}) === "<p>abc</p>", "Buildin 'jade' loader");
window.test(require("../resources/template.jade")({abc: "abc"}) === "<p>abc</p>", "Buildin 'jade' loader, by ext");
window.test(require("json!../../../package.json").name === "webpack", "Buildin 'json' loader");
window.test(require("../../../package.json").name === "webpack", "Buildin 'json' loader, by ext");
window.test(require("coffee!../resources/script.coffee") === "coffee test", "Buildin 'coffee' loader");
window.test(require("../resources/script.coffee") === "coffee test", "Buildin 'coffee' loader, by ext");

View File

@ -0,0 +1,3 @@
module.exports = function(contents, options, callback) {
callback(null, contents[0].split("").reverse().join(""));
}

View File

@ -0,0 +1 @@
module.exports = require("./loader.webpack-loader.js");

View File

@ -0,0 +1,7 @@
module.exports = function(contents, options, callback) {
var content = contents[0];
callback(null, "module.exports=" + stringify(content));
}
function stringify(str) {
return '"' + str.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + '"';
}

View File

@ -0,0 +1,7 @@
module.exports = function(contents, options, callback) {
var content = contents[0];
callback(null, "module.exports=" + stringify(content+"loader"));
}
function stringify(str) {
return '"' + str.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + '"';
}

View File

@ -0,0 +1,7 @@
module.exports = function(contents, options, callback) {
var content = contents[0];
callback(null, "module.exports=" + stringify(content+"web"));
}
function stringify(str) {
return '"' + str.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + '"';
}

View File

@ -0,0 +1,7 @@
module.exports = function(contents, options, callback) {
var content = contents[0];
callback(null, "module.exports=" + stringify(content+"webpack"));
}
function stringify(str) {
return '"' + str.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + '"';
}

View File

@ -0,0 +1,4 @@
{
"name": "testloader",
"webpackLoader": "lib/loader.webpack-loader.js"
}

View File

@ -0,0 +1 @@
abc

View File

@ -0,0 +1,5 @@
condition = true
obj =
text: "coffee test"
module.exports = obj.text if condition?

View File

@ -0,0 +1,2 @@
p
= abc

1
test/fixtures/abc.txt vendored Normal file
View File

@ -0,0 +1 @@
abc

111
test/polyfills_test.js Normal file
View File

@ -0,0 +1,111 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
var vows = require("vows");
var assert = require("assert");
var path = require("path");
require = require("../require-polyfill")(require.valueOf());
vows.describe("polyfills").addBatch({
"polyfill context": {
topic: function() {
return require.context("./fixtures")
},
"simple file": {
topic: function(context) {
return context("./a");
},
"correct file": function(a) {
assert.equal(a(), "This is a");
}
},
"simple file with extension": {
topic: function(context) {
return context("./a.js");
},
"correct file": function(a) {
assert.equal(a(), "This is a");
}
},
"file in folder": {
topic: function(context) {
return context("./lib/complex1");
},
"correct file": function(complex1) {
assert.equal(complex1, "lib complex1");
}
}
},
"polyfill ensure": {
"empty ensure list": {
topic: function() {
var cb = this.callback;
require.ensure([], function(require) {
cb(null, require("./fixtures/a"));
});
},
"executed": function(a) {
assert.equal(a(), "This is a");
}
},
"with ensure list": {
topic: function() {
var cb = this.callback;
require.ensure(["./fixtures/a"], function(require) {
cb(null, require("./fixtures/a"));
});
},
"executed": function(a) {
assert.equal(a(), "This is a");
}
}
},
"polyfill loaders": {
"buildin raw loader": {
topic: require("raw!./fixtures/abc.txt"),
"raw loaded": function(abc) {
assert.equal(abc, "abc");
}
},
"buildin json loader": {
topic: require("json!../package.json"),
"json loaded": function(packageJson) {
assert.equal(packageJson.name, "webpack");
}
},
"buildin jade loader": {
topic: function() {
return require("jade!./browsertest/resources/template.jade");
},
"jade loaded": function(template) {
assert.equal(template({abc:"abc"}), "<p>abc</p>");
}
},
"buildin coffee loader": {
topic: function() {
return require("coffee!./browsertest/resources/script.coffee") || 1;
},
"coffee loaded": function(result) {
assert.equal(result, "coffee test");
}
}
}
}).export(module);

View File

@ -30,6 +30,7 @@ vows.describe("resolve").addBatch({
"resolve complex 1": testResolve(fixtures, "complexm/step1", path.join(fixtures, "node_modules", "complexm", "step1.js")),
"resolve complex 2": testResolve(path.join(fixtures, "node_modules", "complexm", "web_modules", "m1"),
"m2/b.js", path.join(fixtures, "node_modules", "m2", "b.js")),
"resolve loader 1": testResolve(fixtures, "m1/a!./main1.js", path.join(fixtures, "node_modules", "m1", "a.js") + "!" + path.join(fixtures, "main1.js")),
}).export(module);