From 14b42ac18b4d7a2e48bb99d559e268dcd6b82f1d Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Sun, 11 Mar 2012 21:37:18 +0100 Subject: [PATCH] added contexts, docs and examples --- .gitignore | 2 +- README.md | 59 +++++-- .../code-splitted-require.context/README.md | 138 ++++++++++++++++ .../code-splitted-require.context}/build.js | 2 +- .../code-splitted-require.context/example.js | 11 ++ .../code-splitting}/README.md | 2 +- examples/code-splitting/build.js | 14 ++ .../code-splitting}/example.js | 0 .../code-splitting}/node_modules/a.js | 0 .../code-splitting}/node_modules/b.js | 0 .../code-splitting}/node_modules/c.js | 0 .../code-splitting}/node_modules/d.js | 0 examples/require.context/README.md | 119 ++++++++++++++ examples/require.context/build.js | 14 ++ examples/require.context/example.js | 5 + examples/require.context/templates/a.js | 3 + examples/require.context/templates/b.js | 3 + examples/require.context/templates/c.js | 3 + lib/buildDeps.js | 149 ++++++++++++++++-- lib/parse.js | 89 +++++++++-- lib/resolve.js | 56 +++++++ lib/writeSource.js | 31 ++++ test/browsertest/lib/index.web.js | 9 ++ test/browsertest/templates/subdir/tmpl.js | 1 + test/browsertest/templates/tmpl.js | 1 + 25 files changed, 668 insertions(+), 43 deletions(-) create mode 100644 examples/code-splitted-require.context/README.md rename {example => examples/code-splitted-require.context}/build.js (70%) create mode 100644 examples/code-splitted-require.context/example.js rename {example => examples/code-splitting}/README.md (98%) create mode 100644 examples/code-splitting/build.js rename {example => examples/code-splitting}/example.js (100%) rename {example => examples/code-splitting}/node_modules/a.js (100%) rename {example => examples/code-splitting}/node_modules/b.js (100%) rename {example => examples/code-splitting}/node_modules/c.js (100%) rename {example => examples/code-splitting}/node_modules/d.js (100%) create mode 100644 examples/require.context/README.md create mode 100644 examples/require.context/build.js create mode 100644 examples/require.context/example.js create mode 100644 examples/require.context/templates/a.js create mode 100644 examples/require.context/templates/b.js create mode 100644 examples/require.context/templates/c.js create mode 100644 test/browsertest/templates/subdir/tmpl.js create mode 100644 test/browsertest/templates/tmpl.js diff --git a/.gitignore b/.gitignore index ac248cbee22..679a47b8873 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /node_modules /test/js /test/browsertest/js -/example/js \ No newline at end of file +/examples/*/js \ No newline at end of file diff --git a/README.md b/README.md index f08f51fdcb1..9f972dae8be 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ File 2: 1.web.js - code of module d and dependencies ``` -See [details](modules-webpack/tree/master/example) for exact output. +See [details](modules-webpack/tree/master/examples/code-splitting) for exact output. ## Browser replacements @@ -87,19 +87,52 @@ Modules in `web_modules` replace modules in `node_modules`. TODO specify replacements in options +## require.context + +If the `require`d module is not known while compile time we get into a problem. +A solution is the method `require.context` which takes a directory as parameter +and returns a function which behaves like the `require` function issued from a file +in this directory (but only if used for files in that directory). + +### Example + +We have a directory full of templates, which are compiled javascript files. +A template should be loaded by template name. + +``` javascript +var requireTemplate = require.context("./templates"); +function getTemplate(templateName) { + return requireTemplate("./" + templateName); +} +``` + +In addition to that `webpack` uses the `require.context` function automatically +if you use variables or other non-literal things in the `require` function. +That means the following code behaves like the above: + +``` javascript +function getTemplate(templateName) { + return require("./templates/" + templateName); +} +// is compiled like: return require.context("./templates")("./"+templateName) +``` + +See [details](modules-webpack/tree/master/examples/require.context) for complete example. + + +*Warning: The complete code in the directory are included. So use it carefully.* + ## Limitations ### `require`-function As dependencies are resolved before running: * `require` should not be overwritten -* `require` should not be called indirect as `var r = require; r("./a");` -* arguments of `require` should be literals. `"./abc" + "/def"` is allowed to support long lines. -* `require.ensure` has the same limitations as `require` +* `require` should not be called indirect as `var r = require; r("./a");`. Use `require.context`? +* `require.ensure` should not be overwritten or called indirect * the function passed to `require.ensure` should be inlined in the call. - -TODO allow variables passing to `require` like `require("./templates/" + mytemplate)` -(this will cause all modules matching this pattern to be included in addition to a mapping table) +* `require.context` should not be overwritten or called indirect +* the argument to `require.context` should be a literal or addition of multiple literals ### node.js specific modules @@ -173,7 +206,7 @@ add absolute filenames of input files as comments #### `callback` `function(err, source / stats)` `source` if `options.output` is not set -else `stats` as json see [example](/modules-webpack/tree/master/example) +else `stats` as json see [example](/modules-webpack/tree/master/examples/code-splitting) ## medikoo/modules-webmake @@ -183,14 +216,20 @@ So big credit goes to medikoo. However `webpack` has big differences: `webpack` replaces module names and paths with numbers. `webmake` don't do that and do resolves requires on client-side. -This design of `webmake` wes intended to support variables as arguments to require calls. +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 argments will be handled different and with more limitations. 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. -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). +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). `webmake` do (currently) not support Code Splitting. diff --git a/examples/code-splitted-require.context/README.md b/examples/code-splitted-require.context/README.md new file mode 100644 index 00000000000..c31b78aa96f --- /dev/null +++ b/examples/code-splitted-require.context/README.md @@ -0,0 +1,138 @@ +# example.js + +``` javascript +function getTemplate(templateName, callback) { + require.ensure([], function(require) { + callback(require("../require.context/templates/"+templateName)); + }); +} +getTemplate("a", function(a) { + console.log(a); +}); +getTemplate("b", function(b) { + console.log(b); +}); +``` + +# js/output.js + +``` javascript +/******/(function(document, undefined) { +/******/ return function(modules) { +/******/ var installedModules = {}, installedChunks = {0:1}; +/******/ function require(moduleId) { +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; +/******/ var module = installedModules[moduleId] = { +/******/ exports: {} +/******/ }; +/******/ modules[moduleId](module, module.exports, require); +/******/ return module.exports; +/******/ } +/******/ require.ensure = function(chunkId, callback) { +/******/ if(installedChunks[chunkId] === 1) return callback(require); +/******/ if(installedChunks[chunkId] !== undefined) +/******/ installedChunks[chunkId].push(callback); +/******/ else { +/******/ installedChunks[chunkId] = [callback]; +/******/ var head = document.getElementsByTagName('head')[0]; +/******/ var script = document.createElement('script'); +/******/ script.type = 'text/javascript'; +/******/ script.src = modules.c+chunkId+modules.a; +/******/ head.appendChild(script); +/******/ } +/******/ }; +/******/ window[modules.b] = function(chunkId, moreModules) { +/******/ for(var moduleId in moreModules) +/******/ modules[moduleId] = moreModules[moduleId]; +/******/ var callbacks = installedChunks[chunkId]; +/******/ installedChunks[chunkId] = 1; +/******/ for(var i = 0; i < callbacks.length; i++) +/******/ callbacks[i](require); +/******/ }; +/******/ return require(0); +/******/ } +/******/})(document) +/******/({a:".output.js",b:"webpackJsonp",c:"", +/******/0: function(module, exports, require) { + +function getTemplate(templateName, callback) { + require.ensure(1, function(require) { + callback(require(1)("./"+templateName)); + }); +} +getTemplate("a", function(a) { + console.log(a); +}); +getTemplate("b", function(b) { + console.log(b); +}); + +/******/}, +/******/ +/******/}) +``` + +# js/1.output.js + +``` javascript +/******/webpackJsonp(1, { +/******/1: function(module, exports, require) { + +/***/module.exports = function(name) { +/***/ var map = {"./b.js":3,"./a.js":2,"./c.js":4}; +/***/ return require(map[name]||map[name+".web.js"]||map[name+".js"]); +/***/}; + +/******/}, +/******/ +/******/2: function(module, exports, require) { + +module.exports = function() { + return "This text was generated by template A"; +} + +/******/}, +/******/ +/******/3: function(module, exports, require) { + +module.exports = function() { + return "This text was generated by template B"; +} + +/******/}, +/******/ +/******/4: function(module, exports, require) { + +module.exports = function() { + return "This text was generated by template C"; +} + +/******/}, +/******/ +/******/}) +``` + +# Info + +## Uncompressed + +``` javascript +{ chunkCount: 2, + modulesCount: 5, + modulesIncludingDuplicates: 5, + modulesPerChunk: 2.5, + modulesFirstChunk: 1, + fileSizes: { 'output.js': 1855, '1.output.js': 729 } } +``` + +## Minimized (uglify-js, no zip) + +``` javascript +{ chunkCount: 2, + modulesCount: 5, + modulesIncludingDuplicates: 5, + modulesPerChunk: 2.5, + modulesFirstChunk: 1, + fileSizes: { 'output.js': 683, '1.output.js': 404 } } +``` diff --git a/example/build.js b/examples/code-splitted-require.context/build.js similarity index 70% rename from example/build.js rename to examples/code-splitted-require.context/build.js index d97ee66009f..3e7fb9a3271 100644 --- a/example/build.js +++ b/examples/code-splitted-require.context/build.js @@ -5,7 +5,7 @@ argv.shift(); argv.shift(); var extraArgs = argv.join(" "); -cp.exec("node ../bin/webpack.js "+extraArgs+" example.js js/output.js", function (error, stdout, stderr) { +cp.exec("node ../../bin/webpack.js "+extraArgs+" example.js js/output.js", function (error, stdout, stderr) { console.log('stdout:\n' + stdout); console.log('stderr:\n ' + stderr); if (error !== null) { diff --git a/examples/code-splitted-require.context/example.js b/examples/code-splitted-require.context/example.js new file mode 100644 index 00000000000..c7bb135c110 --- /dev/null +++ b/examples/code-splitted-require.context/example.js @@ -0,0 +1,11 @@ +function getTemplate(templateName, callback) { + require.ensure([], function(require) { + callback(require("../require.context/templates/"+templateName)); + }); +} +getTemplate("a", function(a) { + console.log(a); +}); +getTemplate("b", function(b) { + console.log(b); +}); \ No newline at end of file diff --git a/example/README.md b/examples/code-splitting/README.md similarity index 98% rename from example/README.md rename to examples/code-splitting/README.md index 71446670aff..d4073068bae 100644 --- a/example/README.md +++ b/examples/code-splitting/README.md @@ -114,7 +114,7 @@ webpackJsonp(1,{3:function(a,b,c){},4:function(a,b,c){}}) fileSizes: { 'output.js': 1948, '1.output.js': 200 } } ``` -## Compress (uglify-js, no zip) +## Minimized (uglify-js, no zip) ``` javascript { chunkCount: 2, diff --git a/examples/code-splitting/build.js b/examples/code-splitting/build.js new file mode 100644 index 00000000000..3e7fb9a3271 --- /dev/null +++ b/examples/code-splitting/build.js @@ -0,0 +1,14 @@ +var cp = require('child_process'); + +var argv = process.argv; +argv.shift(); +argv.shift(); +var extraArgs = argv.join(" "); + +cp.exec("node ../../bin/webpack.js "+extraArgs+" example.js js/output.js", function (error, stdout, stderr) { + console.log('stdout:\n' + stdout); + console.log('stderr:\n ' + stderr); + if (error !== null) { + console.log('error: ' + error); + } +}); diff --git a/example/example.js b/examples/code-splitting/example.js similarity index 100% rename from example/example.js rename to examples/code-splitting/example.js diff --git a/example/node_modules/a.js b/examples/code-splitting/node_modules/a.js similarity index 100% rename from example/node_modules/a.js rename to examples/code-splitting/node_modules/a.js diff --git a/example/node_modules/b.js b/examples/code-splitting/node_modules/b.js similarity index 100% rename from example/node_modules/b.js rename to examples/code-splitting/node_modules/b.js diff --git a/example/node_modules/c.js b/examples/code-splitting/node_modules/c.js similarity index 100% rename from example/node_modules/c.js rename to examples/code-splitting/node_modules/c.js diff --git a/example/node_modules/d.js b/examples/code-splitting/node_modules/d.js similarity index 100% rename from example/node_modules/d.js rename to examples/code-splitting/node_modules/d.js diff --git a/examples/require.context/README.md b/examples/require.context/README.md new file mode 100644 index 00000000000..952b788f524 --- /dev/null +++ b/examples/require.context/README.md @@ -0,0 +1,119 @@ +# example.js + +``` javascript +function getTemplate(templateName) { + return require("./templates/"+templateName); +} +console.log(getTemplate("a")); +console.log(getTemplate("b")); +``` + +# templates/ + +* a.js +* b.js +* c.js + +All templates are of this pattern: + +``` javascript +module.exports = function() { + return "This text was generated by template X"; +} +``` + +# js/output.js + +``` javascript +/******/(function(document, undefined) { +/******/ return function(modules) { +/******/ var installedModules = {}; +/******/ function require(moduleId) { +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId]; +/******/ var module = installedModules[moduleId] = { +/******/ exports: {} +/******/ }; +/******/ modules[moduleId](module, module.exports, require); +/******/ return module.exports; +/******/ } +/******/ require.ensure = function(chunkId, callback) { +/******/ callback(require); +/******/ }; +/******/ return require(0); +/******/ } +/******/})(document) +/******/({ +/******/0: function(module, exports, require) { + +function getTemplate(templateName) { + return require(1)("./"+templateName); +} +console.log(getTemplate("a")); +console.log(getTemplate("b")); + +/******/}, +/******/ +/******/1: function(module, exports, require) { + +/***/module.exports = function(name) { +/***/ var map = {"./b.js":3,"./c.js":4,"./a.js":2}; +/***/ return require(map[name]||map[name+".web.js"]||map[name+".js"]); +/***/}; + +/******/}, +/******/ +/******/2: function(module, exports, require) { + +module.exports = function() { + return "This text was generated by template A"; +} + +/******/}, +/******/ +/******/3: function(module, exports, require) { + +module.exports = function() { + return "This text was generated by template B"; +} + +/******/}, +/******/ +/******/4: function(module, exports, require) { + +module.exports = function() { + return "This text was generated by template C"; +} + +/******/}, +/******/ +/******/}) +``` + +# Info + +## Uncompressed + +``` javascript +{ chunkCount: 1, + modulesCount: 5, + modulesIncludingDuplicates: 5, + modulesPerChunk: 5, + modulesFirstChunk: 5, + fileSizes: { 'output.js': 1529 } } +``` + +## Minimized (uglify-js, no zip) + +``` javascript +{ chunkCount: 1, + modulesCount: 5, + modulesIncludingDuplicates: 5, + modulesPerChunk: 5, + modulesFirstChunk: 5, + fileSizes: { 'output.js': 674 } } +``` + +# Code Splitting + +See [this example combined with code splitting](code-splitted-require.context) diff --git a/examples/require.context/build.js b/examples/require.context/build.js new file mode 100644 index 00000000000..3e7fb9a3271 --- /dev/null +++ b/examples/require.context/build.js @@ -0,0 +1,14 @@ +var cp = require('child_process'); + +var argv = process.argv; +argv.shift(); +argv.shift(); +var extraArgs = argv.join(" "); + +cp.exec("node ../../bin/webpack.js "+extraArgs+" example.js js/output.js", function (error, stdout, stderr) { + console.log('stdout:\n' + stdout); + console.log('stderr:\n ' + stderr); + if (error !== null) { + console.log('error: ' + error); + } +}); diff --git a/examples/require.context/example.js b/examples/require.context/example.js new file mode 100644 index 00000000000..97d169d98ad --- /dev/null +++ b/examples/require.context/example.js @@ -0,0 +1,5 @@ +function getTemplate(templateName) { + return require("./templates/"+templateName); +} +console.log(getTemplate("a")); +console.log(getTemplate("b")); \ No newline at end of file diff --git a/examples/require.context/templates/a.js b/examples/require.context/templates/a.js new file mode 100644 index 00000000000..0fdaae50121 --- /dev/null +++ b/examples/require.context/templates/a.js @@ -0,0 +1,3 @@ +module.exports = function() { + return "This text was generated by template A"; +} \ No newline at end of file diff --git a/examples/require.context/templates/b.js b/examples/require.context/templates/b.js new file mode 100644 index 00000000000..cf33bd6fdfa --- /dev/null +++ b/examples/require.context/templates/b.js @@ -0,0 +1,3 @@ +module.exports = function() { + return "This text was generated by template B"; +} \ No newline at end of file diff --git a/examples/require.context/templates/c.js b/examples/require.context/templates/c.js new file mode 100644 index 00000000000..f1127812062 --- /dev/null +++ b/examples/require.context/templates/c.js @@ -0,0 +1,3 @@ +module.exports = function() { + return "This text was generated by template C"; +} \ No newline at end of file diff --git a/lib/buildDeps.js b/lib/buildDeps.js index 31c799ebda0..373367e18f5 100644 --- a/lib/buildDeps.js +++ b/lib/buildDeps.js @@ -45,7 +45,11 @@ module.exports = function buildDeps(context, mainModule, options, callback) { } function addModule(depTree, context, module, options, callback) { - resolve(context, module, options.resolve, function(err, filename) { + if(context) + resolve(context, module, options.resolve, resolved); + else + resolved(null, module); + function resolved(err, filename) { if(err) { callback(err); return; @@ -66,24 +70,35 @@ function addModule(depTree, context, module, options, callback) { var deps = parse(source); module.requires = deps.requires || []; module.asyncs = deps.asyncs || []; + module.contexts = deps.contexts || []; module.source = source; var requires = {}; + var contexts = []; 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); + if(module.contexts) + module.contexts.forEach(addContext(module)); if(module.asyncs) - module.asyncs.forEach(function addContext(c) { + module.asyncs.forEach(function addAsync(c) { if(c.requires) c.requires.forEach(add); if(c.asyncs) - c.asyncs.forEach(addContext); + c.asyncs.forEach(addAsync); + if(c.contexts) + c.contexts.forEach(addContext(c)); }); requiresNames = Object.keys(requires); - var count = requiresNames.length; + var count = requiresNames.length + contexts.length + 1; var errors = []; if(requiresNames.length) requiresNames.forEach(function(moduleName) { @@ -95,23 +110,123 @@ function addModule(depTree, context, module, options, callback) { requireItem.id = moduleId; }); } + endOne(); + }); + }); + if(contexts) { + contexts.forEach(function(contextObj) { + var context = contextObj.context; + var module = contextObj.module; + addContextModule(depTree, path.dirname(filename), context.name, options, 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(); + }) + }); + } + endOne(); + function endOne() { + count--; + if(count === 0) { + if(errors.length) { + callback(errors.join("\n")); + } else { + callback(null, module.id); + } + } + } + }); + } + } +} + +function addContextModule(depTree, context, contextModuleName, options, callback) { + resolve.context(context, contextModuleName, options.resolve, resolved); + function resolved(err, dirname) { + if(err) { + callback(err); + return; + } + if(depTree.modules[dirname]) { + callback(null, depTree.modules[dirname].id); + } else { + var contextModule = depTree.modules[dirname] = { + id: depTree.nextModuleId++, + requireMap: {}, + requires: [] + }; + depTree.modulesById[contextModule.id] = contextModule; + 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); + } count--; - if(count === 0) { - if(errors.length) { - callback(errors.join("\n")); + 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(); } else { - end(); + if(stat.isDirectory()) { + doDir(filename, moduleName + "/" + file, endOne); + } else { + addModule(depTree, null, filename, options, function(err, moduleId) { + if(err) { + endOne(err); + } else { + contextModule.requires.push({id: moduleId}); + contextModule.requireMap[moduleName + "/" + file] = moduleId; + endOne(); + } + }); + } } - } + }); }); - }); - else end() - function end() { - callback(null, module.id); + endOne(); + } + }); + } + doDir(dirname, ".", function(err) { + if(err) { + callback(err); + return; } + var extensionsAccess = []; + var extensions = (options.resolve && options.resolve.extensions) || [".web.js", ".js"]; + extensions.forEach(function(ext) { + extensionsAccess.push("||map[name+\""); + extensionsAccess.push(ext.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); }); } - }); + } } function addChunk(depTree, chunkStartpoint, options) { @@ -157,10 +272,10 @@ function addModuleToChunk(depTree, context, chunkId, options) { function removeParentsModules(depTree, chunk) { if(!chunk.parents) return; for(var moduleId in chunk.modules) { - var inParent = false; + var inParent = true; chunk.parents.forEach(function(parentId) { - if(depTree.chunks[parentId].modules[moduleId]) - inParent = true; + if(!depTree.chunks[parentId].modules[moduleId]) + inParent = false; }); if(inParent) { chunk.modules[moduleId] = "in-parent"; diff --git a/lib/parse.js b/lib/parse.js index 463620d7d73..9c25ceda339 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -161,17 +161,40 @@ function walkExpression(context, expression) { break; case "CallExpression": if(expression.callee && expression.arguments && - expression.arguments.length >= 1 && + expression.arguments.length == 1 && expression.callee.type === "Identifier" && expression.callee.name === "require") { - var param = parseString(expression.arguments[0]); - context.requires = context.requires || []; - context.requires.push({ - name: param, - nameRange: expression.arguments[0].range, - line: expression.loc.start.line, - column: expression.loc.start.column - }); + var param = parseCalculatedString(expression.arguments[0]); + if(param.code) { + // make context + var pos = param.value.indexOf("/"); + if(pos === -1) { + throw new Error("require a module by variable is not supported"); + } else { + var match = /\/[^\/]*$/.exec(param.value); + var dirname = param.value.substring(0, match.index); + var remainder = "." + param.value.substring(match.index); + context.contexts = context.contexts || []; + var newContext = { + name: dirname, + require: true, + replace: [param.range, remainder], + calleeRange: expression.callee.range, + line: expression.loc.start.line, + column: expression.loc.start.column + }; + context.contexts.push(newContext); + } + } else { + // normal require + context.requires = context.requires || []; + context.requires.push({ + name: param.value, + nameRange: param.range, + line: expression.loc.start.line, + column: expression.loc.start.column + }); + } } if(expression.callee && expression.arguments && expression.arguments.length >= 1 && @@ -194,6 +217,23 @@ function walkExpression(context, expression) { context.asyncs.push(newContext); context = newContext; } + if(expression.callee && expression.arguments && + expression.arguments.length == 1 && + expression.callee.type === "MemberExpression" && + expression.callee.object.type === "Identifier" && + expression.callee.object.name === "require" && + expression.callee.property.type === "Identifier" && + expression.callee.property.name in {context:1}) { + var param = parseString(expression.arguments[0]); + context.contexts = context.contexts || []; + var newContext = { + name: param, + expressionRange: [expression.callee.range[0], expression.range[1]], + line: expression.loc.start.line, + column: expression.loc.start.column, + }; + context.contexts.push(newContext); + } if(expression.callee) walkExpression(context, expression.callee); @@ -211,14 +251,37 @@ function walkExpression(context, expression) { function parseString(expression) { switch(expression.type) { case "BinaryExpression": - return parseString(expression.left) + parseString(expression.right); + if(expression.operator == "+") + return parseString(expression.left) + parseString(expression.right); + break; case "Literal": - if(typeof expression.value === "string") - return expression.value; + return expression.value+""; } throw new Error(expression.type + " is not supported as parameter for require"); } +function parseCalculatedString(expression) { + switch(expression.type) { + case "BinaryExpression": + if(expression.operator == "+") { + var left = parseCalculatedString(expression.left); + var right = parseCalculatedString(expression.right); + if(left.code) { + return {range: left.range, value: left.value, code: true}; + } else if(right.code) { + return {range: [left.range[0], right.range ? right.range[1] : left.range[1]], value: left.value + right.value, code: true}; + } else { + return {range: [left.range[0], right.range[1]], value: left.value + right.value}; + } + } + break; + case "Literal": + return {range: expression.range, value: expression.value+""}; + break; + } + return {value: "", code: true}; +} + function parseStringArray(expression) { switch(expression.type) { case "ArrayExpression": @@ -233,7 +296,7 @@ function parseStringArray(expression) { } module.exports = function parse(source, options) { - var ast = esprima.parse(source, {range: true, loc: true}); + var ast = esprima.parse(source, {range: true, loc: true, raw: true}); if(!ast || typeof ast != "object") throw new Error("Source couldn't be parsed"); var context = {}; diff --git a/lib/resolve.js b/lib/resolve.js index 4290a078835..eb9338a9387 100644 --- a/lib/resolve.js +++ b/lib/resolve.js @@ -44,6 +44,42 @@ module.exports = function resolve(context, identifier, options, callback) { } } +module.exports.context = function(context, identifier, options, callback) { + if(!callback) { + callback = options; + options = {}; + } + if(!options) + options = {}; + if(!options.paths) + options.paths = []; + function finalResult(err, absoluteFilename) { + if(err) { + callback("Context \"" + identifier + "\" not found in context \"" + context + "\""); + return; + } + callback(null, absoluteFilename); + } + var identArray = identifier.split("/"); + var contextArray = split(context); + if(identArray[0] === "." || identArray[0] === ".." || identArray[0] === "") { + var pathname = join(contextArray, identArray); + fs.stat(pathname, function(err, stat) { + if(err) { + finalResult(err); + return; + } + if(!stat.isDirectory()) { + finalResult("Context \"" + identifier + "\" in not a directory"); + return; + } + callback(null, pathname); + }); + } else { + loadNodeModulesAsContext(contextArray, identArray, options, finalResult); + } +} + function split(a) { return a.split(/[\/\\]/g); } @@ -114,6 +150,26 @@ function loadNodeModules(context, identifier, options, callback) { }); } +function loadNodeModulesAsContext(context, identifier, options, callback) { + nodeModulesPaths(context, options, function(err, dirs) { + function tryDir(dir) { + var pathname = join(split(dir), identifier); + fs.stat(pathname, function(err, stat) { + if(err || !stat.isDirectory()) { + if(dirs.length === 0) { + callback(true); + return; + } + tryDir(dirs.shift()); + return; + } + callback(null, pathname); + }); + } + tryDir(dirs.shift()); + }); +} + function nodeModulesPaths(context, options, callback) { var parts = context; var rootNodeModules = parts.indexOf("node_modules"); diff --git a/lib/writeSource.js b/lib/writeSource.js index f3d9fd20e74..c3133333b00 100644 --- a/lib/writeSource.js +++ b/lib/writeSource.js @@ -1,3 +1,7 @@ +function stringify(str) { + return '"' + str.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + '"'; +} + module.exports = function(module) { var replaces = []; // { from: 123, to: 125, value: "4" } function genReplaceRequire(requireItem) { @@ -9,9 +13,33 @@ module.exports = function(module) { }); } } + function genContextReplaces(contextItem) { + var postfix = ""; + if(contextItem.require) { + replaces.push({ + from: contextItem.calleeRange[0], + to: contextItem.calleeRange[1], + value: "require(" + ((contextItem.id || "throw new Error('there is not id for this')") + "") + ")" + }); + replaces.push({ + from: contextItem.replace[0][0], + to: contextItem.replace[0][1], + value: stringify(contextItem.replace[1]) + }); + } else { + replaces.push({ + from: contextItem.expressionRange[0], + to: contextItem.expressionRange[1], + value: "require(" + ((contextItem.id || "throw new Error('there is not id for this')") + "") + ")" + postfix + }); + } + } if(module.requires) { module.requires.forEach(genReplaceRequire); } + if(module.contexts) { + module.contexts.forEach(genContextReplaces); + } if(module.asyncs) { module.asyncs.forEach(function genReplacesAsync(asyncItem) { if(asyncItem.requires) { @@ -20,6 +48,9 @@ module.exports = function(module) { if(asyncItem.asyncs) { asyncItem.asyncs.forEach(genReplacesAsync); } + if(asyncItem.contexts) { + asyncItem.contexts.forEach(genContextReplaces); + } if(asyncItem.namesRange) { replaces.push({ from: asyncItem.namesRange[0], diff --git a/test/browsertest/lib/index.web.js b/test/browsertest/lib/index.web.js index ac6e9935723..5abe4256661 100644 --- a/test/browsertest/lib/index.web.js +++ b/test/browsertest/lib/index.web.js @@ -16,6 +16,15 @@ window.test(require("./singluar.js").value === 1, "sigular module loaded"); require("./singluar.js").value = 2; window.test(require("./singluar").value === 2, "exported object is singluar"); window.test(require("subfilemodule") === "subfilemodule", "Modules as single file should load"); +window.test(require.context("../templates")("./tmpl") === "test template", "Context should work"); +window.test(require.context("../templates")("./subdir/tmpl.js") === "subdir test template", "Context should work with subdirectories"); +var template = "tmpl"; +window.test(require("../templates/" + template) === "test template", "Automatical context should work"); + +require.ensure([], function(require) { + var contextRequire = require.context("."); + window.test(contextRequire("./singluar").value === 2, "Context works in chunk"); +}); require.ensure([], function(require) { require("./acircular"); diff --git a/test/browsertest/templates/subdir/tmpl.js b/test/browsertest/templates/subdir/tmpl.js new file mode 100644 index 00000000000..554e46c1cc8 --- /dev/null +++ b/test/browsertest/templates/subdir/tmpl.js @@ -0,0 +1 @@ +module.exports = "subdir test template"; \ No newline at end of file diff --git a/test/browsertest/templates/tmpl.js b/test/browsertest/templates/tmpl.js new file mode 100644 index 00000000000..382b914f846 --- /dev/null +++ b/test/browsertest/templates/tmpl.js @@ -0,0 +1 @@ +module.exports = "test template"; \ No newline at end of file