├── deputy.json ├── state.json ├── examples ├── npm │ ├── node_modules │ │ └── index │ ├── test.html │ ├── app │ │ └── app.js │ └── build.js ├── advanced │ ├── data.json │ ├── shared_code │ │ ├── calc.coffee │ │ └── validation.coffee │ ├── outputlibs.js │ ├── libraries │ │ └── monolith.js │ ├── app_code │ │ ├── bigthing │ │ │ ├── sub2.coffee │ │ │ └── sub1.coffee │ │ ├── helper.coffee │ │ └── main.coffee │ ├── dependencies.txt │ ├── test.html │ ├── build.js │ └── output.js ├── minimal │ ├── shared │ │ └── index.js │ ├── app │ │ └── app.js │ ├── test.html │ ├── build.js │ └── output.js ├── simple │ ├── app │ │ ├── utils │ │ │ └── validation.js │ │ ├── controllers │ │ │ └── users.js │ │ ├── app.js │ │ └── models │ │ │ └── user.js │ ├── test.html │ ├── build.js │ └── output.js └── Readme.md ├── test ├── empty.html ├── output │ └── debug.html ├── lib │ ├── brain.js │ └── dirify.js ├── arbiters.js ├── clash.js ├── persist.js ├── plugins.js └── cli.js ├── .travis.yml ├── .gitignore ├── .npmignore ├── lib ├── plugins │ ├── testcutter.js │ └── minifier.js ├── utils.js ├── persist.js ├── require.js ├── analyzer.js ├── bundler.js ├── resolver.js └── api.js ├── docs ├── Readme.md ├── npm.md ├── cli.md ├── plugins.md ├── require.md ├── tinyapi.md ├── xcjs.md └── modularity.md ├── index.js ├── LICENSE ├── package.json ├── builtins ├── path.posix.js └── events.js ├── bin └── cli.js ├── Readme.md └── History.md /deputy.json: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /state.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/npm/node_modules/index: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/advanced/data.json: -------------------------------------------------------------------------------- 1 | {"hi": "there"} 2 | -------------------------------------------------------------------------------- /test/empty.html: -------------------------------------------------------------------------------- 1 | hi 2 | -------------------------------------------------------------------------------- /examples/minimal/shared/index.js: -------------------------------------------------------------------------------- 1 | module.exports = "shared code"; 2 | -------------------------------------------------------------------------------- /examples/minimal/app/app.js: -------------------------------------------------------------------------------- 1 | var shared = require('shared::'); 2 | alert(shared); 3 | -------------------------------------------------------------------------------- /examples/advanced/shared_code/calc.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | divides : (d, n) -> !(d%n) 3 | -------------------------------------------------------------------------------- /test/output/debug.html: -------------------------------------------------------------------------------- 1 | hi 2 | -------------------------------------------------------------------------------- /examples/advanced/outputlibs.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | window.monolith = "I am a huge library"; 3 | })(); 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - 0.6 5 | 6 | notifications: 7 | email: true 8 | -------------------------------------------------------------------------------- /examples/advanced/libraries/monolith.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | window.monolith = "I am a huge library"; 3 | })(); 4 | -------------------------------------------------------------------------------- /examples/simple/app/utils/validation.js: -------------------------------------------------------------------------------- 1 | exports.nameOk = function(name){ 2 | return (name != 'jill'); 3 | }; 4 | -------------------------------------------------------------------------------- /examples/advanced/app_code/bigthing/sub2.coffee: -------------------------------------------------------------------------------- 1 | # here we export a function 2 | 3 | module.exports = (str) -> 4 | console.log(str) 5 | -------------------------------------------------------------------------------- /examples/advanced/dependencies.txt: -------------------------------------------------------------------------------- 1 | app::main 2 | ├───app::helper 3 | ├──┬app::bigthing/sub1 4 | │ └───app::bigthing/sub2 5 | ├──┬shared::validation 6 | │ └───shared::calc 7 | └───M8::monolith -------------------------------------------------------------------------------- /examples/advanced/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | see console 8 | -------------------------------------------------------------------------------- /examples/simple/app/controllers/users.js: -------------------------------------------------------------------------------- 1 | var User = require('models/user'); 2 | 3 | var Users = { 4 | init : function(){ 5 | return User.fetch(); 6 | } 7 | }; 8 | 9 | module.exports = Users; 10 | -------------------------------------------------------------------------------- /examples/minimal/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

output

9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /examples/npm/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

output

9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /examples/advanced/shared_code/validation.coffee: -------------------------------------------------------------------------------- 1 | {divides} = require('./calc') # CoffeeScript destructuring assignment 2 | 3 | 4 | exports.isLeapYear = (yr) -> 5 | divides(yr,4) and (!divides(yr,100) or divides(yr,400)) 6 | 7 | -------------------------------------------------------------------------------- /examples/simple/app/app.js: -------------------------------------------------------------------------------- 1 | var Users = require('controllers/users'); 2 | var $ = require('jQuery'); 3 | 4 | var App = { 5 | init: function(){ 6 | $('#output').text( JSON.stringify(Users.init()) ); 7 | } 8 | }.init(); 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | gh-pages/ 3 | test/input/ 4 | test/clash/ 5 | test/cli/ 6 | test/persist/ 7 | test/arbiters/ 8 | test/plugins/ 9 | examples/simple/cliout.js 10 | test/output/*.js 11 | examples/npm/node_modules/backbone/ 12 | -------------------------------------------------------------------------------- /examples/minimal/build.js: -------------------------------------------------------------------------------- 1 | var modul8 = require('../../'); 2 | 3 | modul8('./app/app.js') 4 | .domains({shared: './shared/'}) 5 | .compile('./output.js'); 6 | 7 | // alternatively use the CLI: 8 | // $ modul8 app/app.js -p shared=shared/ > output.js 9 | -------------------------------------------------------------------------------- /examples/advanced/app_code/bigthing/sub1.coffee: -------------------------------------------------------------------------------- 1 | sub2 = require('./sub2') # relative require 2 | 3 | # we export a doComplex property for main 4 | exports.doComplex = (str) -> # sub1 is an arbiter for sub2 5 | sub2(str+' (sub1 added this, passing to sub2)') 6 | -------------------------------------------------------------------------------- /examples/advanced/app_code/helper.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (str) -> 2 | console.log str 3 | 4 | 5 | if module is require.main 6 | testRunner = require('testmodule') # even though this is required it wont be pulled in 7 | testRunner.assertEqual(2,2, "duh") 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | gh-pages/ 3 | docs/Readme.md 4 | test/input/ 5 | test/collisions/ 6 | test/modified/ 7 | test/cli/ 8 | test/arbiters/ 9 | test/plugins/ 10 | test/output/*.js 11 | examples/simple/cliout.js 12 | examples/npm/node_modules/backbone/ 13 | -------------------------------------------------------------------------------- /lib/plugins/testcutter.js: -------------------------------------------------------------------------------- 1 | // example 'before' function 2 | // coarsely strips inlined tests and test dependencies from code before analysis 3 | 4 | module.exports = function(code) { 5 | return code.replace(/\n.*require.main[\w\W]*$/, ''); //TODO: improve this 6 | }; 7 | -------------------------------------------------------------------------------- /examples/simple/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

output

10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /docs/Readme.md: -------------------------------------------------------------------------------- 1 | ## Documentation 2 | 3 | These files are the raw, most recently updated documentation files (if viewing on github). 4 | 5 | The auto-generated: [clux.github.com/modul8](http://clux.github.com/modul8/) 6 | contains a snapshot of this directory at the last released version. 7 | -------------------------------------------------------------------------------- /examples/npm/app/app.js: -------------------------------------------------------------------------------- 1 | var backbone = require('npm::backbone') 2 | , _ = require('npm::underscore') 3 | , events = require('npm::events'); 4 | 5 | alert('found backbone ' + backbone.VERSION + ', and underscore: ' + _.VERSION + ', and EventEmitter? ' + !!events.EventEmitter); 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/npm/build.js: -------------------------------------------------------------------------------- 1 | var modul8 = require('../../'); 2 | 3 | modul8('./app/app.js') 4 | .analysis(console.log) 5 | .npm('./node_modules') 6 | .compile('./output.js'); 7 | 8 | // requires npm install backbone in this folder first 9 | 10 | // alternatively use the CLI: 11 | // $ modul8 app/app.js > output.js 12 | -------------------------------------------------------------------------------- /examples/simple/build.js: -------------------------------------------------------------------------------- 1 | var modul8 = require('../../'); 2 | 3 | modul8('./app/app.js') 4 | .arbiters({'jQuery':['jQuery','$']}) 5 | .analysis(console.log) 6 | .set('domloader', 'jQuery') 7 | .compile('./output.js'); 8 | 9 | // alternatively use the CLI: 10 | // $ modul8 app/app.js -a jQuery=jQuery,$ -w jQuery > output.js 11 | -------------------------------------------------------------------------------- /lib/plugins/minifier.js: -------------------------------------------------------------------------------- 1 | // example 'after' function 2 | // minifies javascript using UglifyJS 3 | 4 | var u = require('uglify-js') 5 | , uglify = u.uglify 6 | , parser = u.parser; 7 | 8 | module.exports = function(code) { 9 | return uglify.gen_code(uglify.ast_squeeze(uglify.ast_mangle(parser.parse(code)))); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/simple/app/models/user.js: -------------------------------------------------------------------------------- 1 | var validation = require('utils/validation.js'); 2 | 3 | var User = { 4 | records : ['jack', 'jill'], 5 | 6 | fetch : function(){ 7 | return this.records.filter(this.validate); 8 | }, 9 | 10 | validate : function(user) { 11 | return validation.nameOk(user); 12 | } 13 | }; 14 | 15 | module.exports = User; 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , path = require('path') 3 | , join = require('path').join; 4 | 5 | fs.existsSync || (fs.existsSync = path.existsSync); 6 | 7 | var modul8 = require('./lib/api'); 8 | modul8.minifier = require('./lib/plugins/minifier'); 9 | modul8.testcutter = require('./lib/plugins/testcutter'); 10 | modul8.version = require('./package').version; 11 | 12 | module.exports = modul8; 13 | -------------------------------------------------------------------------------- /examples/advanced/app_code/main.coffee: -------------------------------------------------------------------------------- 1 | helper = require('./helper') 2 | # relative require looks only in this domain 3 | 4 | helper('hello from app via helper') 5 | 6 | b = require('bigthing/sub1') 7 | 8 | b.doComplex('app calls up to sub1') 9 | 10 | 11 | v = require('validation.coffee') 12 | # wont be found on clients require path 13 | # but will be found on the shared path 14 | 15 | console.log('2004 isLeapYear?', v.isLeapYear(2004)) 16 | 17 | 18 | #!!window.monolith; # -> false 19 | #monolith uses an arbiter so only require can access it 20 | m = require('monolith') 21 | console.log("monolith:"+m) 22 | 23 | 24 | #injected data 25 | test = require('data::test') 26 | console.log 'injected data:', test 27 | -------------------------------------------------------------------------------- /test/lib/brain.js: -------------------------------------------------------------------------------- 1 | // brain - zombie tap test helper 2 | var zombie = require('zombie') 3 | , singles = ['ok', 'isDefined', 'isUndefined'] 4 | , doubles = ['eql', 'equal', 'deepEqual', 'type']; 5 | 6 | function Brain (t) { 7 | this.t = t; 8 | this.browser = new zombie.Browser(); 9 | } 10 | singles.forEach(function (s) { 11 | Brain.prototype[s] = function (statement, msg) { 12 | return this.t[s](this.browser.evaluate(statement), msg); 13 | }; 14 | }); 15 | 16 | doubles.forEach(function (d) { 17 | Brain.prototype[d] = function (statement, expected, msg) { 18 | return this.t[d](this.browser.evaluate(statement), expected, msg); 19 | }; 20 | }); 21 | 22 | // evaluate hook 23 | Brain.prototype.do = function (statement) { 24 | return this.browser.evaluate(statement); 25 | }; 26 | 27 | function factory (t) { 28 | return new Brain(t); 29 | } 30 | 31 | module.exports = factory; 32 | -------------------------------------------------------------------------------- /test/lib/dirify.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , mkdirp = require('mkdirp').sync 3 | , rimraf = require('rimraf').sync 4 | , path = require('path') 5 | , join = path.join 6 | , type = require('typr'); 7 | 8 | /** 9 | * generate directory trees and files from an object recursively 10 | * an object as a value means key should be a directory 11 | * a string as a value means key should be a file 12 | */ 13 | module.exports = function (name, obj) { 14 | var root = join(__dirname, '..', name); 15 | try { 16 | rimraf(root); 17 | } catch (e) {} 18 | 19 | var mk = function (o, pos) { 20 | Object.keys(o).forEach(function (k) { 21 | var newPos = join(pos, k); 22 | if (type.isObject(o[k])) { 23 | mkdirp(newPos, '0755'); 24 | mk(o[k], newPos); 25 | } 26 | else if (type.isString(o[k])) { 27 | fs.writeFileSync(newPos, o[k]); 28 | } 29 | }); 30 | }; 31 | mk(obj, root); 32 | }; 33 | -------------------------------------------------------------------------------- /examples/advanced/build.js: -------------------------------------------------------------------------------- 1 | var modul8 = require('../../') 2 | , coffee = require('coffee-script') 3 | , fs = require('fs'); 4 | 5 | 6 | modul8('./app_code/main.coffee') 7 | .before(modul8.testcutter) 8 | .libraries() 9 | .list(['monolith.js']) 10 | .path('./libraries/') 11 | .target('./outputlibs.js') 12 | .arbiters() 13 | .add('monolith') 14 | .domains() 15 | .add('shared', './shared_code/') 16 | .analysis() 17 | .output(console.log) 18 | .prefix(false) 19 | .data() 20 | .add('test', fs.readFileSync('./data.json', 'utf8')) 21 | .register('.coffee', function (code, bare) { 22 | return coffee.compile(code, {bare: bare}); 23 | }) 24 | .set('namespace', 'QQ') 25 | .set('force', true) 26 | .compile('./output.js'); 27 | 28 | // Alternatively this CLI command: 29 | // $ modul8 app_code/main.coffee -p shared=shared_code/ -a monolith -tn QQ -d test=data.json > output.js 30 | // would cover everything except coffee-script (if you want that you have to use a scripted file) 31 | 32 | // and same call with replacing '> output.js' with '-z' to get the analysis 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 Eirik Albrigtsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modul8", 3 | "description": "Extensible CommonJS Code Packager and Analyzer", 4 | "version": "0.17.2", 5 | "author": "Eirik Albrigtsen ", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/clux/modul8.git" 9 | }, 10 | "main": "index.js", 11 | "bin": { 12 | "modul8": "./bin/cli.js" 13 | }, 14 | "scripts": { 15 | "test": "tap ./test/*.js" 16 | }, 17 | "dependencies": { 18 | "commander": "~0.6.1", 19 | "uglify-js": "~1.3.1", 20 | "deputy": "~0.0.2", 21 | "logule": "~0.8.1", 22 | "deep-equal": "~0.0.0", 23 | "typr": "~0.1.2", 24 | "topiary": "~0.0.1", 25 | "autonomy": "0.1.0" 26 | }, 27 | "devDependencies": { 28 | "zombie": "~0.12.1", 29 | "tap": "~0.2.5", 30 | "rimraf": "~2.0.2", 31 | "mkdirp": "~0.3.3" 32 | }, 33 | "keywords": [ 34 | "browser", 35 | "require", 36 | "commonjs", 37 | "bundle", 38 | "compiler", 39 | "analyzer", 40 | "javascript", 41 | "cli" 42 | ], 43 | "engines": { 44 | "node": ">=0.6.0" 45 | }, 46 | "bugs": { 47 | "url": "http://github.com/clux/modul8/issues" 48 | }, 49 | "license": "MIT", 50 | "optionalDependencies": {} 51 | } 52 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | , fs = require('fs') 3 | , logule = require('logule').sub('modul8').suppress('debug') 4 | , pkg = require('../package') 5 | , projName = ""; 6 | 7 | exports.logule = logule; 8 | 9 | exports.updateLogger = function (sub) { 10 | if (!logule.verify(sub)) { 11 | exports.error("got an invalid logule instance sent to logger - out of date?"); 12 | } 13 | logule = sub; 14 | exports.logule = sub; 15 | }; 16 | 17 | exports.updateProject = function (name) { 18 | projName = name; 19 | }; 20 | 21 | 22 | // internal error shortcut 23 | // prepends a line to the stacktrace, so look at the previous one 24 | exports.error = function (msg) { 25 | var error = []; 26 | error.push("compile()"); 27 | if (projName) { 28 | error.push("from '" + projName + "'"); 29 | } 30 | error.push("failed for the following reason:"); 31 | logule 32 | .error(error.join(' ')) 33 | .error("") 34 | .error(msg) 35 | .error("") 36 | .error("If you feel this is a problem with " + pkg.name + ", please attach this output to") 37 | .error(pkg.bugs.url); 38 | throw new Error(msg); 39 | }; 40 | 41 | // shortcut because it is used so much 42 | exports.read = function (name) { 43 | return fs.readFileSync(name, 'utf8'); 44 | }; 45 | 46 | exports.makeCompiler = function (external) { 47 | if (!external) { 48 | external = {}; 49 | } 50 | 51 | return function (file, bare) { 52 | if (bare === undefined) { 53 | bare = true; 54 | } 55 | var ext = path.extname(file) 56 | , raw = exports.read(file); 57 | 58 | if (ext === '.js') { 59 | return raw; 60 | } 61 | 62 | var compiler = external[ext]; 63 | if (compiler && compiler instanceof Function) { 64 | return compiler(raw, bare); 65 | } 66 | exports.error('cannot compile ' + file + ' - no compiler registered for this extension'); 67 | }; 68 | }; 69 | 70 | exports.exists = function (file) { 71 | return fs.existsSync(file) && !fs.statSync(file).isDirectory(); 72 | }; 73 | -------------------------------------------------------------------------------- /test/arbiters.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , join = require('path').join 3 | , test = require('tap').test 4 | , modul8 = require('../') 5 | , dirify = require('./lib/dirify') 6 | , log = require('logule').sub('ARBITER') 7 | , utils = require('../lib/utils'); 8 | 9 | var root = join(__dirname, 'arbiters') 10 | , output = join(__dirname, 'output') 11 | , compile = utils.makeCompiler(); 12 | 13 | var out = { 14 | app : join(output, 'arbiters.js') 15 | , libs : join(output, 'arbiterslibs.js') 16 | }; 17 | 18 | function setup () { 19 | dirify('arbiters', { 20 | app : { 21 | 'r.js' : "module.exports = 'app';" // same name as lib arbiter to verify most work 22 | , 'entry.js' : "require('./r');" 23 | } 24 | , libs : { 25 | 'lib.js' : "(function(){window.libVar = 'lib';}());" 26 | } 27 | }); 28 | 29 | //log.trace('compiling'); 30 | modul8(join(root, 'app', 'entry.js')) 31 | .logger(log.sub().suppress('info', 'warn', 'debug')) 32 | //.analysis(console.log) 33 | .arbiters({'r': 'libVar'}) 34 | .libraries() 35 | .list(['lib.js']) 36 | .path(join(root, 'libs')) 37 | .target(out.libs) 38 | .compile(out.app); 39 | } 40 | 41 | 42 | test("arbiters", function (t) { 43 | setup(); 44 | var brain = require('./lib/brain')(t) 45 | 46 | var libCode = compile(out.libs) 47 | , appCode = compile(out.app); 48 | 49 | brain.do(libCode); 50 | brain.ok("window.libVar === 'lib'", "global libVar exists before arbiters kick in"); 51 | 52 | brain.do(appCode); 53 | brain.ok("window.M8 !== undefined", "global namespace is defined"); 54 | // everything evaluated now 55 | 56 | brain.equal("M8.require('app::r.js')", "app", "can require app::r.js when 'r' is an arbiter key"); 57 | brain.equal("M8.require('./r.js')", "app", "can require ./r.js when 'r' is an arbiter key"); 58 | 59 | brain.equal("M8.require('M8::r')", "lib", "can require arbitered libVar through M8::"); 60 | brain.equal("M8.require('r')", "lib", "can require arbitered libVar globally even when ./r exists"); 61 | brain.ok("window.libVar === undefined", "window.libVar has been deleted"); 62 | 63 | t.end(); 64 | }); 65 | 66 | -------------------------------------------------------------------------------- /docs/npm.md: -------------------------------------------------------------------------------- 1 | # npm support 2 | 3 | ### Usage 4 | Set the node modules directory using the `.npm()` command as follows. 5 | 6 | modul8('./app/client/app.js') 7 | .npm('./node_modules') 8 | .compile('./out.js'); 9 | 10 | ### Compatibility 11 | Getting node modules to work on the client requires these modules to be not server reliant. 12 | modul8 goes a long way trying to integrate common node modules (like path), but not everything is going to work. 13 | If you rely on fs file IO, for instance, things will not work. 14 | 15 | ### Requirability 16 | Everything in the npm folder specified can be required, but it is optimized to obtain modules in the root. 17 | Modules are required like on the server, but you have to specify the npm domain to avoid accidentally pulling in big files when requiring from the main domain. 18 | 19 | E.g. `require('npm::backbone')` would pull in underscore, but `require('npm::underscore')` would not necessarily work. 20 | 21 | `require('npm::backbone')` with backbone installed, would pull its underscore dependency from either backbone/node_modules/underscore, or underscore. 22 | For underscore to also be easily requirable (and to prevent pulling in copies or different versions), 23 | you should install dependencies like underscore in root, before installing backbone. 24 | 25 | In other words, if you want a module's dependencies as well, try to make `npm list` look like this: 26 | 27 | app 28 | ├───backbone 29 | └───underscore 30 | 31 | rather than this: 32 | 33 | app 34 | └──┬backbone 35 | └───underscore 36 | 37 | ## Builtins 38 | 39 | Certain core node modules are conditionally included if they are required. 40 | This is to allow npm modules that only use these core modules in a browser compatible way to work on the client. 41 | 42 | Builtins currently include: 43 | 44 | - path 45 | - events 46 | 47 | Node modules can require buitins as normal, and the app domain can require them from the npm domain directly, e.g. `require('npm::path')`. 48 | 49 | Note that path is always included, as it is used ensure the require algorithm used is identical on the server and the browser. 50 | Therefore, requiring it comes at no extra size cost. 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/Readme.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Contains 3 examples of varying complexity. 4 | ## Format 5 | Each directory contains a build script with both CLI code and standard js code included. 6 | Running `build.js` via `node` (or its included cli command) will generate `output.js`. 7 | Browser behaviour can be tested by loading `test.html` in the browser (includes `output.js`). 8 | 9 | ### Minimal Example 10 | Simply analyses from an entry point and loads resources from a `shared` domain. 11 | If it works, what is required from the `shared` domain will be sent to `alert`. 12 | 13 | app::app 14 | └───shared:: 15 | 16 | ### npm Example 17 | Showing basic npm integration with backbone and its underscore dependency. 18 | 19 | app::app 20 | ├──┬npm::backbone 21 | │ └───npm::underscore 22 | ├───npm::underscore 23 | └───npm::events 24 | 25 | Note that there are two different versions of underscore present. As per npm priority, backbone will first look for its local copy first. 26 | If underscore were to be uninstalled from backbone, then the outer one would be resolved. 27 | 28 | This shows that multiple versions can coexist with modul8. 29 | 30 | ### Simple Example 31 | jQuery integration using arbiters for the global variable and waits for the DOM with the `jQuery()` function. 32 | It also contains a sample MVC application structure. That it funnels some data through. 33 | 34 | app::app 35 | ├──┬app::controllers/users 36 | │ └──┬app::models/user 37 | │ └───app::utils/validation 38 | └───M8::jQuery 39 | 40 | If it works, data is passed from model/user via validation down to app::app, and to prove that the domloader works with arbiters, 41 | it outputs the result in a dom element. 42 | 43 | ### Advanced Example 44 | Uses a bunch of stuff. Adds in a custom library, arbiters it. Loads in data from a file attaches it to the data domain. 45 | Changes the default namespace, and does not wait for the DOM. Also cuts out tests from files (in particular dependencies from app::helper does not get included.) 46 | 47 | app::main 48 | ├───app::helper 49 | ├──┬app::bigthing/sub1 50 | │ └───app::bigthing/sub2 51 | ├──┬shared::validation 52 | │ └───shared::calc 53 | └───M8::monolith 54 | 55 | If it works, a bunch of stuff will be logged to console, and no errors will show up. 56 | -------------------------------------------------------------------------------- /lib/persist.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , path = require('path') 3 | , crypto = require('crypto') 4 | , type = require('typr') 5 | , eql = require('deep-equal') 6 | , noop = function () {}; 7 | 8 | function makeGuid(vals) { 9 | var str = vals.concat(fs.realpathSync()).map(function (v) { 10 | return v + ''; 11 | }).join('_'); 12 | return crypto.createHash('md5').update(str).digest("hex"); 13 | } 14 | 15 | /** 16 | * Persist class 17 | * 18 | * @[in] file - path to where persist data is stored 19 | * @[in] keys - list of strings to scramble together with script load path as an internal guid 20 | * @[in] log - log function (such as logule.get('debug') || console.log) for printing 21 | */ 22 | function Persist(cacheFile, keys, log) { 23 | var pdata = (cacheFile) ? JSON.parse(fs.readFileSync(cacheFile, 'utf8')) : {} 24 | , guid = makeGuid(keys); 25 | 26 | pdata[guid] = pdata[guid] || {}; 27 | 28 | // this is the only member that needs access to the full object and the file 29 | this.save = function () { 30 | if (cacheFile) { 31 | fs.writeFileSync(cacheFile, JSON.stringify(pdata)); 32 | } 33 | }; 34 | 35 | // members need only know their config object 36 | this.cfg = pdata[guid]; 37 | this.cfg.opts = this.cfg.opts || {}; 38 | 39 | // was a logger passed down? 40 | this.log = (type.isFunction(log)) ? log : noop; 41 | } 42 | 43 | Persist.prototype.filesModified = function (fileList, doms, type) { 44 | var mTimesTracked = this.cfg[type] || {} 45 | , mTimesNew = {} 46 | , filesTracked = Object.keys(mTimesTracked) 47 | , that = this 48 | , i = 0 49 | , f; 50 | 51 | fileList.forEach(function (pair) { 52 | var d = pair[0] 53 | , f = pair[1]; 54 | mTimesNew[d + '::' + f] = fs.statSync(path.join(doms[d], f)).mtime.valueOf(); 55 | }); 56 | var filesNew = Object.keys(mTimesNew); 57 | 58 | this.cfg[type] = mTimesNew; 59 | this.save(); 60 | if (eql(mTimesTracked, {}) && !eql(mTimesNew, {})) { 61 | this.log("initializing " + type); 62 | return true; 63 | } 64 | 65 | for (i = 0; i < filesNew.length; i += 1) { 66 | f = filesNew[i]; // f is a key of mTimesNew => a uid 67 | var m = mTimesNew[f]; // m is a the value mTimesNew[f] => a mTime 68 | 69 | if (filesTracked.indexOf(f) < 0) { 70 | that.log("files added to " + type); 71 | return true; 72 | } 73 | if (mTimesTracked[f] !== m) { 74 | that.log("files updated in " + type); 75 | return true; 76 | } 77 | } 78 | for (i = 0; i < filesTracked.length; i += 1) { 79 | f = filesTracked[i]; // f is a key of mTimes 80 | if (filesNew.indexOf(f) < 0) { 81 | that.log("files removed from " + type); 82 | return true; 83 | } 84 | } 85 | 86 | return false; 87 | }; 88 | 89 | Persist.prototype.objectModified = function (o) { 90 | if (eql(this.cfg.opts, JSON.parse(JSON.stringify(o)))) { 91 | return false; 92 | } 93 | this.cfg.opts = o; 94 | this.save(); 95 | return true; 96 | }; 97 | 98 | 99 | module.exports = function (a, b, c) { 100 | return new Persist(a, b, c); 101 | }; 102 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | # Command Line Interface 2 | 3 | modul8 defines a command line interface when installed globally with npm, i.e. 4 | 5 | $ npm install -g modul8 6 | 7 | This tool tries to expose the core functionality in as minimal way as possible, and may in a few ways be a little more restrictive with its options, but in most ways it is identical. 8 | 9 | ## Usage 10 | 11 | ### Code Analysis 12 | Analyse application dependencies from an entrypoint via the `-z` flag 13 | 14 | $ modul8 entry.js -z 15 | 16 | This will output only the dependency tree: 17 | 18 | app::app 19 | ├──┬app::controllers/users 20 | │ └──┬app::models/user 21 | │ └───app::utils/validation 22 | └───app::utils 23 | 24 | ### Minimal Compilation 25 | Simply specify the entry point, and pipe the result to a file 26 | 27 | $ modul8 entry.js > output.js 28 | 29 | Alternatively, you can also use the `-o` flag: 30 | 31 | $ modul8 entry.js -o output.js 32 | 33 | The application domain is assumed to be in the location `entry.js` is found in. 34 | 35 | ### Basic Compilation 36 | Suppose you also have a shared code domain in a different folder like so 37 | 38 | code 39 | ├───app 40 | └───shared 41 | 42 | Then either 43 | 44 | $ modul8 entry.js -p shared=../shared/ > output.js 45 | 46 | from the `app` directory, or 47 | 48 | $ modul8 app/entry.js -p shared=shared/ > output.js 49 | 50 | from the `code/` directory. 51 | 52 | If you want to wait for the DOM using jQuery, append the `-w jQuery` option (see wrapper below). 53 | 54 | ## Advanced Features 55 | 56 | ### Domains 57 | Multiple domains are partition of name=path values delimited like a query string: 58 | 59 | $ modul8 app/entry.js -p shared=shared/&bot=../libs/bot/ 60 | 61 | ### Arbiters 62 | Loading of arbiters works like the programmatic API: 63 | 64 | $ modul8 app/entry.js -a Spine=Spine 65 | 66 | We can omit the right hand side of an expression if the shortcut has the same name as the global. 67 | 68 | $ modul8 app/entry.js -a Spine 69 | 70 | Multiple globals for a given shortcuts can be comma separated: 71 | 72 | $ modul8 app/entry.js -a jQuery=jQuery,$ 73 | 74 | Multiple arbiters can be delimited with an & symbol 75 | 76 | $ modul8 app/entry.js -a jQuery=$,jQuery&Spine 77 | 78 | ### Data Injection 79 | Data injection works fundamentally different from the shell than from your node program. 80 | Here you rely on your data pre-existing in a `.json` file and specify what key to attach it to. 81 | 82 | $ modul8 app/entry.js -d myKey=myData.json 83 | 84 | Multiple data elements can be delimited with an ampersand like above. 85 | 86 | ## Loading Libraries 87 | Libraries can be concatenated on in the order they wish to be included. 88 | Load them with the `-b` flag, supplying a path as the key, and a list of files inside that path. 89 | 90 | $ modul8 app/entry.js -b libs/=jQuery.js,jQuery.ui.js,plugins/datepicker.js 91 | 92 | For a blank constructor call, do not use `-g pathToModule=` as this is used to pass the empty string as the first parameter. 93 | Instead omit the equals sign: `-g pathToModule` 94 | 95 | ### Extra Options 96 | The following are equivalent methods for the programmatic API calls to `.set()` 97 | 98 | -w or --wrapper ⇔ set('domloader', ) 99 | -n or --namespace ⇔ set('namespace', ) 100 | -l or --logging ⇔ set('logging', ) 101 | 102 | #### Booleans 103 | The following are slightly limited versions of the programmatic `.before()` and `.after()` API 104 | 105 | -t or --testcutter ⇔ before(modul8.testcutter) 106 | -m or --minifier ⇔ after(modul8.minifier) 107 | 108 | See the [API](api.html) for more details on how these work. 109 | -------------------------------------------------------------------------------- /builtins/path.posix.js: -------------------------------------------------------------------------------- 1 | function filter (xs, fn) { 2 | var res = []; 3 | for (var i = 0; i < xs.length; i++) { 4 | if (fn(xs[i], i, xs)) res.push(xs[i]); 5 | } 6 | return res; 7 | } 8 | 9 | // resolves . and .. elements in a path array with directory names there 10 | // must be no slashes, empty elements, or device names (c:\) in the array 11 | // (so also no leading and trailing slashes - it does not distinguish 12 | // relative and absolute paths) 13 | function normalizeArray(parts, allowAboveRoot) { 14 | // if the path tries to go above the root, `up` ends up > 0 15 | var up = 0; 16 | for (var i = parts.length; i >= 0; i--) { 17 | var last = parts[i]; 18 | if (last == '.') { 19 | parts.splice(i, 1); 20 | } else if (last === '..') { 21 | parts.splice(i, 1); 22 | up++; 23 | } else if (up) { 24 | parts.splice(i, 1); 25 | up--; 26 | } 27 | } 28 | 29 | // if the path is allowed to go above the root, restore leading ..s 30 | if (allowAboveRoot) { 31 | for (; up--; up) { 32 | parts.unshift('..'); 33 | } 34 | } 35 | 36 | return parts; 37 | } 38 | 39 | // Regex to split a filename into [*, dir, basename, ext] 40 | // posix version 41 | var splitPathRe = /^(.+\/(?!$)|\/)?((?:.+?)?(\.[^.]*)?)$/; 42 | 43 | // path.resolve([from ...], to) 44 | // posix version 45 | exports.resolve = function() { 46 | var resolvedPath = '', 47 | resolvedAbsolute = false; 48 | 49 | for (var i = arguments.length; i >= -1 && !resolvedAbsolute; i--) { 50 | var path = (i >= 0) 51 | ? arguments[i] 52 | : process.cwd(); 53 | 54 | // Skip empty and invalid entries 55 | if (typeof path !== 'string' || !path) { 56 | continue; 57 | } 58 | 59 | resolvedPath = path + '/' + resolvedPath; 60 | resolvedAbsolute = path.charAt(0) === '/'; 61 | } 62 | 63 | // At this point the path should be resolved to a full absolute path, but 64 | // handle relative paths to be safe (might happen when process.cwd() fails) 65 | 66 | // Normalize the path 67 | resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { 68 | return !!p; 69 | }), !resolvedAbsolute).join('/'); 70 | 71 | return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; 72 | }; 73 | 74 | // path.normalize(path) 75 | // posix version 76 | exports.normalize = function(path) { 77 | var isAbsolute = path.charAt(0) === '/', 78 | trailingSlash = path.slice(-1) === '/'; 79 | 80 | // Normalize the path 81 | path = normalizeArray(filter(path.split('/'), function(p) { 82 | return !!p; 83 | }), !isAbsolute).join('/'); 84 | 85 | if (!path && !isAbsolute) { 86 | path = '.'; 87 | } 88 | if (path && trailingSlash) { 89 | path += '/'; 90 | } 91 | 92 | return (isAbsolute ? '/' : '') + path; 93 | }; 94 | 95 | 96 | // posix version 97 | exports.join = function() { 98 | var paths = Array.prototype.slice.call(arguments, 0); 99 | return exports.normalize(filter(paths, function(p, index) { 100 | return p && typeof p === 'string'; 101 | }).join('/')); 102 | }; 103 | 104 | 105 | exports.dirname = function(path) { 106 | var dir = splitPathRe.exec(path)[1] || ''; 107 | var isWindows = false; 108 | if (!dir) { 109 | // No dirname 110 | return '.'; 111 | } else if (dir.length === 1 || 112 | (isWindows && dir.length <= 3 && dir.charAt(1) === ':')) { 113 | // It is just a slash or a drive letter with a slash 114 | return dir; 115 | } else { 116 | // It is a full dirname, strip trailing slash 117 | return dir.substring(0, dir.length - 1); 118 | } 119 | }; 120 | 121 | 122 | exports.basename = function(path, ext) { 123 | var f = splitPathRe.exec(path)[2] || ''; 124 | // TODO: make this comparison case-insensitive on windows? 125 | if (ext && f.substr(-1 * ext.length) === ext) { 126 | f = f.substr(0, f.length - ext.length); 127 | } 128 | return f; 129 | }; 130 | 131 | 132 | exports.extname = function(path) { 133 | return splitPathRe.exec(path)[3] || ''; 134 | }; 135 | -------------------------------------------------------------------------------- /test/clash.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , rimraf = require('rimraf') 3 | , join = require('path').join 4 | , log = require('logule').sub('CLASH') 5 | , utils = require('../lib/utils') 6 | , test = require('tap').test 7 | , modul8 = require('../'); 8 | 9 | var root = join(__dirname, 'clash') 10 | , exts = ['.js', '.coffee'] 11 | , output = join(__dirname, 'output', 'clash.js'); 12 | 13 | var domains = { 14 | app : join(root, 'app') 15 | , shared : join(root, 'shared') 16 | }; 17 | 18 | function makeFiles(domain, name, num) { 19 | var ext1 = (name === 'clash') ? exts[0] : exts[num] // clash tests with outer ext fixed 20 | , ext2 = (name === 'revclsh') ? exts[0] : exts[num] // revclash tests with inner ext fixed 21 | , dom = join(root, domain); 22 | 23 | if (name !== 'safe') { // safe tests without ambiguous file/folder pairs existing 24 | fs.writeFileSync(join(dom, name + num + ext1), "module.exports = 'outside'"); 25 | } 26 | fs.writeFileSync(join(dom, name + num, 'index' + ext2), "module.exports = 'inside'"); 27 | } 28 | 29 | function generateApp() { 30 | try { 31 | rimraf.sync(root); 32 | } catch (e) {} 33 | fs.mkdirSync(root, '0755'); 34 | 35 | var l = []; 36 | Object.keys(domains).forEach(function (domain) { 37 | var dom = join(root, domain) 38 | , prefix = (domain === 'app') ? '' : domain + '::'; 39 | 40 | fs.mkdirSync(dom, '0755'); 41 | 42 | for (var i = 0; i < exts.length; i += 1) { 43 | fs.mkdirSync(join(dom, 'safe' + i), '0755'); 44 | fs.mkdirSync(join(dom, 'unsafe' + i), '0755'); 45 | fs.mkdirSync(join(dom, 'clash' + i), '0755'); 46 | fs.mkdirSync(join(dom, 'revclash' + i), '0755'); 47 | 48 | l = l.concat([ 49 | "exports." + domain + "_safe" + i + "slash = require('" + prefix + "safe" + i + "/');" 50 | , "exports." + domain + "_safe" + i + " = require('" + prefix + "safe" + i + "');" 51 | 52 | , "exports." + domain + "_unsafe" + i + "slash = require('" + prefix + "unsafe" + i + "/');" 53 | , "exports." + domain + "_unsafe" + i + " = require('" + prefix + "unsafe" + i + "');" 54 | 55 | , "exports." + domain + "_clash" + i + "slash = require('" + prefix + "clash" + i + "/');" 56 | , "exports." + domain + "_clash" + i + " = require('" + prefix + "clash" + i + "');" 57 | 58 | , "exports." + domain + "_revclash" + i + "slash = require('" + prefix + "revclash" + i + "/');" 59 | , "exports." + domain + "_revclash" + i + " = require('" + prefix + "revclash" + i + "');" 60 | ]); 61 | 62 | makeFiles(domain, 'safe', i); 63 | makeFiles(domain, 'unsafe', i); 64 | makeFiles(domain, 'clash', i); 65 | makeFiles(domain, 'revclash', i); 66 | } 67 | }); 68 | var entry = join(domains.app, 'entry.js'); 69 | fs.writeFileSync(entry, l.join('\n')); 70 | 71 | //log.trace('compiling') 72 | modul8(entry) 73 | .domains({shared: domains.shared}) 74 | .logger(log.sub().suppress('info', 'debug')) 75 | //.analysis(console.log) 76 | .register('.coffee', function (code) { 77 | return code + ';' 78 | }) 79 | .compile(output); 80 | } 81 | 82 | test("clashes", function (t) { 83 | generateApp(); 84 | var brain = require('./lib/brain')(t) 85 | 86 | var compile = utils.makeCompiler() 87 | , mainCode = compile(output); 88 | 89 | brain.do(mainCode); 90 | brain.ok("M8", "global namespace is defined"); 91 | brain.ok("M8.require('entry')", "entry can be required"); 92 | 93 | Object.keys(domains).forEach(function (dom) { 94 | for (var i = 0; i < exts.length; i += 1) { 95 | var reqStr = "M8.require('entry')." + dom + "_"; 96 | 97 | brain.equal(reqStr + "safe" + i + "slash", 'inside', dom + "_safe" + i + "slash is defined and is inside"); 98 | brain.equal(reqStr + "safe" + i, 'inside', dom + "_safe" + i + " is defined and is inside"); 99 | 100 | brain.equal(reqStr + "unsafe" + i + "slash", 'inside', dom + "_unsafe" + i + "slash is defined and is inside"); 101 | brain.equal(reqStr + "unsafe" + i, 'outside', dom + "_unsafe" + i + " is defined and is outside"); 102 | 103 | brain.equal(reqStr + "clash" + i + "slash", 'inside', dom + "_clash" + i + "slash is defined and is inside"); 104 | brain.equal(reqStr + "clash" + i, 'outside', dom + "_clash" + i + " is defined and is outside"); 105 | 106 | brain.equal(reqStr + "revclash" + i + "slash", 'inside', dom + "_revclash" + i + "slash is defined and is inside"); 107 | brain.equal(reqStr + "revclash" + i, 'outside', dom + "_revclash" + i + " is defined and is outside"); 108 | } 109 | }); 110 | t.end(); 111 | }); 112 | -------------------------------------------------------------------------------- /test/persist.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , fs = require('fs') 3 | , modul8 = require('../index.js') 4 | , join = require('path').join 5 | , test = require('tap').test 6 | , log = require('logule').sub('PERSIST') 7 | , dirify = require('./lib/dirify') 8 | , read = require('../lib/utils').read; 9 | 10 | var root = join(__dirname, 'persist') 11 | , out = join(__dirname, 'output'); 12 | 13 | var paths = { 14 | app : join(root, 'app') 15 | , shared : join(root, 'shared') 16 | , libs : join(root, 'libs') 17 | }; 18 | 19 | var out = { 20 | app : join(out, 'persist.js') 21 | , libs : join(out, 'persistlibs.js') 22 | }; 23 | 24 | 25 | function sleep(ms) { 26 | var now = new Date().getTime(); 27 | while (new Date().getTime() < now + ms) {} 28 | } 29 | 30 | var mTimesOld = { 31 | app : 0 32 | , libs : 0 33 | }; 34 | 35 | function modify(type) { 36 | var file = join(paths[type], '0.js'); 37 | //log.warn(file); 38 | sleep(1005); 39 | fs.writeFileSync(file, read(file) + ';'); 40 | sleep(1005); 41 | } 42 | 43 | 44 | function wasUpdated(type) { 45 | var mtime = fs.statSync(out[type]).mtime.valueOf() 46 | , didUpdate = (mtime !== mTimesOld[type]); 47 | 48 | if (didUpdate) { 49 | mTimesOld[type] = mtime; 50 | } 51 | return didUpdate; 52 | } 53 | 54 | 55 | function makeApp() { 56 | dirify('persist', { 57 | shared: { '0.js' : "module.exports = 'ok';" } 58 | , libs : { '0.js' : "(function(){window.libs = 'ok';}());", '1.js' : 'window.arst = "ok";'} 59 | , app : { 60 | '0.js' : "module.exports = 'ok';" 61 | , 'entry.js' : "exports.app = require('0');" + "exports.shared = require('shared::0');" 62 | } 63 | }); 64 | } 65 | 66 | function compile(useLibs, separateLibs) { 67 | modul8(join(paths.app, 'entry.js')) 68 | .logger(log.sub().suppress('info', 'debug')) 69 | .libraries() 70 | .list(useLibs ? ['0.js'] : []) 71 | .path(paths.libs) 72 | .target(separateLibs ? out.libs : false) 73 | .domains({shared : paths.shared}) 74 | .compile(out.app); 75 | } 76 | 77 | function runCase(k, t) { 78 | var withLibs = (k === 1 || k === 2) 79 | , separateLibs = (k === 2); 80 | 81 | Object.keys(paths).forEach(function (name) { 82 | if (!withLibs && name === 'libs') { 83 | return; 84 | } 85 | var modifyingLibs = (name === 'libs'); 86 | compile(withLibs, separateLibs); 87 | 88 | wasUpdated('app'); 89 | if (separateLibs) { 90 | wasUpdated('libs'); 91 | } 92 | 93 | // start (reset state and make sure a blank compile does not change things ever) 94 | 95 | compile(withLibs, separateLibs); 96 | 97 | t.ok(!wasUpdated('app'), "preparing to modify " + name + "::0 - recompile does not change libs mTimes without changes"); 98 | 99 | if (separateLibs) { 100 | t.ok(!wasUpdated('libs'), "preparing to modify " + name + "::0 - recompile does not change libs mTimes without changes"); 101 | } 102 | 103 | // modify sleeps as there is a limited resolution on mtime 104 | // => cant make rapid changes to files programmatically 105 | // and expect modul8 to understand all the time 106 | // TODO: use fs.(f)utimesSync when we ditch 0.4 compatibility 107 | modify(name); 108 | var msg = 'changing ' + name + ' ' + (withLibs ? separateLibs ? 'using separate libs' : 'using included libs' : 'without libs'); 109 | 110 | compile(withLibs, separateLibs); 111 | var appChanged = wasUpdated('app') 112 | , libsChanged = separateLibs ? wasUpdated('libs') : true; 113 | 114 | if ((modifyingLibs && !separateLibs) || (!modifyingLibs && separateLibs)) { 115 | log.trace(msg + ' => appChanged'); 116 | t.ok(appChanged, "modified " + name + "::0 - compiling with lib changes for included libs app mtime"); 117 | } 118 | else if (modifyingLibs && separateLibs) { 119 | t.ok(libsChanged, "modified " + name + "::0 - compiling with lib changes for separate libs changes libs mtime"); 120 | t.ok(!appChanged, "modified " + name + "::0 - compiling with lib changes for separate libs does not change app mtime"); 121 | log.trace(msg + ' => libsChanged && !appChanged'); 122 | } 123 | else if (!modifyingLibs) { 124 | t.ok(appChanged, "modified " + name + "::0 - compiling with app changes changes app mtime"); 125 | log.trace(msg + ' => appChanged'); 126 | } 127 | }); 128 | } 129 | 130 | test("persist", function (t) { 131 | if (false) { 132 | log.info('modified on hold - skipping 16 second test'); 133 | return; 134 | } 135 | makeApp(); 136 | compile(); 137 | wasUpdated('app'); 138 | /*TestPlan 139 | for each file and path 140 | 0. read mtimes 141 | 1. compile 142 | 2. verify that file has NOT changed (always) 143 | 3. modify a file 144 | 4. compile 145 | 5. verify that app file has changed (if it should) 146 | 147 | do above for cases: 148 | with libs, without libs 149 | with libsOnlyTarget, without libsOnlyTarget 150 | */ 151 | var testCount = 0; 152 | for (var k = 0; k < 3; k += 1) { 153 | runCase(k, t); 154 | } 155 | t.end(); 156 | }); 157 | -------------------------------------------------------------------------------- /test/plugins.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | , modul8 = require('../') 3 | , utils = require('../lib/utils') 4 | , dirify = require('./lib/dirify') 5 | , test = require('tap').test 6 | , log = require('logule').sub('PLUGIN') 7 | , join = path.join 8 | , dir = __dirname 9 | , compile = utils.makeCompiler(); 10 | 11 | 12 | var data = { 13 | a: {str: 'hello thar'} 14 | , b: {coolObj: {}} 15 | , c: 5 16 | , d: [2, 3, "abc", {'wee': []}] 17 | , e: {str2: 9 + 'abc'} 18 | }; 19 | 20 | function PluginOne(name) { 21 | this.name = 'plug1'; 22 | } 23 | PluginOne.prototype.data = function () { 24 | return data; 25 | }; 26 | PluginOne.prototype.domain = function () { 27 | return join(dir, 'plugins', 'dom'); 28 | }; 29 | 30 | function PluginTwo(name) { 31 | this.name = 'plug2'; 32 | } 33 | PluginTwo.prototype.data = function () { 34 | return JSON.stringify(data); 35 | }; 36 | 37 | function generateApp() { 38 | dirify('plugins', { 39 | dom : { 40 | 'code1.js' : "module.exports = require('./code2');" 41 | , 'code2.js' : "module.exports = 160;" 42 | , 'code3.js' : "module.exports = 320;" 43 | } 44 | , main : { 45 | 'entry.js' : "require('plug1::code1');" 46 | } 47 | }); 48 | 49 | 50 | modul8(join(dir, 'plugins', 'main', 'entry.js')) 51 | .set('namespace', 'QQ') 52 | .logger(log.sub().suppress('info', 'debug')) 53 | .use(new PluginOne()) 54 | .use(new PluginTwo()) 55 | .data(data) 56 | .add('crazy1', 'new Date()') 57 | .add('crazy2', '(function(){return new Date();})()') 58 | .add('crazy3', 'window') 59 | .compile(join(dir, 'output', 'plugins.js')); 60 | } 61 | 62 | test("plugins", function (t) { 63 | generateApp(); 64 | var brain = require('./lib/brain')(t) 65 | 66 | var mainCode = compile(join(dir, 'output', 'plugins.js')); 67 | brain.do(mainCode); 68 | 69 | // sanity 70 | brain.ok("!!QQ", "global namespace is defined"); 71 | brain.ok("!!QQ.require", "require is globally accessible"); 72 | brain.type("QQ.require", 'function', "require is a function"); 73 | 74 | // plug1 exports the right code 75 | brain.ok("!QQ.require('plug1::code')", "plug1 does not export code when not required"); 76 | brain.ok("QQ.domains().indexOf('plug1') >= 0", "plug1 was exported as a domain"); 77 | brain.equal("QQ.require('plug1::code1')", 160, "plug1::code1 is included as it is required"); 78 | brain.equal("QQ.require('plug1::code2')", 160, "plug1:;code2 is included as it is required by a required module"); 79 | brain.ok("!QQ.require('plug1::code3')", "plug1::code3 is NOT included as it is NOT required"); 80 | 81 | // plug2 exports nothing 82 | brain.equal("QQ.domains().indexOf('plug2')", -1, "plug2 did not export a domain"); 83 | 84 | // both export data 85 | brain.ok("!!QQ.require('data::plug1')", "plug1 exports data"); 86 | brain.ok("!!QQ.require('data::plug2')", "plug2 exports data"); 87 | 88 | Object.keys(data).forEach(function (key) { 89 | brain.ok("QQ.require('data::plug1')." + key, "require('data::plug1')." + key + " exists"); 90 | brain.deepEqual("QQ.require('data::plug1')." + key, data[key], "require('data::plug1')." + key + " is data['" + key + "']"); 91 | brain.ok("QQ.require('data::plug2')." + key, "require('data::plug2')." + key + " exists"); 92 | brain.deepEqual("QQ.require('data::plug2')." + key, data[key], "require('data::plug2')." + key + " is data['" + key + "']"); 93 | }); 94 | // plugin tests over 95 | 96 | // check that crazy data is included 97 | brain.ok("QQ.require('data::crazy1')", "require('data::crazy1') exists"); 98 | brain.ok("QQ.require('data::crazy2')", "require('data::crazy2') exists"); 99 | brain.ok("QQ.require('data::crazy3')", "require('data::crazy3') exists"); 100 | brain.ok("QQ.require('data::crazy1').getDay", "require('data::crazy1') is an instance of Date"); 101 | brain.ok("QQ.require('data::crazy2').getDay", "require('data::crazy2') is an instance of Date"); 102 | brain.ok("QQ.require('data::crazy3').QQ", "require('data::crazy3') returns window (found namespace)"); 103 | 104 | Object.keys(data).forEach(function (key) { 105 | var val = data[key]; 106 | // viewing data 107 | brain.ok("QQ.require('data::" + key + "')", "require('data::" + key + "') exists"); 108 | brain.deepEqual("QQ.require('data::" + key + "')", data[key], "require('data::" + key + "') is data[" + key + "]"); 109 | 110 | // crud interface tool 111 | brain.do("var dataMod = QQ.data;"); 112 | 113 | // creating databrowser.evaluate( 114 | brain.do("dataMod('newKey', 'arst')"); 115 | brain.equal("QQ.require('data::newKey')", 'arst', "can create new data key"); 116 | 117 | // editing data 118 | brain.type("dataMod", 'function', "M8::data is a requirable function"); 119 | brain.do("dataMod('" + key + "','hello')"); 120 | brain.equal("QQ.require('data::" + key + "')", 'hello', "can call data overriding method on data::" + key); 121 | 122 | // deleting data 123 | brain.do("dataMod('" + key + "')"); 124 | brain.equal("QQ.require('data::" + key + "')", undefined, "successfully deleted data::" + key); 125 | }); 126 | 127 | t.end(); 128 | }); 129 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | ## Overview 4 | 5 | Plugins are shortcuts for exporting both data and code to a domain. They facilitate getting data and their helpers to the browser as one unit, 6 | and help encapsulate logic for the server. Additionally, if designed well, code can be reused on the server on the client. 7 | 8 | Plugins can typically be used by calling `.use()` with a `new PluginName(opts)` instance. 9 | 10 | modul8('./client/app.js') 11 | .use(new PluginName(opts)) 12 | .compile('./out.js'); 13 | 14 | Note that all code that is exported by plugins have to be explicitly required to actually get pulled into the bundle. 15 | 16 | Plugins can also be loaded - albeit somewhat primitively - via the [CLI](cli.html) 17 | 18 | ## Available Plugins 19 | A small current selection of available plugins follow. This section might be moved to the wiki. 20 | 21 | ### Template Version Control System 22 | A simple version control system for web application templates. 23 | 24 | If a web application wishes to store all its templates in localStorage, then they can become out of date with the server as they update there. 25 | This module allows these states to simply synchronize with the server. 26 | 27 | See the [project itself](https://github.com/clux/m8-templation) for more information and sample model implementations. 28 | 29 | ### Mongoose in the Browser 30 | A way to use mongoose models on the server to generate validation logic on the client. 31 | 32 | A web application using mongodb as its backend and using mongoose as an ORM layer on the server ends up defining a lot of validation logic on the server. 33 | Logic that should be reused on the client. The mongoose plugin will export sanitized versions of your explicitly exported mongoose models, along with helpers 34 | to utilize this data in a helpful way. 35 | 36 | See the [project itself](https://github.com/clux/m8-mongoose) for more information. 37 | 38 | 39 | # Writing Plugins 40 | A plugin can export as many things as it wants to be used on the server, but it needs a class instance of a particular format to be compatible with modul8. 41 | 42 | ## Structure 43 | The skeleton of such a plugin class should look something like this in CoffeeScript 44 | 45 | class Plugin 46 | constructor : (@name='PluginName') -> 47 | data : -> obj or stringify(obj) 48 | domain : -> __dirname + '/dom/' 49 | 50 | Or something like this, if using plain JavaScript 51 | 52 | var Plugin = function(name) { 53 | this.name = (name != null) ? name : 'PluginName'; 54 | } 55 | Plugin.prototype.data = function() { 56 | return obj || stringify(obj); 57 | }; 58 | Plugin.prototype.domain = function() { 59 | return __dirname + '/domain/'; 60 | }; 61 | 62 | ### Constructor 63 | For compatibility with the CLI, the constructor should be able to take the essential parameters as ordered arguments. 64 | You decide how many parameters are essential, and you can encapsulate the remaining arguments in an object (for instance) to avoid having a huge number of ordered arguments. 65 | 66 | If you design for CLI compatibility, then the constructor should coerce important internal arguments from strings (to what they are supposed to be), 67 | as this is how they are passed in from the CLI. For instance, this design would work well with the CLI. 68 | 69 | constructor : (@name='pluginName', number, @obj={}) -> 70 | @number = number | 0 # force to Int 71 | @obj.foo ?= 'inessential param' 72 | 73 | ### name key 74 | The `name` key must be specified and have a default indicating the name used by the plugin. 75 | It will specify the name of the key exported to the `data` domain (if it exports data), as well as the name of the domain exported to (if it exports behaviour). 76 | 77 | This name should be configurable from the class constructor to avoid clashes, but it should default to the plugin name. 78 | 79 | ### data method 80 | The `data` method must return an object or a pre-serialized object/array to attach to the data domain. 81 | If a string is passed from `data` it will be assumed to be a pre-serialized object that evaluates so something sensible via `eval`. 82 | Anything else will be internally serialized for you with eirther `JSON.stringify` or `.toString` if Function type. 83 | 84 | ### domain method 85 | The `domain` method must return a path corresponding to the root of the exporting domain. 86 | If a domain is exported, it should be clear on the server what files are available to the client by looking at the directory structure of the plugin. 87 | It is recommended to put all these files within a `dom` subdirectory of your node module `lib` root. 88 | 89 | ## Domain 90 | The domain method is set, modul8 will add a domain for named after the plugin (specified in name). 91 | It will not append the files to the output, unless any of them have been required from the client. 92 | If they are, however, they will pull in the dependencies (althoug only from this domain) they need to operate. 93 | 94 | To make most use of domains, try to not duplicate work and note code under `dom/` can be required on the server from the `lib` directory. 95 | For that reason, you may want this code to stay browser agnostic. Of course, sometimes this is not always possible. 96 | If you do have to rely on certain browser elements, do not allow the code to run from this domain (because no non-app domains will wait for the DOM). 97 | Instead make functions that, when called, expects the DOM to be ready, and only use these functions from the app domain. 98 | -------------------------------------------------------------------------------- /builtins/events.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = function () {}; 2 | var isArray = typeof Array.isArray === 'function' 3 | ? Array.isArray 4 | : function (xs) { 5 | return Object.toString.call(xs) === '[object Array]' 6 | } 7 | ; 8 | 9 | // By default EventEmitters will print a warning if more than 10 | // 10 listeners are added to it. This is a useful default which 11 | // helps finding memory leaks. 12 | // 13 | // Obviously not all Emitters should be limited to 10. This function allows 14 | // that to be increased. Set to zero for unlimited. 15 | var defaultMaxListeners = 10; 16 | EventEmitter.prototype.setMaxListeners = function(n) { 17 | if (!this._events) this._events = {}; 18 | this._events.maxListeners = n; 19 | }; 20 | 21 | 22 | EventEmitter.prototype.emit = function(type) { 23 | // If there is no 'error' event listener then throw. 24 | if (type === 'error') { 25 | if (!this._events || !this._events.error || 26 | (isArray(this._events.error) && !this._events.error.length)) 27 | { 28 | if (arguments[1] instanceof Error) { 29 | throw arguments[1]; // Unhandled 'error' event 30 | } else { 31 | throw new Error("Uncaught, unspecified 'error' event."); 32 | } 33 | return false; 34 | } 35 | } 36 | 37 | if (!this._events) return false; 38 | var handler = this._events[type]; 39 | if (!handler) return false; 40 | 41 | if (typeof handler == 'function') { 42 | switch (arguments.length) { 43 | // fast cases 44 | case 1: 45 | handler.call(this); 46 | break; 47 | case 2: 48 | handler.call(this, arguments[1]); 49 | break; 50 | case 3: 51 | handler.call(this, arguments[1], arguments[2]); 52 | break; 53 | // slower 54 | default: 55 | var args = Array.prototype.slice.call(arguments, 1); 56 | handler.apply(this, args); 57 | } 58 | return true; 59 | 60 | } else if (isArray(handler)) { 61 | var args = Array.prototype.slice.call(arguments, 1); 62 | 63 | var listeners = handler.slice(); 64 | for (var i = 0, l = listeners.length; i < l; i++) { 65 | listeners[i].apply(this, args); 66 | } 67 | return true; 68 | 69 | } else { 70 | return false; 71 | } 72 | }; 73 | 74 | // EventEmitter is defined in src/node_events.cc 75 | // EventEmitter.prototype.emit() is also defined there. 76 | EventEmitter.prototype.addListener = function(type, listener) { 77 | if ('function' !== typeof listener) { 78 | throw new Error('addListener only takes instances of Function'); 79 | } 80 | 81 | if (!this._events) this._events = {}; 82 | 83 | // To avoid recursion in the case that type == "newListeners"! Before 84 | // adding it to the listeners, first emit "newListeners". 85 | this.emit('newListener', type, listener); 86 | 87 | if (!this._events[type]) { 88 | // Optimize the case of one listener. Don't need the extra array object. 89 | this._events[type] = listener; 90 | } else if (isArray(this._events[type])) { 91 | 92 | // Check for listener leak 93 | if (!this._events[type].warned) { 94 | var m; 95 | if (this._events.maxListeners !== undefined) { 96 | m = this._events.maxListeners; 97 | } else { 98 | m = defaultMaxListeners; 99 | } 100 | 101 | if (m && m > 0 && this._events[type].length > m) { 102 | this._events[type].warned = true; 103 | console.error('(node) warning: possible EventEmitter memory ' + 104 | 'leak detected. %d listeners added. ' + 105 | 'Use emitter.setMaxListeners() to increase limit.', 106 | this._events[type].length); 107 | console.trace(); 108 | } 109 | } 110 | 111 | // If we've already got an array, just append. 112 | this._events[type].push(listener); 113 | } else { 114 | // Adding the second element, need to change to array. 115 | this._events[type] = [this._events[type], listener]; 116 | } 117 | 118 | return this; 119 | }; 120 | 121 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 122 | 123 | EventEmitter.prototype.once = function(type, listener) { 124 | var self = this; 125 | self.on(type, function g() { 126 | self.removeListener(type, g); 127 | listener.apply(this, arguments); 128 | }); 129 | 130 | return this; 131 | }; 132 | 133 | EventEmitter.prototype.removeListener = function(type, listener) { 134 | if ('function' !== typeof listener) { 135 | throw new Error('removeListener only takes instances of Function'); 136 | } 137 | 138 | // does not use listeners(), so no side effect of creating _events[type] 139 | if (!this._events || !this._events[type]) return this; 140 | 141 | var list = this._events[type]; 142 | 143 | if (isArray(list)) { 144 | var i = list.indexOf(listener); 145 | if (i < 0) return this; 146 | list.splice(i, 1); 147 | if (list.length == 0) 148 | delete this._events[type]; 149 | } else if (this._events[type] === listener) { 150 | delete this._events[type]; 151 | } 152 | 153 | return this; 154 | }; 155 | 156 | EventEmitter.prototype.removeAllListeners = function(type) { 157 | // does not use listeners(), so no side effect of creating _events[type] 158 | if (type && this._events && this._events[type]) this._events[type] = null; 159 | return this; 160 | }; 161 | 162 | EventEmitter.prototype.listeners = function(type) { 163 | if (!this._events) this._events = {}; 164 | if (!this._events[type]) this._events[type] = []; 165 | if (!isArray(this._events[type])) { 166 | this._events[type] = [this._events[type]]; 167 | } 168 | return this._events[type]; 169 | }; 170 | 171 | exports.EventEmitter = EventEmitter; 172 | -------------------------------------------------------------------------------- /lib/require.js: -------------------------------------------------------------------------------- 1 | /** 2 | * modul8 vVERSION 3 | */ 4 | 5 | var config = REQUIRECONFIG // replaced 6 | , ns = window[config.namespace] 7 | , path = ns.path 8 | , slash = config.slash 9 | , domains = config.domains 10 | , builtIns = config.builtIns 11 | , arbiters = [] 12 | , stash = {} 13 | , DomReg = /^([\w]*)::/; 14 | 15 | /** 16 | * Initialize stash with domain names and move data to it 17 | */ 18 | stash.M8 = {}; 19 | stash.external = {}; 20 | stash.data = ns.data; 21 | delete ns.data; 22 | stash.npm = {path : path}; 23 | delete ns.path; 24 | 25 | domains.forEach(function (e) { 26 | stash[e] = {}; 27 | }); 28 | 29 | /** 30 | * Attach arbiters to the require system then delete them from the global scope 31 | */ 32 | Object.keys(config.arbiters).forEach(function (name) { 33 | var arbAry = config.arbiters[name]; 34 | arbiters.push(name); 35 | stash.M8[name] = window[arbAry[0]]; 36 | arbAry.forEach(function (e) { 37 | delete window[e]; 38 | }); 39 | }); 40 | 41 | // same as server function 42 | function isAbsolute(reqStr) { 43 | return reqStr === '' || path.normalize(reqStr) === reqStr; 44 | } 45 | 46 | function resolve(domains, reqStr) { 47 | reqStr = reqStr.split('.')[0]; 48 | 49 | // direct folder require 50 | var skipFolder = false; 51 | if (reqStr.slice(-1) === slash || reqStr === '') { 52 | reqStr = path.join(reqStr, 'index'); 53 | skipFolder = true; 54 | } 55 | 56 | if (config.logging >= 4) { 57 | console.debug('m8 scans : ' + JSON.stringify(domains) + ' for : ' + reqStr); 58 | } 59 | 60 | var dom, k, req; 61 | for (k = 0; k < domains.length; k += 1) { 62 | dom = domains[k]; 63 | if (stash[dom][reqStr]) { 64 | return stash[dom][reqStr]; 65 | } 66 | if (!skipFolder) { 67 | req = path.join(reqStr, 'index'); 68 | if (stash[dom][req]) { 69 | return stash[dom][req]; 70 | } 71 | } 72 | } 73 | 74 | if (config.logging >= 1) { 75 | console.error("m8: Unable to resolve require for: " + reqStr); 76 | } 77 | } 78 | 79 | /** 80 | * Require Factory for ns.define 81 | * Each (domain,path) gets a specialized require function from this 82 | */ 83 | function makeRequire(dom, pathName) { 84 | return function (reqStr) { 85 | if (config.logging >= 3) { // log verbatim pull-ins from dom::pathName 86 | console.log('m8: ' + dom + '::' + pathName + " <- " + reqStr); 87 | } 88 | 89 | if (!isAbsolute(reqStr)) { 90 | return resolve([dom], path.join(path.dirname(pathName), reqStr)); 91 | } 92 | 93 | var domSpecific = DomReg.test(reqStr) 94 | , sDomain = false; 95 | 96 | if (domSpecific) { 97 | sDomain = reqStr.match(DomReg)[1]; 98 | reqStr = reqStr.split('::')[1]; 99 | } 100 | 101 | // require from/to npm domain - sandbox and join in current path if exists 102 | if (dom === 'npm' || (domSpecific && sDomain === 'npm')) { 103 | if (builtIns.indexOf(reqStr) >= 0) { 104 | return resolve(['npm'], reqStr); 105 | } 106 | if (domSpecific) { 107 | return resolve(['npm'], config.npmTree[reqStr].main); 108 | } 109 | // else, absolute: use included npmTree of mains 110 | // find root of module referenced in pathName, by counting number of node_modules referenced 111 | var order = pathName.split('node_modules').length; 112 | var root = pathName.split(slash).slice(0, Math.max(2 * (order - 2) + 1, 1)).join(slash); 113 | var branch = root.split(slash + 'node_modules' + slash).concat(reqStr); 114 | 115 | // use the branch array as the keys needed to traverse the npm tree 116 | var position = config.npmTree[branch[0]]; 117 | for (var i = 1; i < branch.length; i += 1) { 118 | position = position.deps[branch[i]]; 119 | if (!position) { 120 | // should not happen, remove eventually 121 | console.error('m8: expected vertex: ' + branch[i] + ' missing from current npm tree branch ' + pathName); 122 | return; 123 | } 124 | } 125 | return resolve(['npm'], position.main); 126 | } 127 | 128 | // domain specific 129 | if (domSpecific) { 130 | return resolve([sDomain], reqStr); 131 | } 132 | 133 | // general absolute, try arbiters 134 | if (arbiters.indexOf(reqStr) >= 0) { 135 | return resolve(['M8'], reqStr); 136 | } 137 | 138 | // general absolute, not an arbiter, try current domains, then the others 139 | return resolve([dom].concat(domains.filter(function (e) { 140 | return (e !== dom && e !== 'npm'); 141 | })), reqStr); 142 | }; 143 | } 144 | 145 | /** 146 | * define module name on domain container 147 | * expects wrapping fn(require, module, exports) { code }; 148 | */ 149 | ns.define = function (name, domain, fn) { 150 | var mod = {exports : {}} 151 | , exp = {} 152 | , target; 153 | fn.call({}, makeRequire(domain, name), mod, exp); 154 | 155 | if (Object.prototype.toString.call(mod.exports) === '[object Object]') { 156 | target = (Object.keys(mod.exports).length) ? mod.exports : exp; 157 | } 158 | else { 159 | target = mod.exports; 160 | } 161 | stash[domain][name] = target; 162 | }; 163 | 164 | /** 165 | * Public Debug API 166 | */ 167 | 168 | ns.inspect = function (domain) { 169 | console.log(stash[domain]); 170 | }; 171 | 172 | ns.domains = function () { 173 | return domains.concat(['external', 'data']); 174 | }; 175 | 176 | ns.require = makeRequire('app', 'CONSOLE'); 177 | 178 | /** 179 | * Live Extension API 180 | */ 181 | 182 | ns.data = function (name, exported) { 183 | if (stash.data[name]) { 184 | delete stash.data[name]; 185 | } 186 | if (exported) { 187 | stash.data[name] = exported; 188 | } 189 | }; 190 | 191 | ns.external = function (name, exported) { 192 | if (stash.external[name]) { 193 | delete stash.external[name]; 194 | } 195 | if (exported) { 196 | stash.external[name] = exported; 197 | } 198 | }; 199 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs') 4 | , path = require('path') 5 | , program = require('../node_modules/commander') 6 | , modul8 = require('../') 7 | , utils = require('../lib/utils') 8 | , dir = fs.realpathSync() 9 | , basename = path.basename 10 | , dirname = path.dirname 11 | , resolve = path.resolve 12 | , join = path.join; 13 | 14 | // parse query string like options in two ways 15 | function makeQsParser(isValueList) { 16 | return function (val) { 17 | var out = {}; 18 | (val.split('&') || []).forEach(function (e) { 19 | var pair = e.split('=') 20 | , k = pair[0] 21 | , v = pair[1]; 22 | out[k] = v; 23 | if (isValueList && v) { 24 | out[k] = v.split(','); 25 | } 26 | }); 27 | return out; 28 | }; 29 | } 30 | 31 | var simpleQs = makeQsParser(0) 32 | , listQs = makeQsParser(1); 33 | 34 | // options 35 | 36 | program 37 | .version(modul8.version) 38 | .option('-z, --analyze', 'analyze dependencies instead of compiling') 39 | .option('-p, --domains ', 'specify require domains', simpleQs) 40 | .option('-d, --data ', 'attach json data from path to data::key', simpleQs) 41 | 42 | .option('-b, --libraries ', 'include listed libraries first', listQs) 43 | .option('-a, --arbiters ', 'arbiter list of globals to shortcut', listQs) 44 | .option('-g, --plugins ', 'load plugins from module path using constructor arguments', listQs) 45 | 46 | .option('-o, --output ', 'direct output to a file') 47 | .option('-l, --logging ', 'set the logging level') 48 | .option('-n, --namespace ', 'specify the target namespace used in the compiled file') 49 | .option('-w, --wrapper ', 'name of wrapping domloader function') 50 | .option('-t, --testcutter', 'cut out inlined tests in scanned files') 51 | .option('-m, --minifier', 'enable uglifyjs minification'); 52 | 53 | program.on('--help', function () { 54 | console.log(' Examples:'); 55 | console.log(''); 56 | console.log(' # analyze application dependencies from entry point'); 57 | console.log(' $ modul8 app/entry.js -z'); 58 | console.log(''); 59 | console.log(' # compile application from entry point'); 60 | console.log(' $ modul8 app/entry.js > output.js'); 61 | console.log(''); 62 | console.log(' # specify extra domains'); 63 | console.log(' $ modul8 app/entry.js -p shared=shared/&bot=bot/'); 64 | console.log(''); 65 | console.log(' # specify arbiters'); 66 | console.log(' $ modul8 app/entry.js -a jQuery=$,jQuery&Spine'); 67 | console.log(''); 68 | console.log(' # wait for the DOM using the jQuery function'); 69 | console.log(' $ modul8 app/entry.js -w jQuery'); 70 | console.log(''); 71 | }); 72 | 73 | function complete() { 74 | // first arg must be entry 75 | var entry = program.args[0]; 76 | if (!entry) { 77 | console.error("usage: modul8 entry [options]"); 78 | console.log("or modul8 -h for help"); 79 | process.exit(); 80 | } 81 | 82 | // utils 83 | var i_d = function (a) { 84 | return a; 85 | }; 86 | 87 | var construct = function (Ctor, args) { 88 | var F; 89 | F = function () { 90 | return Ctor.apply(this, args); 91 | }; 92 | F.prototype = Ctor.prototype; 93 | return new F(); 94 | }; 95 | 96 | // convenience processing of plugins and data input 97 | var plugins = []; 98 | Object.keys(program.plugins || {}).forEach(function (name) { 99 | var optAry = program.plugins[name]; 100 | if (!name) { 101 | console.error("invalid plugin usage: -g path=[args]"); 102 | process.exit(); 103 | } 104 | var rel = join(fs.realpathSync(), name); 105 | if (fs.existsSync(rel)) { 106 | name = rel; 107 | } 108 | // path can be absolute, relative to execution directory, or relative to CLI dir 109 | var P; 110 | try { 111 | P = require(name).Plugin; 112 | } 113 | catch (e) { 114 | console.error("invalid plugin: " + name + "could not be resolved"); 115 | process.exit(); 116 | } 117 | plugins.push(construct(P, optAry)); 118 | }); 119 | 120 | Object.keys(program.data || {}).forEach(function (k) { 121 | var p = program.data[k]; 122 | if (!p || !fs.existsSync(p)) { 123 | console.error("invalid data usage: value must be a path to a file"); 124 | process.exit(); 125 | } 126 | program.data[k] = fs.readFileSync(p, 'utf8'); 127 | }); 128 | 129 | if (!program.output) { 130 | program.output = console.log; 131 | } 132 | 133 | var libPath = Object.keys(program.libraries || {})[0] 134 | , libs = (program.libraries || {})[libPath]; 135 | 136 | modul8(entry) 137 | .set('logging', program.logging || 'ERROR') 138 | .set('namespace', program.namespace || 'M8') 139 | .set('domloader', program.wrapper || '') 140 | .set('force', true) // bypass persister 141 | .use(plugins) 142 | .before(program.testcutter ? modul8.testcutter : i_d) 143 | .after(program.minifier ? modul8.minifier : i_d) 144 | .domains(program.domains) 145 | .data(program.data) 146 | .analysis(program.analyze ? console.log : void 0) 147 | .arbiters(program.arbiters) 148 | .libraries(libs || [], libPath) 149 | .compile(program.analyze ? false : program.output); 150 | } 151 | 152 | if (module === require.main) { 153 | program.parse(process.argv); 154 | complete(); 155 | } 156 | 157 | // allow injecting of custom argv to test cli 158 | module.exports = function (argv) { 159 | program.parse(argv); 160 | complete(); 161 | 162 | // reset state which program retains between multiple calls from same file 163 | var resettables = [ 164 | 'analyze' // -z 165 | , 'data' // -d 166 | , 'domains' // -p 167 | , 'namespace' // -n 168 | , 'testcutter' // -t 169 | , 'minifier' // -m 170 | , 'wrapper' // -w 171 | , 'output' // -o 172 | , 'arbiters' // -a 173 | , 'logging' // -l 174 | , 'plugins' // -g 175 | , 'libraries' // -b 176 | ]; 177 | 178 | resettables.forEach(function (k) { 179 | delete program[k]; 180 | }); 181 | }; 182 | 183 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , zombie = require('zombie') //TODO: brain! 3 | , assert = require('assert') 4 | , rimraf = require('rimraf') 5 | , log = require('logule').sub('CLI').suppress('trace') 6 | , mkdirp = require('mkdirp').sync 7 | , join = require('path').join 8 | , utils = require('../lib/utils') 9 | , modul8 = require('../') 10 | , test = require('tap').test 11 | , cli = require('../bin/cli') 12 | , dir = __dirname 13 | , compile = utils.makeCompiler() 14 | , makeBrain = require('./lib/brain') 15 | , testCount = 0 16 | , num_tests = 8; 17 | 18 | function callCLI(str) { 19 | var base = ["node", "./bin/cli.js"] 20 | , argv = base.concat(str.split(' ')); 21 | log.trace('call: ', str); 22 | cli(argv); 23 | } 24 | /* 25 | test("CLI/simple", function (t) { 26 | var brain = makeBrain(t) 27 | , workDir = join('..', 'examples', 'simple') 28 | , str = join(workDir, 'app', 'app.js') + " -a jQuery=jQuery,$ -w jQuery -o " + join(workDir, "cliout.js"); 29 | 30 | callCLI(str); 31 | 32 | var libs = compile(join(workDir, 'libs', 'jquery.js')) 33 | , mainCode = compile(join(workDir, 'cliout.js')); 34 | 35 | brain.do(libs); 36 | brain.do(mainCode); 37 | brain.ok("M8.require('jQuery') !== undefined", "jQuery is requirable"); 38 | brain.ok("window.jQuery === undefined", "jQuery is not global"); 39 | brain.ok("window.$ === undefined", "$ is not global"); 40 | t.end(); 41 | }); 42 | */ 43 | 44 | function initDirs(num) { 45 | var folders = ['libs', 'main', 'plug', 'dom']; 46 | var fn = function (k) { 47 | folders.forEach(function (folder) { 48 | mkdirp(join(dir, 'cli', k + '', folder), '0755'); 49 | }); 50 | }; 51 | for (var i = 0; i < num; i += 1) { 52 | fn(i); 53 | } 54 | } 55 | 56 | 57 | function generateApp(opts, i) { 58 | var entry = [] 59 | , plug = []; 60 | 61 | plug.push("Plugin = function(name){this.name = (name != null) ? name : 'defaultName';};"); 62 | plug.push("Plugin.prototype.data = function(){return {plugData:true};};"); 63 | plug.push("exports.Plugin = Plugin;"); 64 | 65 | entry.push("exports.libTest1 = window.libTest1 !== undefined;"); 66 | entry.push("exports.libTest2 = window.libTest2 !== undefined;"); 67 | 68 | if (opts.data) { 69 | entry.push("exports.data = !!require('data::dataKey').hy;"); 70 | } 71 | if (opts.plug) { 72 | entry.push("exports.plugData = !!require('data::" + (opts.plugName || 'defaultName') + "');"); 73 | } 74 | if (opts.dom) { 75 | entry.push("exports.domain = !!require('dom::');"); 76 | } 77 | if (opts.testcutter) { 78 | entry.push("if (module === require.main) { require('server-requirement'); }"); 79 | } 80 | 81 | fs.writeFileSync(join(dir, 'cli', i + '', 'main', 'entry.js'), entry.join('\n')); 82 | fs.writeFileSync(join(dir, 'cli', i + '', 'dom', 'index.js'), "module.exports = 'domainCode';"); 83 | fs.writeFileSync(join(dir, 'cli', i + '', 'libs', 'lib1.js'), "window.lib1 = function(fn){fn();};"); 84 | fs.writeFileSync(join(dir, 'cli', i + '', 'libs', 'lib2.js'), "window.libTest2 = 'lib2';"); 85 | 86 | if (opts.plug) { 87 | fs.writeFileSync(join(dir, 'cli', i + '', 'plug', 'index.js'), plug.join('\n')); 88 | } 89 | if (opts.data) { 90 | fs.writeFileSync(join(dir, 'cli', i + '', 'data.json'), JSON.stringify({ 91 | hy: 'thear', 92 | wee: 122 93 | })); 94 | } 95 | fs.writeFileSync(join(dir, 'cli', i + '', 'main', 'temp.js'), "require('./code1')"); 96 | } 97 | 98 | function runCase(k, t) { 99 | var opts = { 100 | dom : k % 2 === 0 101 | , data : k % 4 === 0 102 | , plug : k % 8 === 0 103 | , libArb1 : k % 2 === 0 104 | , libArb2 : k % 4 === 0 105 | , wrapper : k % 8 === 1 106 | , minifier : k % 4 === 1 107 | , testcutter: k % 5 === 1 108 | }; 109 | 110 | if (k % 3 === 0) { 111 | opts.ns = 'WOWZ'; 112 | } 113 | generateApp(opts, k); 114 | 115 | var workDir = join(__dirname, 'cli', k + '') 116 | , flags = [join(workDir, "main", "entry.js")]; 117 | 118 | if (opts.dom) { 119 | flags.push("-p dom=" + join(workDir, 'dom')); //TODO: used to have a slash at the end 120 | } 121 | if (opts.testcutter) { 122 | flags.push("-t"); 123 | } 124 | if (opts.minifier) { 125 | flags.push("-m"); 126 | } 127 | if (opts.wrapper) { 128 | flags.push("-w lib1"); 129 | } 130 | if (opts.ns) { 131 | flags.push("-n " + opts.ns); 132 | } 133 | if (opts.data) { 134 | flags.push("-d dataKey=" + join(workDir, 'data.json')); 135 | } 136 | if (opts.plug) { 137 | flags.push("-g " + join(workDir, 'plug')); //TODO: used to have a slash 138 | } 139 | flags.push("-o " + join(workDir, 'output.js')); 140 | if (opts.lib1Arb && opts.lib2Arb) { 141 | flags.push("-a lib1&lib2=libTest2"); 142 | } else if (opts.lib1Arb) { 143 | flags.push("-a lib1"); 144 | } else if (opts.lib2Arb) { 145 | flags.push("-a lib2=libTest2"); 146 | } 147 | 148 | callCLI(flags.join(' ')); 149 | 150 | // check output 151 | 152 | var mainCode = compile(join(workDir, 'output.js')) 153 | , libs1 = compile(join(workDir, 'libs', 'lib1.js')) 154 | , libs2 = compile(join(workDir, 'libs', 'lib2.js')) 155 | , ns = opts.ns || 'M8' 156 | , brain = makeBrain(t); 157 | 158 | brain.do(libs1); 159 | brain.do(libs2); 160 | 161 | if (opts.lib1Arb) { 162 | brain.ok("!window.lib1", "lib1 global removed"); 163 | } else { 164 | brain.ok("!!window.lib1", "lib1 global has exists"); 165 | brain.type("window.lib1", "function", "lib1 is a function"); 166 | } 167 | 168 | brain.do(mainCode); 169 | brain.ok(ns, "namespace exists"); 170 | brain.ok(ns + ".require", "require fn exists"); 171 | brain.ok("!!" + ns + ".require('./entry')", "can require entry point run " + k); 172 | 173 | if (opts.lib1Arb) { 174 | brain.ok("!!" + ns + ".require('lib1')", "lib1 is arbitered"); 175 | } 176 | if (opts.lib2Arb) { 177 | brain.ok("!!" + ns + ".require('lib2')", "lib2 is arbitered correctly"); 178 | } 179 | if (opts.data) { 180 | brain.ok("!!" + ns + ".require('data::dataKey')", "can require dataKey"); 181 | brain.ok("!!" + ns + ".require('./entry').data", "data was also required via entry"); 182 | } 183 | if (opts.dom) { 184 | brain.ok("!!" + ns + ".require('dom::')", "domain can be required"); 185 | brain.ok("!!" + ns + ".require('./entry').domain", "domain was successfully required from entry too"); 186 | } 187 | testsDone(t); 188 | } 189 | 190 | function testsDone (t) { 191 | num_tests -= 1; 192 | if (!num_tests) { 193 | t.end(); 194 | } 195 | } 196 | 197 | test("CLI complex", function (t) { 198 | var num = num_tests; 199 | initDirs(num); 200 | 201 | for (var k = 0; k < num; k += 1) { 202 | runCase(k, t); 203 | } 204 | }); 205 | 206 | -------------------------------------------------------------------------------- /docs/require.md: -------------------------------------------------------------------------------- 1 | # require() 2 | 3 | ## Ways to require 4 | 5 | There are four different ways to use require from your application code: 6 | 7 | #### Relatively 8 | 9 | - `require('./module.js')` - Can resolve a module.js file (only) on the current domain in the current folder. 10 | 11 | You can indicate a relative require by using either the `./` prefix, or the folder traversing `../` prefix. Going up folders is done by chaining on `../` strings. 12 | 13 | #### Absolutely 14 | 15 | - `require('subfolder/module.js')` - Can resolve subfolder/module.js on any domain - regardless of what subfolder or domain you are in - but **will scan the current domain first**. 16 | 17 | **NB**: Not recommended as a shorter substitution to relative requires, as collisions can appear. 18 | 19 | #### Domain Specific 20 | 21 | - `require('shared::val.js')` - Like absolute requires, but specifies the only domain which will be searched. If you want to do relative domain specific requires, 22 | just use a pure relative require where the current domain is implicitly assumed. 23 | 24 | Note that `require('dom::')` will look for an index file in the root of that domain. So if you want to minimize the cross-domain interaction, 25 | export everything relevant from there. 26 | 27 | ##### NPM Domain 28 | 29 | - `require('npm::underscore')` - Will find the files from the specified node modules root. Node modules will show up in the dependency tree as a single file (SOON). 30 | 31 | ##### Data Domain 32 | 33 | - `require('data::datakey')` - Data on this domain does not represent actual files, but data injected into the require system on the server. It will not show up in the dependency tree. 34 | 35 | ##### External Domain 36 | 37 | - `require('external::extkey')` - Same as data domain, but only extensible from the client. 38 | 39 | ##### Arbiter Domain 40 | 41 | - `require('jQuery')` - Shortcut domain for old globals that were deleted to help identify hidden dependencies - must be defined on the server. 42 | This does not require a domain prefix because it is assumed this domain gets sufficiently frequent use to have it bumped up on the priority list. 43 | 44 | Note that if a jQuery.js file is found on the current domain, however, it will gain priority over the arbiter domain. 45 | If this coexistence is necessary, any arbiters must be required by specifying the internal domain name: `var $ = require('M8::jQuery')`. 46 | 47 | ## File extensions 48 | 49 | File extensions are never necessary, but you can (and sometimes should) include them for specificity (except for on the data domain). 50 | 51 | The reason you perhaps should is that modul8 allows mixing and matching JavaScript, CoffeeScript, and other AltJS languages, 52 | but is only as forgiving with such mixing as you deserve. If you only use one language and never put files of another language in your directories, 53 | the following warning does not apply to you. 54 | 55 | ### Extension Priority 56 | To see why, consider a simplified resolver algorithm from the server 57 | 58 | name = require input 59 | for domain in domainsScannable 60 | return true if exists(join(domain, name)) 61 | return true if exists(join(domain, name + '.js')) 62 | return true if exists(join(domain, name + altJsExt)) //optional 63 | return false 64 | 65 | If you use _CoffeeScript_ or other registered compilers for AltJS languages, 66 | and if there is even a chance of a file of the same name with a `.js` extension popping up in the same folder, 67 | then it will gain priority over your normal files if you do not specify the extension. 68 | 69 | #### Extension Truncation 70 | The corollary to this extension priority is that we can't accurately distinguish between two extensions on the client when we omit the extension. 71 | Thus, since omitting the extension is generally advantageous for brevity, we have decided to simply truncate the extension on the client. 72 | 73 | This is advantageous also because it makes the require code leaner, more perfomant, and each **dom::name pair is unique**, as they should be. 74 | 75 | You can still have a `.js` duplicate of your `.coffee` file in a directory, but you then have to explicitly define `.coffee` so that the server 76 | can pre-pick the right one to include for you. 77 | 78 | #### Server Error 79 | To help you remember this, modul8 will actually throw an error if you simultaneously try to require both 80 | 81 | - app::subpath/module.js 82 | - app::subpath/module.otherExt 83 | 84 | It is not a complete failsafe, but it helps force usage so that the problem above does not occur. 85 | 86 | **In short**: try not to have different extension versions of your files in the same directory or you can run into the following two problems: 87 | 88 | - **A)** your Coffee changes won't do anything because you are unknowingly requiring a JS version that got in the directory 89 | - **B)** you will have a conflict error thrown at you to help you not challenging fate with techniques that can result in problem A 90 | 91 | Note that the error will only be thrown after the `.analysis()` dependency tree was logged, allowing you to pinpoint the careless `require()`. 92 | 93 | ## Require Priority 94 | Require priority will mostly follow the nodejs require algorithm, but with some slight modifications to get cross domain requires working without require.paths. 95 | 96 | ### Folders Priority 97 | 98 | Require strings that contain a trailing slash or does not point to a file directly, will try to resolve the name as a folder and look for a file named `index` following the above logic. 99 | The following will all resolve a folder, but the last has the possiblility of a collision with a file of the same name as the folder: 100 | 101 | require('controllers/index'); 102 | require('controllers/'); //looks for controllers/index+extension 103 | require('controllers'); //looks for controllers+extension then controllers/index+extension 104 | 105 | ### General Priority 106 | 107 | Requires are attempted resolved with the following priority: 108 | 109 | if require string is relative 110 | resolve require string using current path on current domain 111 | if require string includes npm prefix or is from npm domain 112 | require node modules from builtins or from current node_modules subdir or above one recursively 113 | else if require string includes domain prefix 114 | resolve require string on specified domain absolutely 115 | else //try arbiter search 116 | resolve require string on the M8 domain 117 | 118 | if none of the above true 119 | resolve on all real domains, starting with current domain 120 | 121 | //error 122 | 123 | In other words, collisions should not occur unless you have duplicate files in different domains and 124 | you are overly relaxed about your domain specifiers, or you have actual files with arbiter names lying around. 125 | -------------------------------------------------------------------------------- /lib/analyzer.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , path = require('path') 3 | , deputy = require('deputy') 4 | , utils = require('./utils') 5 | , resolver = require('./resolver') 6 | , topiary = require('topiary') 7 | , error = utils.error 8 | , join = path.join; 9 | 10 | 11 | /** 12 | * Analyzer class 13 | * 14 | */ 15 | function Analyzer(obj, before, compile, builtIns, serverModules) { 16 | this.entryPoint = obj.entryPoint; 17 | this.domains = obj.domains; 18 | this.ignoreDoms = obj.ignoreDoms; 19 | this.deputy = deputy(join(__dirname, '..', 'deputy.json')); 20 | this.before = before; 21 | this.compile = compile; 22 | this.npmTree = { 23 | _builtIns : [] 24 | }; // resolver builds this - makes sanitizing our tree easier as well 25 | this.resolve = resolver(this.domains, Object.keys(obj.arbiters), obj.exts, this.npmTree, builtIns, serverModules); 26 | //TODO: need builtIns / serverModules for analysis purposes? 27 | this.buildTree(); 28 | } 29 | 30 | /** 31 | * dependencies 32 | * 33 | * Resolves all the dependencies of a file through detective 34 | * 35 | * @[in] absReq - absolute path to file 36 | * @[in] extraPath - path relative to domain path 37 | * @[in] dom - name of the domain extraPath is relative to 38 | * @return Array of resolver output 39 | * i.e. triples of form: [absPath, domain, isReal] 40 | */ 41 | Analyzer.prototype.dependencies = function (absReq, extraPath, dom) { 42 | // get 'before' sanitized code 43 | var code = this.before(this.compile(join(this.domains[dom], absReq))) 44 | , resolve = this.resolve; 45 | 46 | // absolutizes and locates everything immediately so that 47 | // we have a unique representation of each file 48 | 49 | // detective scanning each file is the most time consuming process on big codebases so we use the deputy caching layer 50 | //console.log('scanning', absReq) 51 | return this.deputy(code).map(function (req) { 52 | return resolve(req, extraPath, dom); 53 | }); 54 | }; 55 | 56 | /** 57 | * buildTree 58 | * 59 | * Calls this.dependencies recursively from the entry point 60 | * Stores this.tree in instance of form: 61 | * { 62 | * name : domain relative fileName 63 | * domain : domain name with entry in this.domains 64 | * extraPath : dirname of name 65 | * isReal : if true then join(this.domais[domainName], fileName) exists 66 | * deps : object of {uid : recursive this for each file} 67 | * parent : refers to parent structure one up from deps [root does not have this] 68 | * level : number of levels in (in terms of deps) we are [0 indexed] 69 | * } 70 | */ 71 | Analyzer.prototype.buildTree = function () { 72 | this.tree = { 73 | name : this.entryPoint 74 | , domain : 'app' 75 | , extraPath : '' 76 | , deps : {} 77 | , fake : 0 78 | , level : 0 79 | }; 80 | 81 | var circularCheck = function (t, uid) { 82 | var chain = [uid]; 83 | while (true) { // follow the branch up to verify we do not find self 84 | if (!t.parent) { 85 | return; 86 | } 87 | chain.push(t.domain + '::' + t.name); 88 | t = t.parent; 89 | if (chain[chain.length - 1] === chain[0]) { 90 | error("analysis revealed a circular dependency: " + chain.join(' <- ')); 91 | } 92 | } 93 | }; 94 | 95 | var build = function (t) { 96 | var resolveRes = this.dependencies(t.name, t.extraPath, t.domain); 97 | //console.log("resolved: ", JSON.stringify(resolveRes)); 98 | 99 | for (var i = 0; i < resolveRes.length; i += 1) { 100 | var triple = resolveRes[i] 101 | , name = triple[0] 102 | , domain = triple[1] 103 | , isReal = triple[2] 104 | , uid = domain + '::' + name; 105 | 106 | t.deps[uid] = { 107 | name : name 108 | , domain : domain 109 | , isReal : isReal 110 | , extraPath : path.dirname(name) 111 | , deps : {} 112 | , parent : t 113 | , level : t.level + 1 114 | }; 115 | if (isReal) { 116 | circularCheck(t, uid); // throw on circulars 117 | build.call(this, t.deps[uid]); // recurse 118 | } 119 | } 120 | }; 121 | build.call(this, this.tree); 122 | }; 123 | 124 | // print format helper 125 | function makeFormatter(extSuffix, domPrefix) { 126 | return function (ele) { 127 | var n = extSuffix ? ele.name : ele.name.split('.')[0]; 128 | if (ele.domain === 'npm') { 129 | n = path.basename(n); //TODO: improve this 130 | } 131 | else if (n.indexOf('index') >= 0 && path.basename(n) === n) { 132 | n = ''; // take out domain index requires to make print more readable 133 | } 134 | if (domPrefix) { 135 | n = ele.domain + '::' + n; 136 | } 137 | return n; 138 | }; 139 | } 140 | 141 | /** 142 | * print 143 | * constructs a prettified dependency tree 144 | * 145 | * @[in] extSuffix - true iff show extensions everywhere 146 | * @[in] domPrefix - true iff show domain prefixes everywhere 147 | * @return printable string 148 | */ 149 | Analyzer.prototype.print = function (extSuffix, domPrefix) { 150 | var ignores = this.ignoreDoms; 151 | var shapeFn = makeFormatter(extSuffix, domPrefix); 152 | var filterFn = function (el) { 153 | return (ignores.indexOf(el.domain) < 0); 154 | }; 155 | return topiary(this.tree, 'deps', shapeFn, filterFn); 156 | }; 157 | 158 | /** 159 | * sort 160 | * sorts the dependency tree by maximum require level descending 161 | * 162 | * @return Array of pairs [domain, name] where join(domain, name) exists 163 | */ 164 | Analyzer.prototype.sort = function () { 165 | var obj = {}; 166 | obj['app::' + this.entryPoint] = 0; 167 | 168 | var sort = function (t) { 169 | Object.keys(t.deps).forEach(function (uid) { 170 | var dep = t.deps[uid]; 171 | if (!dep.isReal) { 172 | return; 173 | } 174 | obj[uid] = Math.max(dep.level, obj[uid] || 0); 175 | sort(dep); // recurse 176 | }); 177 | }; 178 | sort(this.tree); 179 | 180 | return Object.keys(obj).map(function (uid) { 181 | return [uid, obj[uid]]; // convert obj to sortable array of [uid, level] pairs 182 | }).sort(function (a, b) { 183 | return b[1] - a[1]; // sort by level descending to get correct insertion order 184 | }).map(function (e) { 185 | return e[0].split('::'); // map to pairs of form [domain, name] 186 | }); 187 | }; 188 | 189 | 190 | module.exports = function (obj, before, compile, builtIns, serverModules) { 191 | var o = new Analyzer(obj, before, compile, builtIns, serverModules); 192 | return { 193 | print: function () { 194 | return o.print.apply(o, arguments); 195 | } 196 | , sort: function () { 197 | return o.sort.apply(o, arguments); 198 | } 199 | , npm: function () { 200 | //console.log(JSON.stringify(o.npmTree)); 201 | return o.npmTree; 202 | } 203 | }; 204 | }; 205 | 206 | -------------------------------------------------------------------------------- /docs/tinyapi.md: -------------------------------------------------------------------------------- 1 | # API 2 | Want more details about certain sections? There's a more [extensive API doc](api.html) available. 3 | 4 | ## modul8() and .compile() 5 | 6 | Statically analyze `entryFile` and its requirements recursively, 7 | and bundle up all dependencies in a browser compatible `targetFile`. 8 | 9 | var modul8 = require('modul8'); 10 | modul8(entryFile) 11 | //chain on options here 12 | .compile(targetFile); 13 | 14 | ## Options 15 | The following can be inserted verbatim as chained options on the modul8 16 | constructor, and a working `.compile()` call will end any further chaining. 17 | 18 | ### domains() 19 | Allow requiring code from namespaced domains, 20 | 21 | .domains() 22 | .add('shared', './shared') 23 | .add('internal', './internal') 24 | 25 | Or equivalently: 26 | 27 | .domains({ 28 | 'shared' : './shared' 29 | , 'internal' : './internal' 30 | }) 31 | 32 | Both would allow `require('shared::file')` to resolve on the client when 33 | `file.js` exists on `./shared`. 34 | 35 | Reserved domain names: 36 | 37 | - M8 38 | - data 39 | - external 40 | - npm 41 | 42 | ### data() 43 | Injects raw data in the `data` domain from the server. 44 | 45 | .data() 46 | .add('models', {user: {name: 'clux', type: 'String'}) 47 | .add('versions', "{'templates' : 123}") 48 | 49 | Or equivalently 50 | 51 | .data({ 52 | models : {user: {name: 'clux', type: 'String'}} 53 | , version : "{'templates' : 123}" 54 | }) 55 | 56 | Values will be serialized using `JSON.stringify` if they are not strings, 57 | otherwise, they are assumed to be serialized. 58 | 59 | ### use() 60 | Call with [Plugin](plugins.html) instance(s). 61 | 62 | .use(new Plugin()) 63 | 64 | ### analysis() 65 | View the analyzers prettified output somehow. 66 | 67 | .analysis() 68 | .output(console.log) 69 | .prefix(false) 70 | .suffix(true) 71 | .hide('external') 72 | 73 | Or equivalently: 74 | 75 | .analysis(console.log, false, true, 'external') 76 | 77 | #### output() 78 | Function or file to pipe to. 79 | #### prefix() 80 | Show the domain of each file in the dependency tree. Default true. 81 | #### suffix() 82 | Show the extension of each file in the dependency tree. Default false. 83 | #### hide() 84 | Call with domain(s) to hide from the tree. Default []. 85 | 86 | ### libraries() 87 | Pre-concatenate in an ordered list of libraries to the target output, 88 | or save this concatenation to a separate `libs` file. 89 | 90 | .libraries() 91 | .list(['jQuery.js', 'history.js']) 92 | .path('./app/client/libs/') 93 | .target('./libs.js') 94 | 95 | Or equivalently: 96 | 97 | .libraries(['jQuery.js', 'history.js'], './app/client/libs/', './libs.js') 98 | 99 | AltJS libraries are compiled with a safety wrapper, whenever 100 | the the registered language supports this. 101 | 102 | ### npm() 103 | Set the node_modules directory to allow requiring npm installed modules. 104 | 105 | .npm('./node_modules') 106 | 107 | ### before() 108 | Pre-process all code before it gets sent to analysis with input function(s). 109 | 110 | .before(function (code) { return code;}) 111 | 112 | `modul8.testcutter` is an example of such a function. 113 | 114 | ### after() 115 | Post-process all code after it has been bundled. 116 | 117 | .before(function (code) { return code; }) 118 | 119 | `modul8.minifier` is an example of such a function. 120 | 121 | ### set() 122 | Set a few extra options. Available options to set are: 123 | 124 | - `domloader` 125 | - `namespace` 126 | - `logging` 127 | - `force` 128 | 129 | #### set('domloader', value) 130 | `domloader` is the name of a global or arbitered function, or a direct substitution 131 | function, which wraps the application domain code and (usually) waits for the DOM. 132 | 133 | Examples values: 134 | 135 | - "jQuery" 136 | - "$(document).ready" 137 | - function (code) { return "jQuery(function () {" + code + "})"; } 138 | - "" // blank string - default 139 | 140 | The last example simply wraps the app domain code in an anonymous, 141 | self-executing funciton. 142 | 143 | #### set('namespace', value) 144 | The name of the global variable used in the browser to export console helpers to. 145 | The default value for this is `M8`. 146 | 147 | #### set('logging', value) 148 | This will set the amount of messages sent on the client. Allowed values: 149 | 150 | - 'ERROR' - only failed requires are logs **DEFAULT** 151 | - 'DEBUG' - additionally adds debug of what is sent to require 152 | - `false` - no client side logging 153 | 154 | ### logger() 155 | Pass down a [logule](https://github.com/clux/logule) sub to fully control server side log 156 | output. A passed down instance will attempt to call `.info()`, `.debug()` and `.error()`. 157 | 158 | Error messages are used to aid on throws, recommended kept unsuppressed. 159 | If not used, only debug messages are suppressed. 160 | 161 | ### in() 162 | Only do chainOfStuff() when __NODE_ENV__ matches environment. 163 | 164 | in(environment).chainOfStuff() 165 | 166 | To break out of the environment chain, use: `in('all')` or `in(otherEnv)`. 167 | 168 | ### register() 169 | Register an AltJS language with a compilation function. CoffeeScript can be supported using: 170 | 171 | .register('.coffee', function (code, bare){ 172 | coffee.compile(code, {bare: bare}) 173 | }) 174 | 175 | ### arbiters() 176 | Move browser globals to the require system safely. 177 | 178 | .arbiters() 179 | .add('jQuery', ['$', 'jQuery']) 180 | .add('Spine') 181 | 182 | Or with object style: 183 | 184 | .arbiters({ 185 | jQuery : ['$', 'jQuery'] 186 | Spine : 'Spine' 187 | }) 188 | 189 | Values are either a list of global names to alias under the key's name, or a single name to alias, 190 | or - when using the chaining add style - undefined to indicate same as key. 191 | 192 | ## Client API 193 | modul8 exports debug functions and extension functions on the single global variable configurable 194 | via set('namespace'). 195 | 196 | The following functions exists on this global in the browser only, illustrated with `ns` as the global. 197 | ### require() 198 | A console specific version of require can require relatively from the main `app` domain. 199 | 200 | ns.require('./fileOnAppRoot'); // exportObj of file 201 | 202 | ### data() 203 | Extend the data domain with a new key: 204 | 205 | ns.data('libX', libXobj); 206 | require('data::libX'); // -> libXobj 207 | 208 | Modify a key on the data domain: 209 | 210 | ns.data('libX', {}); 211 | require('data::libX'); // -> {} 212 | 213 | Delete a key on the data domain: 214 | 215 | ns.data('libX'); //unsets 216 | require('data::libX'); // -> undefined 217 | ### external() 218 | Has identical behaviour to `ns.data()`, but modifies the `external` domain, 219 | which is only modified on the server, wheras the `data` domain is initialized on the server. 220 | 221 | ### domains() 222 | List the domains initialized by modul8 223 | 224 | ns.domains(); // ['data', 'external', 'npm', 'app'] or similar 225 | 226 | ### inspect() 227 | Logs the contents of a specific domain by name 228 | 229 | ns.inspect('app'); // logs object of keys -> raw export object 230 | 231 | Can not be used to actually require things in the console. 232 | -------------------------------------------------------------------------------- /lib/bundler.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , path = require('path') 3 | , $ = require('autonomy') 4 | , analyzer = require('./analyzer') 5 | , persister = require('./persist') 6 | , typr = require('typr') 7 | , utils = require('./utils') 8 | , log = utils.logule 9 | , join = path.join 10 | , dir = __dirname 11 | , makeCompiler = utils.makeCompiler 12 | , exists = utils.exists 13 | , read = utils.read 14 | , error = utils.error 15 | , builtInDir = join(__dirname, '..', 'builtins') 16 | , builtIns = ['path', 'events'] 17 | , serverModules = ['fs', 'sys', 'util']; 18 | 19 | function id(a) { 20 | return a; 21 | } 22 | 23 | // helpers 24 | function anonWrap(code) { 25 | return "(function(){\n" + code + "\n}());"; 26 | } 27 | 28 | function makeWrapper(ns, fnstr, hasArbiter) { 29 | if (!fnstr) { 30 | return anonWrap; 31 | } 32 | var location = hasArbiter ? ns + ".require('M8::" + fnstr + "')" : fnstr; 33 | return function (code) { 34 | return location + "(function(){\n" + code + "\n});"; 35 | }; 36 | } 37 | 38 | // analyzer will find files of specified ext, but these may clash on client 39 | function verifyCollisionFree(codeList) { 40 | codeList.forEach(function (pair) { 41 | var dom = pair[0] 42 | , file = pair[1] 43 | , uid = dom + '::' + file.split('.')[0]; 44 | 45 | codeList.forEach(function (inner) { 46 | var d = inner[0] 47 | , f = inner[1] 48 | , uidi = d + '::' + f.split('.')[0]; 49 | 50 | if (!(d === dom && f === file) && uid === uidi) { 51 | error("two files of the same name on the same path will not work on the client: " + dom + "::" + file + " and " + d + "::" + f); 52 | } 53 | }); 54 | }); 55 | } 56 | 57 | // main application packager 58 | function bundleApp(codeList, ns, domload, compile, before, npmTree, o) { 59 | var l = [] 60 | , usedBuiltIns = npmTree._builtIns 61 | , len = 0; 62 | delete npmTree._builtIns; 63 | 64 | // 1. construct the global namespace object 65 | l.push("window." + ns + " = {data:{}, path:{}};"); 66 | 67 | // 2. attach path code to the namespace object so that require.js can efficiently resolve paths 68 | l.push('\n// include npm::path'); 69 | var pathCode = read(join(builtInDir, 'path.posix.js')); 70 | l.push('(function (exports) {\n ' + pathCode + '\n}(window.' + ns + '.path));'); 71 | 72 | // 3. pull in serialized data 73 | Object.keys(o.data).forEach(function (name) { 74 | return l.push(ns + ".data." + name + " = " + o.data[name] + ";"); 75 | }); 76 | 77 | // 4. attach require code 78 | var config = { 79 | namespace : ns 80 | , domains : Object.keys(o.domains) // TODO: remove npm from here 81 | , arbiters : o.arbiters 82 | , logging : o.logLevel 83 | , npmTree : npmTree 84 | , builtIns : builtIns 85 | , slash : '/' //TODO: figure out if different types can coexist, if so, determine in resolver, and on client 86 | }; 87 | 88 | l.push(anonWrap(read(join(dir, 'require.js')) 89 | .replace(/VERSION/, JSON.parse(read(join(dir, '..', 'package.json'))).version) 90 | .replace(/REQUIRECONFIG/, JSON.stringify(config)) 91 | )); 92 | 93 | // 5. include CommonJS compatible code in the order they have to be defined 94 | var defineWrap = function (exportName, domain, code) { 95 | return ns + ".define('" + exportName + "','" + domain + "',function (require, module, exports) {\n" + code + "\n});"; 96 | }; 97 | 98 | // 6. harvest function splits code into app and non-app code and defineWraps 99 | var harvest = function (onlyMain) { 100 | codeList.forEach(function (pair) { 101 | var dom = pair[0] 102 | , file = pair[1] 103 | , basename = file.split('.')[0]; 104 | if ((dom === 'app') !== onlyMain) { 105 | return; 106 | } 107 | var code = before(compile(join(o.domains[dom], file))); 108 | l.push(defineWrap(basename, dom, code)); 109 | }); 110 | }; 111 | 112 | // 7.a) include required builtIns 113 | l.push("\n// node builtins\n"); 114 | len = l.length; 115 | usedBuiltIns.forEach(function (b) { 116 | if (b === 'path') { // already included 117 | return; 118 | } 119 | l.push(defineWrap(b, 'npm', read(join(builtInDir, b + '.js')))); 120 | }); 121 | if (l.length === len) { 122 | l.pop(); 123 | } 124 | 125 | // 7.b) include modules not on the app domain 126 | l.push("\n// shared code\n"); 127 | len = l.length; 128 | harvest(false); 129 | if (l.length === len) { 130 | l.pop(); 131 | } 132 | 133 | // 7.c) include modules on the app domain, and wait for domloader if set 134 | l.push("\n// app code - safety wrapped\n\n"); 135 | domload(harvest(true)); 136 | 137 | // 8. use a closure to encapsulate the private internal data and APIs 138 | return anonWrap(l.join('\n')); 139 | } 140 | 141 | module.exports = function (o) { 142 | var persist = persister(o.options.persist, [o.target, o.libsOnlyTarget], log.get('debug')) 143 | , ns = o.options.namespace 144 | , dw = o.options.domloader 145 | , before = o.pre.length > 0 ? $.seq.apply({}, o.pre) : id 146 | , after = o.post.length > 0 ? $.seq.apply({}, o.post) : id 147 | , compile = makeCompiler(o.compilers) 148 | , forceUpdate = o.options.force || persist.objectModified(o); 149 | 150 | // deleting output should force re-compile 151 | forceUpdate |= !typr.isFunction(o.target) && !exists(o.target); 152 | 153 | if (!typr.isFunction(dw)) { // special domloader string need to be converted to fn 154 | dw = makeWrapper(ns, dw, dw in o.arbiters); 155 | } 156 | 157 | // do the recursive analysis 158 | var a = analyzer(o, before, compile, builtIns, serverModules); 159 | 160 | if (o.treeTarget) { 161 | var tree = a.print(o.extSuffix, o.domPrefix); 162 | if (typr.isFunction(o.treeTarget)) { 163 | o.treeTarget(tree); 164 | } else { 165 | fs.writeFileSync(o.treeTarget, tree); 166 | } 167 | } 168 | 169 | if (o.target) { 170 | var codeList = a.sort(); 171 | 172 | verifyCollisionFree(codeList); 173 | 174 | 175 | // persist ultimately uses dom::relFilePath as a key, and it always needs to.. 176 | // cant generalize it much 177 | var libsUpdated = false 178 | , appUpdated = persist.filesModified(codeList, o.domains, 'app') 179 | , c = after(bundleApp(codeList, ns, dw, compile, before, a.npm(), o)); // application code 180 | 181 | if (o.libDir && o.libFiles) { 182 | 183 | var libMap = o.libFiles.map(function (f) { 184 | return ['libs', f]; 185 | }); 186 | libsUpdated = persist.filesModified(libMap, {libs: o.libDir}, 'libs'); 187 | var libs; 188 | 189 | if (libsUpdated || (appUpdated && !o.libsOnlyTarget) || forceUpdate) { 190 | libs = after(o.libFiles.map(function (file) { 191 | return compile(join(o.libDir, file)); 192 | }).join('\n')); 193 | } 194 | 195 | var forceUpdateLibs = !typr.isFunction(o.libsOnlyTarget) && !exists(o.libsOnlyTarget); 196 | 197 | if (o.libsOnlyTarget && (libsUpdated || forceUpdateLibs) && !typr.isFunction(o.libsOnlyTarget)) { 198 | fs.writeFileSync(o.libsOnlyTarget, libs); 199 | log.info('compiling separate libs'); 200 | libsUpdated = false; 201 | } 202 | else if (typr.isFunction(o.libsOnlyTarget)) { 203 | o.libsOnlyTarget(libs); 204 | } 205 | else if (!o.libsOnlyTarget) { 206 | c = libs + c; 207 | } 208 | } 209 | 210 | if (typr.isFunction(o.target)) { 211 | return o.target(c); 212 | } 213 | 214 | if (appUpdated || (libsUpdated && !o.libsOnlyTarget) || forceUpdate) { 215 | log.info('compiling app'); 216 | fs.writeFileSync(o.target, c); 217 | } 218 | } 219 | }; 220 | 221 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Extensible CommonJS Code Packager and Analyzer [![Build Status](https://secure.travis-ci.org/clux/modul8.png)](http://travis-ci.org/clux/modul8) 2 | ## Intro 3 | 4 | Write a `main.js` as the application entry point 5 | 6 | ````javascript 7 | var determine = require('./determine'); 8 | console.log(determine.isCool(['clux', 'lava'])); 9 | ```` 10 | 11 | the required module `determine.js` 12 | 13 | ````javascript 14 | var cool = require('shared::cool'); // cross-domain require 15 | exports.isCool = function (input) { 16 | return input.filter(cool); 17 | }; 18 | ```` 19 | 20 | and finally its required `cool.js` on the `shared` domain [?](http://clux.github.com/modul8/docs/xcjs.html#modul8extensions) 21 | 22 | ````javascript 23 | module.exports = function (name) { 24 | return (name === 'clux'); 25 | }; 26 | ```` 27 | 28 | To compile these files invoke `modul8()` and chain on options 29 | 30 | ````javascript 31 | var modul8 = require('modul8'); 32 | 33 | modul8('./client/main.js') 34 | .domains({'shared': './shared/'}) 35 | .compile('./out.js'); 36 | ```` 37 | 38 | This will construct a single, browser compatible `out.js` in your execution path, and the generated dependency tree will look as follows: 39 | 40 | app::main 41 | └──┬app::determine 42 | └───shared::cool 43 | 44 | The shared code is independent of the application and **can be reused on the server**. 45 | 46 | Compilation can also be performed via the command line interface by typing 47 | 48 | ````bash 49 | $ modul8 client/main.js -p shared:shared/ > out.js 50 | ```` 51 | 52 | from the path containing the shared/ and client/ folders. 53 | 54 | To load the browser compatible output file from your site simply stick it in the HTML 55 | 56 | ````html 57 | 58 | ```` 59 | 60 | ## Quick Overview 61 | 62 | modul8 is an extensible CommonJS code packager and analyzer for JavaScript and AltJS web applications. 63 | Applications are recursively analyzed for dependencies from an entry point and will pull in + compile just what is needed. 64 | 65 | Code can be shared with the server by isolating modules/libraries in shared _domains_. This means stand alone logic 66 | can exist on the server and be referenced via a normal `require(dir + 'module')`, but also be referenced via `require('shared::module')` on the client. 67 | 68 | To give you full overview and control over what code is pulled in, modul8 automatically generates a per-file depedency tree. This allows 69 | fast analysis and identification of extraneous links, and becomes a very useful tool for refactoring. 70 | 71 | modul8 supports live extensions of certain exports containers via third party script loaders, and server side data injection at compile time. 72 | 73 | Lastly, modul8 aims to eliminate most global variables from your code. It does so using the following approaches 74 | 75 | - Encapsulate all exported data in the closure inhabited by `require()` 76 | - Incorporate globally available libraries into the module system via automatic arbiters 77 | 78 | Additionally, node modules can be required (if installed) as if on the server! 79 | To dive in properly; consult the [api docs](http://clux.github.com/modul8/docs/api.html). 80 | 81 | ## Features 82 | 83 | - highly extensible client side require 84 | - simple and safe code sharing between the server and the client 85 | - dynamic resolution and compilation of dependencies server-side 86 | - compatible with JavaScript, CoffeeScript or (configurable) AltJS languages 87 | - low footprint: <2kB (minified/gzipped) output size inflation 88 | - enforces modularity best practices and logs an npm style dependency tree 89 | - can inject data to require dynamically from the server or live from the client 90 | - can require npm installed modules 91 | - easy to write, modular plugins allows super easy client extensions with server logic and data 92 | - minimizes global usage, encapsulates exports in closures, absorbs library globals 93 | - only rebuilds on repeat calls if necessary (files modified || options changed) 94 | - ideal for single page web applications, 1 or 2 HTTP request to get all code 95 | 96 | ## Installation 97 | 98 | Install the library: 99 | 100 | ````bash 101 | $ npm install modul8 102 | ```` 103 | 104 | Install the command line tool: 105 | 106 | ````bash 107 | $ npm install -g modul8 108 | ```` 109 | 110 | Download the development version: 111 | 112 | ````bash 113 | $ git clone git://github.com/clux/modul8 114 | ```` 115 | 116 | ## Usage 117 | Basic use only only the path to the entry point and an output. 118 | 119 | ````javascript 120 | modul8('./client/app.js').compile('./out.js'); 121 | ```` 122 | 123 | This compiles everything referenced explicitly through `app.js` to the single browser compatible `out.js`. 124 | 125 | 126 | Every `require()` call is tracked and the resulting dependency tree is loggable. Cross domain `require()`s are namespaced 127 | C++ style: `require('shared::validation')` will look for a `.js` then `.coffee` file named `validation` on the shared domain. 128 | This extra domain must be configured using a chained `.domains()` call: 129 | 130 | ````javascript 131 | modul8('./client/app.js') 132 | .domains({'shared': './shared/'}) 133 | .compile('./out.js'); 134 | ```` 135 | 136 | To ensure that the `shared` domain here can work on the server and the client, any `require()` calls within domains 137 | should be relative and not pull in anything outside that folder. 138 | As an example, a same-origin require of `shared::defs` should be done with a **./** prefix: `require('./defs')`. 139 | 140 | The dependency analyzer will typically output something like this if configured 141 | 142 | app::app 143 | ├──┬app::controllers/user 144 | │ └───app::models/user 145 | ├──┬app::controllers/entries 146 | │ └───app::models/entry 147 | ├──┬shared::validation 148 | │ └───shared::defs 149 | └───M8::jQuery 150 | 151 | `jQuery` can be seemlessly integrated (and will show up in the dependency tree as above) by using `.arbiters()` 152 | 153 | ## Injecting Data 154 | 155 | Data can by injected at compile time from the server by specifying keys and serializable/pre-serialized data to be attatched on the specified key 156 | 157 | ````javascript 158 | modul8('./client/app.js') 159 | .data({'models': {'user':'clux'}}) 160 | .compile('./out.js'); 161 | ```` 162 | 163 | The `data` domain is initialized from the server with every key specified to `.data()`, but can be extended live on the client. 164 | The data API is particularly useful for web applications that needs particular application data to always be bundled. 165 | Anything that can be serialized (including pre-serialized javascript input) can be sent to the data domain. 166 | 167 | ## Using Plugins 168 | Extending the data domain in conjunction with creating specialized domains to handle that data, 169 | is a popular method that can be employed by node modules to break browser code down into more managable chunks - while linking them to the server. 170 | 171 | This is so useful that it has become the defacto plugin API. 172 | 173 | ````javascript 174 | modul8('./client/app.js') 175 | .use(new Plugin(opts)) 176 | .compile('./out.js'); 177 | ```` 178 | 179 | This will allow the Plugin to extend 'out.js' with data created in Plugin, as well as add a namespaced require domain on the browser. 180 | Using a Plugin will inflate 'out.js' by the size of the data it creates plus **only the size of the modules you explicitly `require()`**. 181 | 182 | Thus, adding plugins is a remarkably safe, monitorable, and robust way, to get discrete units of code - that shares logic with the server - to the client. 183 | 184 | Writing your own plugins is also really easy. Please share. 185 | 186 | ### Available Plugins 187 | 188 | - [m8-mongoose](https://www.github.com/clux/m8-mongoose) 189 | - [m8-templation](https://www.github.com/clux/m8-templation) 190 | 191 | ## External Injection 192 | 193 | Finally, modul8 defines an `external` domain for asynchronous script loaders to dump their results. This domain can only be used and extended from the client. 194 | 195 | Both the `data` and `external` domains are only allowed to be modified through safe proxies. Objects residing on these domains can be referenced 196 | with `require()` without messing up the compile time code analysis, but they can still show up in the dependency tree if desirable. 197 | 198 | ## Learn more 199 | 200 | The [full documentation site](http://clux.github.com/modul8) should contain everything you could ever want to know about modul8 and probably more. 201 | Read it, try it out, and give feedback if you like or hate it / parts of it, or if you want to contribute. 202 | 203 | modul8 is my first proper open source project. It was crafted out of necessity, but it has grown into something larger. 204 | Version 1.0 should be ready relatively soon - so the current code can be considered mostly stable. 205 | 206 | Version 0.10.0 and up should work fine with node v0.6. 207 | 208 | ## Compatibility 209 | Compiled code will work with ES5 compatible browsers (recent browsers minus Opera) 210 | If you target older browsers, include [ES5-shim](https://github.com/kriskowal/es5-shim). 211 | 212 | ## Running Tests 213 | 214 | Install development dependencies 215 | 216 | ````bash 217 | $ npm install 218 | ```` 219 | 220 | Run the tests 221 | 222 | ````bash 223 | $ npm test 224 | ```` 225 | 226 | modul8 is actively tested with the latest node 0.6 branch. 227 | Many thanks to Travis-CI. 228 | 229 | ## License 230 | 231 | MIT Licensed - See LICENSE file for details 232 | -------------------------------------------------------------------------------- /docs/xcjs.md: -------------------------------------------------------------------------------- 1 | # Extended CommonJS 2 | 3 | This is going to contain more advanced background about what a general 4 | module systems do, and, finally, 5 | what distinguishes modul8 from plain CommonJS bundler. 6 | 7 | ## CommonJS Parsing 8 | Or, how a module system works. 9 | 10 | ### JavaScript Modules 11 | 12 | JavaScript has no module system. 13 | 14 | _we're off to a great start.._ 15 | 16 | On the other hand, got functions. Functions with closures. 17 | 18 | (function(){ 19 | var private = 5; 20 | window.publicFn = function(){ 21 | console.log(private); 22 | } 23 | })(); 24 | 25 | This is the commonly employed method of encapsulating and exposing objects and functions that can reference private variable through a closure. 26 | This works; `private` is inaccessible outside this anonymous function. 27 | 28 | Unfortunately, this just exposes publicFn to the global window object. This is not ideal, as anything, anywhere can just reference it, leaving 29 | us not much wiser. True modularity is clearly impossible when things are just lying around freely like this for everyone. It is fragile, and 30 | it is error prone as conflicting exports will actually just favour the last script to execute - as JavaScript simply runs top to bottom, attaching its 31 | exports to window as we go along. Clearly we need something better than this. 32 | 33 | ### CommonJS Idea 34 | 35 | There is a way to fix this, but first of all it assumes all modules need to support a stadardised format for exporting of modules. 36 | CommonJS is a such a standardization. It has very large traction at the moment, particularly driven by server side environments such as NodeJS. 37 | 38 | Its ideas are simple. Each module avoids the above safety-wrapper, must assume it has a working `require()`, 39 | and instead of attaching its exports to a global object, it attaches them to an opaque `exports` object. 40 | Alternatively, it can replace the `module.exports` object to define all your exports at once. 41 | 42 | By making sure each module is written this way, CommonJS parsers can implement clever trickery on top of it to make this behaviour work. 43 | I.e. having each module's exports objects stored somewhere for `require()` and allocating a singleton for each module. 44 | 45 | ### CommonJS Basics 46 | 47 | From the above rationale, it is clear that a CommonJS parser must turn this: 48 | 49 | var private = 5; 50 | var b = require('b'); 51 | exports.publicFn = function(){ 52 | console.log(private); 53 | }; 54 | 55 | into something like this: 56 | 57 | var module = {}; 58 | (function(require, module, exports){ 59 | var private = 5; 60 | var b = require('b'); 61 | exports.publicFn = function(){ 62 | console.log(private); 63 | }; 64 | })(makeRequire(location), module, stash[location]) 65 | if (module.exports) { 66 | delete stash[location]; 67 | stash[location] = module.exports; 68 | } 69 | 70 | where `location` is a unique identifier passed down from the compiler to indicate where the module lives, so that `require()` can later retrieve it. 71 | The `makeRequire()` factory must be able to construct specifically crafted `require()` functions for given locations. 72 | Finally, `stash` will be a pre-defined object on which all modules are exported. 73 | 74 | Wrapping up this behaviour inside a function, we can write something like this. 75 | 76 | define(location, function(require, module, exports) { 77 | var private = 5; 78 | var b = require('b'); 79 | exports.publicFn = function(){ 80 | console.log(private); 81 | } 82 | }); 83 | 84 | The `makeRequire()` and `define()` functions can cleverly be defined inside a closure with access to `stash`. This way only these functions can access your modules. 85 | 86 | If the module system simply created a global namespace for where your modules resided, say, `stash = window.ModuleSystem`, then this would be **bad**. 87 | You could still bypass the system and end up requiring stuff implicitly again. 88 | 89 | modul8 encapsulates such a `stash` inside a closure for `require()` and `define()`, so that only these functions + a few carefully constructed functions to 90 | debug export information and require strings. 91 | 92 | #### Code Order 93 | Now, a final problem we have glossed over is which order the modules must be included in. The module above requires the module `b`. 94 | What happens if this module has not yet been placed in the document? Syntax error. The indepentent modules must be included first. 95 | 96 | To solve this problem, you can either give a safe ordering yourself - which will become increasingly difficult as your application grows in size - 97 | or you can resolve `require()` calls recursively to create a dependency tree. 98 | 99 | modul8 in particular, does so via the excellently simple `detective` module 100 | that constructs a full Abstract Syntax Tree before it safely scans 101 | for `require()` calls. 102 | Using `detective` data, a tree structure representing the dependencies 103 | can be created. modul8 allows printing of a prettified form of this tree. 104 | 105 | app::main 106 | ├───app::forms 107 | ├──┬app::controllers/user 108 | │ └──┬app::models/user 109 | │ └───app::forms 110 | ├──┬app::controllers/entries 111 | │ └───app::models/entry 112 | └──┬shared::validation 113 | └───shared::defs 114 | 115 | It is clear that the modules on the edges of this tree must get required 116 | first, because they do not depend on anything. And similarly, the previous 117 | level should be safe having included the outmost level. 118 | Note here that `app::forms` is needed both by `app:moduls/user` and 119 | `app::main` so it must be included before both. 120 | Thus, we only care about a module's outmost level. 121 | 122 | To order our modules correctly, we must therefore reduce the tree 123 | into an unique array of modules and their (maximum) level numbers, 124 | and simply sort this by their level numbers descending. 125 | 126 | ## modul8's CommonJS Extensions 127 | ### Require Path Problem 128 | Whilst maintaining compatibility with the basic CommonJS spec, we have 129 | extended `require()` to ameliorate one common problem. 130 | 131 | - `require()` calls is a clash prone property look-up on `stash[reqStr]` 132 | 133 | We wanted to be able to share code between the server and the client by 134 | essentially having multiple _require paths_. But require paths force you 135 | to scan all of them, with no way of specifying what path to do your 136 | look-up on. It also would make it very difficult to whitelist injected 137 | data from the server resolver - as it could simply find files 138 | with the same names as your data somewhere else. 139 | 140 | The relation between the paths are also lost on the browser, 141 | so there is no sense in maintining any illusions about this by using 142 | traditional require paths. 143 | 144 | ### Domains 145 | In the end, namespacing each path became the accepted solution. 146 | To distinguish them from typical require paths, we refer to them 147 | as _domains_ or _require domains_. 148 | 149 | This also simplifies implementation as well, as we can create one object 150 | container directly on `stash` for each domain with key equal to its name. 151 | 152 | Additionally, we can make `require()` functions that know which domains 153 | to look in by passing this extra parameter from the compiler down 154 | to `define`. 155 | 156 | The result, is that with modul8, we can `require()` files relatively as if 157 | it was on a 100% CommonJS environment, but we could also do cross-domain 158 | `require()` by using C++ style namespacing. I.e. calls 159 | like `require('shared::helper.js')` will give access to code on a 'shared' 160 | domain. 161 | 162 | To get the most out of this deal, having certain domains be completely 163 | server and client agnostic necessary: 164 | Code on reusable domains must not reference something from outside 165 | its base directory to work on the client (including npm modules), 166 | and it should not reference DOM/client specific elements to work 167 | on the server. 168 | 169 | Domains also provide 3 more areas of use that each get their own 170 | reserved domain: 171 | 172 | #### Arbiters 173 | modul8 hates globals. They ruin otherwise solid modularity. 174 | Thus, it desperately tries to integrate globally exported libraries 175 | into its require system. With your permission, it removes the global 176 | shortcut(s) from your application code and inserts them onto the reserved 177 | `M8` domain. Why we (can and sometimes) want to do this is explained in the 178 | [modularity doc](modularity.html), whilst the feature itself is fully 179 | documented in the [API doc](api.html). 180 | 181 | #### node modules 182 | The `npm` domain is really a domain with you local node modules folder as 183 | its root. It's, however, heavily special cased to deal with absolute 184 | requires internal to that domain. This means you can use a lot of npm 185 | installable modules right out of the box and with full control 186 | (via the logged dependency tree), over what is included. 187 | Usage is documented in the [npm doc](npm.html) 188 | 189 | #### Live Extensions 190 | Because we have a `require()` function available in all the application 191 | code, and because this is synchronous 192 | (in the sense that it has been resolved on the server already), 193 | we migth want to extend our requiable data with results from third-party 194 | asynchronous script loaders. There's an `external` domain for that, 195 | and a client API for it. It's documented in the [API doc](api.html). 196 | 197 | #### Direct Extension 198 | Finally, modul8 allows exporting of data that exists on the server, 199 | without having to add separate script tags for them. 200 | The `data` domain contains all such data, and like all the above, 201 | it can be gotten with `require()`. 202 | The [API doc](api.html) contains the how-to. 203 | -------------------------------------------------------------------------------- /docs/modularity.md: -------------------------------------------------------------------------------- 1 | # Modularity 2 | 3 | This document contains some basic advice for how to achieve modularity, 4 | then it goes into more advanced ideas as you scroll. 5 | At the bottom lie some information on arbiters. 6 | 7 | ## Welcome to Italy 8 | 9 | It here is one thing you learn quickly in programming, it is this: 10 | 11 | - Spaghetti code is awful 12 | 13 | It is awful to read, but it is even worse to modify or maintain. 14 | 15 | ## What is Bad Code 16 | 17 | Without rehashing the entire internet: 18 | **tightly coupled code is bad code**. Because 19 | 20 | - The more tightly coupled your modules become, 21 | the more side-effects alterations will have 22 | and the harder it will be to reuse that module (violating DRY) 23 | - The more different behaviour per module, 24 | the harder it is to to maintain that module. 25 | - The more one type of behaviour is spread out into different modules, 26 | the harder it is to find the source of that behaviour. 27 | 28 | ## What is Good Code 29 | 30 | #### What it is not: Bad Code 31 | If tightly coupled code is bad code, then good code is loosely coupled. 32 | A ⇒ B ∴ ¬B ⇒ ¬A. 33 | 34 | In other words, if you factor out your behaviour into small separate units 35 | of behaviour, you will have gained maintainability and readibility 36 | properties for free, and your code will inevitably have less unknown 37 | side-effects, leading to more secure code as well. It does, however, 38 | take certain disipline to constantly police your files for multiple types 39 | of behaviour. 40 | 41 | You may shrug and say, well, I'm only going to write this once anyway.. 42 | 43 | ..and you will be right. You will write it once and likely wish you wrote 44 | it zero times. 45 | 46 | In my opinion, the biggest mistake you can make as a learning programmer 47 | is to not bothering to factor out behaviour as early as possible. 48 | ** 49 | 50 | 51 | ## Best Practices 52 | 53 | modul8 provides the means to separate your code into different files 54 | effectively, but how do you as a developer split up your behaviour? 55 | 56 | One of the hardest areas to modularize web applications is the client 57 | application domain. If you are using jQuery, you should be particularly 58 | familiar with this. `$` selector calls all around, 59 | DOM insertion & manipulation code in random places, 60 | identical behaviour existing for each URL. If this is a problem for you, 61 | read on. 62 | 63 | ### Decouping MVC code 64 | If you have tried the one size fits all approach to modularity and 65 | try to cram your project into a MVC/MVVM structure, you may have had 66 | success, or you might have hated it. At any rate, here is some general 67 | ideas to consider if you are currently using or planning to use an MVC framework. 68 | 69 | **Independent entry controller** 70 | 71 | - A base point should be some sort of _controller_ which requires all outside controllers 72 | - This centralized point should control major events on your site (like going to a new page), delegating to the other main controllers 73 | - Nothing should require the main app controller (otherwise you get circulars, keep it simple) 74 | 75 | **Controllers** 76 | 77 | - All other contollers should control events specific to the data type they control 78 | - Each controller should have a corresponding model - which the app might not need to know about 79 | 80 | **Models** 81 | 82 | - This contents of a model may not depend on jQuery (wheras the actual abstract Model class might) 83 | - The model should contain logic to get information about this data type (i.e. maybe from one database table via ajax - which should be built in to the base class) 84 | 85 | **Extras** 86 | 87 | - Templates should have their own model and controller. The model can store them in LocalStorage or fetch them from server, but they can come bundled with modul8's output as well 88 | - Validation logic should exist in the model and should be based on validation rules used on the server - so some data should be passed down to ensure this logic is in sync at all times 89 | - Extra HTML helper logic should have its own module (possibly even DOM independent) 90 | 91 | ### DOM Decoupling 92 | This section is very relevant if you are just using jQuery, or if you are 93 | trying to roll your own application structure rather than using MVC. 94 | 95 | The general jQuery advice is that you think about the behaviour you are 96 | defining. Here are 4 general rules to go on for certain behaviour: 97 | 98 | - non-request based DOM interactivity 99 | ⇒ write a plugin 100 | - request based DOM interactivity 101 | ⇒ use controllers/eventmanagers to handle events, and call above plugin(s) 102 | - calculations needed before DOM manipulation 103 | ⇒ make a standalone module (in a domain / npm) and call it at above stages. 104 | - DOM interactivity changing styles 105 | ⇒ add a className to relevant container, and keep the style in the CSS 106 | 107 | This way if something breaks, you should be able to narrow down the problem to 108 | a UI error (code or style), a signaling error, or a calculation error. 109 | ⇒ Debugging becomes up to 4 times easier. 110 | 111 | ### General Modularity 112 | 113 | modul8 just tries to facilitate the building of maintainable code. 114 | To actually do so, you need to always stay vigilant and remember to: 115 | 116 | - Not blend multiple types of behaviour together in one file. 117 | - Limit the areas from which you reference global variables. 118 | - Look for opportunities to move independent code onto different domains. 119 | - Look for opportunities to refactor code to make bits of it independent. 120 | - Enforce basic rules of JavaScript modularity: don't try to make 121 | circular dependencies work, analyse your require tree. If you are requiring 122 | the same libraries from every file, chances are you are doing something wrong. 123 | 124 | Decouple your code this way and you will save yourself the trouble of later 125 | having to learn from your mistakes the hard way. 126 | 127 | ## Going Further 128 | 129 | ### Domains or Node Modules? 130 | Say you have factored out a piece of behaviour from your client side app. 131 | Where do you put it? Do you put it 132 | 133 | - in a folder under the client root 134 | - in a separate domain 135 | - make a node module out if it 136 | 137 | This are increasingly flexible depending on what you want to do. 138 | If you want to only use it on the client, you should keep it in the client root 139 | so that it is immediately obvious to anyone reading your code where it works. 140 | 141 | If you want to use it on the server, you have to use one of the other approaches. 142 | A shared domain is great if you have completely server and client agnostic code. 143 | This is what extra domains were designed for. One of the key reasons domains were 144 | pushed by modul8 is that having a 'shared' folder next to your 'client' and 145 | 'server' folders screams to the reader that this code should work anywhere 146 | and must be treated with respect. This is good. 147 | 148 | Sometimes however, you need extra npm functionality on either end. 149 | This is possible to achieve, but only if it is itself a node module. 150 | A node module can be required on the client if it 151 | sufficiently server agnostic. The downside to placing your code in here is that 152 | it gives no indication to the people reading your code that it will work on 153 | the client. On the other hand, it might be easier to reuse across projects when 154 | things are checked into npm. Use with caution, but it's worth taking advantage of. 155 | 156 | ### Killing Globals 157 | 158 | Global variable are evil, and should be kept to a minimum. 159 | We know this, and this is were a require system really shines, 160 | but you are generally going to depend on a few global variables. 161 | Not all libraries are CommonJS compliant, and having jQuery plugins showing up 162 | in your dependency tree under every branch that requires jQuery might 163 | just make things more confusing than to load them classically. 164 | 165 | Besides, you may want to load it in from a separate CDN anyway. 166 | 167 | Even in such an environment, it is possible rid yourself of the global 168 | `$` and `jQuery` symbols without breaking everything. 169 | 170 | We will demonstrate such a solution. Begin by constructing a `jQuery.js` 171 | file on your application domain containing: 172 | 173 | module.exports = window.jQuery; 174 | delete window.jQuery; 175 | delete window.$ 176 | 177 | With this you can put `var $ = require('jQuery')` so everything will be explicitly 178 | defined on the application domain. You've also deleted the global shortcuts so that 179 | you will know when you forgot to require. 180 | Finally, jQuery (but none of its dependencies) show up in the dependency tree - 181 | so you will quickly identify what code is actually DOM dependent 182 | , and what isn't or shouldn't be. Clearly this is advantageous. 183 | 184 | Having found this pattern very useful, but also noticing how repeating this pattern 185 | on several libraries pollutes our application code folder with meaningless files, 186 | a modul8 extension was created early on to allow automatic creation of these 187 | arbiters in the internal module system by using the `arbiters()` call. 188 | 189 | This example could be automated by 190 | chaining on `arbiters().add('jQuery', ['jQuery', '$'])`. 191 | See the [API docs](api.html) for more details. 192 | 193 | ### DOM Dependence 194 | Note that modul8 only allows one domain to be DOM dependent 195 | (the application domain), and from the start you are likely going to have 196 | `require('jQuery')` or similar in many places there. 197 | 198 | However, if you just find some areas that do not use it, and as a result move 199 | logic to an environment agnostic domain, then it this is a great success. 200 | Your jQuery dependent code is diminished, so the more tightly coupled code 201 | of your application is smaller. 202 | 203 | If you can efficiently separate code on the domain level, try to keep above 204 | advice in mind; always aim to factor out behavior into small loosely coupled 205 | modules. If you can do this then you are already well on your way to resolving 206 | spaghetti hell. The rest is mostly getting the correct signaling model for your 207 | events to your controllers/controller style entities. 208 | 209 | Good luck. 210 | Hopefully this has been useful on some level : ) 211 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | TODO 2 | ================== 3 | * support windows paths in require.js - include path.win32.js? 4 | * npm tests 5 | * better handling of analysis elements from npm domain 6 | 7 | 8 | 0.17.2 / 2012-07-07 9 | =================== 10 | * 0.6 compat fixed properly 11 | 12 | 0.17.1 / 2012-07-04 13 | =================== 14 | * forgot to use latest logule 15 | 16 | 0.17.0 / 2012-07-04 17 | ================== 18 | * maintenance release to support 0.8 properly 19 | * testability using tap rather than expresso, dependency cleanups and slight code cleaning 20 | * type testing bit moved into the `typr` module 21 | * In hindsight, this should have been `0.16.1`, should not break anything - sorry 22 | 23 | 0.16.0 / 2012-02-25 24 | ================== 25 | * logging now uses one interface for server side logging and one for client side logging 26 | * set('logging', level) now ignores level set as info or warn, only error and debug work (->docs) 27 | * set('logging') controls client side logging for debug only, whereas .logger() controls server logging fully (->docs) 28 | * all crashes now produce prettified output to easier identify what went wrong if something did during compilation 29 | * New slim API docs! 30 | * Modularity doc updated to include some domain vs npm ideas 31 | * Error messages are stylized to highlight the actual problem instead of just throwing 32 | 33 | 0.15.3 / 2012-01-28 34 | ================== 35 | * Factor tree printing code out to a separate module and use that: topiary 36 | * Allow node modules to not include extensions on package.json's main 37 | * 0.15.2 got lost to an accidental ctrl-c mid publish 38 | 39 | 0.15.1 / 2012-01-24 40 | ================== 41 | * Better output formatting 42 | * Fixed hickup in resolver code which failed to resolve properly nested npm modules 43 | to inject suppressed logule sub to control server output 44 | 45 | 0.15.0 / 2012-01-08 46 | ================== 47 | * require parsing is now done with substack's caching layer around detective: deputy - NB: greatly speeds up analysis on large code bases 48 | * proper npm module support (see new npm docs) 49 | * separate libs file is now also recompiled on deletion 50 | * analysis().hide('domainName') now works [regression] 51 | * Coffee-Script has to be registered [only works programmatically and not via CLI] 52 | * plugin usage via CLI is deprecated - was a bad and clunky way of doing things 53 | 54 | 0.14.2 / 2011-12-07 55 | ================== 56 | * Various refactoring and test improvements (travis build now succeeds from blank install on both 0.4 and 0.6) 57 | * Use logule@0.5.3 to reduce dependency graph a little 58 | 59 | 60 | 0.14.1 / 2011-12-01 61 | ================== 62 | * Fix failing CommonJS guards checking for module.exports when we used to expect it to be set if singular export. 63 | * If module.exports has own properties, or is no longer an object at the end a file, then everything attached to exports will be discarded. 64 | * Use logule@0.5.2's verify functionality to ensure correctly passed in logule instance 65 | 66 | 0.14.0 / 2011-11-27 67 | ================== 68 | * Updated to newer logule 69 | * Optionally pass down a logule sub (or something else with compatible API) as an optional way of defining log output 70 | * npm test in npm 1.0.106 works from scratch, 1.1.0 alphas need to manually clone test dependencies for now 71 | * Simple example syntax error fixed 72 | 73 | 0.13.1 / 2011-11-24 74 | ================== 75 | * Logger moved out of modul8 to clux/logule 76 | * Persistance logic moved to its own file 77 | * Persister stores the states in a config file in configurable directory rather than one file per output in the states directory 78 | * Bug in CLI caused libs not to be compiled when going via stdout 79 | 80 | 0.13.0 / 2011-11-22 81 | ================== 82 | * data API for strings now only take code strings (pre-serialized values) 83 | * tests now pass with node 0.6.1 using `npm test` 84 | * tests (when npm installed) now include vital file 85 | * Plugin::name can be a simple key (cleaner Plugin API) 86 | * CLI allows concatenating on libraries like the main API (but without the separate libraries().target() option available) 87 | * Existing CLI API modified to be consistent with itself: Delimiters are querystring style. 88 | * CLI test module included in test/ 89 | * Fixed bug causing collision testing to not be strict enough 90 | * Fixed bug causing arbiters with globals different to its name to not resolve on the client 91 | * Bundled require code passes through JSLint 92 | * Domains can be required with simply `require('dom::')` if an index file is present on the domain 93 | * If compile target is deleted, forceUpdate output regardless of cached states 94 | 95 | 0.12.0 / 2011-11-13 96 | ================== 97 | * Plugin API more semantic, name, data, domain methods all returning singles rather than pairs and triples. 98 | * Deprecates m8-templation <0.2.0 and m8-mongoose <0.2.0 (newer versions released simultaneously - and plugin API expected to freeze soon) 99 | * Tests for Plugin interface 100 | 101 | 0.11.2 / 2011-11-13 102 | ================== 103 | * Better package.json future proofing of build breaking updates from dependencies 104 | 105 | 0.11.0 / 2011-11-09 106 | ================== 107 | * functions passed to `.data()` will self-execute on the client (the fn.toString representation) - to be used with caution 108 | * strings passed in to `.data()` will no longer assumed to be code strings, they will be strings 109 | * strings passed in to `.data().add()` when third parameter is set to true, it will be assumed to be a code string 110 | * Ditto for plugin API (docs updated) 111 | 112 | 0.10.1 / 2011-11-08 113 | ================== 114 | * functions passed to `.data()` will be serialized using fn.toString() - to be used with caution 115 | * Plugin documentation tweaks 116 | * More sensibly, the logging option on the CLI can set the level, so that the unset default corresponds to the API default. 117 | 118 | 0.10.0 / 2011-11-07 119 | ================== 120 | * Data functions are executed in the interface rather than in the last step 121 | * Plugin interface via `.use()` 122 | * Documentation of Plugins + two quick plugins introduced 123 | * `.data()` no longer uses pull functions but expects the data directly - it will serialize itself if needed - can take objects,arrays,numbers or serialized JavaScript 124 | * Server side logging now includes a socket.io style logger class 125 | * Better documentation of logging 126 | * node v0.6.0 shown to work (although some tests segfaults) 127 | 128 | 0.9.3 / 2011-10-30 129 | ================== 130 | * Big documentation improvements 131 | * Intelligent whitespace added to output code when not minified to make it more readible 132 | 133 | 0.9.2 / 2011-10-30 134 | ================== 135 | * Logging level defaults to ERROR (CLI still has to do -l for this) 136 | * External extensions bug in 0.9.<2 137 | 138 | 0.9.1 / 2011-10-29 139 | ================== 140 | * Logging bug fixed 141 | 142 | 0.9.0 / 2011-10-29 143 | ================== 144 | * Logging now has levels - defaults to false, -l flag in CLI sets to ERROR level 145 | * Fixed a bug causing global install not to resolve all dependencies for CLI 146 | * Write the client side code manually in JavaScript for client side readibility 147 | * No longer passing data in to the require closure from outside - simply inject it with RegExps 148 | * Relative requires a little more flexible (../ prefix allowed vs old ./../) 149 | 150 | 0.8.0 / 2011-10-29 151 | ================== 152 | * `.domains()` call no longer required - application domain inferred from entrypoint 153 | * entry point must be specified to modul8(entry) WITH a path (relative or absolute) - 154 | as opposed to just specifying filename and inferring its path from the main domain 155 | * Fixed a bug in 0.7.0 where app would not recompile even if app files had been modified 156 | 157 | 0.7.0 / 2011-10-28 158 | ================== 159 | * Command Line Interface - documented under CLI 160 | * recompiling now happens if settings were changed as well (bug) 161 | * move underscore copied snippets out of src - require underscore instead 162 | * domloader API simplified to work with CLI, also now defaults to anonymous fn rather than jQuery domloader 163 | * `.compile()` will not recompile the file if no changes have been made to previously included files 164 | * modified test suite included to ensure above works 165 | * arbiter test suite included 166 | * `.analysis.hide(domain)` was not working correctly 167 | * server side resolver was ignoring resource names on other domains when clashing with arbiters 168 | 169 | 0.6.1 / 2011-10-18 170 | ================== 171 | * `.arbiters()` allows an object to be inserted at once 172 | * Biggish documentation improvements 173 | 174 | 0.6.0 / 2011-10-17 175 | ================== 176 | * `require('folder')` will look for a `folder` file then an `index` file under `folder/` 177 | * `require('folder/')` will look for an `index` file under `folder/` 178 | * `require()` collision priority updated 179 | * collision test suite included 180 | * `.compile()` will throw error if multiple files with same unique identifier (extensionless dom::filename) are attempted included - but helpfully after `.analysis()` 181 | * `.register('.ext', extCompiler)` will allow bundling of other altJs languages 182 | 183 | 0.5.0 / 2011-10-14 184 | ================== 185 | * `.data()` and `.domains()` now both can take objects directly instead of adding 186 | * `.libraries()` can be specified without all the 3 sub-calls, just specify all htree parameters direcly on this instead 187 | * `.analysis().ignore(domain)` can be used to supress certain domains from printed depedency tree (perhaps good to hide `external` or `M8`) 188 | 189 | 0.4.0 / 2011-10-13 190 | ================== 191 | * Better documentation + examples bundled 192 | * Fixed a collision bug causing same folder structure to be ignored by the bundler in one branch 193 | * Fixed a bug in the circular checker not correctly matching + no longer hanging on cirtain curculars 194 | * Loggability of requires on the client works as in the documentation 195 | * `M8.domains()` now returns a list of strings instead of console.logging it 196 | * 'M8.data()' and 'M8.external()' does not return 197 | * Configured a basic test environment using zombiejs 198 | * Safed up API against subclass calls against on superclass. 199 | 200 | 0.3.0 / 2011-10-08 201 | ================== 202 | 203 | * Full documentation 204 | * `arbiters()` added 205 | 206 | ================== 207 | modul8 was never advertised before this point 208 | ================== 209 | 210 | 0.2.2 / 2011-10-04 211 | ================== 212 | 213 | * Fix a define and a require bug 214 | 215 | 0.2.0 / 2011-10-03 216 | ================== 217 | 218 | * Initial commit on the new name 219 | * Style bundling factored out to a separate module 220 | 221 | 0.1.0 / 2011-09-20 222 | ================== 223 | 224 | * Initial commit on brownie 225 | -------------------------------------------------------------------------------- /examples/minimal/output.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | window.M8 = {data:{}, path:{}}; 3 | 4 | // include npm::path 5 | (function (exports) { 6 | function filter (xs, fn) { 7 | var res = []; 8 | for (var i = 0; i < xs.length; i++) { 9 | if (fn(xs[i], i, xs)) res.push(xs[i]); 10 | } 11 | return res; 12 | } 13 | 14 | // resolves . and .. elements in a path array with directory names there 15 | // must be no slashes, empty elements, or device names (c:\) in the array 16 | // (so also no leading and trailing slashes - it does not distinguish 17 | // relative and absolute paths) 18 | function normalizeArray(parts, allowAboveRoot) { 19 | // if the path tries to go above the root, `up` ends up > 0 20 | var up = 0; 21 | for (var i = parts.length; i >= 0; i--) { 22 | var last = parts[i]; 23 | if (last == '.') { 24 | parts.splice(i, 1); 25 | } else if (last === '..') { 26 | parts.splice(i, 1); 27 | up++; 28 | } else if (up) { 29 | parts.splice(i, 1); 30 | up--; 31 | } 32 | } 33 | 34 | // if the path is allowed to go above the root, restore leading ..s 35 | if (allowAboveRoot) { 36 | for (; up--; up) { 37 | parts.unshift('..'); 38 | } 39 | } 40 | 41 | return parts; 42 | } 43 | 44 | // Regex to split a filename into [*, dir, basename, ext] 45 | // posix version 46 | var splitPathRe = /^(.+\/(?!$)|\/)?((?:.+?)?(\.[^.]*)?)$/; 47 | 48 | // path.resolve([from ...], to) 49 | // posix version 50 | exports.resolve = function() { 51 | var resolvedPath = '', 52 | resolvedAbsolute = false; 53 | 54 | for (var i = arguments.length; i >= -1 && !resolvedAbsolute; i--) { 55 | var path = (i >= 0) 56 | ? arguments[i] 57 | : process.cwd(); 58 | 59 | // Skip empty and invalid entries 60 | if (typeof path !== 'string' || !path) { 61 | continue; 62 | } 63 | 64 | resolvedPath = path + '/' + resolvedPath; 65 | resolvedAbsolute = path.charAt(0) === '/'; 66 | } 67 | 68 | // At this point the path should be resolved to a full absolute path, but 69 | // handle relative paths to be safe (might happen when process.cwd() fails) 70 | 71 | // Normalize the path 72 | resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { 73 | return !!p; 74 | }), !resolvedAbsolute).join('/'); 75 | 76 | return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; 77 | }; 78 | 79 | // path.normalize(path) 80 | // posix version 81 | exports.normalize = function(path) { 82 | var isAbsolute = path.charAt(0) === '/', 83 | trailingSlash = path.slice(-1) === '/'; 84 | 85 | // Normalize the path 86 | path = normalizeArray(filter(path.split('/'), function(p) { 87 | return !!p; 88 | }), !isAbsolute).join('/'); 89 | 90 | if (!path && !isAbsolute) { 91 | path = '.'; 92 | } 93 | if (path && trailingSlash) { 94 | path += '/'; 95 | } 96 | 97 | return (isAbsolute ? '/' : '') + path; 98 | }; 99 | 100 | 101 | // posix version 102 | exports.join = function() { 103 | var paths = Array.prototype.slice.call(arguments, 0); 104 | return exports.normalize(filter(paths, function(p, index) { 105 | return p && typeof p === 'string'; 106 | }).join('/')); 107 | }; 108 | 109 | 110 | exports.dirname = function(path) { 111 | var dir = splitPathRe.exec(path)[1] || ''; 112 | var isWindows = false; 113 | if (!dir) { 114 | // No dirname 115 | return '.'; 116 | } else if (dir.length === 1 || 117 | (isWindows && dir.length <= 3 && dir.charAt(1) === ':')) { 118 | // It is just a slash or a drive letter with a slash 119 | return dir; 120 | } else { 121 | // It is a full dirname, strip trailing slash 122 | return dir.substring(0, dir.length - 1); 123 | } 124 | }; 125 | 126 | 127 | exports.basename = function(path, ext) { 128 | var f = splitPathRe.exec(path)[2] || ''; 129 | // TODO: make this comparison case-insensitive on windows? 130 | if (ext && f.substr(-1 * ext.length) === ext) { 131 | f = f.substr(0, f.length - ext.length); 132 | } 133 | return f; 134 | }; 135 | 136 | 137 | exports.extname = function(path) { 138 | return splitPathRe.exec(path)[3] || ''; 139 | }; 140 | 141 | }(window.M8.path)); 142 | (function(){ 143 | /** 144 | * modul8 v0.15.1 145 | */ 146 | 147 | var config = {"namespace":"M8","domains":["app","shared"],"arbiters":{},"npmTree":{},"builtIns":["path","events"],"slash":"/"} // replaced 148 | , ns = window[config.namespace] 149 | , path = ns.path 150 | , slash = config.slash 151 | , domains = config.domains 152 | , builtIns = config.builtIns 153 | , arbiters = [] 154 | , stash = {} 155 | , DomReg = /^([\w]*)::/; 156 | 157 | /** 158 | * Initialize stash with domain names and move data to it 159 | */ 160 | stash.M8 = {}; 161 | stash.external = {}; 162 | stash.data = ns.data; 163 | delete ns.data; 164 | stash.npm = {path : path}; 165 | delete ns.path; 166 | 167 | domains.forEach(function (e) { 168 | stash[e] = {}; 169 | }); 170 | 171 | /** 172 | * Attach arbiters to the require system then delete them from the global scope 173 | */ 174 | Object.keys(config.arbiters).forEach(function (name) { 175 | var arbAry = config.arbiters[name]; 176 | arbiters.push(name); 177 | stash.M8[name] = window[arbAry[0]]; 178 | arbAry.forEach(function (e) { 179 | delete window[e]; 180 | }); 181 | }); 182 | 183 | // same as server function 184 | function isAbsolute(reqStr) { 185 | return reqStr === '' || path.normalize(reqStr) === reqStr; 186 | } 187 | 188 | function resolve(domains, reqStr) { 189 | reqStr = reqStr.split('.')[0]; // remove extension 190 | 191 | // direct folder require 192 | var skipFolder = false; 193 | if (reqStr.slice(-1) === slash || reqStr === '') { 194 | reqStr = path.join(reqStr, 'index'); 195 | skipFolder = true; 196 | } 197 | 198 | if (config.logging >= 4) { 199 | console.debug('modul8 looks in : ' + JSON.stringify(domains) + ' for : ' + reqStr); 200 | } 201 | 202 | var dom, k, req; 203 | for (k = 0; k < domains.length; k += 1) { 204 | dom = domains[k]; 205 | if (stash[dom][reqStr]) { 206 | return stash[dom][reqStr]; 207 | } 208 | if (!skipFolder) { 209 | req = path.join(reqStr, 'index'); 210 | if (stash[dom][req]) { 211 | return stash[dom][req]; 212 | } 213 | } 214 | } 215 | 216 | if (config.logging >= 1) { 217 | console.error("modul8: Unable to resolve require for: " + reqStr); 218 | } 219 | } 220 | 221 | /** 222 | * Require Factory for ns.define 223 | * Each (domain,path) gets a specialized require function from this 224 | */ 225 | function makeRequire(dom, pathName) { 226 | return function (reqStr) { 227 | if (config.logging >= 3) { // log verbatim pull-ins from dom::pathName 228 | console.log('modul8: ' + dom + '::' + pathName + " <- " + reqStr); 229 | } 230 | 231 | if (!isAbsolute(reqStr)) { 232 | //console.log('relative resolve:', reqStr, 'from domain:', dom, 'join:', path.join(path.dirname(pathName), reqStr)); 233 | return resolve([dom], path.join(path.dirname(pathName), reqStr)); 234 | } 235 | 236 | var domSpecific = DomReg.test(reqStr) 237 | , sDomain = false; 238 | 239 | if (domSpecific) { 240 | sDomain = reqStr.match(DomReg)[1]; 241 | reqStr = reqStr.split('::')[1]; 242 | } 243 | 244 | // require from/to npm domain - sandbox and join in current path if exists 245 | if (dom === 'npm' || (domSpecific && sDomain === 'npm')) { 246 | if (builtIns.indexOf(reqStr) >= 0) { 247 | return resolve(['npm'], reqStr); // => can put builtIns on npm:: 248 | } 249 | if (domSpecific) { 250 | return resolve(['npm'], config.npmTree[reqStr].main); 251 | } 252 | // else, absolute: use included hashmap tree of npm mains 253 | 254 | // find root of module referenced in pathName, by counting number of node_modules referenced 255 | // this ensures our startpoint, when split around /node_modules/ is an array of modules requiring each other 256 | var order = pathName.split('node_modules').length; //TODO: depending on whether multiple slash types can coexist, conditionally split this based on found slash type 257 | var root = pathName.split(slash).slice(0, Math.max(2 * (order - 2) + 1, 1)).join(slash); 258 | 259 | // server side resolver has figured out where the module resides and its main 260 | // use resolvers passed down npmTree to get correct require string 261 | var branch = root.split(slash + 'node_modules' + slash).concat(reqStr); 262 | //console.log(root, order, reqStr, pathName, branch); 263 | // use the branch array to find the keys used to traverse the npm tree, to find the key of this particular npm module's main in stash 264 | var position = config.npmTree[branch[0]]; 265 | for (var i = 1; i < branch.length; i += 1) { 266 | position = position.deps[branch[i]]; 267 | if (!position) { 268 | console.error('expected vertex: ' + branch[i] + ' missing from current npm tree branch ' + pathName); // should not happen, remove eventually 269 | return; 270 | } 271 | } 272 | return resolve(['npm'], position.main); 273 | } 274 | 275 | // domain specific 276 | if (domSpecific) { 277 | return resolve([sDomain], reqStr); 278 | } 279 | 280 | // general absolute, try arbiters 281 | if (arbiters.indexOf(reqStr) >= 0) { 282 | return resolve(['M8'], reqStr); 283 | } 284 | 285 | // general absolute, not an arbiter, try current domains, then the others 286 | return resolve([dom].concat(domains.filter(function (e) { 287 | return (e !== dom && e !== 'npm'); 288 | })), reqStr); 289 | }; 290 | } 291 | 292 | /** 293 | * define module name on domain container 294 | * expects wrapping fn(require, module, exports) { code }; 295 | */ 296 | ns.define = function (name, domain, fn) { 297 | var mod = {exports : {}} 298 | , exp = {} 299 | , target; 300 | fn.call({}, makeRequire(domain, name), mod, exp); 301 | 302 | if (Object.prototype.toString.call(mod.exports) === '[object Object]') { 303 | target = (Object.keys(mod.exports).length) ? mod.exports : exp; 304 | } 305 | else { 306 | target = mod.exports; 307 | } 308 | stash[domain][name] = target; 309 | }; 310 | 311 | /** 312 | * Public Debug API 313 | */ 314 | 315 | ns.inspect = function (domain) { 316 | console.log(stash[domain]); 317 | }; 318 | 319 | ns.domains = function () { 320 | return domains.concat(['external', 'data']); 321 | }; 322 | 323 | ns.require = makeRequire('app', 'CONSOLE'); 324 | 325 | /** 326 | * Live Extension API 327 | */ 328 | 329 | ns.data = function (name, exported) { 330 | if (stash.data[name]) { 331 | delete stash.data[name]; 332 | } 333 | if (exported) { 334 | stash.data[name] = exported; 335 | } 336 | }; 337 | 338 | ns.external = function (name, exported) { 339 | if (stash.external[name]) { 340 | delete stash.external[name]; 341 | } 342 | if (exported) { 343 | stash.external[name] = exported; 344 | } 345 | }; 346 | 347 | }()); 348 | 349 | // shared code 350 | 351 | M8.define('index','shared',function (require, module, exports) { 352 | module.exports = "shared code"; 353 | 354 | }); 355 | 356 | // app code - safety wrapped 357 | 358 | 359 | M8.define('app','app',function (require, module, exports) { 360 | var shared = require('shared::'); 361 | alert(shared); 362 | 363 | }); 364 | }()); -------------------------------------------------------------------------------- /examples/simple/output.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | window.M8 = {data:{}, path:{}}; 3 | 4 | // include npm::path 5 | (function (exports) { 6 | function filter (xs, fn) { 7 | var res = []; 8 | for (var i = 0; i < xs.length; i++) { 9 | if (fn(xs[i], i, xs)) res.push(xs[i]); 10 | } 11 | return res; 12 | } 13 | 14 | // resolves . and .. elements in a path array with directory names there 15 | // must be no slashes, empty elements, or device names (c:\) in the array 16 | // (so also no leading and trailing slashes - it does not distinguish 17 | // relative and absolute paths) 18 | function normalizeArray(parts, allowAboveRoot) { 19 | // if the path tries to go above the root, `up` ends up > 0 20 | var up = 0; 21 | for (var i = parts.length; i >= 0; i--) { 22 | var last = parts[i]; 23 | if (last == '.') { 24 | parts.splice(i, 1); 25 | } else if (last === '..') { 26 | parts.splice(i, 1); 27 | up++; 28 | } else if (up) { 29 | parts.splice(i, 1); 30 | up--; 31 | } 32 | } 33 | 34 | // if the path is allowed to go above the root, restore leading ..s 35 | if (allowAboveRoot) { 36 | for (; up--; up) { 37 | parts.unshift('..'); 38 | } 39 | } 40 | 41 | return parts; 42 | } 43 | 44 | // Regex to split a filename into [*, dir, basename, ext] 45 | // posix version 46 | var splitPathRe = /^(.+\/(?!$)|\/)?((?:.+?)?(\.[^.]*)?)$/; 47 | 48 | // path.resolve([from ...], to) 49 | // posix version 50 | exports.resolve = function() { 51 | var resolvedPath = '', 52 | resolvedAbsolute = false; 53 | 54 | for (var i = arguments.length; i >= -1 && !resolvedAbsolute; i--) { 55 | var path = (i >= 0) 56 | ? arguments[i] 57 | : process.cwd(); 58 | 59 | // Skip empty and invalid entries 60 | if (typeof path !== 'string' || !path) { 61 | continue; 62 | } 63 | 64 | resolvedPath = path + '/' + resolvedPath; 65 | resolvedAbsolute = path.charAt(0) === '/'; 66 | } 67 | 68 | // At this point the path should be resolved to a full absolute path, but 69 | // handle relative paths to be safe (might happen when process.cwd() fails) 70 | 71 | // Normalize the path 72 | resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { 73 | return !!p; 74 | }), !resolvedAbsolute).join('/'); 75 | 76 | return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; 77 | }; 78 | 79 | // path.normalize(path) 80 | // posix version 81 | exports.normalize = function(path) { 82 | var isAbsolute = path.charAt(0) === '/', 83 | trailingSlash = path.slice(-1) === '/'; 84 | 85 | // Normalize the path 86 | path = normalizeArray(filter(path.split('/'), function(p) { 87 | return !!p; 88 | }), !isAbsolute).join('/'); 89 | 90 | if (!path && !isAbsolute) { 91 | path = '.'; 92 | } 93 | if (path && trailingSlash) { 94 | path += '/'; 95 | } 96 | 97 | return (isAbsolute ? '/' : '') + path; 98 | }; 99 | 100 | 101 | // posix version 102 | exports.join = function() { 103 | var paths = Array.prototype.slice.call(arguments, 0); 104 | return exports.normalize(filter(paths, function(p, index) { 105 | return p && typeof p === 'string'; 106 | }).join('/')); 107 | }; 108 | 109 | 110 | exports.dirname = function(path) { 111 | var dir = splitPathRe.exec(path)[1] || ''; 112 | var isWindows = false; 113 | if (!dir) { 114 | // No dirname 115 | return '.'; 116 | } else if (dir.length === 1 || 117 | (isWindows && dir.length <= 3 && dir.charAt(1) === ':')) { 118 | // It is just a slash or a drive letter with a slash 119 | return dir; 120 | } else { 121 | // It is a full dirname, strip trailing slash 122 | return dir.substring(0, dir.length - 1); 123 | } 124 | }; 125 | 126 | 127 | exports.basename = function(path, ext) { 128 | var f = splitPathRe.exec(path)[2] || ''; 129 | // TODO: make this comparison case-insensitive on windows? 130 | if (ext && f.substr(-1 * ext.length) === ext) { 131 | f = f.substr(0, f.length - ext.length); 132 | } 133 | return f; 134 | }; 135 | 136 | 137 | exports.extname = function(path) { 138 | return splitPathRe.exec(path)[3] || ''; 139 | }; 140 | 141 | }(window.M8.path)); 142 | (function(){ 143 | /** 144 | * modul8 v0.15.3 145 | */ 146 | 147 | var config = {"namespace":"M8","domains":["app"],"arbiters":{"jQuery":["jQuery","$"]},"npmTree":{},"builtIns":["path","events"],"slash":"/"} // replaced 148 | , ns = window[config.namespace] 149 | , path = ns.path 150 | , slash = config.slash 151 | , domains = config.domains 152 | , builtIns = config.builtIns 153 | , arbiters = [] 154 | , stash = {} 155 | , DomReg = /^([\w]*)::/; 156 | 157 | /** 158 | * Initialize stash with domain names and move data to it 159 | */ 160 | stash.M8 = {}; 161 | stash.external = {}; 162 | stash.data = ns.data; 163 | delete ns.data; 164 | stash.npm = {path : path}; 165 | delete ns.path; 166 | 167 | domains.forEach(function (e) { 168 | stash[e] = {}; 169 | }); 170 | 171 | /** 172 | * Attach arbiters to the require system then delete them from the global scope 173 | */ 174 | Object.keys(config.arbiters).forEach(function (name) { 175 | var arbAry = config.arbiters[name]; 176 | arbiters.push(name); 177 | stash.M8[name] = window[arbAry[0]]; 178 | arbAry.forEach(function (e) { 179 | delete window[e]; 180 | }); 181 | }); 182 | 183 | // same as server function 184 | function isAbsolute(reqStr) { 185 | return reqStr === '' || path.normalize(reqStr) === reqStr; 186 | } 187 | 188 | function resolve(domains, reqStr) { 189 | reqStr = reqStr.split('.')[0]; // remove extension 190 | 191 | // direct folder require 192 | var skipFolder = false; 193 | if (reqStr.slice(-1) === slash || reqStr === '') { 194 | reqStr = path.join(reqStr, 'index'); 195 | skipFolder = true; 196 | } 197 | 198 | if (config.logging >= 4) { 199 | console.debug('modul8 looks in : ' + JSON.stringify(domains) + ' for : ' + reqStr); 200 | } 201 | 202 | var dom, k, req; 203 | for (k = 0; k < domains.length; k += 1) { 204 | dom = domains[k]; 205 | if (stash[dom][reqStr]) { 206 | return stash[dom][reqStr]; 207 | } 208 | if (!skipFolder) { 209 | req = path.join(reqStr, 'index'); 210 | if (stash[dom][req]) { 211 | return stash[dom][req]; 212 | } 213 | } 214 | } 215 | 216 | if (config.logging >= 1) { 217 | console.error("modul8: Unable to resolve require for: " + reqStr); 218 | } 219 | } 220 | 221 | /** 222 | * Require Factory for ns.define 223 | * Each (domain,path) gets a specialized require function from this 224 | */ 225 | function makeRequire(dom, pathName) { 226 | return function (reqStr) { 227 | if (config.logging >= 3) { // log verbatim pull-ins from dom::pathName 228 | console.log('modul8: ' + dom + '::' + pathName + " <- " + reqStr); 229 | } 230 | 231 | if (!isAbsolute(reqStr)) { 232 | //console.log('relative resolve:', reqStr, 'from domain:', dom, 'join:', path.join(path.dirname(pathName), reqStr)); 233 | return resolve([dom], path.join(path.dirname(pathName), reqStr)); 234 | } 235 | 236 | var domSpecific = DomReg.test(reqStr) 237 | , sDomain = false; 238 | 239 | if (domSpecific) { 240 | sDomain = reqStr.match(DomReg)[1]; 241 | reqStr = reqStr.split('::')[1]; 242 | } 243 | 244 | // require from/to npm domain - sandbox and join in current path if exists 245 | if (dom === 'npm' || (domSpecific && sDomain === 'npm')) { 246 | if (builtIns.indexOf(reqStr) >= 0) { 247 | return resolve(['npm'], reqStr); // => can put builtIns on npm:: 248 | } 249 | if (domSpecific) { 250 | return resolve(['npm'], config.npmTree[reqStr].main); 251 | } 252 | // else, absolute: use included hashmap tree of npm mains 253 | 254 | // find root of module referenced in pathName, by counting number of node_modules referenced 255 | // this ensures our startpoint, when split around /node_modules/ is an array of modules requiring each other 256 | var order = pathName.split('node_modules').length; //TODO: depending on whether multiple slash types can coexist, conditionally split this based on found slash type 257 | var root = pathName.split(slash).slice(0, Math.max(2 * (order - 2) + 1, 1)).join(slash); 258 | 259 | // server side resolver has figured out where the module resides and its main 260 | // use resolvers passed down npmTree to get correct require string 261 | var branch = root.split(slash + 'node_modules' + slash).concat(reqStr); 262 | //console.log(root, order, reqStr, pathName, branch); 263 | // use the branch array to find the keys used to traverse the npm tree, to find the key of this particular npm module's main in stash 264 | var position = config.npmTree[branch[0]]; 265 | for (var i = 1; i < branch.length; i += 1) { 266 | position = position.deps[branch[i]]; 267 | if (!position) { 268 | console.error('expected vertex: ' + branch[i] + ' missing from current npm tree branch ' + pathName); // should not happen, remove eventually 269 | return; 270 | } 271 | } 272 | return resolve(['npm'], position.main); 273 | } 274 | 275 | // domain specific 276 | if (domSpecific) { 277 | return resolve([sDomain], reqStr); 278 | } 279 | 280 | // general absolute, try arbiters 281 | if (arbiters.indexOf(reqStr) >= 0) { 282 | return resolve(['M8'], reqStr); 283 | } 284 | 285 | // general absolute, not an arbiter, try current domains, then the others 286 | return resolve([dom].concat(domains.filter(function (e) { 287 | return (e !== dom && e !== 'npm'); 288 | })), reqStr); 289 | }; 290 | } 291 | 292 | /** 293 | * define module name on domain container 294 | * expects wrapping fn(require, module, exports) { code }; 295 | */ 296 | ns.define = function (name, domain, fn) { 297 | var mod = {exports : {}} 298 | , exp = {} 299 | , target; 300 | fn.call({}, makeRequire(domain, name), mod, exp); 301 | 302 | if (Object.prototype.toString.call(mod.exports) === '[object Object]') { 303 | target = (Object.keys(mod.exports).length) ? mod.exports : exp; 304 | } 305 | else { 306 | target = mod.exports; 307 | } 308 | stash[domain][name] = target; 309 | }; 310 | 311 | /** 312 | * Public Debug API 313 | */ 314 | 315 | ns.inspect = function (domain) { 316 | console.log(stash[domain]); 317 | }; 318 | 319 | ns.domains = function () { 320 | return domains.concat(['external', 'data']); 321 | }; 322 | 323 | ns.require = makeRequire('app', 'CONSOLE'); 324 | 325 | /** 326 | * Live Extension API 327 | */ 328 | 329 | ns.data = function (name, exported) { 330 | if (stash.data[name]) { 331 | delete stash.data[name]; 332 | } 333 | if (exported) { 334 | stash.data[name] = exported; 335 | } 336 | }; 337 | 338 | ns.external = function (name, exported) { 339 | if (stash.external[name]) { 340 | delete stash.external[name]; 341 | } 342 | if (exported) { 343 | stash.external[name] = exported; 344 | } 345 | }; 346 | 347 | }()); 348 | 349 | // app code - safety wrapped 350 | 351 | 352 | M8.define('utils/validation','app',function (require, module, exports) { 353 | exports.nameOk = function(name){ 354 | return (name != 'jill'); 355 | }; 356 | 357 | }); 358 | M8.define('models/user','app',function (require, module, exports) { 359 | var validation = require('utils/validation.js'); 360 | 361 | var User = { 362 | records : ['jack', 'jill'], 363 | 364 | fetch : function(){ 365 | return this.records.filter(this.validate); 366 | }, 367 | 368 | validate : function(user) { 369 | return validation.nameOk(user); 370 | } 371 | }; 372 | 373 | module.exports = User; 374 | 375 | }); 376 | M8.define('controllers/users','app',function (require, module, exports) { 377 | var User = require('models/user'); 378 | 379 | var Users = { 380 | init : function(){ 381 | return User.fetch(); 382 | } 383 | }; 384 | 385 | module.exports = Users; 386 | 387 | }); 388 | M8.define('app','app',function (require, module, exports) { 389 | var Users = require('controllers/users'); 390 | var $ = require('jQuery'); 391 | 392 | var App = { 393 | init: function(){ 394 | $('#output').text( JSON.stringify(Users.init()) ); 395 | } 396 | }.init(); 397 | 398 | }); 399 | }()); -------------------------------------------------------------------------------- /examples/advanced/output.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | window.QQ = {data:{}, path:{}}; 3 | 4 | // include npm::path 5 | (function (exports) { 6 | function filter (xs, fn) { 7 | var res = []; 8 | for (var i = 0; i < xs.length; i++) { 9 | if (fn(xs[i], i, xs)) res.push(xs[i]); 10 | } 11 | return res; 12 | } 13 | 14 | // resolves . and .. elements in a path array with directory names there 15 | // must be no slashes, empty elements, or device names (c:\) in the array 16 | // (so also no leading and trailing slashes - it does not distinguish 17 | // relative and absolute paths) 18 | function normalizeArray(parts, allowAboveRoot) { 19 | // if the path tries to go above the root, `up` ends up > 0 20 | var up = 0; 21 | for (var i = parts.length; i >= 0; i--) { 22 | var last = parts[i]; 23 | if (last == '.') { 24 | parts.splice(i, 1); 25 | } else if (last === '..') { 26 | parts.splice(i, 1); 27 | up++; 28 | } else if (up) { 29 | parts.splice(i, 1); 30 | up--; 31 | } 32 | } 33 | 34 | // if the path is allowed to go above the root, restore leading ..s 35 | if (allowAboveRoot) { 36 | for (; up--; up) { 37 | parts.unshift('..'); 38 | } 39 | } 40 | 41 | return parts; 42 | } 43 | 44 | // Regex to split a filename into [*, dir, basename, ext] 45 | // posix version 46 | var splitPathRe = /^(.+\/(?!$)|\/)?((?:.+?)?(\.[^.]*)?)$/; 47 | 48 | // path.resolve([from ...], to) 49 | // posix version 50 | exports.resolve = function() { 51 | var resolvedPath = '', 52 | resolvedAbsolute = false; 53 | 54 | for (var i = arguments.length; i >= -1 && !resolvedAbsolute; i--) { 55 | var path = (i >= 0) 56 | ? arguments[i] 57 | : process.cwd(); 58 | 59 | // Skip empty and invalid entries 60 | if (typeof path !== 'string' || !path) { 61 | continue; 62 | } 63 | 64 | resolvedPath = path + '/' + resolvedPath; 65 | resolvedAbsolute = path.charAt(0) === '/'; 66 | } 67 | 68 | // At this point the path should be resolved to a full absolute path, but 69 | // handle relative paths to be safe (might happen when process.cwd() fails) 70 | 71 | // Normalize the path 72 | resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { 73 | return !!p; 74 | }), !resolvedAbsolute).join('/'); 75 | 76 | return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; 77 | }; 78 | 79 | // path.normalize(path) 80 | // posix version 81 | exports.normalize = function(path) { 82 | var isAbsolute = path.charAt(0) === '/', 83 | trailingSlash = path.slice(-1) === '/'; 84 | 85 | // Normalize the path 86 | path = normalizeArray(filter(path.split('/'), function(p) { 87 | return !!p; 88 | }), !isAbsolute).join('/'); 89 | 90 | if (!path && !isAbsolute) { 91 | path = '.'; 92 | } 93 | if (path && trailingSlash) { 94 | path += '/'; 95 | } 96 | 97 | return (isAbsolute ? '/' : '') + path; 98 | }; 99 | 100 | 101 | // posix version 102 | exports.join = function() { 103 | var paths = Array.prototype.slice.call(arguments, 0); 104 | return exports.normalize(filter(paths, function(p, index) { 105 | return p && typeof p === 'string'; 106 | }).join('/')); 107 | }; 108 | 109 | 110 | exports.dirname = function(path) { 111 | var dir = splitPathRe.exec(path)[1] || ''; 112 | var isWindows = false; 113 | if (!dir) { 114 | // No dirname 115 | return '.'; 116 | } else if (dir.length === 1 || 117 | (isWindows && dir.length <= 3 && dir.charAt(1) === ':')) { 118 | // It is just a slash or a drive letter with a slash 119 | return dir; 120 | } else { 121 | // It is a full dirname, strip trailing slash 122 | return dir.substring(0, dir.length - 1); 123 | } 124 | }; 125 | 126 | 127 | exports.basename = function(path, ext) { 128 | var f = splitPathRe.exec(path)[2] || ''; 129 | // TODO: make this comparison case-insensitive on windows? 130 | if (ext && f.substr(-1 * ext.length) === ext) { 131 | f = f.substr(0, f.length - ext.length); 132 | } 133 | return f; 134 | }; 135 | 136 | 137 | exports.extname = function(path) { 138 | return splitPathRe.exec(path)[3] || ''; 139 | }; 140 | 141 | }(window.QQ.path)); 142 | QQ.data.test = {"hi": "there"} 143 | ; 144 | (function(){ 145 | /** 146 | * modul8 v0.15.1 147 | */ 148 | 149 | var config = {"namespace":"QQ","domains":["app","shared"],"arbiters":{"monolith":["monolith"]},"npmTree":{},"builtIns":["path","events"],"slash":"/"} // replaced 150 | , ns = window[config.namespace] 151 | , path = ns.path 152 | , slash = config.slash 153 | , domains = config.domains 154 | , builtIns = config.builtIns 155 | , arbiters = [] 156 | , stash = {} 157 | , DomReg = /^([\w]*)::/; 158 | 159 | /** 160 | * Initialize stash with domain names and move data to it 161 | */ 162 | stash.M8 = {}; 163 | stash.external = {}; 164 | stash.data = ns.data; 165 | delete ns.data; 166 | stash.npm = {path : path}; 167 | delete ns.path; 168 | 169 | domains.forEach(function (e) { 170 | stash[e] = {}; 171 | }); 172 | 173 | /** 174 | * Attach arbiters to the require system then delete them from the global scope 175 | */ 176 | Object.keys(config.arbiters).forEach(function (name) { 177 | var arbAry = config.arbiters[name]; 178 | arbiters.push(name); 179 | stash.M8[name] = window[arbAry[0]]; 180 | arbAry.forEach(function (e) { 181 | delete window[e]; 182 | }); 183 | }); 184 | 185 | // same as server function 186 | function isAbsolute(reqStr) { 187 | return reqStr === '' || path.normalize(reqStr) === reqStr; 188 | } 189 | 190 | function resolve(domains, reqStr) { 191 | reqStr = reqStr.split('.')[0]; // remove extension 192 | 193 | // direct folder require 194 | var skipFolder = false; 195 | if (reqStr.slice(-1) === slash || reqStr === '') { 196 | reqStr = path.join(reqStr, 'index'); 197 | skipFolder = true; 198 | } 199 | 200 | if (config.logging >= 4) { 201 | console.debug('modul8 looks in : ' + JSON.stringify(domains) + ' for : ' + reqStr); 202 | } 203 | 204 | var dom, k, req; 205 | for (k = 0; k < domains.length; k += 1) { 206 | dom = domains[k]; 207 | if (stash[dom][reqStr]) { 208 | return stash[dom][reqStr]; 209 | } 210 | if (!skipFolder) { 211 | req = path.join(reqStr, 'index'); 212 | if (stash[dom][req]) { 213 | return stash[dom][req]; 214 | } 215 | } 216 | } 217 | 218 | if (config.logging >= 1) { 219 | console.error("modul8: Unable to resolve require for: " + reqStr); 220 | } 221 | } 222 | 223 | /** 224 | * Require Factory for ns.define 225 | * Each (domain,path) gets a specialized require function from this 226 | */ 227 | function makeRequire(dom, pathName) { 228 | return function (reqStr) { 229 | if (config.logging >= 3) { // log verbatim pull-ins from dom::pathName 230 | console.log('modul8: ' + dom + '::' + pathName + " <- " + reqStr); 231 | } 232 | 233 | if (!isAbsolute(reqStr)) { 234 | //console.log('relative resolve:', reqStr, 'from domain:', dom, 'join:', path.join(path.dirname(pathName), reqStr)); 235 | return resolve([dom], path.join(path.dirname(pathName), reqStr)); 236 | } 237 | 238 | var domSpecific = DomReg.test(reqStr) 239 | , sDomain = false; 240 | 241 | if (domSpecific) { 242 | sDomain = reqStr.match(DomReg)[1]; 243 | reqStr = reqStr.split('::')[1]; 244 | } 245 | 246 | // require from/to npm domain - sandbox and join in current path if exists 247 | if (dom === 'npm' || (domSpecific && sDomain === 'npm')) { 248 | if (builtIns.indexOf(reqStr) >= 0) { 249 | return resolve(['npm'], reqStr); // => can put builtIns on npm:: 250 | } 251 | if (domSpecific) { 252 | return resolve(['npm'], config.npmTree[reqStr].main); 253 | } 254 | // else, absolute: use included hashmap tree of npm mains 255 | 256 | // find root of module referenced in pathName, by counting number of node_modules referenced 257 | // this ensures our startpoint, when split around /node_modules/ is an array of modules requiring each other 258 | var order = pathName.split('node_modules').length; //TODO: depending on whether multiple slash types can coexist, conditionally split this based on found slash type 259 | var root = pathName.split(slash).slice(0, Math.max(2 * (order - 2) + 1, 1)).join(slash); 260 | 261 | // server side resolver has figured out where the module resides and its main 262 | // use resolvers passed down npmTree to get correct require string 263 | var branch = root.split(slash + 'node_modules' + slash).concat(reqStr); 264 | //console.log(root, order, reqStr, pathName, branch); 265 | // use the branch array to find the keys used to traverse the npm tree, to find the key of this particular npm module's main in stash 266 | var position = config.npmTree[branch[0]]; 267 | for (var i = 1; i < branch.length; i += 1) { 268 | position = position.deps[branch[i]]; 269 | if (!position) { 270 | console.error('expected vertex: ' + branch[i] + ' missing from current npm tree branch ' + pathName); // should not happen, remove eventually 271 | return; 272 | } 273 | } 274 | return resolve(['npm'], position.main); 275 | } 276 | 277 | // domain specific 278 | if (domSpecific) { 279 | return resolve([sDomain], reqStr); 280 | } 281 | 282 | // general absolute, try arbiters 283 | if (arbiters.indexOf(reqStr) >= 0) { 284 | return resolve(['M8'], reqStr); 285 | } 286 | 287 | // general absolute, not an arbiter, try current domains, then the others 288 | return resolve([dom].concat(domains.filter(function (e) { 289 | return (e !== dom && e !== 'npm'); 290 | })), reqStr); 291 | }; 292 | } 293 | 294 | /** 295 | * define module name on domain container 296 | * expects wrapping fn(require, module, exports) { code }; 297 | */ 298 | ns.define = function (name, domain, fn) { 299 | var mod = {exports : {}} 300 | , exp = {} 301 | , target; 302 | fn.call({}, makeRequire(domain, name), mod, exp); 303 | 304 | if (Object.prototype.toString.call(mod.exports) === '[object Object]') { 305 | target = (Object.keys(mod.exports).length) ? mod.exports : exp; 306 | } 307 | else { 308 | target = mod.exports; 309 | } 310 | stash[domain][name] = target; 311 | }; 312 | 313 | /** 314 | * Public Debug API 315 | */ 316 | 317 | ns.inspect = function (domain) { 318 | console.log(stash[domain]); 319 | }; 320 | 321 | ns.domains = function () { 322 | return domains.concat(['external', 'data']); 323 | }; 324 | 325 | ns.require = makeRequire('app', 'CONSOLE'); 326 | 327 | /** 328 | * Live Extension API 329 | */ 330 | 331 | ns.data = function (name, exported) { 332 | if (stash.data[name]) { 333 | delete stash.data[name]; 334 | } 335 | if (exported) { 336 | stash.data[name] = exported; 337 | } 338 | }; 339 | 340 | ns.external = function (name, exported) { 341 | if (stash.external[name]) { 342 | delete stash.external[name]; 343 | } 344 | if (exported) { 345 | stash.external[name] = exported; 346 | } 347 | }; 348 | 349 | }()); 350 | 351 | // shared code 352 | 353 | QQ.define('calc','shared',function (require, module, exports) { 354 | 355 | module.exports = { 356 | divides: function(d, n) { 357 | return !(d % n); 358 | } 359 | }; 360 | 361 | }); 362 | QQ.define('validation','shared',function (require, module, exports) { 363 | var divides; 364 | 365 | divides = require('./calc').divides; 366 | 367 | exports.isLeapYear = function(yr) { 368 | return divides(yr, 4) && (!divides(yr, 100) || divides(yr, 400)); 369 | }; 370 | 371 | }); 372 | 373 | // app code - safety wrapped 374 | 375 | 376 | QQ.define('bigthing/sub2','app',function (require, module, exports) { 377 | 378 | module.exports = function(str) { 379 | return console.log(str); 380 | }; 381 | 382 | }); 383 | QQ.define('helper','app',function (require, module, exports) { 384 | var testRunner; 385 | 386 | module.exports = function(str) { 387 | return console.log(str); 388 | }; 389 | 390 | }); 391 | QQ.define('bigthing/sub1','app',function (require, module, exports) { 392 | var sub2; 393 | 394 | sub2 = require('./sub2'); 395 | 396 | exports.doComplex = function(str) { 397 | return sub2(str + ' (sub1 added this, passing to sub2)'); 398 | }; 399 | 400 | }); 401 | QQ.define('main','app',function (require, module, exports) { 402 | var b, helper, m, test, v; 403 | 404 | helper = require('./helper'); 405 | 406 | helper('hello from app via helper'); 407 | 408 | b = require('bigthing/sub1'); 409 | 410 | b.doComplex('app calls up to sub1'); 411 | 412 | v = require('validation.coffee'); 413 | 414 | console.log('2004 isLeapYear?', v.isLeapYear(2004)); 415 | 416 | m = require('monolith'); 417 | 418 | console.log("monolith:" + m); 419 | 420 | test = require('data::test'); 421 | 422 | console.log('injected data:', test); 423 | 424 | }); 425 | }()); -------------------------------------------------------------------------------- /lib/resolver.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | , utils = require('./utils') 3 | , join = path.join 4 | , exists = utils.exists 5 | , read = utils.read 6 | , error = utils.error 7 | , domainIgnoreR = /^data(?=::)|^external(?=::)|^M8(?=::)|^npm(?=::)/ 8 | , domainIgnoreL = ['data', 'external', 'M8', 'npm'] 9 | , domainPresent = /^([\w]*)::/; 10 | 11 | // isAbsolute pass => reqStr and lies directly on the domain root 12 | // isAbsolute fail => reqStr is relative to extraPath 13 | function isAbsolute(reqStr) { 14 | return reqStr === '' || path.normalize(reqStr) === reqStr; 15 | } 16 | 17 | // Takes out domain prefix from a request string if exists 18 | function stripDomain(reqStr) { 19 | var ary = reqStr.split('::'); 20 | return ary[ary.length - 1]; 21 | } 22 | 23 | /** 24 | * makeFinder 25 | * 26 | * finder factory for Resolver 27 | * @return finder function 28 | * 29 | * finder function scans join(base, req + ext) for closure held extensions 30 | */ 31 | function makeFinder(exts) { 32 | return function (base, req) { 33 | for (var i = 0; i < exts.length; i += 1) { 34 | var ext = exts[i] 35 | , attempt = join(base, req + ext); 36 | 37 | if (exists(attempt)) { 38 | return (req + ext); 39 | } 40 | } 41 | return false; 42 | }; 43 | } 44 | 45 | /** 46 | * npmResolve factory 47 | * 48 | * creates a function which finds the entry point of an npm module 49 | * create with the node_modules root 50 | * 51 | * @[in] absReq - path node module should exist on 52 | * @[in] silent - bool === !(errors should throw) 53 | * @[in] name - name of the module for good error msgs 54 | */ 55 | function makeNpmResolver(root) { 56 | return function (absReq, name) { 57 | var folderPath = join(root, absReq) 58 | , jsonPath = join(folderPath, 'package.json') 59 | , json = {}; 60 | 61 | // folder must exists if we commit to it 62 | if (!fs.existsSync(folderPath)) { 63 | return false; 64 | } 65 | 66 | // package.json must exist 67 | if (!exists(jsonPath)) { 68 | error("resolver could not load npm module " + name + " - package.json not found"); 69 | return false; 70 | } 71 | 72 | // package.json must be valid json 73 | try { 74 | json = JSON.parse(read(jsonPath)); 75 | } catch (e) { 76 | error("could not load npm module " + name + " - package.json invalid"); 77 | } 78 | 79 | // prefer browserMain entry point if specified, then main, then index 80 | var mainFile = json.browserMain || json.main || 'index'; 81 | if (!exists(join(folderPath, mainFile))) { 82 | if (!exists(join(folderPath, mainFile + '.js'))) { 83 | error("resolver could not load npm module " + name + "'s package.json lies about main: " + mainFile + " does not exist"); 84 | } 85 | mainFile += '.js'; 86 | } 87 | return join(absReq, mainFile); 88 | }; 89 | } 90 | 91 | /** 92 | * toAbsPath 93 | * 94 | * converts a generic detective request string to an absolute one 95 | * throws if it receives an obviously bad request 96 | * 97 | * @[in] reqStr - raw request string 98 | * @[in] extraPath - path of requiree's relative position to domain root 99 | * @[in] domain 100 | */ 101 | // absolutizes a path - special cases the npm domain 102 | function toAbsPath(reqStr, extraPath, domain) { 103 | if (domainPresent.test(reqStr)) { 104 | domain = reqStr.match(domainPresent)[1]; // override current domain if explicitly specified 105 | if (!isAbsolute(reqStr)) { 106 | error("does not allow cross-domain relative requires (" + reqStr + ")"); 107 | } 108 | return [stripDomain(reqStr), domain]; 109 | } 110 | else if (isAbsolute(reqStr)) { 111 | // special case absolutes from npm domain 112 | // this sandboxes the npm domain (sensible) 113 | if (domain === 'npm') { 114 | return [reqStr, 'npm']; // explicit npms need to be handled elsewhere 115 | } 116 | else { 117 | return [reqStr, null]; 118 | } 119 | } 120 | else { 121 | return [join(extraPath, reqStr), domain]; 122 | } 123 | } 124 | 125 | /** 126 | * Resolver constructor 127 | * @[in] domains - {name : path} object 128 | * @[in] arbiters - list of requirable arbiters 129 | * @[in] exts - list of registered extensions 130 | */ 131 | function Resolver(domains, arbiters, exts, npmTree, builtIns, serverModules) { 132 | this.finder = makeFinder(exts); 133 | this.npmResolve = makeNpmResolver(domains.npm); 134 | this.domains = domains; 135 | this.arbiters = arbiters; 136 | this.exts = exts; 137 | this.npmTree = npmTree; 138 | this.builtIns = builtIns; 139 | this.serverModules = serverModules; 140 | } 141 | 142 | /** 143 | * scan [private] 144 | * scans a set of domain names for absReq 145 | * 146 | * @[in] absReq - require string relative to a domain root 147 | * @[in] scannable - ordered list of domain names to scan 148 | * @return === locate's return if found, else false 149 | **/ 150 | Resolver.prototype.scan = function (absReq, scannable) { 151 | var noTryFolder = false 152 | , lastChar = absReq.slice(-1); 153 | 154 | if (lastChar === '/' || lastChar === '\\') { 155 | absReq = join(absReq, 'index'); 156 | noTryFolder = true; 157 | } 158 | for (var i = 0; i < scannable.length; i += 1) { 159 | var dom = scannable[i] 160 | , found = this.finder(this.domains[dom], absReq); 161 | 162 | if (found) { 163 | return [found, dom, true]; 164 | } 165 | 166 | if (noTryFolder) { 167 | continue; 168 | } 169 | 170 | found = this.finder(this.domains[dom], join(absReq, 'index')); 171 | if (found) { 172 | return [found, dom, true]; 173 | } 174 | } 175 | return false; 176 | }; 177 | 178 | /** 179 | * locate 180 | * 181 | * locates a required file - throws on bad requests - main interface 182 | * 183 | * @[in] reqStr from detective 184 | * @[in] path of requiree's relative position to domain root 185 | * @[in] requiree's domain 186 | * @return [foundPath, domainName, isFake] where: 187 | * 188 | * [str] foundPath - full path of the chosen file to use 189 | * [str] domainName - name of the domain it was found on 190 | * [bool] isReal - true iff foundPath represents a real file 191 | **/ 192 | Resolver.prototype.locate = function (reqStr, currentPath, currentDomain) { 193 | var absResult = toAbsPath(reqStr, currentPath, currentDomain) 194 | , absReq = absResult[0] 195 | , foundDomain = absResult[1] 196 | , result = false 197 | , msg = ''; 198 | 199 | if (domainIgnoreL.indexOf(foundDomain) >= 0) { 200 | 201 | if (foundDomain === 'data' || foundDomain === 'external') { 202 | return [absReq, foundDomain, false]; 203 | } 204 | if (foundDomain === 'M8') { 205 | if (this.arbiters.indexOf(absReq) >= 0) { 206 | return [absReq, 'M8', false]; 207 | } 208 | error("resolver could not require non-existent arbiter: " + reqStr + " (from " + currentDomain + ")"); 209 | } 210 | if (foundDomain === 'npm') { 211 | var name = absReq; // at this point, this is safe 212 | 213 | // node module priority: 214 | // 1. if we are requiring a builtin (sanitized) server-side module - return that 215 | // 2a. if required from root, do 216 | // 2b1. try to locate node module in node_module subfolder of current node module 217 | // 2b2. look up one level in the tree recursively until a hit is found || we hit the domain root 218 | // 4. throw 219 | 220 | 221 | // 1. builtins 222 | if (this.builtIns.indexOf(name) >= 0) { 223 | if (this.npmTree._builtIns.indexOf(name) < 0) { 224 | this.npmTree._builtIns.push(name); 225 | } 226 | return [name, 'npm', false]; // this should be fine, should really be on the npm namespace 227 | } 228 | // 1. unhandled node base modules 229 | else if (this.serverModules.indexOf(name) >= 0) { 230 | error("cannot require server side node module " + name); 231 | } 232 | 233 | // catch illegal use if not builtIn (they do not require a node_modules root) 234 | if (!this.domains.npm) { 235 | error("resolver cannot require non-builtin node modules without specifying the node_modules root. tried " + name); 236 | } 237 | 238 | var npmMain; 239 | 240 | // 2a. entry point branch to npm domain 241 | if (currentDomain !== 'npm') { 242 | npmMain = this.npmResolve(name, name); 243 | if (npmMain) { 244 | this.npmTree[name] = { 245 | main : npmMain 246 | , deps : {} 247 | }; 248 | return [npmMain, 'npm', true]; 249 | } 250 | error('could not find module', reqStr); 251 | } 252 | 253 | 254 | // 2b. try current + node_modules + the string specified 255 | // but first make sure we start at the module's root before we join on node_modules 256 | var slash = '/'; // TODO.. 257 | var order = currentPath.split('node_modules').length; 258 | var moduleRoot = currentPath.split(slash).slice(0, Math.max(2 * (order - 2) + 1, 1)).join(slash); 259 | 260 | var expected = join(moduleRoot, 'node_modules', name); 261 | var branch = moduleRoot.split(slash + 'node_modules' + slash); 262 | 263 | npmMain = this.npmResolve(expected, name); 264 | if (npmMain) { 265 | var position = this.npmTree[branch[0]]; 266 | for (var i = 1; i < branch.length; i += 1) { 267 | position = position.deps[branch[i]]; 268 | if (!position) { 269 | error('internal resolver error 1'); 270 | // if this happens then we are not able to walk up to requiree's point 271 | // recursive solving => this should be impossible 272 | } 273 | } 274 | position.deps[name] = { 275 | main : npmMain 276 | , deps : {} 277 | }; 278 | return [npmMain, 'npm', true]; 279 | } 280 | 281 | // 2b2. 282 | var oldBranch = branch; 283 | while (true) { 284 | expected = join(expected, '..', '..', '..', name); 285 | if (expected === '.' || expected.slice(0, 2) === '..') { // broken out of domain root 286 | break; 287 | } 288 | branch = branch.slice(0, -1); // we went one up 289 | 290 | npmMain = this.npmResolve(expected, name); 291 | if (npmMain) { 292 | // need to give a link to this module regardless of where in the hierarchy it is 293 | var oldPos = this.npmTree[oldBranch[0]]; 294 | for (var j = 1; j < oldBranch.length; j += 1) { 295 | oldPos = oldPos.deps[oldBranch[j]]; 296 | if (!oldPos) { 297 | error('internal resolver error 3'); 298 | // if this happens then we are not able to walk up to the point we have 299 | // already filled in. recursive solving => this should be impossible 300 | } 301 | } 302 | oldPos.deps[name] = { 303 | main : npmMain 304 | , deps : {} 305 | }; 306 | 307 | // now make it requirable to everything above 308 | if (!branch.length) { 309 | // structure of the tree is different at base for historical reasons 310 | this.npmTree[name] = { 311 | main : npmMain 312 | , deps : {} 313 | }; 314 | return [npmMain, 'npm', true]; 315 | } 316 | var pos = this.npmTree[branch[0]]; 317 | for (var k = 1; k < branch.length; k += 1) { 318 | pos = pos.deps[branch[k]]; 319 | if (!pos) { 320 | error('internal resolver error 2'); 321 | // if this happens then we are not able to walk _partially_ up towards 322 | // a point we have defined. recursive solving => this should be impossible 323 | } 324 | } 325 | pos.deps[name] = { 326 | main : npmMain 327 | , deps : {} 328 | }; 329 | return [npmMain, 'npm', true]; 330 | } 331 | } 332 | 333 | // 4. nothing worked 334 | error("failed to require npm module " + name); 335 | 336 | } 337 | } 338 | else { // anything that was absolutely required from outside npm 339 | if (foundDomain && !this.domains[foundDomain]) { // also prevents second call to npmResolve 340 | error("resolver could not require from an unconfigured domain: " + foundDomain); 341 | } 342 | if (foundDomain === 'app' && currentDomain !== 'app') { 343 | error("does not allow other domains to reference the app domain. required from " + currentDomain); 344 | } 345 | 346 | if (foundDomain) { // require to a specific/same domain - we must succeed in here 347 | result = this.scan(absReq, [foundDomain]); 348 | if (result) { 349 | return result; 350 | } 351 | msg = " for extensions [" + this.exts.slice(1).join(', ') + "]"; 352 | error("resolver failed to resolve require('" + reqStr + "') in " + foundDomain + msg); 353 | } 354 | 355 | // absolute requires from app domain cannot indirectly require npm modules for safety 356 | // but this behaviour needs to work inside the npm domain, so toAbsPath sandboxes it meaning we never get 357 | 358 | // arbiters check - safe to do globally as arbiters were specified explicitly 359 | if (this.arbiters.indexOf(absReq) >= 0) { 360 | return [absReq, 'M8', false]; 361 | } 362 | 363 | // anything else - check currentDomain then all other standard ones for absReq 364 | var scannable = Object.keys(this.domains).filter(function (d) { 365 | return (d !== currentDomain && d !== 'npm'); 366 | }); 367 | scannable.unshift(currentDomain); 368 | 369 | result = this.scan(absReq, scannable); 370 | if (result) { 371 | return result; 372 | } 373 | msg = " - looked in " + scannable + " for extensions " + this.exts.slice(1).join(', '); 374 | error("resolver failed to resolve require('" + reqStr + "') from " + currentDomain + msg); 375 | } 376 | }; 377 | 378 | module.exports = function (domains, arbiters, exts, npmTree, builtIns, serverModules) { 379 | var r = new Resolver(domains, arbiters, exts, npmTree, builtIns, serverModules); 380 | return function () { 381 | return r.locate.apply(r, arguments); 382 | }; 383 | }; 384 | -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , path = require('path') 3 | , bundle = require('./bundler') 4 | , type = require('typr') 5 | , utils = require('./utils') 6 | , log = utils.logule 7 | , exists = utils.exists 8 | , error = utils.error 9 | , environment = process.env.NODE_ENV || 'development' 10 | , envCurrent = 'all' 11 | , o = {}; 12 | 13 | var logLevels = { 14 | error : 1 15 | , debug : 4 16 | }; 17 | 18 | var reserved = { 19 | 'app' : 'the main code where the entry point resides' 20 | , 'data' : 'injected data' 21 | , 'external' : 'externally loaded code' 22 | , 'M8' : 'the internal debug API' 23 | , 'npm' : 'node mdules' 24 | }; 25 | 26 | /** 27 | * Base class 28 | */ 29 | function Base(sub) { 30 | this.sub = (sub) ? sub : 'None'; 31 | } 32 | 33 | Base.prototype.subclassMatches = function (subclass, method) { 34 | if (this.sub !== subclass) { 35 | log.warn("Ignoring an invalid call to " + subclass + "::" + method + " after having broken out from the " + subclass + " subclass"); 36 | return false; 37 | } 38 | return true; 39 | }; 40 | 41 | Base.prototype.removeSubClassMethods = function () { 42 | this.sub = 'None'; 43 | }; 44 | 45 | // Helper for Base.prototype.in 46 | function envCheck() { 47 | return (environment === envCurrent || envCurrent === 'all'); 48 | } 49 | 50 | /** 51 | * Module Start 52 | * API starts by filling in the static options object 53 | * Then returning the chainable Base class instance 54 | */ 55 | module.exports = function (entry) { 56 | var dom = path.dirname(path.resolve(entry)) 57 | , file = path.basename(entry); 58 | 59 | utils.updateProject(file); 60 | 61 | if (!exists(path.join(dom, file))) { 62 | error("cannot find entry file: " + path.join(dom, file)); 63 | } 64 | o = { 65 | data : {} 66 | , arbiters : {} 67 | , domains : { 68 | 'app' : dom 69 | } 70 | , pre : [] 71 | , post : [] 72 | , ignoreDoms : [] 73 | , compilers : {} 74 | , entryPoint : file 75 | , extSuffix : false 76 | , domPrefix : true 77 | , options : { 78 | namespace : 'M8' 79 | , domloader : '' 80 | , logging : 'ERROR' 81 | , force : false 82 | , persist : path.join(__dirname, '..', 'state.json') 83 | } 84 | }; 85 | return new Base(); 86 | }; 87 | 88 | /** 89 | * in(env) 90 | * Do the next chained calls only if we are in env 91 | * Lasts until in('all') is called 92 | */ 93 | Base.prototype.in = function (env) { 94 | envCurrent = env; 95 | return this; 96 | }; 97 | 98 | /** 99 | * logger 100 | * Any subsequent work will use the configured logule sub if called 101 | */ 102 | Base.prototype.logger = function (sub) { 103 | this.removeSubClassMethods(); 104 | if (envCheck()) { 105 | // if a sub is passed in, it is assumed to be filtered outside this anyway 106 | utils.updateLogger(sub); 107 | } 108 | return this; 109 | }; 110 | 111 | /** 112 | * before 113 | * Can be (repeat) called with a function or list of functions that will executed on the code before analysis 114 | * Good for internal test dependencies 115 | */ 116 | Base.prototype.before = function (fn) { 117 | this.removeSubClassMethods(); 118 | if (envCheck()) { 119 | if (type.isArray(fn)) { 120 | var that = this; 121 | fn.forEach(function (f) { 122 | that.before(f); 123 | }); 124 | } 125 | else if (!type.isFunction(fn)) { 126 | log.warn("require 'before' functions to actually be functions - got " + fn); 127 | } 128 | else { 129 | o.pre.unshift(fn); 130 | } 131 | } 132 | return this; 133 | }; 134 | 135 | /** 136 | * after 137 | * Can be (repeat) called with a function or list of functions that will be executed on the code at compiliation time 138 | * Good for custom minification 139 | */ 140 | Base.prototype.after = function (fn) { 141 | this.removeSubClassMethods(); 142 | if (envCheck()) { 143 | if (type.isArray(fn)) { 144 | var that = this; 145 | fn.forEach(function (f) { 146 | that.after(f); 147 | }); 148 | } 149 | else if (!type.isFunction(fn)) { 150 | log.warn("require 'after' functions to actually be functions - got " + fn); 151 | } 152 | else { 153 | o.post.unshift(fn); 154 | } 155 | } 156 | return this; 157 | }; 158 | 159 | /** 160 | * register 161 | * Call with an extension and a compiler to register compile-to-JS languages 162 | */ 163 | Base.prototype.register = function (ext, compiler) { 164 | this.removeSubClassMethods(); 165 | if (envCheck()) { 166 | if (ext === '' || ext === '.js') { 167 | error("cannot re-register the " + (ext === '' ? 'blank' : ext) + " extension"); 168 | } 169 | if (!type.isFunction(compiler)) { 170 | error("registered compilers must be a function returning a string - " + ext + " extension failed"); 171 | } 172 | o.compilers[ext] = compiler; 173 | } 174 | return this; 175 | }; 176 | 177 | /** 178 | * npm 179 | * Call with a path to a node_modules folder to enable requiring of browser compatible npm modules 180 | */ 181 | Base.prototype.npm = function (dir) { 182 | this.removeSubClassMethods(); 183 | if (envCheck()) { 184 | o.domains.npm = path.resolve(dir); 185 | if (!fs.existsSync(o.domains.npm)) { 186 | error('could not resolve the npm path - ' + o.domains.npm + ' does not exist'); 187 | } 188 | } 189 | return this; 190 | }; 191 | 192 | /** 193 | * set 194 | * Set one of the valid options 195 | */ 196 | Base.prototype.set = function (key, val) { 197 | this.removeSubClassMethods(); 198 | if (envCheck()) { 199 | if (Object.keys(o.options).indexOf(key) >= 0) { 200 | 201 | if (key === 'namespace') { 202 | if (!type.isString(val) || val === '') { 203 | error("cannot use a non-string or blank namespace"); 204 | } 205 | if (!/^[\w_$]*$/.test(val) || !/^[A-Za-z_$]/.test(val)) { 206 | error("require a namespace valid as a variable name, got " + val); 207 | } 208 | } 209 | else if (key === 'persist') { 210 | if (!exists(val)) { 211 | error("got an invalid persist file: " + val); 212 | } 213 | } 214 | else if (key === 'logging') { 215 | o.logLevel = logLevels[(val + '').toLowerCase()] || 0; 216 | } 217 | else if (key === 'domloader') { 218 | if (!type.isString(val) && !type.isFunction(val)) { 219 | error("got an invalid domloader options - must be string of function"); 220 | } 221 | } 222 | 223 | o.options[key] = val; 224 | } 225 | } 226 | return this; 227 | }; 228 | 229 | 230 | /** 231 | * Data subclass 232 | */ 233 | function Data() {} 234 | Data.prototype = new Base('Data'); 235 | 236 | /** 237 | * Entry point for Data subclass 238 | */ 239 | Base.prototype.data = function (input) { 240 | this.removeSubClassMethods(); 241 | var dt = new Data(); 242 | if (envCheck()) { 243 | if (type.isObject(input)) { 244 | Object.keys(input).forEach(function (key) { 245 | dt.add(key, input[key]); 246 | }); 247 | } 248 | } 249 | return dt; 250 | }; 251 | 252 | /** 253 | * Data subclass methods 254 | */ 255 | Data.prototype.add = function (key, val) { 256 | if (this.subclassMatches('Data', 'add') && envCheck()) { 257 | if (key && val) { 258 | key += ''; 259 | o.data[key] = (type.isString(val)) ? val : JSON.stringify(val); 260 | } 261 | } 262 | return this; 263 | }; 264 | 265 | 266 | /** 267 | * Domains subclass 268 | */ 269 | function Domains() {} 270 | Domains.prototype = new Base('Domains'); 271 | 272 | /** 273 | * Entry point for Domains subclass 274 | */ 275 | Base.prototype.domains = function (input) { 276 | this.removeSubClassMethods(); 277 | var dom = new Domains(); 278 | if (envCheck()) { 279 | if (type.isObject(input)) { 280 | Object.keys(input).forEach(function (key) { 281 | dom.add(key, input[key]); 282 | }); 283 | } 284 | } 285 | return dom; 286 | }; 287 | 288 | /** 289 | * Domains subclass methods 290 | */ 291 | Domains.prototype.add = function (key, val) { 292 | if (this.subclassMatches('Domains', 'add') && envCheck()) { 293 | 294 | if (Object.keys(reserved).indexOf(key) >= 0) { 295 | error("reserves the '" + key + "' domain for " + reserved[key]); 296 | } 297 | 298 | o.domains[key] = path.resolve(val); 299 | if (!fs.existsSync(o.domains[key])) { 300 | error('could not resolve the ' + key + ' domain - ' + o.domains[key] + ' does not exist'); 301 | } 302 | } 303 | return this; 304 | }; 305 | 306 | 307 | /** 308 | * Libraries subclass 309 | */ 310 | function Libraries() {} 311 | Libraries.prototype = new Base('Libraries'); 312 | 313 | /** 314 | * Entry point for Libraries subclass 315 | */ 316 | Base.prototype.libraries = function (list, dir, target) { 317 | this.removeSubClassMethods(); 318 | var libs = new Libraries(); 319 | if (envCheck()) { 320 | libs.list(list).path(dir).target(target); 321 | } 322 | return libs; 323 | }; 324 | 325 | /** 326 | * Libraries subclass methods 327 | */ 328 | Libraries.prototype.list = function (list) { 329 | if (this.subclassMatches('Libraries', 'list') && envCheck()) { 330 | if (type.isArray(list)) { 331 | o.libFiles = list; 332 | } 333 | } 334 | return this; 335 | }; 336 | 337 | Libraries.prototype.path = function (dir) { 338 | if (this.subclassMatches('Libraries', 'path') && envCheck()) { 339 | if (type.isString(dir)) { 340 | dir = path.resolve(dir); 341 | if (!fs.existsSync(dir)) { 342 | error('could not resolve the libraries path - ' + dir + ' does not exist'); 343 | } 344 | o.libDir = dir; 345 | } 346 | } 347 | return this; 348 | }; 349 | 350 | Libraries.prototype.target = function (target) { 351 | if (this.subclassMatches('Libraries', 'target') && envCheck()) { 352 | if (type.isString(target)) { 353 | o.libsOnlyTarget = path.resolve(target); 354 | } 355 | else if (type.isFunction(target)) { 356 | o.libsOnlyTarget = target; 357 | } 358 | } 359 | return this; 360 | }; 361 | 362 | 363 | /** 364 | * Analysis subclass 365 | */ 366 | function Analysis() {} 367 | Analysis.prototype = new Base('Analysis'); 368 | 369 | /** 370 | * Entry point for Analysis subclass 371 | */ 372 | Base.prototype.analysis = function (target, prefix, suffix, hide) { 373 | this.removeSubClassMethods(); 374 | var ana = new Analysis(); 375 | if (envCheck()) { 376 | ana.output(target).prefix(prefix).suffix(suffix).hide(hide); 377 | } 378 | return ana; 379 | }; 380 | 381 | /** 382 | * Analysis subclass methods 383 | */ 384 | Analysis.prototype.output = function (target) { 385 | if (this.subclassMatches('Analysis', 'output') && envCheck()) { 386 | if (type.isString(target)) { 387 | o.treeTarget = path.resolve(target); 388 | } 389 | else if (type.isFunction(target)) { 390 | o.treeTarget = target; 391 | } 392 | } 393 | return this; 394 | }; 395 | 396 | Analysis.prototype.prefix = function (prefix) { 397 | if (this.subclassMatches('Analysis', 'prefix') && envCheck()) { 398 | if (!type.isUndefined(prefix)) { 399 | o.domPrefix = !!prefix; 400 | } 401 | } 402 | return this; 403 | }; 404 | 405 | Analysis.prototype.suffix = function (suffix) { 406 | if (this.subclassMatches('Analysis', 'suffix') && envCheck()) { 407 | if (!type.isUndefined(suffix)) { 408 | o.extSuffix = !!suffix; 409 | } 410 | } 411 | return this; 412 | }; 413 | 414 | Analysis.prototype.hide = function (domain) { 415 | if (this.subclassMatches('Analysis', 'hide') && envCheck()) { 416 | if (type.isArray(domain)) { 417 | var that = this; 418 | domain.forEach(function (d) { 419 | that.hide(d); 420 | }); 421 | } 422 | else if (type.isString(domain)) { 423 | if (domain === 'app') { 424 | log.warn("cannot ignore the app domain"); 425 | } 426 | else { 427 | o.ignoreDoms.push(domain); 428 | } 429 | } 430 | } 431 | return this; 432 | }; 433 | 434 | 435 | /** 436 | * Arbiters subclass 437 | */ 438 | function Arbiters() {} 439 | Arbiters.prototype = new Base('Arbiters'); 440 | 441 | /** 442 | * Entry point for Arbiters subclass 443 | */ 444 | Base.prototype.arbiters = function (arbObj) { 445 | this.removeSubClassMethods(); 446 | var arb = new Arbiters(); 447 | 448 | if (envCheck()) { 449 | if (type.isObject(arbObj)) { 450 | Object.keys(arbObj).forEach(function (key) { 451 | arb.add(key, arbObj[key]); 452 | }); 453 | } 454 | else if (type.isArray(arbObj)) { 455 | arbObj.forEach(function (a) { 456 | arb.add(a); 457 | }); 458 | } 459 | } 460 | return arb; 461 | }; 462 | 463 | /** 464 | * Arbiters subclass methods 465 | */ 466 | Arbiters.prototype.add = function (name, globs) { 467 | if (this.subclassMatches('Arbiters', 'add') && envCheck()) { 468 | if (type.isArray(globs) && name) { 469 | globs = globs.filter(function (e) { 470 | return (type.isString(e) && e !== ''); 471 | }); 472 | o.arbiters[name] = (globs.length === 0) ? name : globs; 473 | } else if (type.isString(globs) && globs !== '') { 474 | o.arbiters[name] = [globs]; 475 | } else { 476 | o.arbiters[name] = [name]; 477 | } 478 | } 479 | return this; 480 | }; 481 | 482 | 483 | // use helper 484 | function addPlugin(inst) { 485 | var name = inst.name 486 | , data = inst.data 487 | , dom = inst.domain; 488 | 489 | if (!name) { 490 | error('plugin has an bad/undefined name key'); 491 | } 492 | 493 | if (type.isFunction(data)) { 494 | var dataval = data(); 495 | if (!dataval) { 496 | error('plugin ' + name + 'returned bad value from its defined data method ' + dataval); 497 | } 498 | (new Data()).add(name, dataval); 499 | } 500 | 501 | if (type.isFunction(dom)) { 502 | var domval = dom(); 503 | if (!type.isString(domval)) { 504 | error('plugin "' + name + '" returned bad value from its defined domain method'); 505 | } 506 | (new Domains()).add(name, path.resolve(domval)); 507 | } 508 | } 509 | 510 | /** 511 | * use 512 | * Call with a Plugin instance (or list of) to pull in bundled data &&|| code from a domain 513 | */ 514 | Base.prototype.use = function (inst) { 515 | this.removeSubClassMethods(); 516 | if (envCheck()) { 517 | if (type.isArray(inst)) { 518 | inst.forEach(function (plugin) { 519 | addPlugin(plugin); 520 | }); 521 | } 522 | else { 523 | addPlugin(inst); 524 | } 525 | } 526 | return this; 527 | }; 528 | 529 | 530 | 531 | /** 532 | * End point of the chain if in the right environment 533 | * compile initiates the one time call to the bundler 534 | */ 535 | Base.prototype.compile = function (target) { 536 | this.removeSubClassMethods(); 537 | if (envCheck()) { 538 | if (type.isFunction(target)) { 539 | o.target = target; 540 | } 541 | else if (type.isString(target)) { 542 | o.target = path.resolve(target); 543 | } 544 | o.exts = ['', '.js'].concat(Object.keys(o.compilers)); 545 | bundle(o); 546 | // end chain here 547 | } 548 | return this; 549 | }; 550 | 551 | 552 | 553 | // internal mini-tests 554 | if (module === require.main) { 555 | var modul8 = { 556 | minifier: function () {}, 557 | testcutter: function () {} 558 | }; 559 | module.exports('app.cs') 560 | .set('domloader', function (code) { 561 | return code; 562 | }) 563 | .set('namespace', 'QQ') 564 | .set('logging', 'INFO') 565 | .register('.cs', function (code, bare) { 566 | return code; 567 | }) 568 | .before(modul8.testcutter) 569 | .libraries() 570 | .list(['jQuery.js', 'history.js']) 571 | .path('/app/client/libs/') 572 | .target('dm-libs.js') 573 | .arbiters() 574 | .add('jQuery', ['$', 'jQuery']) 575 | .add('Spine') 576 | .arbiters({ 577 | 'underscore': 'underscore', 578 | '_': '_' 579 | }) 580 | .domains() 581 | .add('app', '/app/client/') 582 | .add('shared', '/app/shared/') 583 | .data() 584 | .add('models', '{modeldata:{getssenttoclient}}') 585 | .add('versions', {'users/view': [0, 2, 5]}) 586 | .analysis() 587 | .prefix(true) 588 | .suffix(false) 589 | .in('development') 590 | .output(console.log) 591 | .in('production') 592 | .output('filepath!') 593 | .in('all') 594 | .after(modul8.minifier) 595 | .compile('dm.js'); 596 | } 597 | 598 | --------------------------------------------------------------------------------