better error handling, added conditional (?: operator) support, some docs

This commit is contained in:
Tobias Koppers 2012-03-19 19:59:38 +01:00
parent f5651c8fa8
commit 4c6fa00428
15 changed files with 481 additions and 72 deletions

405
README.md
View File

@ -1,7 +1,5 @@
# modules-webpack
## Goal
As developer you want to reuse existing code.
As with node.js and web all file are already in the same language, but it is extra work to use your code with the node.js module system and the browser.
The goal of `webpack` is to bundle CommonJs modules into javascript files which can be loaded by `<script>`-tags.
@ -14,9 +12,18 @@ The result is a smaller inital code download which results in faster page load.
* bundle CommonJs modules for browser
* reuse server-side code (node.js) on client-side
* create multiple files which are loaded on demand
* dependencies managed for you
* faster page load in big webapps
* create multiple files which are loaded on demand (faster page load in big webapps)
* dependencies managed for you, on compile time
## Goals
* minimize code size
* minimize code size on inital download
* download code only on demand
* hide development details, like module names and folder structure
* require minimal configuration
* load polyfills for node-specific things if used
* offer replacements for node buildin libaries
## Example
@ -67,6 +74,7 @@ require.ensure(["c"], function(require) {
```
File 1: web.js
- code of that file
- code of module a and dependencies
- code of module b and dependencies
@ -77,6 +85,20 @@ File 2: 1.web.js
See [details](modules-webpack/tree/master/examples/code-splitting) for exact output.
## Reusing node.js code
`webpack` was built to support most of the code that was coded for node.js environment.
For example this works out of the box:
* `require("./templates/" + templateName);`
* `require(condition ? "moduleA" : condition2 ? "moduleB" : "./localStuff");`
* `function xyz(require) { require("text"); } xyz(function(a) { console.log(a) });`
* `var r = require; r("./file");` with warning
* `function xyz(require) { require("./file"); } xyz(require);` with warning
* `try { require("missingModule"); } catch(e) { console.log("missing") }` with warning
* `var require = function(a) { console.log(a) }; require("text");`
* `if(condition) require("optionalModule")` with warning if missing
## Browser replacements
Somethings it happens that browsers require other code than node.js do.
@ -131,7 +153,6 @@ There is a warning emitted in this case.
As dependencies are resolved before running:
* `require` should not be overwritten by variable declaration (`var require = ...`), by function parameter is allowed `function(require) {...}`.
* `require.ensure` should not be overwritten or called indirect
* `require.context` should not be overwritten or called indirect
* the argument to `require.context` should be a literal or addition of multiple literals
@ -141,7 +162,7 @@ The following cases could result in too much code in result file if used wrong:
* indirect call of `require`: `var r = require; r("./file");`
* `require.context`. It includes the whole directory.
* expressions in require arguments: `require(variable)`, `require(condition ? "a" : "b")` (TODO)
* expressions in require arguments: `require(variable)`, webpack is smart enough for this `require(condition ? "a" : "b")`
* the function passed to `require.ensure` is not inlined in the call.
@ -149,6 +170,8 @@ The following cases could result in too much code in result file if used wrong:
As node.js specific modules like `fs` will not work in browser they are not included and cause an error.
You should replace them by own modules if you want to use them.
For some modules are replacements included in `webpack`.
Some credit goes to the browserify contributors, as I took some replacements from them.
```
web_modules
@ -157,7 +180,7 @@ web_modules
...
```
TODO provide some replacements
TODO provide some replacements (half way done...)
## Usage
@ -225,33 +248,359 @@ add absolute filenames of input files as comments
`source` if `options.output` is not set
else `stats` as json see [example](/modules-webpack/tree/master/examples/code-splitting)
## medikoo/modules-webmake
## Comparison
`webpack` as originally intended as fork for `webmake` for @medikoo so it shared several ideas with it.
So big credit goes to medikoo.
<table>
<tr>
<th>
Feature
</th>
<th>
sokra/<br/>modules-<br/>webpack
</th>
<th>
medikoo/<br/>modules-<br/>webmake
</th>
<th>
substack/<br/>node-<br/>browserify
</th>
</tr>
However `webpack` has big differences:
<tr>
<td>
single bundle
</td>
<td>
yes
</td>
<td>
yes
</td>
<td>
yes
</td>
</tr>
`webpack` replaces module names and paths with numbers. `webmake` don't do that and do resolves requires on client-side.
This design of `webmake` was intended to support variables as arguments to require calls.
`webpack` resolves requires in compile time and have no resolve code on client side. This results in smaller bundles.
Variables as arguments will be handled different and with more limitations in `webpack`.
<tr>
<td>
multiple bundles, Code Splitting
</td>
<td>
yes
</td>
<td>
no
</td>
<td>
no
</td>
</tr>
Another limitation in `webmake` which are based on the previous one is that modules must be in the current package scope.
In `webpack` this is not a restriction.
<tr>
<td>
indirect require
<code>var r = require; r("./file");</code>
</td>
<td>
in directory
</td>
<td>
include by config option
</td>
<td>
no
</td>
</tr>
There is no `require.context` in `webmake`. Therefore there is a forced include list in options which allows modules to be required even if the names were not available at compile time.
<tr>
<td>
concat in require
<code>require("./fi" + "le")</code>
</td>
<td>
yes
</td>
<td>
yes
</td>
<td>
no
</td>
</tr>
The design of `webmake` causes all modules with the same name to overlap.
This can be problematic if different submodules rely on specific versions of the same module.
The behaivior also differs from the behaivior of node.js, because node.js installs a module for each instance in submodules and `webmake` cause them the merge into a single module which is only installed once.
In `webpack` this is not the case.
Different versions do not overlap and modules are installed multiple times.
But in `webpack` this can (currently) cause duplicate code if a module is used in multiple modules.
I want to face this issue (TODO).
<tr>
<td>
variables in require (local)
<code>require("./templates/"+template)</code>
</td>
<td>
yes, complete directory included
</td>
<td>
include by config option
</td>
<td>
no
</td>
</tr>
`webmake` do (currently) not support Code Splitting.
But medikoo said he works at some related feature.
<tr>
<td>
variables in require (global)
<code>require(moduleName)</code>
</td>
<td>
no
</td>
<td>
include by config option
</td>
<td>
no
</td>
</tr>
<tr>
<td>
node buildin libs
<code>require("http");</code>
</td>
<td>
some
</td>
<td>
no
</td>
<td>
many
</td>
</tr>
<tr>
<td>
<code>process</code> polyfill
</td>
<td>
yes, on demand
</td>
<td>
no
</td>
<td>
yes, ever
</td>
</tr>
<tr>
<td>
<code>module</code> polyfill
</td>
<td>
yes, on demand
</td>
<td>
no
</td>
<td>
no
</td>
</tr>
<tr>
<td>
<code>require.resolve</code>
</td>
<td>
no
</td>
<td>
no
</td>
<td>
yes
</td>
</tr>
<tr>
<td>
<code>global</code> to <code>window</code> mapping
</td>
<td>
yes
</td>
<td>
no
</td>
<td>
no
</td>
</tr>
<tr>
<td>
requirable files
</td>
<td>
filesystem
</td>
<td>
directory scope
</td>
<td>
filesystem
</td>
</tr>
<tr>
<td>
different modules with same name
</td>
<td>
yes
</td>
<td>
no
</td>
<td>
yes
</td>
</tr>
<tr>
<td>
eliminate duplicate code
</td>
<td>
yes
</td>
<td>
no
</td>
<td>
yes
</td>
</tr>
<tr>
<td>
require JSON
</td>
<td>
no
</td>
<td>
no
</td>
<td>
no
</td>
</tr>
<tr>
<td>
plugins
</td>
<td>
no
</td>
<td>
no
</td>
<td>
yes
</td>
</tr>
<tr>
<td>
compile coffee script
</td>
<td>
no
</td>
<td>
no
</td>
<td>
yes
</td>
</tr>
<tr>
<td>
watch mode
</td>
<td>
no
</td>
<td>
no
</td>
<td>
yes
</td>
</tr>
<tr>
<td>
debug mode
</td>
<td>
no
</td>
<td>
no
</td>
<td>
yes
</td>
</tr>
<tr>
<td>
libaries
</td>
<td>
on global obj
</td>
<td>
no
</td>
<td>
requirable
</td>
</tr>
<tr>
<td>
browser replacements
</td>
<td>
<code>web_modules</code> and <code>.web.js</code>
</td>
<td>
no
</td>
<td>
by alias config option
</td>
</tr>
<tr>
<td>
compiles with (optional) modules missing
</td>
<td>
yes, emit warnings
</td>
<td>
no
</td>
<td>
no
</td>
</tr>
</table>
## Tests

View File

@ -138,7 +138,8 @@ function addModule(depTree, context, module, options, reason, callback) {
};
addModule(depTree, path.dirname(filename), moduleName, options, reason, function(err, moduleId) {
if(err) {
errors.push(err+"\n @ " + filename + " (line " + requires[moduleName][0].line + ", column " + requires[moduleName][0].column + ")");
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;
@ -254,7 +255,8 @@ function addContextModule(depTree, context, contextModuleName, options, reason,
};
addModule(depTree, null, filename, options, reason, function(err, moduleId) {
if(err) {
endOne(err);
depTree.warnings.push("A file in context was excluded because of error: " + err);
endOne();
} else {
contextModule.requires.push({id: moduleId});
contextModule.requireMap[moduleName + "/" + file] = moduleId;
@ -274,17 +276,6 @@ function addContextModule(depTree, context, contextModuleName, options, reason,
callback(err);
return;
}
var extensionsAccess = [];
extensions.forEach(function(ext) {
extensionsAccess.push("||map[name+\"");
extensionsAccess.push(ext.replace(/\\/g, "\\\\").replace(/"/g, "\\\""));
extensionsAccess.push("\"]");
});
contextModule.source = "/***/module.exports = function(name) {\n" +
"/***/\tvar map = " + JSON.stringify(contextModule.requireMap) + ";\n" +
"/***/\treturn require(map[name]" + extensionsAccess.join("") + ");\n" +
"/***/};";
callback(null, contextModule.id);
});
}
@ -313,7 +304,8 @@ function addModuleToChunk(depTree, context, chunkId, options) {
depTree.chunks[chunkId].modules[context.id] = "include";
if(context.requires) {
context.requires.forEach(function(requireItem) {
addModuleToChunk(depTree, depTree.modulesById[requireItem.id], chunkId, options);
if(requireItem.id)
addModuleToChunk(depTree, depTree.modulesById[requireItem.id], chunkId, options);
});
}
if(context.asyncs) {

View File

@ -177,13 +177,24 @@ function walkExpression(context, expression) {
break;
case "CallExpression":
var noCallee = false;
if(context.overwrite.indexOf("require") === -1 &&
if(context.overwrite.indexOf("require") === -1 &&
expression.callee && expression.arguments &&
expression.arguments.length == 1 &&
expression.callee.type === "Identifier" &&
expression.callee.name === "require") {
var param = parseCalculatedString(expression.arguments[0]);
if(param.code) {
if(param.conditional) {
context.requires = context.requires || [];
param.conditional.forEach(function(paramItem) {
context.requires.push({
name: paramItem.value,
valueRange: paramItem.range,
line: expression.loc.start.line,
column: expression.loc.start.column
});
console.dir(context.requires[context.requires.length-1]);
});
} else if(param.code) {
// make context
var pos = param.value.indexOf("/");
context.contexts = context.contexts || [];
@ -222,7 +233,7 @@ function walkExpression(context, expression) {
}
noCallee = true;
}
if(context.overwrite.indexOf("require") === -1 &&
if(context.overwrite.indexOf("require") === -1 &&
expression.callee && expression.arguments &&
expression.arguments.length >= 1 &&
expression.callee.type === "MemberExpression" &&
@ -248,7 +259,7 @@ function walkExpression(context, expression) {
context = newContext;
noCallee = true;
}
if(context.overwrite.indexOf("require") === -1 &&
if(context.overwrite.indexOf("require") === -1 &&
expression.callee && expression.arguments &&
expression.arguments.length == 1 &&
expression.callee.type === "MemberExpression" &&
@ -284,7 +295,7 @@ function walkExpression(context, expression) {
walkExpression(context, expression.property);
break;
case "Identifier":
if(context.overwrite.indexOf("require") === -1 &&
if(context.overwrite.indexOf("require") === -1 &&
expression.name === "require") {
context.contexts = context.contexts || [];
var newContext = {
@ -296,7 +307,7 @@ function walkExpression(context, expression) {
column: expression.loc.start.column
};
context.contexts.push(newContext);
} else if(context.overwrite.indexOf(expression.name) === -1 &&
} else if(context.overwrite.indexOf(expression.name) === -1 &&
expression.name in context.options.overwrites) {
context.requires = context.requires || [];
context.requires.push({
@ -310,17 +321,6 @@ function walkExpression(context, expression) {
}
}
function functionParamsContainsRequire(params) {
if(!params) return false;
var found = false;
params.forEach(function(param) {
if(param.type === "Identifier" &&
param.name === "require")
found = true;
});
return found;
}
function addOverwrites(context, params) {
var l = context.overwrite.length;
if(!params) return l;
@ -329,7 +329,7 @@ function addOverwrites(context, params) {
context.ignoreOverride = false;
return;
}
if(param.type === "Identifier" &&
if(param.type === "Identifier" &&
param.name in context.options.overwrites)
context.overwrite.push(param.name);
});
@ -363,6 +363,21 @@ function parseCalculatedString(expression) {
}
}
break;
case "ConditionalExpression":
var consequent = parseCalculatedString(expression.consequent);
var alternate = parseCalculatedString(expression.alternate);
var items = [];
if(consequent.conditional)
Array.prototype.push.apply(items, consequent.conditional);
else if(!consequent.code)
items.push(consequent);
else break;
if(alternate.conditional)
Array.prototype.push.apply(items, alternate.conditional);
else if(!alternate.code)
items.push(alternate);
else break;
return {value: "", code: true, conditional: items};
case "Literal":
return {range: expression.range, value: expression.value+""};
break;

View File

@ -30,7 +30,7 @@ module.exports = function resolve(context, identifier, options, callback) {
function finalResult(err, absoluteFilename) {
if(err) {
callback("Module \"" + identifier + "\" not found in context \"" +
context + "\"\n" + err);
context + "\"\n " + err);
return;
}
callback(null, absoluteFilename);

View File

@ -2,6 +2,7 @@
/******/ return function(modules) {
/******/ var installedModules = {}, installedChunks = {0:1};
/******/ function require(moduleId) {
/******/ if(typeof moduleId !== "number") throw new Error("Cannot find module '"+moduleId+"'");
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ var module = installedModules[moduleId] = {

View File

@ -1,6 +1,7 @@
/******/(function(modules) {
/******/ var installedModules = {};
/******/ function require(moduleId) {
/******/ if(typeof moduleId !== "number") throw new Error("Cannot find module '"+moduleId+"'");
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ var module = installedModules[moduleId] = {

View File

@ -25,7 +25,7 @@ module.exports = function(depTree, chunk, options) {
buffer.push(module.filename);
buffer.push(" ***/\n\n");
}
buffer.push(writeSource(module));
buffer.push(writeSource(module, options));
buffer.push("\n\n/******/},\n/******/\n");
}
return buffer.join("");

View File

@ -6,18 +6,44 @@ function stringify(str) {
return '"' + str.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + '"';
}
module.exports = function(module) {
module.exports = function(module, options) {
if(!module.source) {
if(module.requireMap) {
var extensions = (options.resolve && options.resolve.extensions) || [".web.js", ".js"];
var extensionsAccess = [];
extensions.forEach(function(ext) {
extensionsAccess.push("||map[name+\"");
extensionsAccess.push(ext.replace(/\\/g, "\\\\").replace(/"/g, "\\\""));
extensionsAccess.push("\"]");
});
return "/***/function err(name) { throw new Error(\"Cannot find module '\"+name+\"'\") }\n"+
"/***/module.exports = function(name) {\n" +
"/***/\tvar map = " + JSON.stringify(module.requireMap) + ";\n" +
"/***/\treturn require(map[name]" + extensionsAccess.join("") + "||(err(name)));\n" +
"/***/};";
}
return;
}
var replaces = []; // { from: 123, to: 125, value: "4" }
function genReplaceRequire(requireItem) {
if(requireItem.expressionRange && requireItem.id !== undefined) {
if(requireItem.id !== undefined) {
var prefix = "";
if(requireItem.name)
prefix += "/* " + requireItem.name + " */";
replaces.push({
from: requireItem.expressionRange[0],
to: requireItem.expressionRange[1],
value: "require(" + prefix + requireItem.id + ")"
});
if(requireItem.expressionRange) {
replaces.push({
from: requireItem.expressionRange[0],
to: requireItem.expressionRange[1],
value: "require(" + prefix + requireItem.id + ")"
});
} else if(requireItem.valueRange) {
replaces.push({
from: requireItem.valueRange[0],
to: requireItem.valueRange[1],
value: prefix + requireItem.id
});
}
}
}
function genContextReplaces(contextItem) {

View File

@ -1,6 +1,6 @@
{
"name": "webpack",
"version": "0.2.5",
"version": "0.2.6",
"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": {

View File

@ -1,8 +1,8 @@
// Polyfill for node.js
// adds require.ensure
// call it like this: require("webpack/require-polyfill")(require);
// This is only required when you want to use require.ensure in server-side code
// which should be so only in rar cases.
// This is only required when you want to use require.ensure or require.context
// in server-side code which should be so only in rar cases.
module.exports = function(req) {
if(!req.ensure) {
req.ensure = function(array, callback) {

View File

@ -0,0 +1 @@
))) This results in a syntax error loading this file.

View File

@ -0,0 +1 @@
module.exports = "file1";

View File

@ -0,0 +1 @@
module.exports = "file2";

View File

@ -0,0 +1 @@
module.exports = "file3";

View File

@ -27,7 +27,28 @@ function testFunc(abc, require) {
return require;
}
window.test(testFunc(333, 678) === 678, "require overwrite in named function");
function testCase(number) {
//window.test(require("./folder/file" + (number === 1 ? 1 : "2")) === "file" + number);
window.test(require(number === 1 ? "../folder/file1" : number === 2 ? "../folder/file2" : number === 3 ? "../folder/file3" : "./missingModule") === "file" + number, "?: operator in require do not create context, test "+number);
}
testCase(1);
testCase(2);
testCase(3);
var error = null;
try {
testCase(4);
} catch(e) {
error = e;
}
window.test(error instanceof Error, "Missing module should throw Error, indirect");
error = null;
try {
require("./missingModule2");
} catch(e) {
error = e;
}
window.test(error instanceof Error, "Missing module should throw Error, direct");
require.ensure([], function(require) {
var contextRequire = require.context(".");