├── .gitignore ├── .npmignore ├── README.md ├── app.js ├── bin ├── cli.js └── quickstart ├── browser.js ├── browser └── process.js ├── lib └── quickstart.js ├── main.js ├── package.json ├── runtime ├── browser.js └── node.js ├── test ├── main.js ├── parsers │ ├── empty.js │ └── error.js ├── resolver.js ├── root │ ├── circular-a.js │ ├── circular-b.js │ ├── false.js │ ├── index.js │ ├── invalid-javascript.js │ ├── invalid-require.js │ ├── lib │ │ └── fs.js │ ├── main.js │ ├── module.empty │ ├── module.json │ ├── module.txt │ ├── new.js │ ├── node_modules │ │ ├── five │ │ │ ├── browser.js │ │ │ ├── main.js │ │ │ ├── new.js │ │ │ ├── old.js │ │ │ └── package.json │ │ ├── four │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── one │ │ │ ├── index.js │ │ │ ├── new.js │ │ │ ├── old.js │ │ │ ├── one.js │ │ │ └── package.json │ │ ├── seven │ │ │ ├── lib │ │ │ │ └── index.js │ │ │ └── package.json │ │ ├── six │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── three │ │ │ ├── lib │ │ │ │ └── three.js │ │ │ └── package.json │ │ └── two │ │ │ ├── lib.js │ │ │ ├── lib │ │ │ ├── index.js │ │ │ └── lib.js │ │ │ ├── lib2 │ │ │ └── index.js │ │ │ ├── package.json │ │ │ └── two.js │ ├── old.js │ ├── package.json │ ├── test-browser.js │ ├── test-circular.js │ ├── test-json.js │ └── test-node.js ├── sequence.js └── transforms │ ├── error.js │ └── passthrough.js ├── transforms ├── inject-globals.js └── require-dependencies.js └── util ├── konsole.js ├── messages.js ├── program.js ├── resolver.js ├── sequence.js └── transport.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/quickstart/f9f12f3e3dc2a041e4d8ad7b5b46a2d9494d919c/.npmignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Deprecated] 2 | 3 | QuickStart is no longer maintained. It will still exist in npm, but no more updates will happen. We no longer use QuickStart at Spotify, and instead recommend other bundlers such as [Browserify](http://browserify.org/) or [Webpack](https://webpack.github.io/). 4 | 5 | # [QuickStart](http://spotify.github.io/quickstart) 6 | 7 | A CommonJS module resolver, loader and compiler for node.js and browsers. 8 | 9 | ## Features 10 | 11 | * Runs in node.js **and browsers**. 12 | * Supports (most) node builtins and globals. 13 | * SpiderMonkey AST based Plugin system. 14 | * Stylish logs. 15 | 16 | ## General Usage 17 | 18 | ### Install QuickStart globally (for the cli) 19 | 20 | ``` 21 | npm install quickstart -g 22 | ``` 23 | 24 | ### Add index.html, package.json for your application 25 | 26 | ``` 27 | cd my-awesome-app 28 | ``` 29 | 30 | index.html 31 | ```html 32 | 33 | 34 | 35 | Awesomeness 36 | 37 | 38 | 39 | 40 | ``` 41 | 42 | package.json 43 | ```json 44 | { 45 | "name": "my-awesome-app" 46 | } 47 | ``` 48 | 49 | ### Install needed npm packages, QuickStart and plugins locally 50 | 51 | ``` 52 | npm install underscore --save 53 | npm install quickstart some-quickstart-transform --save-dev 54 | ``` 55 | 56 | ### Build a development QuickStart file. 57 | 58 | ``` 59 | quickstart --transforms some-quickstart-transform --self > quickstart.js 60 | ``` 61 | 62 | QuickStart will build a standalone QuickStart compiler (for browsers) that includes plugins. 63 | If you want to install or remove QuickStart plugins, or change options, repeat this step. 64 | 65 | After that, simply link `quickstart.js` in the `` of a document. It will compile and load your application at runtime in the browser. 66 | 67 | ### Deploy 68 | 69 | ``` 70 | quickstart --transforms some-quickstart-transform > awesome.js 71 | ``` 72 | 73 | This will create a compiled application for deployment. 74 | 75 | Now simply replace `quickstart.js` with `awesome.js` 76 | 77 | ```html 78 | 79 | ``` 80 | 81 | ## Entry Point 82 | 83 | QuickStart always starts compiling your application from the entry point. 84 | This value might be read from these locations in this order: 85 | 86 | 1. Manually specified `main` option (command line or node.js). 87 | 2. Resolved automatically with the built-in node-style module resolver. 88 | 89 | ## Plugin system 90 | 91 | QuickStart has two types of plugins: transforms and parsers. 92 | 93 | * Parser plugins transform a specific type of source code to a SpiderMonkey AST object. 94 | * Transform plugins transform a SpiderMonkey AST object to a SpiderMonkey AST object. 95 | 96 | ## node.js interface 97 | 98 | ```js 99 | var quickstart = require('quickstart'); 100 | 101 | // the quickstart function returns a promise. 102 | quickstart({/* options */}).then(function(compiled) { 103 | var ast = compiled.ast; 104 | var source = compiled.source; 105 | var sourceMap = compiled.sourceMap; 106 | // print the generated JavaScript, or print / work with the abstract syntax tree, work with sourceMaps, etc. 107 | }); 108 | ``` 109 | 110 | ### options 111 | 112 | Note: options might be augmented with the parameter `--config jsonFile.json`. It defaults to quickstart.json, and will be ignored if not found. 113 | 114 | Command line options look the same, except hyphenated. 115 | 116 | ```js 117 | { 118 | runtime: 'quickstart/runtime/browser', // override the default runtime, defaults to quickstart/runtime/browser 119 | transforms: [], // which transforms to use, defaults to none 120 | parsers: {}, // which parsers to use for each file extension, defaults to none, except embedded ones such as .js and .json. 121 | compress: false, // optimize and mangle the ast and JavaScript output 122 | output: true, // generates the (compressed if {compress: true}) JavaScript output, defaults to true 123 | sourceMap: false, // generates the (compressed if {compress: true}) source map, defaults to false 124 | self: false, // compiles the QuickStart compiler instead of the current app, defaults to false 125 | main: false, // override the application's main, defaults to the QuickStart resolver 126 | warnings: true // display warning messages, defaults to true 127 | } 128 | ``` 129 | 130 | ## command line interface 131 | 132 | ``` 133 | quickstart --help 134 | ``` 135 | 136 | ### options 137 | 138 | ``` 139 | --runtime runtimeModule # override the default runtime 140 | --transforms transformModule # which transforms to use 141 | --parsers ext=parserModule # which parsers to use 142 | --compress # optimize and mangle the ast and JavaScript output 143 | --output # generates the (compressed if `--compress` is set) JavaScript output, defaults to true 144 | --source-map # generates the (compressed if `--compress` is set) source map, defaults to false 145 | --self # compiles the QuickStart compiler instead of the current app, defaults to false 146 | --main ./path/to/entry-point # override the application's entry point, defaults to the QuickStart resolver 147 | --warnings # display warnings messages, defaults to true 148 | --ast ./path/to/source.ast # writes the ast to a file or *STDOUT*, defaults to false 149 | ``` 150 | 151 | When `--output` is set to a string, it will send the JavaScript output to that file instead of STDOUT. 152 | ``` 153 | quickstart --output output.js 154 | ``` 155 | 156 | When `--source-map` is set to a string, it will send the source map output to that file instead of STDOUT. 157 | ``` 158 | quickstart --source-map output.map > output.js 159 | ``` 160 | 161 | When `--source-map` is set without a value, and `--output` is set, it will append an inline base64 encoded source map to the output. 162 | ``` 163 | quickstart --source-map > output.js 164 | quickstart --source-map --output output.js 165 | ``` 166 | 167 | When `--source-map` is set and `--output` is unset (`--no-output`) it will write the source map to STDOUT (no value) or the file (value). 168 | ``` 169 | quickstart --no-output --source-map > output.map 170 | quickstart --no-output --source-map output.map 171 | ``` 172 | 173 | When `--ast` is set without a value the ast is printed to STDOUT. 174 | ``` 175 | quickstart --ast > output.ast 176 | ``` 177 | 178 | This is useful, for instance, to pipe the AST to UglifyJS or any other program that accepts a SpiderMonkey AST: 179 | ``` 180 | quickstart --ast --source-map | uglifyjs --spidermonkey > out.js 181 | ``` 182 | 183 | Note: the `--source-map` option must be set if you need location information in the AST (to have UglifyJS generate a source map, for instance). 184 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var quickstart = require('quickstart'); 4 | var config = require('./@config.json'); 5 | quickstart(config); 6 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | 5 | var requireRelative = require('require-relative'); 6 | 7 | var clc = require('cli-color'); 8 | 9 | var pathogen = require('pathogen'); 10 | 11 | var isString = require('mout/lang/isString'); 12 | var forIn = require('mout/object/forIn'); 13 | var mixIn = require('mout/object/mixIn'); 14 | var isArray = require('mout/lang/isArray'); 15 | var camelCase = require('mout/string/camelCase'); 16 | 17 | var Konsole = require('../util/konsole'); 18 | var Messages = require('../util/messages'); 19 | 20 | var compile = require('../'); 21 | var manifest = require('../package.json'); 22 | 23 | // # argv 24 | module.exports = function(argv) { 25 | 26 | var root = argv.root; 27 | 28 | if (argv.config == null) argv.config = pathogen.resolve(root, './quickstart.json'); 29 | 30 | if (argv.o != null) { 31 | argv.output = argv.o; 32 | delete argv.o; 33 | } 34 | 35 | if (argv.parsers) { 36 | var parsers = {}; 37 | if (!isArray(argv.parsers)) argv.parsers = [argv.parsers]; 38 | argv.parsers.forEach(function(parser) { 39 | var parts = parser.split('='); 40 | parsers[parts[0].trim()] = parts[1].trim(); 41 | }); 42 | argv.parsers = parsers; 43 | } 44 | 45 | if (argv.transforms) { 46 | if (!isArray(argv.transforms)) argv.transforms = [argv.transforms]; 47 | } 48 | 49 | var jsonConf; 50 | 51 | if (/\.json$/.test(argv.config)) try { 52 | jsonConf = requireRelative(pathogen(argv.config), root); 53 | } catch(e) {} 54 | 55 | var options = {}; 56 | 57 | // augment options with config file, if specified and valid 58 | if (jsonConf) mixIn(options, jsonConf); 59 | 60 | // augment options with argv 61 | forIn(argv, function(value, name) { 62 | if (name.length > 1 && name !== 'config' && name !== 'version' && name !== 'help') { 63 | options[camelCase(name)] = value; 64 | } 65 | }); 66 | 67 | if (!options.parsers) options.parsers = {}; 68 | if (!options.transforms) options.transforms = []; 69 | 70 | // set warnings to true by default 71 | if (options.warnings == null) options.warnings = true; 72 | 73 | // clean up options.output 74 | if (options.output == null) options.output = true; 75 | 76 | // # help konsole 77 | 78 | var help = new Konsole('log'); 79 | 80 | var logo = 'QuickStart ' + clc.white('v' + manifest.version); 81 | 82 | help.group(logo); 83 | help.write(''); 84 | 85 | help.write(clc.green('--self '), 86 | 'compile quickstart for browser compilation. defaults to', clc.red('false')); 87 | 88 | help.write(clc.green('--root '), 89 | 'use this as the root of each path. defaults to', clc.blue(pathogen.cwd())); 90 | 91 | help.write(clc.green('--main '), 92 | 'override the default entry point. defaults to', clc.red('false')); 93 | 94 | help.write(clc.green('--output, -o '), 95 | 'output the compiled source to a file or STDOUT. defaults to', clc.blue('STDOUT')); 96 | 97 | help.write(clc.green('--source-map '), 98 | 'output the source map to a file, STDOUT or inline. defaults to', clc.red('false')); 99 | 100 | help.write(clc.green('--ast '), 101 | 'output the Esprima generated AST to a file or STDOUT. defaults to', clc.red('false')); 102 | 103 | help.write(clc.green('--optimize '), 104 | 'feed transforms with an optimized ast. defaults to', clc.red('false')); 105 | 106 | help.write(clc.green('--compress '), 107 | 'compress the resulting AST using esmangle. defaults to', clc.red('false')); 108 | 109 | help.write(clc.green('--runtime '), 110 | 'specify a custom runtime. defaults to', clc.red('false')); 111 | 112 | help.write(clc.green('--config '), 113 | 'specify a configuration json file to augment command line options. defaults to', clc.red('false')); 114 | 115 | help.write(clc.green('--warnings '), 116 | 'display warning messages. defaults to', clc.blue('true')); 117 | 118 | help.write(clc.green('--help, -h '), 119 | 'display this help screen'); 120 | 121 | help.write(clc.green('--version, -v'), 122 | 'display the current version'); 123 | 124 | help.write(''); 125 | 126 | help.groupEnd(); // QuickStart 127 | 128 | // # help command 129 | 130 | var printOptions = { 131 | last: clc.whiteBright('└─'), 132 | item: clc.whiteBright('├─'), 133 | join: clc.whiteBright(' '), 134 | line: clc.whiteBright('│'), 135 | spcr: clc.whiteBright(' ') 136 | }; 137 | 138 | if (argv.help || argv.h) { 139 | help.print(' '); 140 | process.exit(0); 141 | } 142 | 143 | // # version command 144 | 145 | if (argv.version || argv.v) { 146 | console.log('v' + manifest.version); 147 | process.exit(0); 148 | } 149 | 150 | // # beep beep 151 | 152 | var beep = function() { 153 | process.stderr.write('\x07'); // beep! 154 | }; 155 | 156 | // # format messages 157 | 158 | function format(type, statement) { 159 | 160 | if (type === 'group' || type === 'groupCollapsed') { 161 | if ((/warning/i).test(statement)) { 162 | if (options.warnings) konsole.group(clc.yellow(statement)); 163 | } else if ((/error/i).test(statement)) { 164 | konsole.group(clc.red(statement)); 165 | } else { 166 | konsole.group(statement); 167 | } 168 | return; 169 | } 170 | 171 | if (type === 'groupEnd') { 172 | konsole.groupEnd(); 173 | return; 174 | } 175 | 176 | var message, source, line, column, id; 177 | 178 | message = statement.message; 179 | 180 | id = statement.id; 181 | 182 | source = statement.source; 183 | line = statement.line; 184 | column = statement.column; 185 | 186 | if (source != null) { 187 | source = clc.green(pathogen.resolve(root, source)); 188 | if (line != null) { 189 | source += ' on line ' + line; 190 | if (column != null) { 191 | source += ', column' + column; 192 | } 193 | } 194 | message = message ? [message, source].join(' ') : source; 195 | } 196 | 197 | switch (type) { 198 | case 'error': 199 | konsole.write(clc.red(id + ': ') + message); 200 | break; 201 | case 'warn': 202 | if (options.warnings) konsole.write(clc.yellow(id + ': ') + message); 203 | break; 204 | case 'time': 205 | konsole.write(clc.blue(id + ': ') + message); 206 | break; 207 | default: 208 | konsole.write(id + ': ' + message); 209 | break; 210 | } 211 | 212 | } 213 | 214 | var messages = new Messages(logo); 215 | var konsole = new Konsole('error'); 216 | 217 | var compilation = messages.group('Compilation'); 218 | compilation.time('time'); 219 | 220 | var optionsGroup = messages.group('Options'); 221 | forIn(options, function(value, key) { 222 | var string = JSON.stringify(value); 223 | if (string) optionsGroup.log({ id: key, message: string }); 224 | }); 225 | 226 | compile(options, messages).then(function(compiled) { 227 | 228 | var ast = compiled.ast; 229 | var source = compiled.source; 230 | var sourceMap = compiled.sourceMap; 231 | 232 | if (source) source = '/* compiled with ' + manifest.name + '@' + manifest.version + ' */' + source; 233 | 234 | if (options.output && options.sourceMap === true) { 235 | sourceMap = JSON.stringify(sourceMap); 236 | source += '\n//# sourceMappingURL=data:application/json;base64,' + new Buffer(sourceMap).toString('base64'); 237 | compilation.log({ id: 'source map', message: 'embedded' }); 238 | } 239 | 240 | if (isString(options.sourceMap)) { 241 | var sourceMapPath = pathogen.resolve(root, options.sourceMap); 242 | fs.writeFileSync(pathogen.sys(sourceMapPath), JSON.stringify(sourceMap)); 243 | if (options.output) source += '\n//# sourceMappingURL=' + pathogen.relative(root, sourceMapPath); 244 | 245 | compilation.log({ id: 'sourceMap', message: 'file written', source: pathogen.relative(root, sourceMapPath) }); 246 | } 247 | 248 | if (isString(options.output)) { 249 | var sourcePath = pathogen.sys(pathogen.resolve(root, options.output)); 250 | fs.writeFileSync(sourcePath, source); 251 | compilation.log({ id: 'source', message: 'file written', source: pathogen.relative(root, sourcePath) }); 252 | } 253 | 254 | if (isString(options.ast)) { 255 | var astPath = pathogen.sys(pathogen.resolve(root, options.ast)); 256 | fs.writeFileSync(astPath, JSON.stringify(ast)); 257 | 258 | compilation.log({ id: 'ast', message: 'file written', source: pathogen.relative(root, astPath) }); 259 | } 260 | 261 | if (options.ast === true) { 262 | console.log(JSON.stringify(ast)); 263 | 264 | compilation.log({ id: 'ast', message: 'stdout' }); 265 | } else if (options.output === true) { 266 | console.log(source); 267 | 268 | compilation.log({ id: 'source', message: 'stdout' }); 269 | } else if (options.sourceMap === true) { 270 | console.log(sourceMap); 271 | 272 | compilation.log({ id: 'sourceMap', message: 'stdout' }); 273 | } 274 | 275 | compilation.timeEnd('time', options.self ? 'compiled itself in' : 'compiled in', true, true); 276 | 277 | messages.print(format); 278 | konsole.print(printOptions); 279 | messages.reset(); 280 | 281 | beep(); 282 | 283 | }).catch(function(error) { 284 | 285 | messages.print(format); 286 | konsole.print(printOptions); 287 | 288 | beep(); beep(); // beepbeep! 289 | process.nextTick(function() { 290 | throw error; 291 | }); 292 | return; 293 | }); 294 | 295 | }; 296 | -------------------------------------------------------------------------------- /bin/quickstart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var pathogen = require('pathogen'); 4 | var minimist = require('minimist'); 5 | var requireRelative = require('require-relative'); 6 | 7 | var argv = minimist(process.argv.slice(2)); 8 | // clean up options.root, save root 9 | var root = argv.root = argv.root ? pathogen.resolve(argv.root + '/') : pathogen.cwd(); 10 | 11 | var cli; 12 | 13 | try { 14 | cli = requireRelative('quickstart/bin/cli', root); 15 | } catch(e) { 16 | cli = require('./cli'); 17 | } 18 | 19 | cli(argv); 20 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | /* jshint evil: true */ 2 | 'use strict'; 3 | 4 | 5 | // This is the main for browsers, and runs in the browser only. 6 | 7 | var pathogen = require('pathogen'); 8 | var escodegen = require('escodegen'); 9 | var esprima = require('esprima'); 10 | 11 | var forEach = require('mout/array/forEach'); 12 | var forIn = require('mout/object/forIn'); 13 | var map = require('mout/object/map'); 14 | 15 | var QuickStart = require('./lib/quickstart'); 16 | var program = require('./util/program'); 17 | var Messages = require('./util/messages'); 18 | 19 | var version = require('./package.json').version; 20 | 21 | var noop = function(){}; 22 | 23 | if (!global.console) global.console = {}; 24 | if (!console.log) console.log = noop; 25 | if (!console.warn) console.warn = console.log; 26 | if (!console.error) console.error = console.log; 27 | if (!console.group) console.group = console.log; 28 | if (!console.groupCollapsed) console.groupCollapsed = console.group; 29 | if (!console.groupEnd) console.groupEnd = noop; 30 | 31 | var root = pathogen.cwd(); 32 | 33 | var theme = { 34 | red: 'color: #d44;', 35 | green: 'color: #6b4;', 36 | blue: 'color: #49d;', 37 | yellow: 'color: #f90;', 38 | grey: 'color: #666;', 39 | greyBright: 'color: #999;', 40 | bold: 'font-weight: bold;' 41 | }; 42 | 43 | var format = function(type, statement) { 44 | if (type === 'group' || type === 'groupCollapsed') { 45 | 46 | var color; 47 | 48 | if ((/warning/i).test(statement)) { 49 | color = theme.yellow + theme.bold; 50 | } else if ((/error/i).test(statement)) { 51 | color = theme.red + theme.bold; 52 | } else { 53 | color = theme.grey + theme.bold; 54 | } 55 | 56 | console[type]('%c' + statement, color); 57 | 58 | return; 59 | } 60 | 61 | if (type === 'groupEnd') { 62 | console.groupEnd(); 63 | return; 64 | } 65 | 66 | var message, source, line, column, id; 67 | 68 | message = statement.message; 69 | 70 | var colors = [theme.grey]; 71 | 72 | id = statement.id; 73 | 74 | source = statement.source; 75 | line = statement.line; 76 | column = statement.column; 77 | 78 | if (source != null) { 79 | source = ' %c' + location.origin + pathogen.resolve(root, source); 80 | if (line != null) { 81 | source += ':' + line; 82 | if (column != null) { 83 | source += ':' + column; 84 | } 85 | } 86 | message += source; 87 | colors.push(theme.green); 88 | } 89 | 90 | message = '%c' + id + ': %c' + message; 91 | 92 | switch (type) { 93 | case 'error': 94 | colors.unshift(theme.red); 95 | console.error.apply(console, [message].concat(colors)); 96 | break; 97 | 98 | case 'warn': 99 | colors.unshift(theme.yellow); 100 | console.warn.apply(console, [message].concat(colors)); 101 | break; 102 | 103 | case 'time': 104 | colors.unshift(theme.blue); 105 | console.log.apply(console, [message].concat(colors)); 106 | break; 107 | 108 | default: 109 | colors.unshift(theme.grey); 110 | console.log.apply(console, [message].concat(colors)); 111 | break; 112 | } 113 | }; 114 | 115 | function generate(module) { 116 | var sourceURL = '\n//# sourceURL='; 117 | 118 | var output = escodegen.generate(module.ast, { 119 | format: { 120 | indent: { style: ' ' }, 121 | quotes: 'single' 122 | } 123 | }); 124 | 125 | output += sourceURL + module.path.substr(2); 126 | return new Function('require', 'module', 'exports', 'global', output); 127 | } 128 | 129 | module.exports = function(config) { 130 | var parsers = {}; 131 | var transforms = []; 132 | 133 | // config(parsers|transforms) contains resolved paths we can just require(). 134 | 135 | forIn(config.parsers, function(id, ext) { 136 | var parser = require(id); 137 | parsers[ext] = parser; 138 | }); 139 | 140 | forEach(config.transforms, function(id) { 141 | var transform = require(id); 142 | transforms.push(transform); 143 | }); 144 | 145 | var messages = new Messages; 146 | var compilation = messages.group('Compilation'); 147 | compilation.time('time'); 148 | 149 | var quickstart = new QuickStart({ 150 | messages: messages, 151 | parsers: parsers, 152 | transforms: transforms, 153 | loc: !!config.sourceMap, 154 | defaultPath: config.defaultPath, 155 | root: root 156 | }); 157 | 158 | console.group("%cQuick%cStart " + "%cv" + version, theme.red, theme.grey, theme.greyBright); 159 | 160 | console.groupCollapsed('%cXMLHttpRequests', theme.grey + theme.bold); 161 | 162 | var modules = quickstart.modules; 163 | var runtimeData = config.runtimeData; 164 | var runtimePath = config.runtimePath; 165 | 166 | return quickstart.require(root, config.main).then(function(id) { 167 | console.groupEnd(); // XMLHttpRequests 168 | 169 | var done; 170 | 171 | if (config.sourceMap) { 172 | compilation.log({ id: 'sourceMap', message: 'embedded' }); 173 | 174 | var runtimeTree = esprima.parse(runtimeData, {loc: true, source: runtimePath}); 175 | 176 | var tree = program(id, modules, runtimeTree); 177 | 178 | var sourceMappingURL = '\n//# sourceMappingURL=data:application/json;base64,'; 179 | 180 | var output = escodegen.generate(tree, { 181 | format: { 182 | indent: { style: ' ' }, 183 | quotes: 'single' 184 | }, 185 | sourceMap: true, 186 | sourceMapRoot: location.origin + root, 187 | sourceMapWithCode: true 188 | }); 189 | 190 | var source = output.code + sourceMappingURL + btoa(JSON.stringify(output.map)); 191 | 192 | done = function() { return global.eval(source); }; 193 | 194 | } else { 195 | 196 | var sourceURL = '\n//# sourceURL='; 197 | 198 | var evaluated = map(modules, generate); 199 | 200 | var runtimeFn = new Function('main', 'modules', runtimeData + sourceURL + runtimePath.substr(2)); 201 | 202 | done = function() { return runtimeFn(id, evaluated); }; 203 | } 204 | 205 | compilation.timeEnd('time', 'compiled in', true, true); 206 | messages.print(format).reset(); 207 | console.groupEnd(); // QuickStart 208 | 209 | setTimeout(function() { done(); }, 1); 210 | 211 | }).catch(function(error) { 212 | 213 | console.groupEnd(); // XMLHttpRequests 214 | messages.print(format).reset(); 215 | console.groupEnd(); // QuickStart 216 | 217 | setTimeout(function() { throw error; }, 1); // app error; 218 | 219 | }); 220 | 221 | }; 222 | -------------------------------------------------------------------------------- /browser/process.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.title = document.title; 4 | exports.browser = true; 5 | 6 | exports.cwd = function() { 7 | return location.pathname.split(/\/+/g).slice(0, -1).join('/') || '/'; 8 | }; 9 | -------------------------------------------------------------------------------- /lib/quickstart.js: -------------------------------------------------------------------------------- 1 | /* global -Promise */ 2 | 'use strict'; 3 | 4 | var Promise = require('promise'); 5 | 6 | var prime = require('prime'); 7 | var pathogen = require('pathogen'); 8 | 9 | var mixIn = require('mout/object/mixIn'); 10 | var append = require('mout/array/append'); 11 | var find = require('mout/array/find'); 12 | 13 | var esprima = require('esprima'); 14 | 15 | var sequence = require('../util/sequence').use(Promise); 16 | var transport = require('../util/transport'); 17 | var Resolver = require('../util/resolver'); 18 | var Messages = require('../util/messages'); 19 | 20 | var requireDependencies = require('../transforms/require-dependencies'); 21 | 22 | var isNative = Resolver.isNative; 23 | 24 | // built in parsers 25 | var parsers = { 26 | // The default string parser. 27 | // When a type is unknown it will be processed as a string. 28 | txt: function(path, text) { 29 | var tree = esprima.parse('module.exports = ""'); 30 | tree.body[0].expression.right = { 31 | type: 'Literal', 32 | value: text 33 | }; 34 | return tree; 35 | }, 36 | 37 | // The default JavaScript parser. 38 | // When a file extension is .js it will be processed as JavaScript using esprima. 39 | js: function(path, text) { 40 | return esprima.parse(text, {loc: this.loc, source: path}); 41 | }, 42 | 43 | // The default JSON parser. 44 | // When a file extension is .json it will be processed as JavaScript using esprima. 45 | // No location information is necessary, as this is simply JSON. 46 | json: function(path, json) { 47 | return esprima.parse('module.exports = ' + json); 48 | } 49 | 50 | }; 51 | 52 | var QuickStart = prime({ 53 | 54 | constructor: function QuickStart(options) { 55 | if (!options) options = {}; 56 | this.options = options; 57 | 58 | // Tells esprima to store location information. 59 | // This is enabled when source maps are enabled. 60 | // Using this makes esprima slower, but it's necessary for source maps. 61 | this.loc = !!options.loc; 62 | // Override the working directory, defaults to cwd. 63 | this.root = options.root ? pathogen.resolve(options.root) : pathogen.cwd(); 64 | 65 | this.index = 0; 66 | 67 | this.node = !!options.node; 68 | 69 | this.resolver = new Resolver({ 70 | browser: !this.node, 71 | defaultPath: options.defaultPath 72 | }); 73 | 74 | // Initialize the modules object. 75 | this.modules = {}; 76 | 77 | // Store plugins. 78 | 79 | this.parsers = mixIn({}, parsers, options.parsers); 80 | this.transforms = append(append([], options.transforms), [requireDependencies]); 81 | 82 | this.packages = {}; 83 | 84 | this.messages = options.messages || new Messages; 85 | 86 | this.cache = { 87 | parse: {} 88 | }; 89 | }, 90 | 91 | // > resolve 92 | resolve: function(from, required) { 93 | var self = this; 94 | 95 | var messages = self.messages; 96 | 97 | var dir1 = pathogen.dirname(from); 98 | 99 | var selfPkg = /^quickstart$|^quickstart\//; 100 | if (selfPkg.test(required)) { 101 | required = pathogen(required.replace(selfPkg, pathogen(__dirname + '/../'))); 102 | } 103 | 104 | return self.resolver.resolve(dir1, required).then(function(resolved) { 105 | 106 | if (isNative(resolved)) { 107 | // resolved to native module, try to resolve from quickstart. 108 | var dir2 = pathogen(__dirname + '/'); 109 | return (dir1 !== dir2) ? self.resolver.resolve(dir2, required) : resolved; 110 | } else { 111 | return resolved; 112 | } 113 | }).catch(function(error) { 114 | messages.group('Errors').error({ 115 | id: 'ResolveError', 116 | message: 'unable to resolve ' + required, 117 | source: pathogen.relative(self.root, from) 118 | }); 119 | throw error; 120 | }); 121 | }, 122 | 123 | // resolve > transport > parse 124 | require: function(from, required) { 125 | var self = this; 126 | 127 | return self.resolve(from, required).then(function(resolved) { 128 | if (isNative(resolved)) return resolved; 129 | if (resolved === false) return false; 130 | 131 | return self.analyze(from, required, resolved).then(function() { 132 | return self.include(resolved); 133 | }); 134 | }); 135 | }, 136 | 137 | // transport > parse 138 | include: function(path) { 139 | var self = this; 140 | 141 | var messages = self.messages; 142 | 143 | var uid = self.uid(path); 144 | 145 | var module = self.modules[uid]; 146 | if (module) return Promise.resolve(uid); 147 | 148 | return transport(path).then(function(data) { 149 | return self.parse(path, data); 150 | }, function(error) { 151 | messages.group('Errors').error({ 152 | id: 'TransportError', 153 | message: 'unable to read', 154 | source: pathogen.relative(self.root, path) 155 | }); 156 | throw error; 157 | }).then(function() { 158 | return uid; 159 | }); 160 | }, 161 | 162 | analyze: function(from, required, resolved) { 163 | var self = this; 164 | 165 | var packages = self.packages; 166 | var messages = self.messages; 167 | var root = self.root; 168 | 169 | return self.resolver.findRoot(resolved).then(function(path) { 170 | return transport.json(path + 'package.json').then(function(json) { 171 | return { json : json, path: path }; 172 | }); 173 | }).then(function(result) { 174 | var path = result.path; 175 | var name = result.json.name; 176 | var version = result.json.version; 177 | 178 | path = pathogen.relative(root, path); 179 | 180 | var pkg = packages[name] || (packages[name] = []); 181 | 182 | var same = find(pkg, function(obj) { 183 | return (obj.path === path); 184 | }); 185 | 186 | if (same) return; 187 | 188 | var instance = { version: version, path: path }; 189 | pkg.push(instance); 190 | 191 | if (pkg.length > 1) { 192 | var group = messages.group('Warnings'); 193 | 194 | // warn about the first at length 2 195 | if (pkg.length === 2) group.warn({ 196 | id: name, 197 | message: 'duplicate v' + pkg[0].version + ' found', 198 | source: pkg[0].path 199 | }); 200 | 201 | // warn about every subsequent 202 | group.warn({ 203 | id: name, 204 | message: 'duplicate v' + version + ' found', 205 | source: path 206 | }); 207 | } 208 | 209 | }, function() { }); // into the void 210 | }, 211 | 212 | uid: function(full) { 213 | return pathogen.relative(this.root, full); 214 | }, 215 | 216 | // > parse 217 | parse: function(full, data) { 218 | var self = this; 219 | 220 | var cache = self.cache.parse; 221 | if (cache[full]) return cache[full]; 222 | 223 | var modules = self.modules; 224 | var messages = self.messages; 225 | 226 | var uid = self.uid(full); 227 | 228 | var relative = pathogen.relative(self.root, full); 229 | 230 | var module = modules[uid] = { uid: uid }; 231 | 232 | var extname = pathogen.extname(full).substr(1); 233 | 234 | var parse = (extname && self.parsers[extname]) || self.parsers.txt; 235 | 236 | // Process flow 237 | 238 | // use Promise.resolve so that the (possibly) public parser can return a promise or a syntax tree. 239 | return cache[full] = Promise.resolve().then(function() { 240 | // 1. process the code to AST, based on extension 241 | return parse.call(self, relative, data); 242 | }).catch(function(error) { 243 | 244 | messages.group('Errors').error({ 245 | id: 'ParseError', 246 | message: error.message, 247 | source: relative 248 | }); 249 | 250 | throw error; 251 | }).then(function(tree) { 252 | // 2. transform the AST, based on specified transforms 253 | return self.transform(relative, tree); 254 | }).then(function(tree) { 255 | // 4. callback with module object 256 | module.ast = tree; 257 | module.path = relative; 258 | return module; 259 | }); 260 | 261 | }, 262 | 263 | // Apply transformations. 264 | transform: function(path, tree) { 265 | var self = this; 266 | // these are applied as a promises reduce operation 267 | return sequence.reduce(self.transforms, function(tree, transform) { 268 | return transform.call(self, path, tree); 269 | }, tree); 270 | } 271 | 272 | }); 273 | 274 | module.exports = QuickStart; 275 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* global -Promise */ 2 | 'use strict'; 3 | 4 | // This is the main for node.js, and runs in node.js only. 5 | 6 | var Promise = require('promise'); 7 | 8 | var pathogen = require('pathogen'); 9 | 10 | var esprima = require('esprima'); 11 | var escodegen = require('escodegen'); 12 | var esmangle = require('esmangle'); 13 | 14 | var QuickStart = require('./lib/quickstart'); 15 | 16 | var Resolver = require('./util/resolver'); 17 | var program = require('./util/program'); 18 | var sequence = require('./util/sequence').use(Promise); 19 | var transport = require('./util/transport'); 20 | var Messages = require('./util/messages'); 21 | 22 | var injectGlobals = require('./transforms/inject-globals'); 23 | 24 | var resolver = new Resolver; 25 | 26 | function compileSelf(options, appRuntimePath, appRuntimeData, messages) { 27 | 28 | var root = options.root; 29 | if (options.browserSourceMap == null) options.browserSourceMap = true; 30 | 31 | // Instantiate QuickStart with the root specified in options 32 | // which gives the QuickStart compiler a different root to work on 33 | // it defaults to the current working directory. 34 | var compiler = new QuickStart({ 35 | messages: messages, 36 | loc: !!options.sourceMap, 37 | root: root, 38 | node: false, 39 | parsers: {}, 40 | transforms: [injectGlobals] 41 | }); 42 | 43 | // Configuration object that we will inject in the compiled compiler 44 | // as the compiler never reads from external sources for portability, except for builtins. 45 | // This includes the app's runtime as a string, parser and transforms (populated in a later step) 46 | // and some options. 47 | var config = { 48 | main: options.main ? pathogen(options.main) : './', 49 | runtimeData: appRuntimeData, 50 | runtimePath: pathogen.relative(root, appRuntimePath), 51 | sourceMap: !!options.browserSourceMap 52 | }; 53 | 54 | if (options.defaultPath) config.defaultPath = pathogen.relative(root, options.defaultPath); 55 | 56 | var runtimePath = pathogen(__dirname + '/runtime/browser.js'); 57 | 58 | return sequence.map(options.parsers, function(path) { 59 | 60 | return compiler.require(root, path); 61 | 62 | }).then(function(parsers) { 63 | // all the resolved parser paths 64 | config.parsers = parsers || {}; 65 | 66 | return sequence.map(options.transforms, function(path) { 67 | 68 | return compiler.require(root, path); 69 | 70 | }); 71 | 72 | }).then(function(transforms) { 73 | 74 | // all the resolved transforms paths 75 | config.transforms = transforms || []; 76 | 77 | // Add a fake module called @config.json, in the app root, with the config object we created 78 | 79 | var configPath = pathogen.resolve(root, './@config.json'); 80 | var configData = JSON.stringify(config); 81 | transport.cache.get[configPath] = Promise.resolve(configData); 82 | return compiler.parse(configPath, configData); 83 | 84 | }).then(function() { 85 | 86 | return transport(pathogen(__dirname + '/app.js')); 87 | 88 | }).then(function(appData) { 89 | 90 | return compiler.parse(pathogen.resolve(root, './@app.js'), appData); 91 | 92 | }).then(function(module) { 93 | return transport(runtimePath).then(function(runtimeData) { 94 | return { 95 | main: module.uid, 96 | modules: compiler.modules, 97 | runtimePath: runtimePath, 98 | runtimeData: runtimeData 99 | }; 100 | }); 101 | }); 102 | 103 | } 104 | 105 | function compileApp(options, appRuntimePath, appRuntimeData, messages) { 106 | 107 | var root = options.root; 108 | 109 | var transforms; 110 | var parsers; 111 | 112 | return sequence.map(options.parsers, function(parserPath) { 113 | 114 | return resolver.resolve(root, parserPath).then(require).catch(function(error) { 115 | messages.group('Errors').error({ 116 | id: 'RequireError', 117 | message: 'unable to require parser ' + parserPath, 118 | source: './' 119 | }); 120 | // load error 121 | throw error; 122 | }); 123 | 124 | }).then(function(parserModules) { 125 | parsers = parserModules; 126 | 127 | return sequence.map(options.transforms, function(transformPath) { 128 | return resolver.resolve(root, transformPath).then(require).catch(function(error) { 129 | messages.group('Errors').error({ 130 | id: 'RequireError', 131 | message: 'unable to require transform ' + transformPath, 132 | source: './' 133 | }); 134 | // load error 135 | throw error; 136 | }); 137 | 138 | }); 139 | 140 | }).then(function(transformModules) { 141 | 142 | transforms = transformModules; 143 | 144 | // instantiate QuickStart with parsers, transforms and options 145 | var compiler = new QuickStart({ 146 | messages: messages, 147 | transforms: transforms, 148 | parsers: parsers, 149 | root: root, 150 | loc: !!options.sourceMap, 151 | node: options.node, 152 | defaultPath: options.defaultPath 153 | }); 154 | 155 | return compiler.require(root, options.main ? pathogen(options.main) : './').then(function(main) { 156 | 157 | return { 158 | main: main, 159 | modules: compiler.modules, 160 | runtimePath: appRuntimePath, 161 | runtimeData: appRuntimeData 162 | }; 163 | 164 | }); 165 | 166 | }); 167 | } 168 | 169 | function compile(options, messages) { 170 | 171 | if (options == null) options = {}; 172 | 173 | if (!messages) messages = new Messages; 174 | 175 | // options.output should default to true. 176 | if (options.output == null) options.output = true; 177 | 178 | var root = options.root = options.root ? pathogen.resolve(options.root) : pathogen.cwd(); 179 | if (options.defaultPath) options.defaultPath = pathogen.resolve(root, options.defaultPath + '/'); 180 | 181 | var appRuntimePath; 182 | 183 | // Get the appropriate runtime path based on options. 184 | if (options.runtime) appRuntimePath = options.runtime; // let it resolve 185 | else if (options.node) appRuntimePath = pathogen.resolve(__dirname, './runtime/node.js'); 186 | else appRuntimePath = pathogen.resolve(__dirname, './runtime/browser.js'); 187 | 188 | return resolver.resolve(root, appRuntimePath).then(function(resolved) { 189 | return appRuntimePath = resolved; 190 | }).then(transport).then(function(appRuntimeData) { 191 | 192 | // building with --self means compiling a compiler for a browser. 193 | if (options.self) return compileSelf(options, appRuntimePath, appRuntimeData, messages); 194 | // otherwise we compile the app. 195 | return compileApp(options, appRuntimePath, appRuntimeData, messages); 196 | 197 | }).then(function(compileResult) { 198 | 199 | var main = compileResult.main; 200 | var modules = compileResult.modules; 201 | var runtimePath = compileResult.runtimePath; 202 | var runtimeData = compileResult.runtimeData; 203 | 204 | var runtimeSource = pathogen.relative(root, runtimePath); 205 | 206 | return Promise.resolve().then(function() { 207 | 208 | var runtimeTree = esprima.parse(runtimeData, { 209 | loc: !!options.sourceMap, 210 | source: runtimeSource 211 | }); 212 | 213 | // program the full ast 214 | return program(main, modules, runtimeTree); 215 | 216 | }).catch(function(error) { 217 | messages.group('Errors').error({ 218 | id: 'ParseError', 219 | message: 'unable to parse', 220 | source: pathogen.relative(root, runtimePath) 221 | }); 222 | throw error; 223 | }); 224 | 225 | }).then(function(tree) { 226 | // compress 227 | 228 | if (!options.compress) return tree; 229 | 230 | return Promise.resolve().then(function() { 231 | 232 | tree = esmangle.optimize(tree); 233 | tree = esmangle.mangle(tree); 234 | 235 | return tree; 236 | 237 | }).catch(function(error) { 238 | messages.group('Errors').error({ 239 | id: 'CompressionError', 240 | message: error.message 241 | }); 242 | throw error; 243 | }); 244 | 245 | }).then(function(tree) { 246 | 247 | return Promise.resolve().then(function() { 248 | 249 | var output = escodegen.generate(tree, { 250 | format: options.compress ? { 251 | compact: true, 252 | parentheses: false 253 | } : { 254 | indent: { style: ' ' }, 255 | quotes: 'single' 256 | }, 257 | sourceMap: !!options.sourceMap, 258 | sourceMapWithCode: true 259 | }); 260 | 261 | // trigger the callback with everything: 262 | // the sourceTree (AST) JavaScript as a string, and the sourceMap. 263 | 264 | return { 265 | ast: tree, 266 | source: output.code, 267 | sourceMap: output.map && output.map.toJSON() 268 | }; 269 | 270 | }).catch(function(error) { 271 | messages.group('Errors').error({ 272 | id: 'GenerationError', 273 | message: error.message 274 | }); 275 | throw error; 276 | }); 277 | 278 | }); 279 | } 280 | 281 | module.exports = compile; 282 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickstart", 3 | "version": "1.2.2", 4 | "description": "CommonJS module compiler for node.js and browsers", 5 | "main": "./main.js", 6 | "browser": { 7 | "./main": "./browser", 8 | "assert": "assert", 9 | "buffer": "buffer", 10 | "console": "console-browserify", 11 | "constants": "constants-browserify", 12 | "crypto": "crypto-browserify", 13 | "domain": "domain-browser", 14 | "events": "events", 15 | "http": "http-browserify", 16 | "https": "https-browserify", 17 | "os": "os-browserify/browser", 18 | "path": "path-browserify", 19 | "punycode": "punycode", 20 | "querystring": "querystring-es3", 21 | "stream": "stream-browserify", 22 | "_stream_duplex": "readable-stream/duplex", 23 | "_stream_passthrough": "readable-stream/passthrough", 24 | "_stream_readable": "readable-stream/readable", 25 | "_stream_transform": "readable-stream/transform", 26 | "_stream_writable": "readable-stream/writable", 27 | "string_decoder": "string_decoder", 28 | "sys": "util", 29 | "timers": "timers-browserify", 30 | "tty": "tty-browserify", 31 | "url": "url", 32 | "util": "util", 33 | "vm": "vm-browserify", 34 | "zlib": "browserify-zlib" 35 | }, 36 | "bin": { 37 | "quickstart": "./bin/quickstart" 38 | }, 39 | "scripts": { 40 | "test": "istanbul cover _mocha test -- -R spec" 41 | }, 42 | "dependencies": { 43 | "promise": "~5.0.0", 44 | "prime": "^0.4.2", 45 | "agent": "^0.2.0", 46 | "pathogen": "^0.1.5", 47 | "mout": "^0.9.1", 48 | "esprima": "~1.2.1", 49 | "escodegen": "^1.3.2", 50 | "estraverse": "~1.5.0", 51 | "escope": "~1.0.1", 52 | "esmangle": "~1.0.1", 53 | "microseconds": "^0.1.0", 54 | "cli-color": "~0.3.2", 55 | "require-relative": "^0.8.7", 56 | "minimist": "~0.1.0", 57 | "util": "~0.10.3", 58 | "assert": "~1.1.1", 59 | "url": "~0.10.1", 60 | "path-browserify": "0.0.0", 61 | "buffer": "~2.3.0", 62 | "https-browserify": "0.0.0", 63 | "tty-browserify": "0.0.0", 64 | "constants-browserify": "0.0.1", 65 | "os-browserify": "~0.1.2", 66 | "string_decoder": "~0.10.25-1", 67 | "domain-browser": "~1.1.1", 68 | "querystring-es3": "~0.2.1-0", 69 | "punycode": "~1.2.4", 70 | "events": "~1.0.1", 71 | "stream-browserify": "~1.0.0", 72 | "readable-stream": "~1.0.27-1", 73 | "vm-browserify": "0.0.4", 74 | "timers-browserify": "~1.0.1", 75 | "console-browserify": "~1.1.0", 76 | "http-browserify": "~1.3.2", 77 | "browserify-zlib": "~0.1.4", 78 | "crypto-browserify": "~2.1.8" 79 | }, 80 | "devDependencies": { 81 | "mocha": "^1.18.2", 82 | "istanbul": "^0.2.9", 83 | "chai": "~1.9.1", 84 | "chai-as-promised": "~4.1.1" 85 | }, 86 | "homepage": "http://spotify.github.io/quickstart", 87 | "repository": "https://github.com/spotify/quickstart", 88 | "author": { 89 | "name": "Valerio Proietti", 90 | "email": "kamicane@gmail.com" 91 | }, 92 | "contributors": [ 93 | { 94 | "name": "Johannes Koggdal", 95 | "email": "johannes@koggdal.com" 96 | }, 97 | { 98 | "name": "Daniel Herzog", 99 | "email": "daniel.herzog@gmail.com" 100 | } 101 | ], 102 | "licenses": [ 103 | { 104 | "type": "Apache-2.0", 105 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 106 | } 107 | ] 108 | } 109 | -------------------------------------------------------------------------------- /runtime/browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | Browser runtime 3 | */'use strict'; 4 | /* global main, modules, -require, -module, -exports, -global */ 5 | 6 | var cache = require.cache = {}; 7 | 8 | function require(id) { 9 | var module = cache[id]; 10 | if (!module) { 11 | var moduleFn = modules[id]; 12 | if (!moduleFn) throw new Error('module ' + id + ' not found'); 13 | module = cache[id] = {}; 14 | var exports = module.exports = {}; 15 | moduleFn.call(exports, require, module, exports, window); 16 | } 17 | return module.exports; 18 | } 19 | 20 | require.resolve = function(resolved) { 21 | return resolved; 22 | }; 23 | 24 | require.node = function() { 25 | return {}; 26 | }; 27 | 28 | require(main); 29 | -------------------------------------------------------------------------------- /runtime/node.js: -------------------------------------------------------------------------------- 1 | /* 2 | Node.js runtime 3 | */'use strict'; 4 | /* global main, modules */ 5 | 6 | var cache = _require.cache = {}; 7 | 8 | function _require(id) { 9 | var module = cache[id]; 10 | if (!module) { 11 | var moduleFn = modules[id]; 12 | if (!moduleFn) throw new Error('module ' + id + ' not found'); 13 | module = cache[id] = {}; 14 | var exports = module.exports = {}; 15 | moduleFn.call(exports, _require, module, exports, global); 16 | } 17 | return module.exports; 18 | } 19 | 20 | _require.node = require; 21 | 22 | _require.resolve = function(resolved) { 23 | return resolved; 24 | }; 25 | 26 | _require(main); 27 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | /* jshint strict:false, evil: true */ 2 | /* global describe, it */ 3 | 4 | var chai = require('chai'); 5 | chai.use(require('chai-as-promised')); 6 | var expect = chai.expect; 7 | 8 | /* 9 | useful methods 10 | ├─ .to.be.fulfilled 11 | ├─ .to.be.rejected 12 | ├─ .to.be.rejectedWith() 13 | ├─ .to.eventually.equal() 14 | ├─ .to.eventually.eql() 15 | └─ .to.become() 16 | */ 17 | 18 | var quickstart = require('../'); 19 | 20 | // these test check the output of required modules. 21 | 22 | describe('quickstart', function() { 23 | 24 | it('should compile a simple module, using the browser resolver', function() { 25 | 26 | return expect(quickstart({ 27 | transforms: ['../transforms/passthrough'], 28 | parsers: { empty: '../parsers/empty' }, 29 | sourceMap: true, 30 | root: __dirname + '/root/', 31 | runtime: '../../runtime/node', 32 | main: './test-browser' 33 | }).then(function(result) { 34 | expect(result.source).to.be.a('string'); 35 | expect(result.ast).to.be.an('object'); 36 | expect(result.sourceMap).to.be.an('object'); 37 | eval(result.source); 38 | })).to.be.fulfilled; 39 | 40 | }); 41 | 42 | it('should compile a simple module, using the node resolver', function() { 43 | 44 | return expect(quickstart({ 45 | transforms: ['../transforms/passthrough'], 46 | parsers: { empty: '../parsers/empty' }, 47 | compress: true, 48 | node: true, 49 | root: __dirname + '/root/', 50 | main: './test-node' 51 | }).then(function(result) { 52 | expect(result.source).to.be.a('string'); 53 | expect(result.ast).to.be.an('object'); 54 | eval(result.source); 55 | })).to.be.fulfilled; 56 | 57 | }); 58 | 59 | it('should compile itself', function() { 60 | 61 | return expect(quickstart({ 62 | self: true, 63 | output: false, 64 | root: __dirname + '/../' 65 | }).then(function(result) { 66 | expect(result.ast).to.be.an('object'); 67 | })).to.be.fulfilled; 68 | 69 | }); 70 | 71 | it('should handle browser require-based errors', function() { 72 | 73 | return expect(quickstart({ 74 | output: false, 75 | compress: true, 76 | root: __dirname + '/root/', 77 | main: './invalid-require.js' 78 | })).to.be.rejected; 79 | 80 | }); 81 | 82 | it('should handle node require-based errors', function() { 83 | 84 | return expect(quickstart({ 85 | root: __dirname + '/root/', 86 | main: './invalid-require.js', 87 | node: true 88 | })).to.be.rejected; 89 | 90 | }); 91 | 92 | it('should handle javascript errors', function() { 93 | 94 | return expect(quickstart({ 95 | root: __dirname + '/root/', 96 | main: './invalid-javascript.js' 97 | })).to.be.rejected; 98 | 99 | }); 100 | 101 | it('should handle not found transforms errors', function() { 102 | 103 | return expect(quickstart({ 104 | self: true, 105 | transforms: ['./does-not-exist'], 106 | root: __dirname + '/../' 107 | })).to.be.rejected; 108 | 109 | }); 110 | 111 | it('should handle not found parsers errors', function() { 112 | 113 | return expect(quickstart({ 114 | self: true, 115 | parsers: { empty: './does-not-exist' }, 116 | root: __dirname + '/../' 117 | })).to.be.rejected; 118 | 119 | }); 120 | 121 | it('should handle transforms plugin errors', function() { 122 | 123 | return expect(quickstart({ 124 | transforms: ['../transforms/error'], 125 | root: __dirname + '/root/' 126 | })).to.be.rejected; 127 | 128 | }); 129 | 130 | it('should handle parser plugin errors', function() { 131 | 132 | return expect(quickstart({ 133 | parsers: { empty: '../parsers/error' }, 134 | root: __dirname + '/root/' 135 | })).to.be.rejected; 136 | 137 | }); 138 | 139 | it('should require circular dependencies', function() { 140 | 141 | return expect(quickstart({ 142 | root: __dirname + '/root/', 143 | runtime: '../../runtime/node', 144 | main: './test-circular' 145 | }).then(function(result) { 146 | eval(result.source); 147 | })).to.be.fulfilled; 148 | 149 | }); 150 | 151 | it('should require JSON files', function() { 152 | 153 | return expect(quickstart({ 154 | root: __dirname + '/root/', 155 | runtime: '../../runtime/node', 156 | main: './test-json' 157 | }).then(function(result) { 158 | eval(result.source); 159 | })).to.be.fulfilled; 160 | 161 | }); 162 | 163 | }); 164 | -------------------------------------------------------------------------------- /test/parsers/empty.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/*path, text*/) { 4 | return { 5 | "type": "Program", 6 | "body": [] 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /test/parsers/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/*path, text*/) { 4 | throw new Error('parser error'); 5 | }; 6 | -------------------------------------------------------------------------------- /test/resolver.js: -------------------------------------------------------------------------------- 1 | /* jshint strict:false */ 2 | /* global describe, it */ 3 | 4 | var chai = require('chai'); 5 | chai.use(require('chai-as-promised')); 6 | var expect = chai.expect; 7 | 8 | /* 9 | useful methods 10 | ├─ .to.be.fulfilled 11 | ├─ .to.be.rejected 12 | ├─ .to.be.rejectedWith() 13 | ├─ .to.eventually.equal() 14 | ├─ .to.eventually.eql() 15 | └─ .to.become() 16 | */ 17 | 18 | var pathogen = require('pathogen'); 19 | 20 | var Resolver = require('../util/resolver'); 21 | 22 | var resolver = new Resolver; 23 | 24 | var resolve = function(path) { 25 | return resolver.resolve(pathogen(__dirname + '/root/'), path); 26 | }; 27 | 28 | var relative = function(path) { 29 | return pathogen.relative(pathogen(__dirname + '/root/'), path); 30 | }; 31 | 32 | describe('QuickStart Resolver', function() { 33 | 34 | it('should resolve a relative module', function() { 35 | return expect(resolve('./index').then(relative)).to.eventually.equal('./index.js'); 36 | }); 37 | 38 | it('should resolve the main', function() { 39 | return expect(resolve('./').then(relative)).to.eventually.equal('./main.js'); 40 | }); 41 | 42 | it('should resolve a package with implicit index.js main', function() { 43 | return expect(resolve('one').then(relative)).to.eventually.equal('./node_modules/one/index.js'); 44 | }); 45 | 46 | it('should resolve a package with specified main', function() { 47 | return expect(resolve('two').then(relative)).to.eventually.equal('./node_modules/two/two.js'); 48 | }); 49 | 50 | it('should resolve a package with specified module', function() { 51 | return expect(resolve('two/two').then(relative)).to.eventually.equal('./node_modules/two/two.js'); 52 | }); 53 | 54 | it('should resolve a package with specified module giving priority to file matches', function() { 55 | return expect(resolve('two/lib').then(relative)).to.eventually.equal('./node_modules/two/lib.js'); 56 | }); 57 | 58 | it('should resolve a package with specified module giving priority to folder matches', function() { 59 | return expect(resolve('two/lib/').then(relative)).to.eventually.equal('./node_modules/two/lib/index.js'); 60 | }); 61 | 62 | it('should resolve a package with specified module index.js', function() { 63 | return expect(resolve('two/lib2').then(relative)).to.eventually.equal('./node_modules/two/lib2/index.js'); 64 | }); 65 | 66 | it('should resolve a package with specified module index.js', function() { 67 | return expect(resolve('two/lib2/').then(relative)).to.eventually.equal('./node_modules/two/lib2/index.js'); 68 | }); 69 | 70 | it('should use the browser field to resolve faux packages', function() { 71 | return expect(resolve('fs').then(relative)).to.eventually.equal('./lib/fs.js'); 72 | }); 73 | 74 | it('should use the browser field to swap local modules', function() { 75 | return expect(resolve('./old.js').then(relative)).to.eventually.equal('./new.js'); 76 | }); 77 | 78 | it('should resolve browser keys to swap local modules', function() { 79 | return expect(resolve('./old').then(relative)).to.eventually.equal('./new.js'); 80 | }); 81 | 82 | it('should use the browser field to resolve main modules', function() { 83 | return expect(resolve('three').then(relative)).to.eventually.equal('./node_modules/three/lib/three.js'); 84 | }); 85 | 86 | it('should use the browser field to resolve package modules', function() { 87 | return expect(resolve('one/old').then(relative)).to.eventually.equal('./node_modules/one/new.js'); 88 | }); 89 | 90 | it('should use the browser field to resolve falsy modules to false', function() { 91 | return expect(resolve('./false')).to.eventually.equal(false); 92 | }); 93 | 94 | it('should use the browser field to resolve falsy packages to false', function() { 95 | return expect(resolve('four')).to.eventually.equal(false); 96 | }); 97 | 98 | it('should ignore empty browser fields', function() { 99 | return expect(resolve('six').then(relative)).to.eventually.equal('./node_modules/six/index.js'); 100 | }); 101 | 102 | it('should evaluate main fields as folders too', function() { 103 | return expect(resolve('seven').then(relative)).to.eventually.equal('./node_modules/seven/lib/index.js'); 104 | }); 105 | 106 | it('should resolve a package with specified main overridden by the browser field', function() { 107 | return expect(resolve('five').then(relative)).to.eventually.equal('./node_modules/five/browser.js'); 108 | }); 109 | 110 | it('should resolve a package with specified module overridden by the browser field', function() { 111 | return expect(resolve('five/old').then(relative)).to.eventually.equal('./node_modules/five/new.js'); 112 | }); 113 | 114 | it('should return an error for unresolvable modules', function() { 115 | return expect(resolve('./null')).to.be.rejected; 116 | }); 117 | 118 | it('should return an error for unresolvable packages', function() { 119 | return expect(resolve('non-existing-package')).to.be.rejected; 120 | }); 121 | 122 | it('should resolve unmatched core_modules to the base value', function() { 123 | return expect(resolve('_debugger')).to.eventually.equal('_debugger'); 124 | }); 125 | 126 | it('should handle absolute paths (windows)', function() { 127 | var paths = resolver._paths('C:\\Users\\username\\app\\node_modules\\package\\lib\\'); 128 | for (var i = 0, l = paths.length; i < l; i++) { 129 | expect(paths[i].substr(0, 2)).to.equal('C:'); 130 | } 131 | }); 132 | 133 | it('should handle absolute paths (unix)', function() { 134 | var paths = resolver._paths('/Users/username/app/node_modules/package/lib/'); 135 | for (var i = 0, l = paths.length; i < l; i++) { 136 | expect(paths[i].substr(0, 1)).to.equal('/'); 137 | } 138 | }); 139 | 140 | it('should always have a trailing slash in paths', function() { 141 | var resolver = new Resolver({defaultPath: 'folder'}); 142 | var paths = resolver._paths('folder'); 143 | for (var i = 0, l = paths.length; i < l; i++) { 144 | expect(paths[i].substr(-1, 1)).to.equal('/'); 145 | } 146 | }); 147 | 148 | }); 149 | -------------------------------------------------------------------------------- /test/root/circular-a.js: -------------------------------------------------------------------------------- 1 | /* jshint strict:false */ 2 | 3 | var expect = require('chai').expect; 4 | 5 | module.exports = Readable; 6 | var Writable = require('./circular-b'); 7 | 8 | expect(Readable).to.be.a('function'); 9 | expect(Readable.name).to.equal('Readable'); 10 | expect(Writable).to.be.a('function'); 11 | expect(Writable.name).to.equal('Writable'); 12 | 13 | function Readable() {}; 14 | -------------------------------------------------------------------------------- /test/root/circular-b.js: -------------------------------------------------------------------------------- 1 | /* jshint strict:false */ 2 | 3 | var expect = require('chai').expect; 4 | 5 | module.exports = Writable; 6 | var Readable = require('./circular-a'); 7 | 8 | expect(Readable).to.be.a('function'); 9 | expect(Readable.name).to.equal('Readable'); 10 | expect(Writable).to.be.a('function'); 11 | expect(Writable.name).to.equal('Writable'); 12 | 13 | function Writable() {} 14 | -------------------------------------------------------------------------------- /test/root/false.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'false'; 4 | -------------------------------------------------------------------------------- /test/root/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'index'; 4 | -------------------------------------------------------------------------------- /test/root/invalid-javascript.js: -------------------------------------------------------------------------------- 1 | this is not a good idea. ever. 2 | 3 | // this is so invalid omg 4 | -------------------------------------------------------------------------------- /test/root/invalid-require.js: -------------------------------------------------------------------------------- 1 | require('this-does-not-exist'); 2 | require('./this-does-not-exist'); 3 | -------------------------------------------------------------------------------- /test/root/lib/fs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.readFile = function() {}; 4 | -------------------------------------------------------------------------------- /test/root/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var old = require('./old'); // routing module 4 | var fs = require('fs'); // routing native module 5 | require('events'); // non-routing native module 6 | 7 | // non-resolving require 8 | var events = 'events'; 9 | require(events); 10 | 11 | var json = require('./module.json'); // json module 12 | var text = require('./module.txt'); // text module 13 | var empty = require('./module.empty'); // custom module 14 | 15 | module.exports = 'main'; 16 | -------------------------------------------------------------------------------- /test/root/module.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/quickstart/f9f12f3e3dc2a041e4d8ad7b5b46a2d9494d919c/test/root/module.empty -------------------------------------------------------------------------------- /test/root/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "animals": [ 3 | 4 | { 5 | "name": "fox", 6 | "color": "brown", 7 | "quick": true, 8 | "lazy": false, 9 | "jumps": "dog" 10 | }, 11 | 12 | { 13 | "name": "dog", 14 | "color": null, 15 | "quick": false, 16 | "lazy": true, 17 | "jumps": null 18 | } 19 | 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /test/root/module.txt: -------------------------------------------------------------------------------- 1 | The quick brown fox jumped over the lazy dog. 2 | -------------------------------------------------------------------------------- /test/root/new.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'new'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/five/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'five/browser'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/five/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'five/main'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/five/new.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'five/new'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/five/old.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'five/old'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/five/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "five", 3 | "main": "./main.js", 4 | "browser": { 5 | "./main": "./browser", 6 | "./old": "./new" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/root/node_modules/four/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'four/index'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/four/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "four" 3 | } 4 | -------------------------------------------------------------------------------- /test/root/node_modules/one/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'one/index'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/one/new.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'one/new'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/one/old.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'one/old'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/one/one.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'one/one'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/one/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "one", 3 | "browser": { 4 | "./old.js": "./new.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/root/node_modules/seven/lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = "seven/index"; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/seven/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "lib" 3 | } 4 | -------------------------------------------------------------------------------- /test/root/node_modules/six/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = "six/index"; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/six/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser": {}, 3 | "main": "index" 4 | } 5 | -------------------------------------------------------------------------------- /test/root/node_modules/three/lib/three.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'three/lib/three'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/three/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three", 3 | "browser": "./lib/three.js" 4 | } 5 | -------------------------------------------------------------------------------- /test/root/node_modules/two/lib.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'two/lib'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/two/lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'two/lib/index'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/two/lib/lib.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'two/lib/lib'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/two/lib2/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'two/lib2/index'; 4 | -------------------------------------------------------------------------------- /test/root/node_modules/two/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "two", 3 | "main": "two.js" 4 | } 5 | -------------------------------------------------------------------------------- /test/root/node_modules/two/two.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'two/two'; 4 | -------------------------------------------------------------------------------- /test/root/old.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'old'; 4 | -------------------------------------------------------------------------------- /test/root/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "main": "main.js", 4 | "browser": { 5 | "fs": "./lib/fs", 6 | "./old": "./new", 7 | "./invalid-route": "./anything-really", 8 | "one/old": "./new", 9 | "./false": false, 10 | "four": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/root/test-browser.js: -------------------------------------------------------------------------------- 1 | /* jshint strict:false */ 2 | 3 | var expect = require('chai').expect; 4 | 5 | var old = require('./old'); // routing module 6 | expect(old).to.equal('new'); 7 | 8 | var oneOld = require('one/old'); // routing module 9 | expect(oneOld).to.equal('one/new'); 10 | 11 | var fs = require('fs'); // routing native module 12 | expect(fs).to.be.an('object'); 13 | 14 | var events = require('events').EventEmitter; // routing native module 15 | expect(events).to.be.a('function'); 16 | 17 | var f = require('./false'); 18 | expect(f).to.be.an('object'); 19 | 20 | // non-resolving require 21 | try { 22 | var xoxo = '_'; 23 | require(xoxo); 24 | } catch (e) { 25 | expect(e instanceof Error).to.be.ok; 26 | } 27 | 28 | var json = require('./module.json'); // json module 29 | expect(json).to.be.an('object'); 30 | 31 | var text = require('./module.txt'); // text module 32 | expect(text).to.be.a('string'); 33 | 34 | var empty = require('./module.empty'); // custom module 35 | expect(empty).to.be.an('object'); 36 | 37 | module.exports = 'test-browser'; 38 | -------------------------------------------------------------------------------- /test/root/test-circular.js: -------------------------------------------------------------------------------- 1 | /* jshint strict:false */ 2 | 3 | var expect = require('chai').expect; 4 | 5 | var Readable = require('./circular-a'); 6 | var Writable = require('./circular-b'); 7 | 8 | expect(Readable).to.be.a('function'); 9 | expect(Writable).to.be.a('function'); 10 | 11 | module.exports = 'test-circular'; 12 | -------------------------------------------------------------------------------- /test/root/test-json.js: -------------------------------------------------------------------------------- 1 | /* jshint strict:false */ 2 | 3 | var expect = require('chai').expect; 4 | 5 | var jsonModule = require('./module.json'); 6 | expect(jsonModule).to.be.an('object'); 7 | expect(jsonModule.animals).to.be.an('array'); 8 | 9 | var animals = require('./module.json').animals; 10 | expect(animals).to.be.an('array'); 11 | 12 | var animals2 = require('./module.json')['animals']; 13 | expect(animals2).to.be.an('array'); 14 | 15 | var hasProperty = require('./module.json').hasOwnProperty('animals'); 16 | expect(hasProperty).to.equal(true); 17 | 18 | module.exports = 'test-json'; 19 | -------------------------------------------------------------------------------- /test/root/test-node.js: -------------------------------------------------------------------------------- 1 | /* jshint strict:false */ 2 | 3 | var expect = require('chai').expect; 4 | 5 | var old = require('./old'); // routing module 6 | expect(old).to.equal('old'); 7 | 8 | var oneOld = require('one/old'); // routing module 9 | expect(oneOld).to.equal('one/old'); 10 | 11 | var fs = require('fs'); // routing native module 12 | expect(fs).to.be.an('object'); 13 | 14 | var _debugger = require('_debugger'); // non-routing native module 15 | expect(_debugger).to.be.an('object'); 16 | 17 | var f = require('./false'); 18 | expect(f).to.equal('false'); 19 | 20 | // non-resolving require 21 | try { 22 | var xoxo = 'xoxo'; 23 | require(xoxo); 24 | } catch (e) { 25 | expect(e instanceof Error).to.be.ok; 26 | } 27 | 28 | var json = require('./module.json'); // json module 29 | expect(json).to.be.an('object'); 30 | 31 | var text = require('./module.txt'); // text module 32 | expect(text).to.be.a('string'); 33 | 34 | var empty = require('./module.empty'); // custom module 35 | expect(empty).to.be.an('object'); 36 | 37 | module.exports = 'test-node'; 38 | -------------------------------------------------------------------------------- /test/sequence.js: -------------------------------------------------------------------------------- 1 | /* jshint strict:false */ 2 | /* global describe, it */ 3 | 4 | var chai = require('chai'); 5 | chai.use(require('chai-as-promised')); 6 | var expect = chai.expect; 7 | 8 | /* 9 | useful methods 10 | ├─ .to.be.fulfilled 11 | ├─ .to.be.rejected 12 | ├─ .to.be.rejectedWith() 13 | ├─ .to.eventually.equal() 14 | ├─ .to.eventually.eql() 15 | └─ .to.become() 16 | */ 17 | 18 | var Promise = global.Promise || require('promise'); 19 | var sequence = require('../util/sequence').use(Promise); 20 | 21 | // utility functions to test async 22 | var resolve = function(value, time) { 23 | return new Promise(function(resolve, reject) { 24 | setTimeout(function() { 25 | resolve(value); 26 | }, time || 10); 27 | }); 28 | }; 29 | 30 | var error = new Error; 31 | 32 | // always rejects with the same error to asses correct error rejection, 33 | // so I can throw inside handlers and have them show as rejection failures 34 | var reject = function(time) { 35 | return new Promise(function(resolve, reject) { 36 | setTimeout(function() { 37 | reject(error); 38 | }, time || 10); 39 | }); 40 | }; 41 | 42 | describe('sequence', function() { 43 | 44 | describe('find', function() { 45 | 46 | it('should fulfill', function() { 47 | 48 | return expect(sequence.find([1,2,3,4], function(value) { 49 | if (value === 4) return resolve(value); 50 | return reject(); 51 | })).to.become(4); 52 | 53 | }); 54 | 55 | it('should fulfill with values', function() { 56 | return expect(sequence.find([1,2,3,4], function(value) { 57 | if (value === 4) return value; 58 | throw error; 59 | })).to.become(4); 60 | }); 61 | 62 | it('should reject', function() { 63 | 64 | return expect(sequence.find([1,2,3,4], function(value) { 65 | if (value < 5) return reject(); 66 | return resolve(value); 67 | })).to.be.rejectedWith(error); 68 | 69 | }); 70 | 71 | it('should reject with values', function() { 72 | return expect(sequence.find([1,2,3,4], function(value) { 73 | if (value < 5) throw error; 74 | return value; 75 | })).to.be.rejectedWith(error); 76 | }); 77 | 78 | }); 79 | 80 | describe('filter', function() { 81 | 82 | it('should fulfill', function() { 83 | 84 | return expect(sequence.filter([1,2,3,4], function(value) { 85 | if (value === 2) return reject(); 86 | return resolve(value * 2); 87 | })).to.eventually.eql([2,6,8]); 88 | 89 | }); 90 | 91 | it('should fulfill with values', function() { 92 | 93 | return expect(sequence.filter([1,2,3,4], function(value) { 94 | if (value === 2) throw error; 95 | return value * 2; 96 | })).to.eventually.eql([2,6,8]); 97 | 98 | }); 99 | 100 | it('should fulfill empty', function() { 101 | 102 | return expect(sequence.filter([1,2,3,4], function(value) { 103 | return reject(); 104 | })).to.eventually.be.empty; 105 | 106 | }); 107 | 108 | it('should fulfill empty with values', function() { 109 | 110 | return expect(sequence.filter([1,2,3,4], function(value) { 111 | throw error; 112 | })).to.eventually.be.empty; 113 | 114 | }); 115 | 116 | }); 117 | 118 | describe('map', function() { 119 | 120 | it('should fulfill', function() { 121 | 122 | return expect(sequence.map([1,2,3,4], function(value) { 123 | return resolve(value * 2); 124 | })).to.eventually.eql([2,4,6,8]); 125 | 126 | }); 127 | 128 | it('should fulfill with values', function() { 129 | 130 | return expect(sequence.map([1,2,3,4], function(value) { 131 | return value * 2; 132 | })).to.eventually.eql([2,4,6,8]); 133 | 134 | }); 135 | 136 | it('should reject and not visit after rejection', function() { 137 | 138 | return expect(sequence.map([1,2,3,4], function(value) { 139 | expect(value).to.not.equal(3); 140 | expect(value).to.not.equal(4); 141 | if (value === 2) return reject(); 142 | return resolve(value * 2); 143 | })).to.be.rejectedWith(error); 144 | 145 | }); 146 | 147 | it('should reject with values, and not visit after rejection', function() { 148 | 149 | return expect(sequence.map([1,2,3,4], function(value) { 150 | expect(value).to.not.equal(3); 151 | expect(value).to.not.equal(4); 152 | if (value === 2) throw error; 153 | return value * 2; 154 | })).to.be.rejectedWith(error); 155 | 156 | }); 157 | 158 | }); 159 | 160 | describe('all', function() { 161 | 162 | it('should fulfill', function() { 163 | 164 | return expect(sequence.all([1,2,3,4], function(value) { 165 | return resolve(value * 2); 166 | })).to.eventually.eql([2,4,6,8]); 167 | 168 | }); 169 | 170 | it('should fulfill with values', function() { 171 | 172 | return expect(sequence.all([1,2,3,4], function(value) { 173 | return value * 2; 174 | })).to.eventually.eql([2,4,6,8]); 175 | 176 | }); 177 | 178 | it('should reject all', function() { 179 | 180 | return expect(sequence.all([1,2,3,4], function(value) { 181 | if (value === 2) return reject(); 182 | return resolve(value * 2); 183 | })).to.be.rejectedWith(error); 184 | 185 | }); 186 | 187 | it('should reject with values', function() { 188 | 189 | return expect(sequence.all([1,2,3,4], function(value) { 190 | if (value === 2) throw error; 191 | return value * 2; 192 | })).to.be.rejectedWith(error); 193 | 194 | }); 195 | 196 | }); 197 | 198 | describe('every', function() { 199 | 200 | it('should fulfill', function() { 201 | 202 | return expect(sequence.every([1,2,3,4], function(value) { 203 | return resolve(value * 2); 204 | })).to.eventually.eql([2,4,6,8]); 205 | 206 | }); 207 | 208 | it('should fulfill with values', function() { 209 | 210 | return expect(sequence.every([1,2,3,4], function(value) { 211 | return value * 2; 212 | })).to.eventually.eql([2,4,6,8]); 213 | 214 | }); 215 | 216 | it('should reject', function() { 217 | 218 | return expect(sequence.every([1,2,3,4], function(value) { 219 | if (value === 2) return reject(); 220 | return resolve(value * 2); 221 | })).to.be.rejectedWith(error); 222 | 223 | }); 224 | 225 | it('should reject with values', function() { 226 | 227 | return expect(sequence.every([1,2,3,4], function(value) { 228 | if (value === 2) throw error; 229 | return value * 2; 230 | })).to.be.rejectedWith(error); 231 | 232 | }); 233 | 234 | }); 235 | 236 | describe('reduce', function() { 237 | 238 | it('should fulfill', function() { 239 | 240 | return expect(sequence.reduce([2,3,4], function(previous, value) { 241 | return resolve(previous + value); 242 | }, 1)).to.eventually.equal(10); 243 | 244 | }); 245 | 246 | it('should fulfill with values', function() { 247 | 248 | return expect(sequence.reduce([2,3,4], function(previous, value) { 249 | return previous + value; 250 | }, 1)).to.eventually.equal(10); 251 | 252 | }); 253 | 254 | it('should reject and not visit after rejection', function() { 255 | 256 | return expect(sequence.reduce([2,3,4], function(previous, value) { 257 | expect(value).to.not.equal(4); 258 | if (value === 3) return reject(); 259 | return resolve(previous + value); 260 | }, 1)).to.be.rejectedWith(error); 261 | 262 | }); 263 | 264 | it('should reject with values, and not visit after rejection', function() { 265 | 266 | return expect(sequence.reduce([2,3,4], function(previous, value) { 267 | expect(value).to.not.equal(4); 268 | if (value === 3) throw error; 269 | return previous + value; 270 | }, 1)).to.be.rejectedWith(error); 271 | 272 | }); 273 | 274 | }); 275 | 276 | describe('race', function() { 277 | 278 | it('should fulfill', function() { 279 | 280 | return expect(sequence.race([40, 20, 60], function(time) { 281 | if (time === 40) reject(time); 282 | return resolve(time, time); 283 | })).to.eventually.equal(20); 284 | 285 | }); 286 | 287 | it('should fulfill with values', function() { 288 | 289 | return expect(sequence.race([40, 20, 60], function(time) { 290 | if (time === 60) throw error; 291 | return time; 292 | })).to.eventually.equal(40); 293 | 294 | }); 295 | 296 | it('should reject', function() { 297 | 298 | return expect(sequence.race([40, 20, 60], function(time) { 299 | if (time === 20) return reject(time); 300 | return resolve(time, time); 301 | })).to.be.rejectedWith(error); 302 | 303 | }); 304 | 305 | it('should reject with values', function() { 306 | 307 | return expect(sequence.race([40, 20, 60], function(time) { 308 | if (time === 40) throw error; 309 | return time; 310 | })).to.be.rejectedWith(error); 311 | 312 | }); 313 | 314 | }); 315 | 316 | }); 317 | -------------------------------------------------------------------------------- /test/transforms/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/*path, ast*/) { 4 | throw new Error('transform error'); 5 | }; 6 | -------------------------------------------------------------------------------- /test/transforms/passthrough.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(path, tree) { 4 | return tree; 5 | }; 6 | -------------------------------------------------------------------------------- /transforms/inject-globals.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var esprima = require('esprima'); 4 | var escope = require('escope'); 5 | 6 | var pathogen = require('pathogen'); 7 | 8 | var contains = require('mout/array/contains'); 9 | var map = require('mout/array/map'); 10 | 11 | var Syntax = esprima.Syntax; 12 | 13 | var declaration = function(name, init) { 14 | return { 15 | type: Syntax.VariableDeclaration, 16 | declarations: [{ 17 | type: Syntax.VariableDeclarator, 18 | id: { 19 | type: Syntax.Identifier, 20 | name: name 21 | }, 22 | init: init 23 | }], 24 | kind: "var" 25 | }; 26 | }; 27 | 28 | var express = function(string) { 29 | return esprima.parse(string).body[0].expression; 30 | }; 31 | 32 | function injectGlobals(path, tree) { 33 | var self = this; 34 | 35 | // escope 36 | var global = escope.analyze(tree, { optimistic: true }).scopes[0]; 37 | var variables = map(global.variables, function(v) { return v.name; }); 38 | var references = map(global.through, function(r) { return r.identifier.name; }); 39 | 40 | var needsProcess = false; 41 | var needsFileName = false; 42 | var needsDirName = false; 43 | 44 | if (!contains(variables, 'process') && contains(references, 'process')) { 45 | needsProcess = true; 46 | } 47 | 48 | if (!contains(variables, '__dirname') && contains(references, '__dirname')) { 49 | needsDirName = true; 50 | } 51 | 52 | if (!contains(variables, '__filename') && contains(references, '__filename')) { 53 | needsFileName = true; 54 | } 55 | 56 | var processName = needsProcess ? 'process' : '__process'; 57 | 58 | var processInit = { 59 | type: Syntax.CallExpression, 60 | callee: { 61 | type: Syntax.Identifier, 62 | name: 'require' 63 | }, 64 | arguments: [{ 65 | type: Syntax.Literal, 66 | value: 'quickstart/browser/process' 67 | }] 68 | }; 69 | 70 | var index = 0; 71 | 72 | if (!self.node && (needsProcess || needsFileName || needsDirName)) { 73 | tree.body.splice(index++, 0, declaration(processName, processInit)); 74 | } 75 | 76 | // declare __dirname if found 77 | if (needsDirName) { 78 | var dirnameExpression = 79 | '(' + processName + '.cwd() + "' + pathogen.dirname(path).slice(1, -1) + '").replace(/\\/+/g, "/")'; 80 | tree.body.splice(index++, 0, declaration('__dirname', express(dirnameExpression))); 81 | } 82 | 83 | // declare __fileName if found 84 | if (needsFileName) { 85 | var filenameExpression = 86 | '(' + processName + '.cwd() + "' + path.slice(1) + '").replace(/\\/+/g, "/")'; 87 | 88 | tree.body.splice(index++, 0, declaration('__filename', express(filenameExpression))); 89 | } 90 | 91 | // declare Buffer if found 92 | if (!self.node && !contains(variables, 'Buffer') && contains(references, 'Buffer')) { 93 | var bufferExpression = '(require("buffer").Buffer)'; 94 | tree.body.splice(index++, 0, declaration('Buffer', express(bufferExpression))); 95 | } 96 | 97 | return tree; 98 | } 99 | 100 | module.exports = injectGlobals; 101 | -------------------------------------------------------------------------------- /transforms/require-dependencies.js: -------------------------------------------------------------------------------- 1 | /* global -Promise */ 2 | 'use strict'; 3 | 4 | var esprima = require('esprima'); 5 | var estraverse = require('estraverse'); 6 | var Promise = require('promise'); 7 | var pathogen = require('pathogen'); 8 | 9 | var sequence = require('../util/sequence').use(Promise); 10 | 11 | var transport = require('../util/transport'); 12 | var Resolver = require('../util/resolver'); 13 | 14 | var isNative = Resolver.isNative; 15 | 16 | var Syntax = esprima.Syntax; 17 | 18 | var traverse = estraverse.traverse; 19 | 20 | var express = function(string) { 21 | return esprima.parse(string).body[0].expression; 22 | }; 23 | 24 | // Find dependencies in the AST, callback the modified AST. 25 | function requireDependencies(path, tree) { 26 | var self = this; 27 | 28 | // Finds valid require and resolve nodes. 29 | var requireNodes = []; 30 | var resolveNodes = []; 31 | 32 | traverse(tree, { enter: function(node, parent) { 33 | // callexpression, one argument 34 | if (node.type !== Syntax.CallExpression || node.arguments.length !== 1) return; 35 | 36 | var argument = node.arguments[0]; 37 | 38 | // literal require 39 | if (argument.type !== Syntax.Literal) return; 40 | 41 | var callee = node.callee; 42 | 43 | if (callee.type === Syntax.Identifier && callee.name === 'require') { 44 | 45 | requireNodes.push({ 46 | node: node, 47 | value: argument.value, 48 | parent: parent 49 | }); 50 | 51 | } else if ( 52 | // require.resolve 53 | callee.type === Syntax.MemberExpression && callee.object.type === Syntax.Identifier && 54 | callee.object.name === 'require' && callee.property.type === Syntax.Identifier && 55 | callee.property.name === 'resolve' 56 | ) { 57 | resolveNodes.push({ 58 | node: node, 59 | value: argument.value, 60 | parent: parent 61 | }); 62 | } 63 | }}); 64 | 65 | // require nodes 66 | 67 | var requireNodesPromise = sequence.every(requireNodes, function(result) { 68 | 69 | var requireNode = result.node; 70 | var requireNodeValue = result.value; 71 | 72 | return self.require(pathogen(self.root + path), requireNodeValue).then(function(uid) { 73 | if (uid) { 74 | 75 | // replace native requires with require.node 76 | if (isNative(uid)) requireNode.callee = { 77 | type: Syntax.MemberExpression, 78 | computed: false, 79 | object: { 80 | type: Syntax.Identifier, 81 | name: 'require' 82 | }, 83 | property: { 84 | type: Syntax.Identifier, 85 | name: 'node' 86 | } 87 | }; 88 | 89 | requireNode.arguments[0].value = uid; // replace require argument 90 | } else { // false 91 | // replace not found requires with {} 92 | // this happens when requires are nullified by the browser field 93 | requireNode.type = Syntax.ObjectExpression; 94 | requireNode.properties = []; 95 | delete requireNode.callee; 96 | delete requireNode.arguments; 97 | } 98 | }); 99 | }); 100 | 101 | var resolveNodesPromise = sequence.every(resolveNodes, function(result) { 102 | var resolveNode = result.node; 103 | var resolveNodeValue = result.value; 104 | return self.require(pathogen(self.root + path), resolveNodeValue).then(function(uid) { 105 | resolveNode.arguments[0].value = uid; // replace require.resolve argument 106 | }); 107 | }); 108 | 109 | // wait for those sequences to finish then return the tree 110 | return sequence.every([requireNodesPromise, resolveNodesPromise]).then(function() { 111 | return tree; 112 | }); 113 | } 114 | 115 | module.exports = requireDependencies; 116 | -------------------------------------------------------------------------------- /util/konsole.js: -------------------------------------------------------------------------------- 1 | /* 2 | Konsole 3 | A small utility to mimic console.group / groupEnd in node.js 4 | prints table characters like this: 5 | Group 1 6 | ├ message1 7 | ├ message2 8 | ├ Nested Group 1 9 | │ └ Super Nested Group 1 10 | │ ├ message3 11 | │ └ message4 12 | └ Nested Group 2 13 | ├ Super Nested Group 2 14 | │ ├ message5 15 | │ └ message6 16 | └ message7 17 | Group 2 18 | ├ message8 19 | └ message9 20 | */'use strict'; 21 | 22 | var prime = require('prime'); 23 | var isString = require('mout/lang/isString'); 24 | 25 | var slice = Array.prototype.slice; 26 | 27 | var Message = prime({ 28 | 29 | constructor: function(type, value, parent) { 30 | this.type = type; 31 | this.value = value; 32 | this.parent = parent; 33 | }, 34 | 35 | print: function(last, opts) { 36 | if (!this.parent) return this; // don't print messages without a parent. 37 | 38 | if (isString(opts)) { 39 | var str = opts; 40 | opts = {}; 41 | opts.last = opts.item = opts.join = opts.line = opts.spcr = str; 42 | } else { 43 | opts = opts || {}; 44 | if (!opts.last) opts.last = '└─'; 45 | if (!opts.item) opts.item = '├─'; 46 | if (!opts.join) opts.join = ' '; 47 | if (!opts.line) opts.line = '│'; 48 | if (!opts.spcr) opts.spcr = ' '; 49 | } 50 | 51 | var isRoot = !this.parent.parent; 52 | 53 | if (isRoot) { 54 | console[this.type](this.value); 55 | } else { 56 | // if printing as last item use a different character 57 | var chr = [last ? opts.last : opts.item]; 58 | var parent = this; 59 | // recurse parents up 60 | while ((parent = parent.parent)) { 61 | var grand = parent.parent; 62 | // break when no parent of grandparent is found. 63 | if (!grand.parent) break; 64 | // if parent is the last parent use a different character 65 | var isParentLastOfGrand = (grand.values[grand.values.length - 1] === parent); 66 | chr.unshift(isParentLastOfGrand ? opts.spcr : opts.line); 67 | } 68 | console[this.type](chr.join(opts.join), this.value); 69 | } 70 | 71 | return this; 72 | } 73 | }); 74 | 75 | var Group = prime({ 76 | 77 | inherits: Message, 78 | 79 | constructor: function(type, name, parent) { 80 | Group.parent.constructor.call(this, type, name, parent); 81 | this.values = []; 82 | }, 83 | 84 | push: function(stmnt) { 85 | this.values.push(stmnt); 86 | return this; 87 | }, 88 | 89 | print: function(last, opts) { 90 | Group.parent.print.call(this, last, opts); 91 | var values = this.values; 92 | for (var i = 0; i < values.length; i++) values[i].print(i === values.length - 1, opts); 93 | return this; 94 | } 95 | 96 | }); 97 | 98 | var Konsole = prime({ 99 | 100 | constructor: function(type) { 101 | this.type = type; 102 | this.history = [this.current = new Group(this.type)]; 103 | }, 104 | 105 | write: function() { 106 | var parent = this.current; 107 | parent.push(new Message(this.type, slice.call(arguments).join(' '), parent)); 108 | return this; 109 | }, 110 | 111 | group: function(name) { 112 | var parent = this.current; 113 | var group = this.current = new Group(this.type, name, parent); 114 | parent.push(group); 115 | this.history.unshift(group); 116 | return this; 117 | }, 118 | 119 | groupEnd: function() { 120 | var history = this.history; 121 | if (history.length > 1) { 122 | history.shift(); 123 | this.current = history[0]; 124 | } 125 | return this; 126 | }, 127 | 128 | print: function(opts) { 129 | this.history[0].print(true, opts); 130 | return this; 131 | } 132 | 133 | }); 134 | 135 | Konsole.Message = Message; 136 | Konsole.Group = Group; 137 | 138 | module.exports = Konsole; 139 | -------------------------------------------------------------------------------- /util/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | simple messages 3 | - log / error / warn 4 | - messages = new Messages; 5 | - messages.group(groupName).log(message); 6 | - messages.group(groupName) === message.group(groupName); 7 | - messages.group(groupName).group(subGroupName).log(message); 8 | - messages.group(groupName).group(subGroupName).log(message); 9 | - messages.log(message); 10 | - messages.print(printer); // printer is a simple function that gets (type, message). 11 | */'use strict'; 12 | 13 | var prime = require('prime'); 14 | 15 | var forIn = require('mout/object/forIn'); 16 | var forEach = require('mout/array/forEach'); 17 | var size = require('mout/object/size'); 18 | 19 | var microseconds = require('microseconds'); 20 | 21 | var Message = prime({ 22 | 23 | constructor: function(type, statement) { 24 | this.type = type; 25 | this.statement = statement; 26 | } 27 | 28 | }); 29 | 30 | var Messages = prime({ 31 | 32 | constructor: function(name) { 33 | this.name = name; 34 | this.reset(); 35 | }, 36 | 37 | group: function(name, collapsed) { 38 | var group = this.groups[name] || (this.groups[name] = new Messages(name)); 39 | group.collapsed = !!collapsed; 40 | return group; 41 | }, 42 | 43 | groupCollapsed: function(name) { 44 | return this.group(name, true); 45 | }, 46 | 47 | error: function(statement) { 48 | this.messages.push(new Message('error', statement)); 49 | return this; 50 | }, 51 | 52 | warn: function(statement) { 53 | this.messages.push(new Message('warn', statement)); 54 | return this; 55 | }, 56 | 57 | info: function(statement) { 58 | this.messages.push(new Message('info', statement)); 59 | return this; 60 | }, 61 | 62 | log: function(statement) { 63 | return this.info(statement); 64 | }, 65 | 66 | time: function(id) { 67 | this.timeStamps[id] = microseconds.now(); 68 | return this; 69 | }, 70 | 71 | timeEnd: function(id, name) { 72 | var timestamp = this.timeStamps[id]; 73 | if (timestamp) { 74 | var end = microseconds.since(timestamp); 75 | var timeStampString = microseconds.parse(end).toString(); 76 | this.messages.push(new Message('time', {id: name || id, message: timeStampString})); 77 | } 78 | return this; 79 | }, 80 | 81 | print: function(format) { 82 | if (!this.messages.length && !size(this.groups)) return; 83 | 84 | if (this.name) format(this.collapsed ? 'groupCollapsed' : 'group', this.name); 85 | 86 | forIn(this.groups, function(group) { 87 | group.print(format); 88 | }); 89 | 90 | forEach(this.messages, function(message) { 91 | format(message.type, message.statement); 92 | }); 93 | 94 | if (this.name) format('groupEnd'); 95 | 96 | return this; 97 | }, 98 | 99 | reset: function() { 100 | this.messages = []; 101 | this.groups = {}; 102 | this.timeStamps = {}; 103 | } 104 | 105 | }); 106 | 107 | module.exports = Messages; 108 | -------------------------------------------------------------------------------- /util/program.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var esprima = require('esprima'); 4 | var forIn = require('mout/object/forIn'); 5 | 6 | var Syntax = esprima.Syntax; 7 | 8 | // This generates the full single-file AST, with the runtime and modules. 9 | // main and modules will get fed to the runtime. 10 | module.exports = function program(main, modules, runtime){ 11 | var Program = esprima.parse('(function(main, modules) {})({})'); 12 | 13 | var Runtime = runtime; 14 | Runtime.type = Syntax.BlockStatement; // change to a BlockStatement 15 | Program.body[0].expression.callee.body = Runtime; 16 | 17 | var ProgramArguments = Program.body[0].expression.arguments; 18 | var ObjectExpression = ProgramArguments[0]; 19 | ProgramArguments.unshift({ 20 | type: Syntax.Literal, 21 | value: main 22 | }); 23 | 24 | forIn(modules, function(module, id) { 25 | var tree = module.ast; 26 | var ModuleProgram = esprima.parse('(function(require, module, exports, global){})'); 27 | tree.type = Syntax.BlockStatement; // change to a BlockStatement 28 | ModuleProgram.body[0].expression.body = tree; 29 | 30 | ObjectExpression.properties.push({ 31 | type: Syntax.Property, 32 | key: { 33 | type: Syntax.Literal, 34 | value: id 35 | }, 36 | value: ModuleProgram.body[0].expression, 37 | kind: 'init' 38 | }); 39 | }); 40 | 41 | return Program; 42 | }; 43 | -------------------------------------------------------------------------------- /util/resolver.js: -------------------------------------------------------------------------------- 1 | /* global -Promise*/ 2 | 'use strict'; 3 | 4 | var pathModule = require('path'); 5 | var pathogen = require('pathogen'); 6 | var prime = require('prime'); 7 | 8 | var Promise = require('promise'); 9 | 10 | var isString = require('mout/lang/isString'); 11 | var isPlainObject = require('mout/lang/isPlainObject'); 12 | var contains = require('mout/array/contains'); 13 | 14 | var transport = require('./transport'); 15 | 16 | var sequence = require('./sequence').use(Promise); 17 | 18 | // absolute path 19 | var absRe = /^(\/|.+:\/)/; 20 | // relative path 21 | var relRe = /^(\.\/|\.\.\/)/; 22 | 23 | // implementation of http://nodejs.org/api/modules.html#modules_all_together 24 | 25 | var natives = [ 26 | '_debugger', '_linklist', 'assert', 'buffer', 'child_process', 'console', 'constants', 'crypto', 27 | 'cluster', 'dgram', 'dns', 'domain', 'events', 'freelist', 'fs', 'http', 'https', 'module', 28 | 'net', 'os', 'path', 'punycode', 'querystring', 'readline', 'repl', 'stream', '_stream_readable', 29 | '_stream_writable', '_stream_duplex', '_stream_transform', '_stream_passthrough', 30 | 'string_decoder', 'sys', 'timers', 'tls', 'tty', 'url', 'util', 'vm', 'zlib' 31 | ]; 32 | 33 | var isNative = function(pkg) { 34 | return contains(natives, pkg); 35 | }; 36 | 37 | var Resolver = prime({ 38 | 39 | constructor: function(options) { 40 | if (!options) options = {}; 41 | 42 | this.browser = options.browser == null ? true : !!options.browser; 43 | this.nodeModules = options.nodeModules || 'node_modules'; 44 | this.defaultPath = options.defaultPath ? pathogen.resolve(options.defaultPath) : null; 45 | }, 46 | 47 | // resolve required from a specific location 48 | // follows the browser field in package.json 49 | resolve: function(from, required) { 50 | from = pathogen.resolve(pathogen.dirname(from)); 51 | 52 | if (isNative(required)) { 53 | if (!this.browser) return Promise.resolve(required); 54 | else return this._findRoute(this._paths(from), required); 55 | } else { 56 | if (!this.browser) return this._resolve(from, required); 57 | else return this._resolveAndRoute(from, required); 58 | } 59 | }, 60 | 61 | _resolveAndRoute: function(from, required) { 62 | var self = this; 63 | 64 | return self._resolve(from, required).then(function(resolved) { 65 | return self._route(from, resolved); 66 | }); 67 | }, 68 | 69 | // resolve required from a specific location 70 | // does not follow the browser field in package.json 71 | _resolve: function(from, required) { 72 | if (relRe.test(required)) return this._load(pathogen.resolve(from, required)); 73 | else if (absRe.test(required)) return this._load(required); 74 | else return this._package(from, required); 75 | }, 76 | 77 | _findRouteInBrowserField: function(browser, path, resolved) { 78 | var self = this; 79 | 80 | return sequence(browser, function(value, key, control) { 81 | 82 | if (isNative(key)) { 83 | 84 | if (key === resolved) { 85 | if (!value) control.resolve(false); 86 | else self._resolveAndRoute(path, value).then(control.resolve, control.reject); 87 | } else { 88 | control.save(null).continue(); 89 | } 90 | 91 | } else { 92 | 93 | self._resolve(path, key).then(function(res) { 94 | if (res === resolved) { 95 | if (!value) control.resolve(false); 96 | else self.resolve(path, value).then(control.resolve, control.reject); 97 | } else { 98 | control.save(null).continue(); 99 | } 100 | }, function() { 101 | control.save(null).continue(); 102 | }); 103 | 104 | } 105 | 106 | }); 107 | }, 108 | 109 | _findRoute: function(paths, resolved) { 110 | var self = this; 111 | return sequence(paths, function(path, i, control) { 112 | 113 | transport.json(path + 'package.json').then(function(json) { 114 | 115 | if (isPlainObject(json.browser)) { 116 | 117 | self._findRouteInBrowserField(json.browser, path, resolved).then(function(route) { 118 | if (route == null) control.save(resolved).continue(); // no route found 119 | else control.resolve(route); 120 | }, control.reject); 121 | 122 | } else { 123 | 124 | control.save(resolved).continue(); 125 | 126 | } 127 | 128 | }, function(/*read json error*/) { 129 | control.save(resolved).continue(); 130 | }); 131 | 132 | }); 133 | 134 | }, 135 | 136 | // finds the routed entry in the browser field in package.json 137 | // when not found it will simply callback resolved 138 | _route: function(from, resolved) { 139 | var self = this; 140 | 141 | var paths = self._paths(from); 142 | 143 | return self.findRoot(resolved).then(function(path) { 144 | if (paths[0] !== path) paths.unshift(path); 145 | return self._findRoute(paths, resolved); 146 | }); 147 | }, 148 | 149 | // resolves either a file or a directory to a module, based on pattern 150 | _load: function(required) { 151 | var self = this; 152 | var promise = Promise.reject(); 153 | if (!(/\/$/).test(required)) promise = self._file(required); 154 | return promise.catch(function() { 155 | return self._directory(required); 156 | }); 157 | }, 158 | 159 | // resolves a file name to a module 160 | _file: function(required) { 161 | var exts = ['.js']; 162 | 163 | if (pathogen.extname(required)) exts.unshift(''); 164 | 165 | return sequence.find(exts, function(ext) { 166 | var path = required + ext; 167 | return transport(required + ext).then(function() { 168 | return path; 169 | }); 170 | }); 171 | }, 172 | 173 | // resolves a directory to a module 174 | // takes into account the main field (or browser field as string) 175 | _directory: function(full) { 176 | var self = this; 177 | 178 | return transport.json(full + 'package.json').catch(function() { 179 | return {}; 180 | }).then(function(json) { 181 | var main = isString(json.browser) ? json.browser : json.main; 182 | if (main == null) return self._file(pathogen.resolve(full, 'index')); 183 | return self._load(pathogen.resolve(full, main)); 184 | }); 185 | }, 186 | 187 | // resolves a packaged require to a module 188 | _package: function(from, required) { 189 | var self = this; 190 | 191 | var split = required.split('/'); 192 | var packageName = split.shift(); 193 | 194 | // make it into an explicit folder if it's only a package 195 | if (required.indexOf('/') === -1) required += '/'; 196 | 197 | return self.resolvePackage(from, packageName).then(function(jsonPath) { 198 | return self._load(pathogen.dirname(jsonPath) + split.join('/')); 199 | }); 200 | }, 201 | 202 | // generates a list of possible node modules paths 203 | _paths: function(path) { 204 | var node_modules = this.nodeModules; 205 | 206 | var paths = []; 207 | var parts = (path).split('/').slice(1, -1); 208 | var drive = new pathogen(path).drive; 209 | 210 | for (var i = parts.length, part; i; part = parts[i--]) { 211 | if (part === node_modules) continue; 212 | var dir = drive + '/' + parts.slice(0, i).join('/') + '/'; 213 | paths.push(dir); 214 | } 215 | paths.push(drive + '/'); 216 | if (this.defaultPath) paths.push(this.defaultPath + '/'); 217 | return paths; 218 | }, 219 | 220 | resolvePackage: function(path, packageName) { 221 | var self = this; 222 | var node_modules = self.nodeModules; 223 | var nodePath = (typeof process !== 'undefined') && process.env && process.env.NODE_PATH; 224 | 225 | path = pathogen.resolve(pathogen.dirname(path)); 226 | 227 | var paths = self._paths(path); 228 | 229 | paths = paths.map(function(path) { 230 | return path + node_modules; 231 | }); 232 | 233 | if (nodePath) { 234 | paths = nodePath.split(pathModule.delimiter).filter(function(path) { 235 | return !!path; 236 | }).concat(paths); 237 | } 238 | 239 | return sequence.find(paths, function(path) { 240 | var jsonPath = path + '/' + packageName + '/package.json'; 241 | 242 | return transport(jsonPath).then(function() { 243 | return jsonPath; 244 | }); 245 | 246 | }); 247 | }, 248 | 249 | // find the package root of a specified file (or dir) 250 | findRoot: function(path) { 251 | var self = this; 252 | var paths = self._paths(pathogen.resolve(pathogen.dirname(path))); 253 | 254 | return sequence.find(paths, function(path) { 255 | return transport(path + 'package.json').then(function() { 256 | return path; 257 | }); 258 | }); 259 | 260 | } 261 | 262 | }); 263 | 264 | Resolver.natives = natives; 265 | Resolver.isNative = isNative; 266 | 267 | module.exports = Resolver; 268 | -------------------------------------------------------------------------------- /util/sequence.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var forEach = require('mout/collection/forEach'); 4 | var isInteger = require('mout/lang/isInteger'); 5 | var isArray = require('mout/lang/isArray'); 6 | var fillIn = require('mout/object/fillIn'); 7 | 8 | function Control(collection, length, keys, values, next, resolve, reject) { 9 | 10 | var pending = true; 11 | var remaining = length; 12 | 13 | var caught, saved; 14 | 15 | var done = function() { 16 | pending = false; 17 | if (caught) { 18 | reject(caught.error); 19 | } else if (saved) { 20 | resolve(saved.value); 21 | } else { 22 | if (isArray(collection)) { 23 | var result = []; 24 | for (var i = 0; i < length; i++) if (i in collection) result.push(collection[i]); 25 | resolve(result); 26 | } else { 27 | resolve(collection); 28 | } 29 | } 30 | }; 31 | 32 | this.resolve = function(value) { 33 | if (pending) { 34 | pending = false; 35 | resolve(value); 36 | } 37 | return this; 38 | }; 39 | 40 | this.reject = function(error) { 41 | if (pending) { 42 | pending = false; 43 | reject(error); 44 | } 45 | return this; 46 | }; 47 | 48 | this.collect = function(index, value) { 49 | if (pending) { 50 | collection[keys[index]] = value; 51 | if (!--remaining) done(); 52 | } 53 | return this; 54 | }; 55 | 56 | this.catch = function(error) { 57 | if (pending) { 58 | caught = { error: error }; 59 | if (!--remaining) done(); 60 | } 61 | return this; 62 | }; 63 | 64 | this.save = function(value) { 65 | if (pending) { 66 | saved = { value: value }; 67 | if (!--remaining) done(); 68 | } 69 | return this; 70 | }; 71 | 72 | this.skip = function() { 73 | if (pending && !--remaining) done(); 74 | return this; 75 | }; 76 | 77 | this.continue = function() { 78 | if (pending) next(); 79 | return this; 80 | }; 81 | 82 | } 83 | 84 | var identity = function(promise) { return promise; }; 85 | 86 | function use(Promise) { 87 | 88 | if (!Promise) throw new Error('sequence needs a Promise implementation'); 89 | 90 | function sequence(list, iterator, previous) { 91 | var length = 0; 92 | var keys = []; 93 | var values = []; 94 | 95 | forEach(list, function(value, key) { 96 | values.push(value); 97 | keys.push(key); 98 | length++; 99 | }); 100 | 101 | if (!length) return Promise.resolve(previous); 102 | 103 | var index = 0; 104 | 105 | var collection = (isInteger(list.length)) ? [] : {}; 106 | 107 | return new Promise(function(resolve, reject) { 108 | 109 | var control = new Control(collection, length, keys, values, next, resolve, reject); 110 | 111 | function next() { 112 | if (index === length) return; 113 | var current = index++; 114 | 115 | var key = keys[current]; 116 | var value = values[current]; 117 | 118 | var ctrl = fillIn({ 119 | index: current, 120 | last: index === length, 121 | collect: function(value) { 122 | return control.collect(current, value); 123 | } 124 | }, control); 125 | 126 | previous = iterator(value, key, ctrl, previous); 127 | } 128 | 129 | next(); 130 | }); 131 | 132 | } 133 | 134 | // find 135 | // ├─ sequential execution 136 | // ├─ resolves with a value when one iterator is resolveed 137 | // └─ rejects with one error when the last iterator is rejected 138 | sequence.find = function find(values, iterator) { 139 | if (!iterator) iterator = identity; 140 | 141 | return sequence(values, function(value, key, control) { 142 | Promise.resolve().then(function() { 143 | return iterator(value, key); 144 | }).then(control.resolve, function(error) { 145 | control.catch(error).continue(); 146 | }); 147 | }); 148 | }; 149 | 150 | // filter 151 | // ├─ parallel execution 152 | // └─ always resolves with an array of values (or empty array) with the value of every resolveed iterator. 153 | sequence.filter = function filter(values, iterator) { 154 | if (!iterator) iterator = identity; 155 | 156 | return sequence(values, function(value, key, control) { 157 | Promise.resolve().then(function() { 158 | return iterator(value, key); 159 | }).then(control.collect, control.skip); 160 | control.continue(); 161 | }); 162 | }; 163 | 164 | // map 165 | // ├─ sequential execution 166 | // ├─ resolves with an array of values representing every resolveed iterator. 167 | // └─ rejects when one iterator rejects 168 | sequence.map = function map(values, iterator) { 169 | if (!iterator) iterator = identity; 170 | 171 | return sequence(values, function(value, key, control) { 172 | Promise.resolve().then(function() { 173 | return iterator(value, key); 174 | }).then(function(value) { 175 | control.collect(value).continue(); 176 | }, control.reject); 177 | }); 178 | }; 179 | 180 | // every 181 | // ├─ parallel execution 182 | // ├─ resolves with an array of values when all iterators are resolveed 183 | // ├─ rejects with the last error when one iterator is rejected 184 | // ├─ same as map, but parallel. 185 | // ├─ executes all iterators 186 | // └─ waits for all iterators to be either resolveed or rejected before resolving or rejecting. 187 | sequence.every = function every(values, iterator) { 188 | if (!iterator) iterator = identity; 189 | 190 | return sequence(values, function(value, key, control) { 191 | Promise.resolve().then(function() { 192 | return iterator(value, key); 193 | }).then(control.collect, control.catch); 194 | control.continue(); 195 | }); 196 | }; 197 | 198 | // some 199 | // ├─ parallel execution 200 | // ├─ resolves with an array of values when some iterators are resolveed 201 | // ├─ rejects when no iterators are resolved 202 | // ├─ executes all iterators 203 | // └─ waits for all iterators to be either resolveed or rejected before resolving or rejecting. 204 | sequence.some = function some(values, iterator) { 205 | if (!iterator) iterator = identity; 206 | 207 | var found = false; 208 | return sequence(values, function(value, key, control) { 209 | Promise.resolve().then(function() { 210 | return iterator(value, key); 211 | }).then(function(value) { 212 | found = true; 213 | control.collect(value); 214 | }, function(error) { 215 | if (control.last && !found) control.reject(error); 216 | else control.skip(); 217 | }); 218 | control.continue(); 219 | }); 220 | }; 221 | 222 | // all 223 | // ├─ parallel execution 224 | // ├─ resolves with an array of values when all iterators are resolveed 225 | // ├─ rejects with the first error when one iterator is rejected 226 | // ├─ same as map, but parallel 227 | // └─ executes all iterators 228 | sequence.all = function all(values, iterator) { 229 | if (!iterator) iterator = identity; 230 | 231 | return sequence(values, function(value, key, control) { 232 | Promise.resolve().then(function() { 233 | return iterator(value, key); 234 | }).then(control.collect, control.reject); 235 | control.continue(); 236 | }); 237 | }; 238 | 239 | // reduce 240 | // ├─ sequential execution 241 | // ├─ resolves with one value when all iterators are resolveed 242 | // └─ rejects with one error when one iterator is rejected 243 | sequence.reduce = function reduce(values, iterator, init) { 244 | if (!iterator) iterator = identity; 245 | 246 | return sequence(values, function(value, key, control, promise) { 247 | return promise.then(function(resolved) { 248 | return iterator(resolved, value, key); 249 | }).then(function(value) { 250 | control.save(value).continue(); 251 | return value; 252 | }, control.reject); 253 | }, Promise.resolve(init)); 254 | }; 255 | 256 | // race 257 | // ├─ parallel execution 258 | // ├─ resolves with first resolveed iterator 259 | // └─ rejects with first rejected iterator 260 | sequence.race = function race(values, iterator) { 261 | if (!iterator) iterator = identity; 262 | 263 | return sequence(values, function(value, key, control) { 264 | Promise.resolve().then(function() { 265 | return iterator(value, key); 266 | }).then(control.resolve, control.reject); 267 | control.continue(); 268 | }); 269 | }; 270 | 271 | return sequence; 272 | 273 | } 274 | 275 | exports.use = use; 276 | -------------------------------------------------------------------------------- /util/transport.js: -------------------------------------------------------------------------------- 1 | /* global -Promise */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | var pathogen = require('pathogen'); 6 | var Promise = require('promise'); 7 | 8 | var transport; 9 | var cache = { get: {}, json: {} }; 10 | 11 | if ('readFile' in fs) { 12 | 13 | transport = function get(url) { 14 | var cached = cache.get[url]; 15 | if (cached) return cached; 16 | 17 | return cache.get[url] = new Promise(function(fulfill, reject) { 18 | fs.readFile(pathogen.sys(url), 'utf-8', function(error, data) { 19 | error ? reject(error) : fulfill(data); 20 | }); 21 | }); 22 | }; 23 | 24 | } else { 25 | 26 | // conditional require for node.js, this could be a non-node friendly package. 27 | var agent = require('agent'); 28 | 29 | // this goes easy on the browser 30 | agent.MAX_REQUESTS = 4; 31 | 32 | // we don't want agent to automatically decode json, to match the readFile behavior. 33 | agent.decoder('application/json', null); 34 | 35 | transport = function get(url) { 36 | var cached = cache.get[url]; 37 | if (cached) return cached; 38 | 39 | return cache.get[url] = new Promise(function(fulfill, reject) { 40 | 41 | agent.get(url, function(error, response) { 42 | if (error) return reject(error); 43 | var status = response.status; 44 | if (status >= 300 || status < 200) return reject(new Error('GET ' + url + ' ' + status)); 45 | 46 | if (pathogen.extname(url) !== '.html' && response.header['Content-Type'] === 'text/html') { 47 | // reject mismatching html content types (in case of file listing, treat as error) 48 | reject(new Error('GET ' + url + ' content-type mismatch')); 49 | } else { 50 | fulfill(response.body); 51 | } 52 | 53 | }); 54 | 55 | }); 56 | 57 | }; 58 | 59 | } 60 | 61 | transport.json = function json(url) { 62 | var cached = cache.json[url]; 63 | if (cached) return cached; 64 | return cache.json[url] = transport(url).then(JSON.parse); 65 | }; 66 | 67 | transport.cache = cache; 68 | 69 | module.exports = transport; 70 | --------------------------------------------------------------------------------