├── README.md ├── .gitignore ├── package.json ├── LICENSE └── index.js /README.md: -------------------------------------------------------------------------------- 1 | # wallabify 2 | 3 | Wallaby.js postprocessor to support browserify. 4 | 5 | The plugin documentation lives at http://wallabyjs.com/docs/integration/browserify.html 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | .idea 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wallabify", 3 | "version": "0.0.15", 4 | "description": "Browserify support in wallaby.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/wallabyjs/wallabify.git" 12 | }, 13 | "keywords": [ 14 | "browserify-plugin", 15 | "browserify", 16 | "wallaby.js" 17 | ], 18 | "author": "Artem Govorov", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/wallabyjs/wallabify/issues" 22 | }, 23 | "homepage": "https://github.com/wallabyjs/wallabify", 24 | "devDependencies": { 25 | "browserify": "^9.0.3" 26 | }, 27 | "dependencies": { 28 | "convert-source-map": "^1.0.0", 29 | "graceful-fs": "^4.1.3", 30 | "lodash": "^3.5.0", 31 | "minimatch": "2.0.1", 32 | "through2": "^0.6.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 wallaby.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fs = require('graceful-fs'); 5 | var _ = require('lodash'); 6 | var through = require('through2'); 7 | var convert = require('convert-source-map'); 8 | var mm = require('minimatch'); 9 | 10 | /* 11 | Postprocessor for wallaby.js runs module bundler compiler incrementally 12 | to only build changed or not yet built modules. The compiler is stopped from emitting the bundle/chunks to disk, 13 | because while concatenating files is beneficial for production environment, in testing environment it is different. 14 | Serving a large bundle/chunk every time when one of many files (that the bundle consists of) changes, is wasteful. 15 | So instead, each compiled module code is passed to wallaby, wallaby caches it in memory (and when required, writes 16 | it on disk) and serves each requested module file separately to properly leverage browser caching. 17 | 18 | Apart from emitting module files, the postprocessor also emits a test loader script that executes in browser before 19 | any modules. The test loader sets up a global object so that each wrapped module can add itself to the loader cache. 20 | 21 | Each module code is wrapped in such a way that when the module file is loaded in browser, it doesn't execute 22 | the module code immediately. Instead, it just adds the function that executes the module code to test loader's cache. 23 | 24 | Modules are loaded from tests (that are entry points) when the tests are loaded. The tests are loaded from wallaby 25 | bootstrap function, by calling `__moduleBundler.loadTests()`. 26 | 27 | When wallaby runs tests first time, browser caches all modules and each subsequent test run only needs to load a 28 | changed module files from the server (and not the full bundle). 29 | */ 30 | 31 | class Wallabify { 32 | 33 | constructor(opts, initializer) { 34 | this._patchModuleDependenciesModule(); 35 | 36 | this._opts = opts || {}; 37 | 38 | this._prelude = this._opts.prelude; 39 | this._entryPatterns = this._opts.entryPatterns; 40 | delete this._opts.prelude; 41 | delete this._opts.entryPatterns; 42 | 43 | if (this._entryPatterns && _.isString(this._entryPatterns)) { 44 | this._entryPatterns = [this._entryPatterns]; 45 | } 46 | 47 | this._initializer = initializer; 48 | 49 | this._b = null; 50 | this._browserifyCache = {}; 51 | this._affectedFilesCache = {}; 52 | this._initRequired = false; 53 | this._allTrackedFiles = {}; 54 | this._entryFiles = {}; 55 | } 56 | 57 | createPostprocessor() { 58 | var self = this; 59 | try { 60 | this._browserify = require('browserify'); 61 | } 62 | catch (e) { 63 | console.error('Browserify node module is not found, missing `npm install browserify --save-dev`?'); 64 | return; 65 | } 66 | 67 | return wallaby => { 68 | var logger = wallaby.logger; 69 | var affectedFiles = wallaby.affectedFiles; 70 | if (!self._b || wallaby.anyFilesAdded || wallaby.anyFilesDeleted) { 71 | 72 | if (!self._b) { 73 | logger.debug('New browserify instance created'); 74 | } 75 | else { 76 | logger.debug('Browserify instance re-created because some tracked files were added or deleted'); 77 | } 78 | 79 | self._initRequired = true; 80 | self._affectedFilesCache = {}; 81 | self._allTrackedFiles = _.reduce(wallaby.allFiles, function (memo, file) { 82 | memo[file.fullPath] = file; 83 | return memo; 84 | }, {}); 85 | affectedFiles = wallaby.allFiles; 86 | 87 | self._entryFiles = _.reduce(!self._entryPatterns 88 | ? wallaby.allTestFiles 89 | : _.filter(self._allTrackedFiles, file => _.find(self._entryPatterns, pattern => mm(file.path, pattern))), 90 | function (memo, file) { 91 | memo[file.fullPath] = file; 92 | return memo; 93 | }, {}); 94 | 95 | self._b = self._createBrowserify({ 96 | entries: _.map(self._entryFiles, entryFile => entryFile.fullPath), 97 | paths: wallaby.nodeModulesDir ? [wallaby.nodeModulesDir] : [], 98 | cache: {}, packageCache: {}, fullPaths: true 99 | }); 100 | 101 | self._browserifyCache = self._b._options.cache; 102 | 103 | self._b.on('dep', function (dep) { 104 | if (typeof dep.id === 'string') { 105 | var key = dep.id; 106 | if (!self._browserifyCache[key]) { 107 | self._browserifyCache[key] = dep; 108 | // external files are cached by file name as well to avoid re-processing them every time 109 | if (dep.expose) { 110 | self._browserifyCache[dep.file] = dep; 111 | } 112 | // new file that has not been cached before (node module or a source file) 113 | self._affectedFilesCache[key] = dep; 114 | } 115 | } 116 | }); 117 | 118 | // no need to pack the bundle, wallaby.js serves files one by one to leverage browser caching 119 | self._b.pipeline.splice('pack'); 120 | } 121 | 122 | // removing changed files tracked by wallaby.js from browserify cache 123 | if (!self._initRequired) { 124 | _.each(affectedFiles, file => { 125 | delete self._browserifyCache[file.fullPath]; 126 | }); 127 | } 128 | 129 | return new Promise( 130 | function (resolve, reject) { 131 | try { 132 | // incremental bundling 133 | self._b.bundle() 134 | .on('data', () => { 135 | }) 136 | .on('error', err => reject(err.stack ? err : new Error(err.toString()))) 137 | .on('end', () => resolve()); 138 | } catch (err) { 139 | reject(err); 140 | } 141 | }) 142 | .then(function () { 143 | var createFilePromises = []; 144 | 145 | // test loader for wallaby.js 146 | // works exactly as browserify bundle loading, but with separate files as opposed to a single bundle file 147 | if (self._initRequired) { 148 | self._initRequired = false; 149 | 150 | createFilePromises.push(wallaby.createFile({ 151 | order: -1, // need to be the first file to load 152 | path: 'wallabify.js', 153 | content: self._getLoaderContent() 154 | })); 155 | 156 | // Executing all entry files 157 | if (self._entryPatterns && self._entryFiles && !_.isEmpty(self._entryFiles)) { 158 | createFilePromises.push(wallaby.createFile({ 159 | order: Infinity, 160 | path: 'wallabify_entry.js', 161 | content: _.reduce(_.values(self._entryFiles), 162 | (memo, file) => memo + (file.test ? '' : 'window.__moduleBundler.require(' + JSON.stringify(file.fullPath) + ');'), '') 163 | })); 164 | } 165 | } 166 | 167 | // handling changed files tracked by wallaby.js 168 | _.each(affectedFiles, function (file) { 169 | var cached = self._browserifyCache[file.fullPath]; 170 | if (cached) { 171 | var code = cached.source; 172 | var sourceMap; 173 | var sourceMapConverter = convert.fromSource(code); 174 | if (sourceMapConverter) { 175 | sourceMap = sourceMapConverter.toJSON(); 176 | code = convert.removeComments(code); 177 | } 178 | 179 | var isEntryFile = self._entryPatterns && self._entryFiles[file.fullPath]; 180 | 181 | // cloning an original file and browserify-ing it 182 | createFilePromises.push(wallaby.createFile({ 183 | // adding the suffix to store browserified file along with the original copies 184 | path: file.path + '.bro.js', 185 | original: file, 186 | content: Wallabify._wallabifyFile(file.fullPath, code, cached.deps), 187 | sourceMap: sourceMap, 188 | order: isEntryFile ? file.order : undefined 189 | })); 190 | delete self._affectedFilesCache[file.fullPath]; 191 | } 192 | }); 193 | 194 | // handling externally added and not tracked files (such as node modules and external files) 195 | try { 196 | _.each(self._affectedFilesCache, function (file) { 197 | var ext = path.extname(file.id); 198 | var basename = path.basename(file.id, ext); 199 | createFilePromises.push(wallaby.createFile({ 200 | // file path/name doesn't matter, just has to be unique for each file 201 | path: path.join('__modules', basename + '.' + require('crypto').createHash('md5').update(file.id).digest('hex') + '.js'), 202 | content: Wallabify._wallabifyFile(file.id, file.source, file.deps) 203 | })); 204 | }); 205 | } 206 | catch (e) { 207 | return Promise.reject(e); 208 | } 209 | 210 | // resetting till next incremental bundle run 211 | self._affectedFilesCache = {}; 212 | 213 | logger.debug('Emitting %s files', createFilePromises.length); 214 | 215 | return Promise.all(createFilePromises); 216 | }); 217 | } 218 | } 219 | 220 | _createBrowserify(mandatoryOpts) { 221 | var paths = mandatoryOpts.paths.concat(this._opts.paths || []); 222 | var mergedOpts = _.merge({}, this._opts, mandatoryOpts); 223 | mergedOpts.paths = paths; 224 | 225 | var instance = this._browserify(mergedOpts); 226 | if (this._initializer && _.isFunction(this._initializer)) { 227 | return this._initializer(instance) || instance; 228 | } 229 | 230 | return instance; 231 | } 232 | 233 | static _wallabifyFile(id, content, deps) { 234 | return 'window.__moduleBundler.cache[' + JSON.stringify(id) + '] = [function(require, module, exports) {' 235 | + content + '\n}, ' + JSON.stringify(deps) + '];'; 236 | } 237 | 238 | _getLoaderContent() { 239 | var prelude = this._prelude || 240 | '(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module \'"+o+"\'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o