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 # modules-webpack
## Goal
As developer you want to reuse existing code. 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. 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. 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 * bundle CommonJs modules for browser
* reuse server-side code (node.js) on client-side * reuse server-side code (node.js) on client-side
* create multiple files which are loaded on demand * create multiple files which are loaded on demand (faster page load in big webapps)
* dependencies managed for you * dependencies managed for you, on compile time
* faster page load in big webapps
## 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 ## Example
@ -67,6 +74,7 @@ require.ensure(["c"], function(require) {
``` ```
File 1: web.js File 1: web.js
- code of that file
- code of module a and dependencies - code of module a and dependencies
- code of module b 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. 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 ## Browser replacements
Somethings it happens that browsers require other code than node.js do. 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: 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.ensure` should not be overwritten or called indirect
* `require.context` 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 * 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");` * indirect call of `require`: `var r = require; r("./file");`
* `require.context`. It includes the whole directory. * `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. * 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. 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. 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 web_modules
@ -157,7 +180,7 @@ web_modules
... ...
``` ```
TODO provide some replacements TODO provide some replacements (half way done...)
## Usage ## Usage
@ -225,33 +248,359 @@ add absolute filenames of input files as comments
`source` if `options.output` is not set `source` if `options.output` is not set
else `stats` as json see [example](/modules-webpack/tree/master/examples/code-splitting) 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. <table>
So big credit goes to medikoo. <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. <tr>
This design of `webmake` was intended to support variables as arguments to require calls. <td>
`webpack` resolves requires in compile time and have no resolve code on client side. This results in smaller bundles. multiple bundles, Code Splitting
Variables as arguments will be handled different and with more limitations in `webpack`. </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. <tr>
In `webpack` this is not a restriction. <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. <tr>
This can be problematic if different submodules rely on specific versions of the same module. <td>
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. variables in require (local)
In `webpack` this is not the case. <code>require("./templates/"+template)</code>
Different versions do not overlap and modules are installed multiple times. </td>
But in `webpack` this can (currently) cause duplicate code if a module is used in multiple modules. <td>
I want to face this issue (TODO). yes, complete directory included
</td>
<td>
include by config option
</td>
<td>
no
</td>
</tr>
`webmake` do (currently) not support Code Splitting. <tr>
But medikoo said he works at some related feature. <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 ## 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) { addModule(depTree, path.dirname(filename), moduleName, options, reason, function(err, moduleId) {
if(err) { 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 { } else {
requires[moduleName].forEach(function(requireItem) { requires[moduleName].forEach(function(requireItem) {
requireItem.id = moduleId; requireItem.id = moduleId;
@ -254,7 +255,8 @@ function addContextModule(depTree, context, contextModuleName, options, reason,
}; };
addModule(depTree, null, filename, options, reason, function(err, moduleId) { addModule(depTree, null, filename, options, reason, function(err, moduleId) {
if(err) { if(err) {
endOne(err); depTree.warnings.push("A file in context was excluded because of error: " + err);
endOne();
} else { } else {
contextModule.requires.push({id: moduleId}); contextModule.requires.push({id: moduleId});
contextModule.requireMap[moduleName + "/" + file] = moduleId; contextModule.requireMap[moduleName + "/" + file] = moduleId;
@ -274,17 +276,6 @@ function addContextModule(depTree, context, contextModuleName, options, reason,
callback(err); callback(err);
return; 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); callback(null, contextModule.id);
}); });
} }
@ -313,7 +304,8 @@ function addModuleToChunk(depTree, context, chunkId, options) {
depTree.chunks[chunkId].modules[context.id] = "include"; depTree.chunks[chunkId].modules[context.id] = "include";
if(context.requires) { if(context.requires) {
context.requires.forEach(function(requireItem) { 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) { if(context.asyncs) {

View File

@ -177,13 +177,24 @@ function walkExpression(context, expression) {
break; break;
case "CallExpression": case "CallExpression":
var noCallee = false; var noCallee = false;
if(context.overwrite.indexOf("require") === -1 && if(context.overwrite.indexOf("require") === -1 &&
expression.callee && expression.arguments && expression.callee && expression.arguments &&
expression.arguments.length == 1 && expression.arguments.length == 1 &&
expression.callee.type === "Identifier" && expression.callee.type === "Identifier" &&
expression.callee.name === "require") { expression.callee.name === "require") {
var param = parseCalculatedString(expression.arguments[0]); 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 // make context
var pos = param.value.indexOf("/"); var pos = param.value.indexOf("/");
context.contexts = context.contexts || []; context.contexts = context.contexts || [];
@ -222,7 +233,7 @@ function walkExpression(context, expression) {
} }
noCallee = true; noCallee = true;
} }
if(context.overwrite.indexOf("require") === -1 && if(context.overwrite.indexOf("require") === -1 &&
expression.callee && expression.arguments && expression.callee && expression.arguments &&
expression.arguments.length >= 1 && expression.arguments.length >= 1 &&
expression.callee.type === "MemberExpression" && expression.callee.type === "MemberExpression" &&
@ -248,7 +259,7 @@ function walkExpression(context, expression) {
context = newContext; context = newContext;
noCallee = true; noCallee = true;
} }
if(context.overwrite.indexOf("require") === -1 && if(context.overwrite.indexOf("require") === -1 &&
expression.callee && expression.arguments && expression.callee && expression.arguments &&
expression.arguments.length == 1 && expression.arguments.length == 1 &&
expression.callee.type === "MemberExpression" && expression.callee.type === "MemberExpression" &&
@ -284,7 +295,7 @@ function walkExpression(context, expression) {
walkExpression(context, expression.property); walkExpression(context, expression.property);
break; break;
case "Identifier": case "Identifier":
if(context.overwrite.indexOf("require") === -1 && if(context.overwrite.indexOf("require") === -1 &&
expression.name === "require") { expression.name === "require") {
context.contexts = context.contexts || []; context.contexts = context.contexts || [];
var newContext = { var newContext = {
@ -296,7 +307,7 @@ function walkExpression(context, expression) {
column: expression.loc.start.column column: expression.loc.start.column
}; };
context.contexts.push(newContext); 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) { expression.name in context.options.overwrites) {
context.requires = context.requires || []; context.requires = context.requires || [];
context.requires.push({ 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) { function addOverwrites(context, params) {
var l = context.overwrite.length; var l = context.overwrite.length;
if(!params) return l; if(!params) return l;
@ -329,7 +329,7 @@ function addOverwrites(context, params) {
context.ignoreOverride = false; context.ignoreOverride = false;
return; return;
} }
if(param.type === "Identifier" && if(param.type === "Identifier" &&
param.name in context.options.overwrites) param.name in context.options.overwrites)
context.overwrite.push(param.name); context.overwrite.push(param.name);
}); });
@ -363,6 +363,21 @@ function parseCalculatedString(expression) {
} }
} }
break; 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": case "Literal":
return {range: expression.range, value: expression.value+""}; return {range: expression.range, value: expression.value+""};
break; break;

View File

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

View File

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

View File

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

View File

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

View File

@ -6,18 +6,44 @@ function stringify(str) {
return '"' + str.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + '"'; 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" } var replaces = []; // { from: 123, to: 125, value: "4" }
function genReplaceRequire(requireItem) { function genReplaceRequire(requireItem) {
if(requireItem.expressionRange && requireItem.id !== undefined) { if(requireItem.id !== undefined) {
var prefix = ""; var prefix = "";
if(requireItem.name) if(requireItem.name)
prefix += "/* " + requireItem.name + " */"; prefix += "/* " + requireItem.name + " */";
replaces.push({ if(requireItem.expressionRange) {
from: requireItem.expressionRange[0], replaces.push({
to: requireItem.expressionRange[1], from: requireItem.expressionRange[0],
value: "require(" + prefix + requireItem.id + ")" 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) { function genContextReplaces(contextItem) {

View File

@ -1,6 +1,6 @@
{ {
"name": "webpack", "name": "webpack",
"version": "0.2.5", "version": "0.2.6",
"author": "Tobias Koppers @sokra", "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.", "description": "Packs CommonJs Modules for the browser. Allows to split your codebase into multiple bundles, which can be loaded on demand.",
"dependencies": { "dependencies": {

View File

@ -1,8 +1,8 @@
// Polyfill for node.js // Polyfill for node.js
// adds require.ensure // adds require.ensure
// call it like this: require("webpack/require-polyfill")(require); // call it like this: require("webpack/require-polyfill")(require);
// This is only required when you want to use require.ensure in server-side code // This is only required when you want to use require.ensure or require.context
// which should be so only in rar cases. // in server-side code which should be so only in rar cases.
module.exports = function(req) { module.exports = function(req) {
if(!req.ensure) { if(!req.ensure) {
req.ensure = function(array, callback) { 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; return require;
} }
window.test(testFunc(333, 678) === 678, "require overwrite in named function"); 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) { require.ensure([], function(require) {
var contextRequire = require.context("."); var contextRequire = require.context(".");