├── test ├── fixtures │ ├── text.txt │ ├── a.js │ ├── error.js │ ├── plugin.ts │ ├── configure.js │ ├── b.js │ ├── c.js │ └── transform.js ├── integration │ ├── test │ │ ├── expected.txt │ │ ├── error │ │ │ ├── foo.js │ │ │ ├── testErrorSpec.js │ │ │ └── dependencyErrorSpec.js │ │ ├── aSpec.js │ │ ├── helper.js │ │ ├── commonSpec.js │ │ ├── failingSpec.js │ │ ├── bSpec.js │ │ └── externalSpec.js │ ├── lib │ │ ├── a.js │ │ ├── b.js │ │ ├── fail.js │ │ ├── common.js │ │ └── external.js │ ├── ts │ │ ├── A.ts │ │ └── A.spec.ts │ ├── vendor │ │ └── external.js │ ├── auto-watch.conf.js │ ├── test-error.conf.js │ ├── dependency-error.conf.js │ ├── configure.js │ ├── error.conf.js │ ├── typescript-error.conf.js │ ├── single-run.conf.js │ ├── no-auto-watch.conf.js │ ├── prebundled │ │ ├── common.js │ │ └── common-win.js │ ├── README.md │ └── karma.conf.js ├── expect.js ├── restorable-file.js ├── spec │ ├── logger-factory.js │ ├── browserifySpec.js │ ├── integrationSpec.js │ └── pluginSpec.js └── runner.js ├── example ├── test │ ├── expected.txt │ ├── externalSpec.js │ └── bSpec.js ├── lib │ ├── a.js │ └── b.js ├── vendor │ └── external.js ├── karma.conf.js └── package.json ├── .gitignore ├── .eslintignore ├── .github └── workflows │ └── CI.yml ├── .eslintrc ├── index.js ├── LICENSE ├── lib ├── bundle-file.js ├── preprocessor.js └── bro.js ├── package.json ├── CONTRIBUTING.md └── README.md /test/fixtures/text.txt: -------------------------------------------------------------------------------- 1 | HALLO -------------------------------------------------------------------------------- /example/test/expected.txt: -------------------------------------------------------------------------------- 1 | A and b -------------------------------------------------------------------------------- /example/lib/a.js: -------------------------------------------------------------------------------- 1 | module.exports = 'A'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | .idea/ -------------------------------------------------------------------------------- /test/integration/test/expected.txt: -------------------------------------------------------------------------------- 1 | A and b -------------------------------------------------------------------------------- /test/fixtures/a.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'A'; -------------------------------------------------------------------------------- /test/integration/test/error/foo.js: -------------------------------------------------------------------------------- 1 | module.exports = ('foo' { }); -------------------------------------------------------------------------------- /test/integration/lib/a.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./common').a; -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | test/integration 3 | example/node_modules -------------------------------------------------------------------------------- /test/integration/test/error/testErrorSpec.js: -------------------------------------------------------------------------------- 1 | 2 | someFn({a}, b: 'c'); -------------------------------------------------------------------------------- /test/fixtures/error.js: -------------------------------------------------------------------------------- 1 | // intentionally broken file 2 | 3 | invalid[) syntax] -------------------------------------------------------------------------------- /example/lib/b.js: -------------------------------------------------------------------------------- 1 | var a = require('./a'); 2 | 3 | module.exports = a + ' and b'; -------------------------------------------------------------------------------- /test/integration/lib/b.js: -------------------------------------------------------------------------------- 1 | var a = require('./a'); 2 | 3 | module.exports = a + ' and b'; -------------------------------------------------------------------------------- /test/integration/test/error/dependencyErrorSpec.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./foo'); -------------------------------------------------------------------------------- /test/fixtures/plugin.ts: -------------------------------------------------------------------------------- 1 | // typescript 2 | var plugin = 'plugin'; 3 | export = plugin; 4 | -------------------------------------------------------------------------------- /test/fixtures/configure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var foo = require('foobar'); 3 | module.exports = 'A'; -------------------------------------------------------------------------------- /test/fixtures/b.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var a = require('./a'); 4 | 5 | module.exports = 'B' + a; -------------------------------------------------------------------------------- /test/fixtures/c.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var a = require('./a'); 4 | 5 | module.exports = 'C' + a; -------------------------------------------------------------------------------- /test/integration/lib/fail.js: -------------------------------------------------------------------------------- 1 | module.exports.throwError = function(str) { 2 | throw new Error(str); 3 | } -------------------------------------------------------------------------------- /test/integration/ts/A.ts: -------------------------------------------------------------------------------- 1 | export class A { 2 | private time:Date; 3 | 4 | constructor() { 5 | this.time = new Date() 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/transform.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | module.exports.text = '<' + fs.readFileSync(__dirname + '/text.txt', 'utf-8') + '>'; -------------------------------------------------------------------------------- /example/vendor/external.js: -------------------------------------------------------------------------------- 1 | /** this file simulates a non-commonJS friendly library that binds to window */ 2 | 3 | /* global window */ 4 | 5 | window.External = { version: '0.0.0-alpha' }; -------------------------------------------------------------------------------- /test/expect.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinonChai = require('sinon-chai'); 3 | 4 | chai.use(sinonChai); 5 | 6 | // expose expect as global 7 | global.expect = chai.expect; 8 | -------------------------------------------------------------------------------- /test/integration/lib/common.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file gets prebundled into a dist/common.js bundle. 3 | * 4 | * It is included before the browserified test files. 5 | */ 6 | module.exports.a = 'A'; -------------------------------------------------------------------------------- /test/integration/vendor/external.js: -------------------------------------------------------------------------------- 1 | /** this file simulates a non-commonJS friendly library that binds to window */ 2 | 3 | /* global window */ 4 | 5 | window.External = { version: '0.0.0-alpha' }; -------------------------------------------------------------------------------- /test/integration/test/aSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var a = require('../lib/a'); 4 | 5 | describe('a', function() { 6 | it('should equal fixture contents', function() { 7 | expect(a).toEqual('A'); 8 | }); 9 | }); -------------------------------------------------------------------------------- /test/integration/test/helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file represents a helper script that gets included 3 | * directly via karma and via a test file (externalSpec.js) 4 | */ 5 | module.exports.externalVersion = '0.0.0-alpha'; -------------------------------------------------------------------------------- /test/integration/lib/external.js: -------------------------------------------------------------------------------- 1 | /* This file requires that an external, non-commonJS friendly library has been loaded before */ 2 | 3 | /* global window */ 4 | 5 | var External = window.External; 6 | 7 | module.exports = External; -------------------------------------------------------------------------------- /test/integration/test/commonSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var common = require('../lib/common'); 4 | 5 | 6 | describe('common', function() { 7 | 8 | it('should expose a', function() { 9 | expect(common.a).toEqual('A'); 10 | }); 11 | 12 | }); -------------------------------------------------------------------------------- /test/integration/test/failingSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fail = require('../lib/fail'); 4 | 5 | describe('failing spec', function() { 6 | 7 | it('should result in nicely formated stack trace', function() { 8 | fail.throwError('intentional'); 9 | }); 10 | 11 | }); -------------------------------------------------------------------------------- /example/test/externalSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var external = require('external'); 4 | 5 | 6 | describe('external', function() { 7 | 8 | it('should access non-commonJS library', function() { 9 | expect(external.version).toEqual('0.0.0-alpha'); 10 | }); 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /test/integration/auto-watch.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var configure = require('./configure'); 4 | 5 | var singleRun = require('./single-run.conf'); 6 | 7 | module.exports = configure(singleRun, function(karma) { 8 | 9 | karma.set({ 10 | singleRun: false, 11 | autoWatch: true 12 | }); 13 | }); -------------------------------------------------------------------------------- /test/integration/test-error.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var configure = require('./configure'); 4 | 5 | var error = require('./error.conf'); 6 | 7 | module.exports = configure(error, function(karma) { 8 | 9 | karma.set({ 10 | files: [ 11 | 'test/error/testErrorSpec.js' 12 | ] 13 | }); 14 | }); -------------------------------------------------------------------------------- /test/integration/dependency-error.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var configure = require('./configure'); 4 | 5 | var error = require('./error.conf'); 6 | 7 | module.exports = configure(error, function(karma) { 8 | 9 | karma.set({ 10 | files: [ 11 | 'test/error/dependencyErrorSpec.js' 12 | ] 13 | }); 14 | }); -------------------------------------------------------------------------------- /example/test/bSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | 5 | var b = require('../lib/b'); 6 | 7 | 8 | describe('b', function() { 9 | 10 | var expected = fs.readFileSync(__dirname + '/expected.txt', 'utf8'); 11 | 12 | 13 | it('should equal fixture contents', function() { 14 | expect(b).toEqual(expected); 15 | }); 16 | 17 | }); -------------------------------------------------------------------------------- /test/integration/test/bSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | 5 | var b = require('../lib/b'); 6 | 7 | 8 | describe('b', function() { 9 | 10 | var expected = fs.readFileSync(__dirname + '/expected.txt', 'utf8'); 11 | 12 | 13 | it('should equal fixture contents', function() { 14 | expect(b).toEqual(expected); 15 | }); 16 | 17 | }); -------------------------------------------------------------------------------- /test/integration/test/externalSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var external = require('../lib/external'), 4 | expectedExternalVersion = require('./helper').externalVersion; 5 | 6 | 7 | describe('external', function() { 8 | 9 | it('should access non-commonJS library', function() { 10 | expect(external.version).toEqual(expectedExternalVersion); 11 | }); 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /test/integration/configure.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | // hooks 4 | var hooks = Array.prototype.slice.call(arguments); 5 | 6 | // actual config fn 7 | return function(karma) { 8 | 9 | var opts = {}; 10 | 11 | var fakeKarma = { 12 | set: function(props) { 13 | opts = Object.assign(opts, props); 14 | } 15 | }; 16 | 17 | hooks.forEach(function(h) { 18 | h(fakeKarma); 19 | }); 20 | 21 | karma.set(opts); 22 | }; 23 | }; -------------------------------------------------------------------------------- /test/integration/error.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var configure = require('./configure'); 4 | 5 | var base = require('./karma.conf'); 6 | 7 | module.exports = configure(base, function(karma) { 8 | 9 | karma.set({ 10 | 11 | preprocessors: { 12 | 'test/**/*.js': [ 'browserify' ] 13 | }, 14 | 15 | browserify: { 16 | debug: true 17 | }, 18 | 19 | reporters: [ 'dots' ], 20 | 21 | logLevel: 'DEBUG', 22 | 23 | singleRun: true, 24 | autoWatch: false, 25 | }); 26 | }); -------------------------------------------------------------------------------- /test/integration/typescript-error.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var configure = require('./configure'); 4 | 5 | var base = require('./karma.conf'); 6 | 7 | module.exports = configure(base, function(karma) { 8 | 9 | karma.set({ 10 | files: [ 11 | 'ts/*.spec.ts' 12 | ], 13 | 14 | preprocessors: { 15 | 'ts/*.ts': [ 'browserify' ] 16 | }, 17 | 18 | browserify: { 19 | debug: true, 20 | plugin: [ 'tsify' ] 21 | }, 22 | 23 | reporters: [ ], 24 | 25 | logLevel: 'ERROR' 26 | }); 27 | }); -------------------------------------------------------------------------------- /test/restorable-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | 5 | function RestorableFile(location) { 6 | 7 | var contents; 8 | 9 | function update(newContents) { 10 | fs.writeFileSync(location, newContents); 11 | } 12 | 13 | function load() { 14 | contents = fs.readFileSync(location); 15 | } 16 | 17 | this.load = load; 18 | this.update = update; 19 | 20 | this.remove = function() { 21 | fs.unlinkSync(location); 22 | }; 23 | 24 | this.restore = function() { 25 | update(contents); 26 | }; 27 | } 28 | 29 | module.exports = RestorableFile; 30 | -------------------------------------------------------------------------------- /test/integration/single-run.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var configure = require('./configure'); 4 | 5 | var base = require('./karma.conf'); 6 | 7 | module.exports = configure(base, function(karma) { 8 | 9 | karma.set({ 10 | files: [ 11 | 'vendor/external.js', 12 | 'test/aSpec.js', 13 | 'test/xxaSpec.js', 14 | 'test/externalSpec.js' 15 | ], 16 | 17 | preprocessors: { 18 | 'test/*.js': [ 'browserify' ] 19 | }, 20 | 21 | browserify: { 22 | debug: true 23 | }, 24 | 25 | reporters: [], 26 | 27 | logLevel: 'ERROR' 28 | }); 29 | }); -------------------------------------------------------------------------------- /test/integration/ts/A.spec.ts: -------------------------------------------------------------------------------- 1 | import {A} from "./A"; 2 | 3 | describe('TypeScript Class A', () => { 4 | 5 | it('should instantiate a class', () => { 6 | let instance:A = new A(); 7 | expect(instance).toBeDefined(); 8 | }) 9 | 10 | }) 11 | 12 | // THIS FILE SHOULD NOT COMPILE BECAUSE TYPESCRIPT IS MISSING 13 | // THE TYPE INFORMATION COMMENTED OUT BELOW 14 | 15 | // declare class assert { 16 | // toBeDefined() 17 | // } 18 | // declare function expect(obj:any):assert 19 | // declare function it(descrption:string, cb:Function) 20 | // declare function describe(descrption:string, cb:Function) -------------------------------------------------------------------------------- /test/integration/no-auto-watch.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var configure = require('./configure'); 4 | 5 | var base = require('./karma.conf'); 6 | 7 | module.exports = configure(base, function(karma) { 8 | 9 | karma.set({ 10 | files: [ 11 | 'vendor/external.js', 12 | 'test/aSpec.js', 13 | 'test/externalSpec.js' 14 | ], 15 | 16 | preprocessors: { 17 | 'test/*.js': [ 'browserify' ] 18 | }, 19 | 20 | singleRun: false, 21 | autoWatch: false, 22 | 23 | browserify: { 24 | debug: true 25 | }, 26 | 27 | logLevel: 'DEBUG' 28 | }); 29 | }); -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [ push, pull_request ] 3 | jobs: 4 | build: 5 | continue-on-error: true 6 | runs-on: ubuntu-latest 7 | 8 | strategy: 9 | matrix: 10 | node-version: [10, 12, 14, 16, 20] 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v5 15 | - name: Use Node.js ${{matrix.node-version}} 16 | uses: actions/setup-node@v5 17 | with: 18 | node-version: ${{matrix.node-version}} 19 | cache: 'npm' 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Build 23 | run: npm run all -------------------------------------------------------------------------------- /example/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(karma) { 4 | karma.set({ 5 | 6 | frameworks: [ 'jasmine', 'browserify' ], 7 | 8 | files: [ 9 | 'vendor/external.js', 10 | 'test/**/*Spec.js' 11 | ], 12 | 13 | reporters: [ 'dots' ], 14 | 15 | preprocessors: { 16 | 'test/**/*Spec.js': [ 'browserify' ] 17 | }, 18 | 19 | browsers: [ 'ChromeHeadless' ], 20 | 21 | logLevel: 'LOG_DEBUG', 22 | 23 | singleRun: true, 24 | autoWatch: false, 25 | 26 | // browserify configuration 27 | browserify: { 28 | debug: true, 29 | transform: [ 'brfs', 'browserify-shim' ] 30 | } 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /test/integration/prebundled/common.js: -------------------------------------------------------------------------------- 1 | require=(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", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "brfs": "^1.4.0", 17 | "browserify": "^14.5.0", 18 | "browserify-shim": "^3.8.0", 19 | "karma": "^0.13.0", 20 | "karma-browserify": "^5.0.0", 21 | "karma-jasmine": "^0.1.5", 22 | "karma-chrome-launcher": "^2.2.0", 23 | "watchify": "^3.9.0" 24 | }, 25 | "browserify-shim": { 26 | "external": "global:External" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6 4 | }, 5 | "rules": { 6 | "keyword-spacing": [ 2 ], 7 | "object-curly-spacing": [ 2, "always"], 8 | "space-before-blocks": [ 2, "always"], 9 | "indent": [ 2, 2, { "VariableDeclarator": { "var": 2, "let": 2, "const": 3 } } ], 10 | "no-unused-vars": [ 2, { "args": "none" } ], 11 | "space-before-function-paren": [ 2, "never" ], 12 | "quotes": [ 2, "single" ], 13 | "semi": [ 2, "always" ], 14 | "no-console": 0, 15 | "mocha/no-exclusive-tests": 2 16 | }, 17 | "env": { 18 | "node": true 19 | }, 20 | "extends": "eslint:recommended", 21 | "plugins": [ 22 | "mocha" 23 | ], 24 | "globals": { 25 | "expect": false, 26 | "it": false, 27 | "describe": false, 28 | "beforeEach": true, 29 | "afterEach": true, 30 | "before": true, 31 | "after": true 32 | } 33 | } -------------------------------------------------------------------------------- /test/integration/README.md: -------------------------------------------------------------------------------- 1 | This folder contains an integration test project that handles various use cases we cannot write simple tests for. 2 | 3 | ## Expected Result 4 | 5 | All test cases should run, only one should fail with the following error message: 6 | 7 | ``` 8 | PhantomJS 1.9.8 (Linux) failing spec should result in nicely formated stack trace FAILED 9 | Error: intentional 10 | at /tmp/51c9709e217c50aa330289ba59e2372f6db88164.browserify.js:21:0 <- lib/fail.js:2:0 11 | at /tmp/51c9709e217c50aa330289ba59e2372f6db88164.browserify.js:66:0 <- test/failingSpec.js:8:0 12 | ``` 13 | 14 | 15 | ## Run 16 | 17 | ``` 18 | karma start 19 | ``` 20 | 21 | 22 | ## Debug 23 | 24 | ``` 25 | karma start --auto-watch --no-single-run --browsers=Chrome 26 | ``` 27 | 28 | 29 | ## Recreate prebundled common module 30 | 31 | ``` 32 | npm install browserify && node_modules/.bin/browserify -r ./lib/common.js -o prebundled/common.js 33 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Bro = require('./lib/bro'); 4 | 5 | function framework(injector, bro) { 6 | return injector.invoke(bro.framework); 7 | } 8 | 9 | function testFilePreprocessor(injector, bro) { 10 | return injector.invoke(bro.testFilePreprocessor); 11 | } 12 | 13 | function bundlePreprocessor(injector, bro) { 14 | return injector.invoke(bro.bundlePreprocessor); 15 | } 16 | 17 | module.exports = { 18 | 'bro': [ 'type', Bro ], 19 | 'framework:browserify': [ 'factory', framework ], 20 | 'preprocessor:browserify': [ 'factory', testFilePreprocessor ], 21 | 'preprocessor:browserify-bundle': [ 'factory', bundlePreprocessor ] 22 | }; 23 | 24 | 25 | // override the default preprocess factory to add our 26 | // preprocessor for *.browserify.js files 27 | 28 | try { 29 | module.exports.preprocess = [ 'factory', require('./lib/preprocessor').createPreprocessor ]; 30 | } catch (e) { 31 | console.warn('failed to add custom browserify preprocessor'); 32 | console.warn(e); 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nico Rehwaldt 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /lib/bundle-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'), 4 | path = require('path'), 5 | os = require('os-shim'), 6 | hat = require('hat'); 7 | 8 | /** 9 | * Create a temp file unique to the project 10 | */ 11 | function getTempFileName(suffix) { 12 | return path.join(os.tmpdir(), hat() + suffix); 13 | } 14 | 15 | /** 16 | * A instance of a bundle file 17 | */ 18 | function BundleFile() { 19 | 20 | var location = getTempFileName('.browserify.js'); 21 | 22 | function write(content) { 23 | fs.writeFileSync(location, content); 24 | } 25 | 26 | function exists() { 27 | return fs.existsSync(location); 28 | } 29 | 30 | function remove() { 31 | if (exists()) { 32 | fs.unlinkSync(location); 33 | } 34 | } 35 | 36 | function touch() { 37 | if (!exists()) { 38 | write(''); 39 | } 40 | } 41 | 42 | 43 | // API 44 | 45 | this.touch = touch; 46 | 47 | this.update = write; 48 | this.remove = remove; 49 | 50 | this.location = location; 51 | } 52 | 53 | 54 | // module exports 55 | 56 | module.exports = BundleFile; 57 | module.exports.getTempFileName = getTempFileName; 58 | -------------------------------------------------------------------------------- /lib/preprocessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var preprocessor = require('karma/lib/preprocessor'); 4 | var os = require('os-shim'); 5 | var path = require('path'); 6 | 7 | function getPreprocessorFactory() { 8 | var factory = preprocessor.createPriorityPreprocessor; 9 | 10 | if (factory.$inject[0] !== 'config.preprocessors') { 11 | console.log('incompatible karma preprocessor: the first parametr should be config.preprocessors but', factory.$inject[0]); 12 | throw new Error('incompatible karma preprocessor'); 13 | } 14 | 15 | return factory; 16 | } 17 | 18 | 19 | /** 20 | * Monkey patch preprocessors to preprocess *.browserify.js 21 | */ 22 | 23 | var originalFactory = getPreprocessorFactory(); 24 | 25 | var createPreprocessor = function(config, ...args) { 26 | // add our preprocessor for .browserify.js files 27 | config[path.resolve(os.tmpdir(), '*.browserify.js')] = ['browserify-bundle']; 28 | 29 | return originalFactory.apply(null, [config, ...args]); 30 | }; 31 | 32 | createPreprocessor.$inject = originalFactory.$inject; 33 | 34 | 35 | // publish patched preprocessor 36 | module.exports.createPreprocessor = createPreprocessor; 37 | -------------------------------------------------------------------------------- /test/spec/logger-factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isString = require('lodash/isString'); 4 | 5 | function Logger() { 6 | 7 | var logged = this.logged = []; 8 | 9 | function log(mode, args) { 10 | var prefix = '[' + mode + '] '; 11 | 12 | args = Array.prototype.slice.call(args); 13 | logged.push(prefix + args.join(' ')); 14 | 15 | if (isString(args[0])) { 16 | args[0] = prefix + args[0]; 17 | } else { 18 | args.unshift(prefix); 19 | } 20 | 21 | console.log.apply(console, args); 22 | } 23 | 24 | this.info = function() { 25 | log('info', arguments); 26 | }; 27 | 28 | this.error = function() { 29 | log('error', arguments); 30 | }; 31 | 32 | this.warn = function() { 33 | log('warn', arguments); 34 | }; 35 | 36 | this.debug = function() { 37 | log('debug', arguments); 38 | }; 39 | 40 | this.reset = function() { 41 | logged.length = 0; 42 | }; 43 | } 44 | 45 | 46 | function LoggerFactory() { 47 | 48 | var loggers = this.loggers = {}; 49 | 50 | this.create = function(name) { 51 | 52 | var logger = new Logger(); 53 | 54 | loggers[name] = logger; 55 | 56 | return logger; 57 | }; 58 | } 59 | 60 | module.exports = LoggerFactory; -------------------------------------------------------------------------------- /test/integration/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var platformSuffix = /^win/.test(process.platform) ? '-win' : '', 4 | prebundledCommon = 'prebundled/common' + platformSuffix + '.js'; 5 | 6 | module.exports = function(karma) { 7 | 8 | karma.set({ 9 | 10 | plugins: ['karma-*', require('../..')], 11 | 12 | frameworks: [ 'jasmine', 'browserify' ], 13 | 14 | files: [ 15 | // external (non-browserified) library that exposes a global 16 | 'vendor/external.js', 17 | 18 | // external (browserified) bundle 19 | prebundledCommon, 20 | 21 | // source file, accidently included 22 | // (there is usually no reason to do this) 23 | 'lib/a.js', 24 | 25 | // tests 26 | 'test/*Spec.js', 27 | 28 | // a helper, accidently included with the tests 29 | 'test/helper.js' 30 | ], 31 | 32 | reporters: [ 'dots' ], 33 | 34 | preprocessors: { 35 | 'lib/a.js': [ 'browserify' ], 36 | 'test/*Spec.js': [ 'browserify' ], 37 | 'test/helper.js': [ 'browserify' ] 38 | }, 39 | 40 | browsers: [ 'ChromeHeadless' ], 41 | 42 | logLevel: 'DEBUG', 43 | 44 | singleRun: true, 45 | autoWatch: false, 46 | 47 | // browserify configuration 48 | browserify: { 49 | debug: true, 50 | transform: [ 'brfs' ], 51 | 52 | // configure browserify 53 | configure: function(b) { 54 | b.on('prebundle', function() { 55 | b.external('lib/common.js'); 56 | }); 57 | } 58 | } 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "karma-browserify", 3 | "description": "A fast browserify integration for Karma that handles large projects with ease", 4 | "keywords": [ 5 | "karma-plugin", 6 | "karma-preprocessor", 7 | "browserify-tool", 8 | "browserify" 9 | ], 10 | "version": "8.1.0", 11 | "scripts": { 12 | "all": "run-s lint test", 13 | "lint": "eslint .", 14 | "test": "mocha --exit -r test/expect test/spec/*.js", 15 | "release": "np" 16 | }, 17 | "authors": [ 18 | "Nico Rehwaldt ", 19 | "Ben Drucker " 20 | ], 21 | "main": "index.js", 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "git@github.com:nikku/karma-browserify.git" 26 | }, 27 | "bugs": "https://github.com/nikku/karma-browserify/issues", 28 | "dependencies": { 29 | "convert-source-map": "^1.8.0", 30 | "hat": "^0.0.3", 31 | "js-string-escape": "^1.0.0", 32 | "lodash": "^4.17.21", 33 | "minimatch": "^3.0.0", 34 | "os-shim": "^0.1.3" 35 | }, 36 | "devDependencies": { 37 | "brfs": "^2.0.2", 38 | "browser-unpack": "^1.4.2", 39 | "browserify": "^17.0.0", 40 | "chai": "^4.3.4", 41 | "eslint": "^7.30.0", 42 | "eslint-plugin-mocha": "^8.2.0", 43 | "jasmine-core": "^3.8.0", 44 | "karma": "^5.2.3", 45 | "karma-chrome-launcher": "^3.1.0", 46 | "karma-jasmine": "^4.0.1", 47 | "mocha": "^8.4.0", 48 | "np": "^7.5.0", 49 | "npm-run-all": "^4.1.5", 50 | "resolve": "^1.20.0", 51 | "sinon": "^9.2.4", 52 | "sinon-chai": "^3.7.0", 53 | "touch": "^3.1.0", 54 | "tsify": "^5.0.4", 55 | "typescript": "^4.3.5", 56 | "watchify": "^4.0.0" 57 | }, 58 | "peerDependencies": { 59 | "browserify": ">=10 <18", 60 | "karma": ">=4.3.0", 61 | "watchify": ">=3 <5" 62 | }, 63 | "engines": { 64 | "node": ">=10" 65 | }, 66 | "files": [ 67 | "lib", 68 | "index.js" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We love you to contribute to this project by filing bugs or helping others on the [issue tracker](https://github.com/Nikku/karma-browserify/issues) or by contributing features/bug fixes through pull requests. 4 | 5 | ## Working with issues 6 | 7 | We use our [issue tracker](https://github.com/Nikku/karma-browserify/issues) for project communication. 8 | When using the issue tracker, 9 | 10 | * Be descriptive when creating an issue (what, where, when and how does a problem pop up)? 11 | * Attach steps to reproduce (if applicable) 12 | * Attach code samples, configuration options or stack traces that may indicate a problem 13 | * Be helpful and respect others when commenting 14 | 15 | Create a pull request if you would like to have an in-depth discussion about some piece of code. 16 | 17 | ## Creating pull requests 18 | 19 | We use pull requests for feature discussion and bug fixes. If you are not yet familiar on how to create a pull request, [read this great guide](https://gun.io/blog/how-to-github-fork-branch-and-pull-request). 20 | 21 | Some things that make it easier for us to accept your pull requests 22 | 23 | * The code adheres to our conventions 24 | * spaces instead of tabs 25 | * single-quotes 26 | * ... 27 | * The code is tested 28 | * The `npm run all` build passes (executes tests + linting) 29 | * The work is combined into a single commit 30 | * The commit messages adhere to our [guideline](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y) 31 | 32 | 33 | We'd be glad to assist you if you do not get these things right in the first place. 34 | 35 | ## Maintaining the project 36 | 37 | Some notes for us maintainers only. 38 | 39 | ### Merge pull-requests 40 | 41 | When merging, try to do it manually (rebase on current master). This avoids merge messages. 42 | 43 | ### Release the project 44 | 45 | To release execute `npm run release`. Respect [semantic versioning](http://semver.org/) and choose correct next version based on latest changes. 46 | -------------------------------------------------------------------------------- /test/spec/browserifySpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var browserify = require('browserify'); 4 | var vm = require('vm'); 5 | 6 | function delay(fn, time) { 7 | setTimeout(fn, time); 8 | } 9 | 10 | 11 | describe('browserify', function() { 12 | 13 | var bundler; 14 | 15 | beforeEach(function() { 16 | bundler = browserify(); 17 | }); 18 | 19 | 20 | this.timeout(5000); 21 | 22 | it('should expose external require', function(done) { 23 | 24 | // given 25 | bundler.require('./test/fixtures/a.js', { expose: 'a' }); 26 | 27 | // when 28 | bundler.bundle(function(err, content) { 29 | content = content && content.toString('utf-8'); 30 | 31 | // then 32 | content = content + '\nexpect(require(moduleName)).to.equal("A");'; 33 | vm.runInNewContext(content, { 34 | expect: expect, 35 | moduleName: 'a' 36 | }); 37 | 38 | done(err); 39 | }); 40 | 41 | }); 42 | 43 | 44 | it('should remove already added files on reset', function(done) { 45 | 46 | // given 47 | bundler.add('./test/fixtures/a.js'); 48 | bundler.require('./test/fixtures/a.js', { expose: './test/fixtures/a.js' }); 49 | 50 | bundler.bundle(function(err, content) { 51 | expect(err).not.to.exist; 52 | }); 53 | 54 | // when 55 | delay(function() { 56 | 57 | bundler.reset(); 58 | bundler.add('./test/fixtures/b.js'); 59 | bundler.require('./test/fixtures/b.js', { expose: './test/fixtures/b.js' }); 60 | 61 | bundler.bundle(function(err, content) { 62 | content = content && content.toString('utf-8'); 63 | 64 | // then 65 | expect(content).to.match(/^require=/); 66 | 67 | expect(content).not.to.contain('{"./test/fixtures/a.js":[function('); 68 | }); 69 | }, 200); 70 | 71 | 72 | delay(done, 2000); 73 | }); 74 | 75 | 76 | it('should run transforms on required files', function(done) { 77 | 78 | // given 79 | bundler 80 | .transform('brfs') 81 | .add('./test/fixtures/transform.js') 82 | .require('./test/fixtures/transform.js', { expose: './test/fixtures/a.js' }); 83 | 84 | // when 85 | bundler.bundle(function(err, content) { 86 | content = content && content.toString('utf-8'); 87 | 88 | // then 89 | expect(content).to.match(/^require=/); 90 | 91 | expect(content).to.contain('HALLO'); 92 | 93 | done(err); 94 | }); 95 | 96 | }); 97 | 98 | 99 | it('should reuse callback', function(done) { 100 | 101 | // given 102 | var count = 0; 103 | 104 | function bundled(err, content) { 105 | count++; 106 | } 107 | 108 | // when 109 | bundler.bundle(bundled); 110 | 111 | delay(function() { 112 | bundler.reset(); 113 | bundler.add('./test/fixtures/a.js'); 114 | 115 | bundler.bundle(bundled); 116 | }, 300); 117 | 118 | delay(function() { 119 | bundler.reset(); 120 | bundler.add('./test/fixtures/b.js'); 121 | 122 | bundler.bundle(bundled); 123 | }, 600); 124 | 125 | 126 | // then 127 | delay(function() { 128 | expect(count).to.eql(3); 129 | done(); 130 | }, 2000); 131 | 132 | }); 133 | 134 | }); 135 | -------------------------------------------------------------------------------- /test/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var karma = require('karma'), 4 | events = require('events'), 5 | util = require('util'), 6 | path = require('path'); 7 | 8 | var assign = require('lodash/assign'); 9 | 10 | function Runner() { 11 | this.reset(); 12 | this.on('newListener', this.listen.bind(this)); 13 | } 14 | 15 | util.inherits(Runner, events.EventEmitter); 16 | 17 | Runner.prototype.start = function(configFile, config, done) { 18 | if (this.running) throw new Error('Already running'); 19 | if ('function' === typeof config) { 20 | done = config; 21 | config = {}; 22 | } 23 | if ('function' !== typeof done) done = function() {}; 24 | 25 | this.running = true; 26 | 27 | var karmaConfiguration = this.configure(configFile, config); 28 | 29 | // karma >= v0.13.x 30 | if (karma.Server) { 31 | var server = new karma.Server(karmaConfiguration, done); 32 | server.start(); 33 | } 34 | // karma <= v0.12.x 35 | else { 36 | karma.server.start(karmaConfiguration, done); 37 | } 38 | }; 39 | 40 | Runner.prototype.stop = function() { 41 | if (!this.stopFn) return; 42 | this.stopFn(0); 43 | this.stopFn = null; 44 | }; 45 | 46 | Runner.prototype.stopAfter = function(ms) { 47 | if (this.stopTimeout) clearTimeout(this.stopTimeout); 48 | return this.stopTimeout = setTimeout(this.stop.bind(this), ms); 49 | }; 50 | 51 | Runner.prototype.reset = function() { 52 | this.removeAllListeners(); 53 | this.running = false; 54 | this.reporter = null; 55 | this.listened = {}; 56 | this.queuedListeners = {}; 57 | this.stopFn = null; 58 | if (this.stopTimeout) clearTimeout(this.stopTimeout); 59 | this.stopTimeout = null; 60 | this.bundler = null; 61 | }; 62 | 63 | Runner.prototype.configure = function(configFile, config) { 64 | configFile = path.resolve(configFile); 65 | var karmaConfig = {}; 66 | require(configFile)({ 67 | set: function(conf) { assign(karmaConfig, conf); } 68 | }); 69 | config = assign({}, karmaConfig, config); 70 | return { 71 | configFile: configFile, 72 | frameworks: (config.frameworks || ['browserify']).concat('runner'), 73 | plugins: (config.plugins || ['karma-*']).concat(this.plugin()), 74 | }; 75 | }; 76 | 77 | Runner.prototype.plugin = function() { 78 | return { 'framework:runner': ['factory', this.factory()] }; 79 | }; 80 | 81 | Runner.prototype.factory = function() { 82 | var factory = function(emitter, bundler) { 83 | this.emitter = emitter; 84 | this.bundler = bundler; 85 | emitter.on('exit', function(done) { 86 | this.reset(); 87 | done(); 88 | }.bind(this)); 89 | 90 | emitter.once('run_start', function() { 91 | // find karma runner SIGINT handler 92 | this.stopFn = process.listeners('SIGINT').filter(function(fn, idx) { 93 | return /disconnectBrowsers/.test(fn.toString()); 94 | })[0]; 95 | }.bind(this)); 96 | for (var k in this.queuedListeners) this.listen(k); 97 | this.emit('framework'); 98 | }.bind(this); 99 | factory.$inject = ['emitter', 'framework:browserify']; 100 | return factory; 101 | }; 102 | 103 | Runner.prototype.listen = function(name) { 104 | if (!this.emitter) return this.queuedListeners[name] = true; 105 | if (name in this.listened) return; 106 | this.listened[name] = true; 107 | this.emitter.on(name, function() { 108 | this.emit.apply(this, [name].concat([].slice.apply(arguments))); 109 | }.bind(this)); 110 | }; 111 | 112 | module.exports = Runner; 113 | -------------------------------------------------------------------------------- /test/spec/integrationSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Runner = require('../runner'), 4 | touch = require('touch'); 5 | 6 | var singleRunConfig = require.resolve('../integration/single-run.conf'), 7 | noAutoWatchConfig = require.resolve('../integration/no-auto-watch.conf'), 8 | autoWatchConfig = require.resolve('../integration/auto-watch.conf'); 9 | 10 | var path = require('path'); 11 | var resolve = require('resolve'); 12 | var sinon = require('sinon'); 13 | var chokidar = require(resolve.sync('chokidar', { 14 | basedir: path.resolve(__dirname, '../../node_modules/watchify') 15 | })); 16 | 17 | 18 | function triggerRun(configFile, done) { 19 | var config = { 20 | configFile: configFile 21 | }; 22 | 23 | done = done || function() { }; 24 | 25 | return require('karma/lib/runner').run(config, done); 26 | } 27 | 28 | 29 | describe('karma-browserify', function() { 30 | 31 | var runner, 32 | watchSpy; 33 | 34 | beforeEach(function() { 35 | runner = new Runner(); 36 | }); 37 | 38 | afterEach(function() { 39 | runner.stop(); 40 | 41 | if (watchSpy) { 42 | watchSpy.restore(); 43 | watchSpy = null; 44 | } 45 | }); 46 | 47 | this.timeout(10 * 1000); 48 | 49 | 50 | it('should perform a simple run', function(done) { 51 | 52 | watchSpy = sinon.spy(chokidar, 'watch'); 53 | 54 | runner.start(singleRunConfig, function(result) { 55 | expect(result).to.equal(0); 56 | 57 | // Verify that a single run doesn't create a bunch of watchers. 58 | // (This test assumes that watchify uses chokidar for watching). 59 | expect(watchSpy).not.to.have.been.called; 60 | done(); 61 | }); 62 | }); 63 | 64 | 65 | it('should manually trigger no-auto-watch run', function(done) { 66 | 67 | runner.on('run_complete', function(karma, results) { 68 | runner.stopAfter(500); 69 | 70 | expect(results.success).to.eql(2); 71 | }); 72 | 73 | runner.on('browsers_ready', function() { 74 | triggerRun(__dirname + '/../integration/no-auto-watch.conf.js'); 75 | }); 76 | 77 | runner.start(noAutoWatchConfig, function() { 78 | done(); 79 | }); 80 | }); 81 | 82 | 83 | it('should not double bundle on test file change', function(done) { 84 | 85 | var runCount = 0; 86 | runner.on('run_start', function() { 87 | runCount++; 88 | }); 89 | 90 | // touch file to trigger additional run 91 | runner.once('run_complete', function() { 92 | touch('test/integration/test/externalSpec.js'); 93 | 94 | runner.stopAfter(4000); 95 | }); 96 | 97 | runner.start(autoWatchConfig, function() { 98 | expect(runCount).to.equal(2); 99 | done(); 100 | }); 101 | }); 102 | 103 | 104 | it('should not rebundle if file change is outside bundle', function(done) { 105 | 106 | var bundleCount = 0; 107 | 108 | runner.once('framework', function() { 109 | runner.bundler.on('bundle', function() { 110 | bundleCount++; 111 | }); 112 | }); 113 | 114 | // touch external file 115 | runner.once('run_complete', function() { 116 | touch('test/integration/vendor/external.js'); 117 | 118 | runner.stopAfter(4000); 119 | }); 120 | 121 | runner.start(autoWatchConfig, function() { 122 | // assert external touch did not trigger an additional run 123 | expect(bundleCount).to.equal(1); 124 | done(); 125 | }); 126 | }); 127 | 128 | 129 | it('should detect file rename', function(done) { 130 | 131 | var fs = require('fs'); 132 | 133 | var FILE_NAME = 'test/integration/test/aSpec.js', 134 | UPDATED_FILE_NAME = 'test/integration/test/xxaSpec.js'; 135 | 136 | this.timeout(20000); 137 | 138 | var bundleCount = 0; 139 | 140 | runner.once('framework', function() { 141 | runner.bundler.on('bundle', function() { 142 | bundleCount++; 143 | }); 144 | }); 145 | 146 | runner.once('run_complete', function() { 147 | fs.renameSync(FILE_NAME, UPDATED_FILE_NAME); 148 | 149 | runner.once('run_complete', function() { 150 | fs.renameSync(UPDATED_FILE_NAME, FILE_NAME); 151 | }); 152 | 153 | runner.stopAfter(7000); 154 | }); 155 | 156 | runner.start(autoWatchConfig, function() { 157 | // assert file remove + restore triggered 158 | // two additional bundling runs 159 | expect(bundleCount >= 2).to.equal(true); 160 | 161 | done(); 162 | }); 163 | 164 | }); 165 | 166 | 167 | describe('error handling', function() { 168 | 169 | var testErrorConfig = require.resolve('../integration/test-error.conf'), 170 | dependencyErrorConfig = require.resolve('../integration/dependency-error.conf'), 171 | typescriptErrorConfig = require.resolve('../integration/typescript-error.conf'); 172 | 173 | it('should handle test error', function(done) { 174 | 175 | runner.start(testErrorConfig, function(result) { 176 | expect(result).to.equal(1); 177 | 178 | done(); 179 | }); 180 | }); 181 | 182 | 183 | it('should handle dependency error', function(done) { 184 | 185 | runner.start(dependencyErrorConfig, function(result) { 186 | expect(result).to.equal(1); 187 | 188 | done(); 189 | }); 190 | }); 191 | 192 | 193 | it('should handle typescript compile error', function(done) { 194 | 195 | runner.start(typescriptErrorConfig, function(result) { 196 | 197 | // karma fails with an error if typescript 198 | // cannot be compiled 199 | expect(result).to.equal(1); 200 | 201 | done(); 202 | }); 203 | }); 204 | 205 | }); 206 | 207 | }); 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # karma-browserify 2 | 3 | [![CI](https://github.com/nikku/karma-browserify/actions/workflows/CI.yml/badge.svg)](https://github.com/nikku/karma-browserify/actions/workflows/CI.yml) 4 | 5 | [karma-browserify](https://github.com/nikku/karma-browserify) is a fast [Browserify](http://browserify.org) integration for [Karma](https://karma-runner.github.io) that handles large projects with ease. 6 | 7 | 8 | ## Installation 9 | 10 | Get the plug-in via [npm](https://www.npmjs.org/). 11 | 12 | You will also need to install [browserify](https://www.npmjs.com/package/browserify) and [watchify](https://www.npmjs.com/package/watchify) (for auto-watch only) with it. 13 | 14 | ``` 15 | npm install --save-dev karma-browserify browserify watchify 16 | ``` 17 | 18 | 19 | ## Usage 20 | 21 | Add `browserify` as a framework to your Karma configuration file. For each file that should be processed and bundled by Karma, configure the `browserify` preprocessor. Optionally use the `browserify` config entry to configure how the bundle gets created. 22 | 23 | 24 | ```javascript 25 | module.exports = function(karma) { 26 | karma.set({ 27 | 28 | frameworks: [ 'browserify', 'jasmine', 'or', 'any', 'other', 'framework' ], 29 | files: ['test/**/*.js'], 30 | preprocessors: { 31 | 'test/**/*.js': [ 'browserify' ] 32 | }, 33 | 34 | browserify: { 35 | debug: true, 36 | transform: [ 'brfs' ] 37 | } 38 | }); 39 | } 40 | ``` 41 | 42 | Look at the [example directory](https://github.com/nikku/karma-browserify/tree/master/example) for a simple [browserify](http://browserify.org) + [jasmine](http://jasmine.github.io) project that uses this plug-in. 43 | 44 | 45 | ### Browserify Config 46 | 47 | Test bundles can be configured through the `browserify` Karma configuration property. [Configuration options](https://github.com/substack/node-browserify#var-b--browserifyfiles-or-opts) are passed directly to browserify. 48 | 49 | For example to generate source maps for easier debugging, specify: 50 | 51 | ```javascript 52 | browserify: { 53 | debug: true 54 | } 55 | ``` 56 | 57 | There are three properties that are not passed directly: 58 | 59 | * [transform](#transforms) 60 | * [plugin](#plugins) 61 | * [configure](#additional-bundle-configuration) 62 | * bundleDelay 63 | 64 | 65 | #### Transforms 66 | 67 | If you use CoffeeScript, JSX or other tools that need to transform the source file before bundling, specify a [browserify transform](https://github.com/substack/node-browserify#btransformtr-opts) (Karma preprocessors are [not supported](https://github.com/nikku/karma-browserify/issues/36)). 68 | 69 | ```javascript 70 | browserify: { 71 | transform: [ 'reactify', 'coffeeify', 'brfs' ] 72 | 73 | // don't forget to register the extensions 74 | extensions: ['.js', '.jsx', '.coffee'] 75 | } 76 | ``` 77 | 78 | You can also specify options for the transformations: 79 | 80 | ```javascript 81 | browserify: { 82 | transform: [ ['reactify', {'es6': true}], 'coffeeify', 'brfs' ] 83 | } 84 | ``` 85 | 86 | #### Plugins 87 | 88 | The [browserify plugin](https://github.com/substack/node-browserify#bpluginplugin-opts) option supports the same syntax as `transform`. 89 | 90 | ```javascript 91 | browserify: { 92 | plugin: [ 'stringify' ] 93 | } 94 | ``` 95 | 96 | #### Additional Bundle Configuration 97 | 98 | You may perform additional configuration in a function passed as the `configure` option and that receives the browserify instance as an argument. A custom `prebundle` event is emitted on the bundle right before a bundling operation takes place. This is useful when setting up things like [externals](https://github.com/substack/node-browserify#external-requires): 99 | 100 | ```javascript 101 | browserify: { 102 | configure: function(bundle) { 103 | bundle.on('prebundle', function() { 104 | bundle.external('foobar'); 105 | }); 106 | } 107 | } 108 | ``` 109 | 110 | You'll also need to use the `'prebundle'` event for full control over the order of transforms and plugins: 111 | 112 | ```javascript 113 | browserify: { 114 | configure: function(bundle) { 115 | bundle.once('prebundle', function() { 116 | bundle.transform('babelify').plugin('proxyquireify/plugin'); 117 | }); 118 | } 119 | } 120 | ``` 121 | 122 | Note that transforms must only be added once. 123 | 124 | 125 | ### Watchify Config 126 | 127 | You can configure the underlying [watchify](https://github.com/substack/watchify) instance via `config.watchify`. This is helpful if you need to fine tune the change detection used during `autoWatch=true`. 128 | 129 | ```javascript 130 | watchify: { 131 | poll: true 132 | } 133 | ``` 134 | 135 | 136 | ## How it Works 137 | 138 | This project is a preprocessor for Karma that combines test files and dependencies into a browserified bundle. It relies on [watchify](https://github.com/substack/watchify) to generate the bundle and to keep it updated during `autoWatch=true`. 139 | 140 | Before the initial test run we build one browserify bundle for all test cases and dependencies. Once any of the files change, it incrementally updates the bundle. Each file included in Karma is required from the file bundle via a stub. Thereby it ensures tests are only executed once per test run. 141 | 142 | 143 | ## Detailed Configuration 144 | 145 | The following code snippet shows a Karma configuration file with all browserify-related options. 146 | 147 | ```javascript 148 | module.exports = function(karma) { 149 | karma.set({ 150 | 151 | // include browserify first in used frameworks 152 | frameworks: [ 'browserify', 'jasmine' ], 153 | 154 | // add all your files here, 155 | // including non-commonJS files you need to load before your test cases 156 | files: [ 157 | 'some-non-cjs-library.js', 158 | 'test/**/*.js' 159 | ], 160 | 161 | // add preprocessor to the files that should be 162 | // processed via browserify 163 | preprocessors: { 164 | 'test/**/*.js': [ 'browserify' ] 165 | }, 166 | 167 | // see what is going on 168 | logLevel: 'LOG_DEBUG', 169 | 170 | // use autoWatch=true for quick and easy test re-execution once files change 171 | autoWatch: true, 172 | 173 | // add additional browserify configuration properties here 174 | // such as transform and/or debug=true to generate source maps 175 | browserify: { 176 | debug: true, 177 | transform: [ 'brfs' ], 178 | configure: function(bundle) { 179 | bundle.on('prebundle', function() { 180 | bundle.external('foobar'); 181 | }); 182 | } 183 | } 184 | }); 185 | }; 186 | ``` 187 | 188 | 189 | ## Related 190 | 191 | Credit goes to to the original [karma-browserify](https://github.com/xdissent/karma-browserify) and [karma-browserifast](https://github.com/cjohansen/karma-browserifast). This library builds on the lessons learned in these projects and offers improved configurability, speed and/or the ability to handle large projects. 192 | 193 | 194 | ## License 195 | 196 | MIT 197 | -------------------------------------------------------------------------------- /lib/bro.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var browserify = require('browserify'), 4 | watchify; 5 | 6 | try { 7 | watchify = require('watchify'); 8 | } catch (e) { 9 | // watchify is an optional dependency 10 | 11 | // we will fail as soon as a user tires to use autoWatch without 12 | // watchify installed. 13 | } 14 | 15 | 16 | var convert = require('convert-source-map'), 17 | minimatch = require('minimatch'), 18 | escape = require('js-string-escape'); 19 | 20 | var path = require('path'), 21 | fs = require('fs'); 22 | 23 | var reduce = require('lodash/reduce'), 24 | find = require('lodash/find'), 25 | some = require('lodash/some'), 26 | forEach = require('lodash/forEach'), 27 | assign = require('lodash/assign'), 28 | omit = require('lodash/omit'), 29 | debounce = require('lodash/debounce'); 30 | 31 | 32 | var BundleFile = require('./bundle-file'); 33 | 34 | 35 | /** 36 | * The time to wait for additional file change nofifications 37 | * before performing a rebundling operation. 38 | * 39 | * This value must be chosen with care. The smaller it is, the 40 | * faster the rebundling + testing cycle is. At the same time 41 | * the chance increases karma-browserify performs bundling steps 42 | * twice because it triggers a rebundle before all file change 43 | * triggers have been transmitted. 44 | */ 45 | var DEFAULT_BUNDLE_DELAY = 700; 46 | 47 | var BUNDLE_ERROR_TPL = 'throw new Error("bundle error (see logs)");'; 48 | 49 | 50 | /** 51 | * Extract the source map from the given bundle contents 52 | * 53 | * @param {String} source 54 | * @return {SourceMap} if it could be parsed 55 | */ 56 | function extractSourceMap(bundleContents) { 57 | var start = bundleContents.lastIndexOf('//# sourceMappingURL'); 58 | var sourceMapComment = start !== -1 ? bundleContents.substring(start) : ''; 59 | 60 | return sourceMapComment && convert.fromComment(sourceMapComment); 61 | } 62 | 63 | /** 64 | * Creates an instance of karma-browserify that provides the 65 | * neccessary framework and preprocessors. 66 | * 67 | * @param {BundleFile} [bundleFile] 68 | */ 69 | function Bro(bundleFile) { 70 | 71 | var log; 72 | 73 | /** 74 | * Add bundle file to the list of files in the 75 | * configuration, right before the first browserified 76 | * test file and after everything else. 77 | * 78 | * That makes sure users can include non-commonJS files 79 | * prior to the browserified bundle. 80 | * 81 | * @param {BundleFile} bundleFile the file containing the browserify bundle 82 | * @param {Object} config the karma configuration to be updated 83 | */ 84 | function addBundleFile(bundleFile, config) { 85 | 86 | var files = config.files, 87 | preprocessors = config.preprocessors; 88 | 89 | // list of patterns using our preprocessor 90 | var patterns = reduce(preprocessors, function(matched, val, key) { 91 | if (val.indexOf('browserify') !== -1) { 92 | matched.push(key); 93 | } 94 | return matched; 95 | }, []); 96 | 97 | // first file being preprocessed 98 | var file = find(files, function(f) { 99 | return some(patterns, function(p) { 100 | return minimatch(f.pattern, p); 101 | }); 102 | }); 103 | 104 | var idx = 0; 105 | 106 | if (file) { 107 | idx = files.indexOf(file); 108 | } else { 109 | log.debug('no matching preprocessed file was found, defaulting to prepend'); 110 | } 111 | 112 | log.debug('add bundle to config.files at position', idx); 113 | 114 | // insert bundle on the correct spot 115 | files.splice(idx, 0, { 116 | pattern: bundleFile.location, 117 | served: true, 118 | included: true, 119 | watched: true 120 | }); 121 | } 122 | 123 | 124 | /** 125 | * The browserify instance that creates the 126 | * minified bundle and gets added all test files to it. 127 | */ 128 | var b; 129 | 130 | 131 | /** 132 | * The browserify framework that creates the initial logger and bundle file 133 | * as well as prepends the bundle file to the karma file configuration. 134 | */ 135 | function framework(emitter, config, logger) { 136 | 137 | log = logger.create('framework.browserify'); 138 | 139 | if (!bundleFile) { 140 | bundleFile = new BundleFile(); 141 | } 142 | 143 | bundleFile.touch(); 144 | log.debug('created browserify bundle: %s', bundleFile.location); 145 | 146 | b = createBundle(config); 147 | 148 | // TODO(Nikku): hook into karma karmas file update facilities 149 | // to remove files from the bundle once karma detects the deletion 150 | 151 | // hook into exit for cleanup 152 | emitter.on('exit', function(done) { 153 | log.debug('cleaning up'); 154 | 155 | if (b.close) { 156 | b.close(); 157 | } 158 | 159 | bundleFile.remove(); 160 | done(); 161 | }); 162 | 163 | 164 | // add bundle file to the list of files defined in the 165 | // configuration. be smart by doing so. 166 | addBundleFile(bundleFile, config); 167 | 168 | return b; 169 | } 170 | 171 | framework.$inject = [ 'emitter', 'config', 'logger' ]; 172 | 173 | 174 | /** 175 | * Create the browserify bundle 176 | */ 177 | function createBundle(config) { 178 | 179 | var bopts = config.browserify || {}, 180 | bundleDelay = bopts.bundleDelay || DEFAULT_BUNDLE_DELAY, 181 | requireName = bopts.externalRequireName || 'require'; 182 | 183 | function warn(key) { 184 | log.warn('Invalid config option: "' + key + 's" should be "' + key + '"'); 185 | } 186 | 187 | forEach([ 'transform', 'plugin' ], function(key) { 188 | if (bopts[key + 's']) { 189 | warn(key); 190 | } 191 | }); 192 | 193 | var browserifyOptions = assign({ 194 | basedir: path.resolve(config.basePath), 195 | // watchify.args 196 | cache: {}, 197 | packageCache: {} 198 | }, omit(bopts, [ 199 | 'transform', 'plugin', 'configure', 'bundleDelay' 200 | ])); 201 | 202 | if ('prebundle' in browserifyOptions) { 203 | log.warn('The prebundle hook got removed in favor of configure'); 204 | } 205 | 206 | if ('watchify' in browserifyOptions) { 207 | log.warn('Configure watchify via config.watchify'); 208 | } 209 | 210 | var w = browserify(browserifyOptions); 211 | w.setMaxListeners(Infinity); 212 | 213 | forEach(bopts.plugin, function(p) { 214 | // ensure we can pass plugin options as 215 | // the first parameter 216 | if (!Array.isArray(p)) { 217 | p = [ p ]; 218 | } 219 | w.plugin.apply(w, p); 220 | }); 221 | 222 | forEach(bopts.transform, function(t) { 223 | // ensure we can pass transform options as 224 | // the first parameter 225 | if (!Array.isArray(t)) { 226 | t = [ t ]; 227 | } 228 | w.transform.apply(w, t); 229 | }); 230 | 231 | // test if we have a configure function 232 | if (bopts.configure && typeof bopts.configure === 'function') { 233 | bopts.configure(w); 234 | } 235 | 236 | // register rebuild bundle on change 237 | if (config.autoWatch) { 238 | 239 | if (!watchify) { 240 | log.error('watchify not found; install it via npm install --save-dev watchify'); 241 | log.error('cannot perform incremental rebuild'); 242 | 243 | throw new Error('watchify not found'); 244 | } 245 | 246 | w = watchify(w, config.watchify); 247 | 248 | log.info('registering rebuild (autoWatch=true)'); 249 | 250 | w.on('update', function(updated) { 251 | 252 | // we perform an update, karma will trigger one, too 253 | // because the bundling is deferred only one change will 254 | // be triggered. Anything else is the result of a 255 | // raise condition or a problem of watchify firing file 256 | // changes to late 257 | 258 | log.debug('files changed'); 259 | deferredBundle(); 260 | }); 261 | 262 | w.on('log', function(msg) { 263 | log.info(msg); 264 | }); 265 | 266 | // update bundle file 267 | w.on('bundled', function(err, content) { 268 | if (w._builtOnce) { 269 | bundleFile.update(err ? BUNDLE_ERROR_TPL : content.toString('utf-8')); 270 | log.info('bundle updated'); 271 | } 272 | }); 273 | } 274 | 275 | function deferredBundle(cb) { 276 | if (cb) { 277 | w.once('bundled', cb); 278 | } 279 | 280 | rebuild(); 281 | } 282 | 283 | var rebuild = debounce(function rebuild() { 284 | 285 | if (w._bundled) { 286 | log.debug('resetting bundle'); 287 | 288 | var recorded = w._recorded; 289 | w.reset(); 290 | 291 | recorded.forEach(function(e) { 292 | // we remove missing files on the fly 293 | // to cope with bundle internals missing 294 | if (e.file && !fs.existsSync(path.resolve(config.basePath, e.file))) { 295 | log.debug('removing missing file', e.file); 296 | } else { 297 | w.pipeline.write(e); 298 | } 299 | }); 300 | } 301 | 302 | w.emit('prebundle', w); 303 | 304 | log.debug('bundling'); 305 | 306 | w.bundle(function(err, content) { 307 | 308 | if (err) { 309 | log.error('bundle error'); 310 | log.error(String(err)); 311 | } 312 | 313 | w.emit('bundled', err, content); 314 | }); 315 | }, bundleDelay); 316 | 317 | 318 | w.bundleFile = function(file, done) { 319 | 320 | var absolutePath = path.resolve(file.path), 321 | relativePath = path.relative(config.basePath, absolutePath); 322 | 323 | // add file 324 | log.debug('updating %s in bundle', relativePath); 325 | 326 | // add the file during next prebundle step 327 | w.once('prebundle', function() { 328 | w.require('./' + relativePath, { expose: absolutePath }); 329 | }); 330 | 331 | deferredBundle(function(err) { 332 | var stub = 'typeof ' + requireName + ' === "function" && ' + requireName + '("' + escape(absolutePath) + '");'; 333 | 334 | done(err, stub); 335 | }); 336 | }; 337 | 338 | 339 | /** 340 | * Wait for the bundle creation to have stabilized (no more additions) and invoke a callback. 341 | * 342 | * @param {Function} [callback] invoked with (err, content) 343 | */ 344 | w.deferredBundle = deferredBundle; 345 | 346 | return w; 347 | } 348 | 349 | 350 | /** 351 | * A processor that preprocesses commonjs test files which should be 352 | * delivered via browserify. 353 | */ 354 | function testFilePreprocessor() { 355 | 356 | return function(content, file, done) { 357 | b.bundleFile(file, function(err, content) { 358 | done(content && content.toString()); 359 | }); 360 | }; 361 | } 362 | 363 | testFilePreprocessor.$inject = [ ]; 364 | 365 | 366 | /** 367 | * A special preprocessor that builds the main browserify bundle once and 368 | * passes the bundle contents through on all later preprocessing request. 369 | */ 370 | function bundlePreprocessor(config) { 371 | 372 | var debug = config.browserify && config.browserify.debug; 373 | 374 | function updateSourceMap(file, content) { 375 | var map; 376 | 377 | if (debug) { 378 | 379 | map = extractSourceMap(content); 380 | 381 | file.sourceMap = map && map.sourcemap; 382 | } 383 | } 384 | 385 | return function(content, file, done) { 386 | 387 | if (b._builtOnce) { 388 | updateSourceMap(file, content); 389 | return done(content); 390 | } 391 | 392 | log.debug('building bundle'); 393 | 394 | // wait for the initial bundle to be created 395 | b.deferredBundle(function(err, content) { 396 | 397 | b._builtOnce = config.autoWatch; 398 | 399 | if (err) { 400 | return done(BUNDLE_ERROR_TPL); 401 | } 402 | 403 | content = content.toString('utf-8'); 404 | updateSourceMap(file, content); 405 | 406 | log.info('bundle built'); 407 | 408 | done(content); 409 | }); 410 | }; 411 | } 412 | 413 | bundlePreprocessor.$inject = [ 'config' ]; 414 | 415 | 416 | // API 417 | 418 | this.framework = framework; 419 | 420 | this.testFilePreprocessor = testFilePreprocessor; 421 | this.bundlePreprocessor = bundlePreprocessor; 422 | } 423 | 424 | Bro.$inject = []; 425 | 426 | module.exports = Bro; 427 | -------------------------------------------------------------------------------- /test/spec/pluginSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var events = require('events'); 4 | var Bro = require('../../lib/bro'); 5 | var BundleFile = require('../../lib/bundle-file'); 6 | var RestorableFile = require('../restorable-file'); 7 | var LoggerFactory = require('./logger-factory'); 8 | var sinon = require('sinon'); 9 | var path = require('path'); 10 | var fs = require('fs'); 11 | var unpack = require('browser-unpack'); 12 | var escape = require('js-string-escape'); 13 | 14 | var assign = require('lodash/assign'), 15 | forEach = require('lodash/forEach'); 16 | 17 | var BUNDLE_UPDATE_CHECK_DELAY = 3000; 18 | 19 | function delay(fn, time) { 20 | setTimeout(fn, time || 205); 21 | } 22 | 23 | 24 | function createFilePattern(file) { 25 | return { 26 | pattern: file, 27 | served: true, 28 | included: true, 29 | watched: true 30 | }; 31 | } 32 | 33 | function createFile(p) { 34 | return { 35 | path: path.resolve(p), 36 | realContents: function() { 37 | return fs.readFileSync(this.path, 'utf-8'); 38 | } 39 | }; 40 | } 41 | 42 | function createConfig(config) { 43 | config = config || {}; 44 | 45 | return assign({}, { 46 | basePath: '', 47 | files: [ createFilePattern('*.js') ], 48 | preprocessors: { 49 | '*.js': [ 'browserify' ] 50 | } 51 | }, config); 52 | } 53 | 54 | function expectedBundleFile(filename) { 55 | return escape(path.resolve(filename)); 56 | } 57 | 58 | function expectedBundle(filename, requireName) { 59 | requireName = requireName || 'require'; 60 | return 'typeof ' + requireName + ' === "function" && ' + requireName + '("' + expectedBundleFile(filename) + '");'; 61 | } 62 | 63 | function expectBundleContainments(bundleFile, testFiles) { 64 | var extractedFiles = unpack(bundleFile.bundled).map(function(row) { return row.id; }); 65 | 66 | forEach(testFiles, function(f) { 67 | expect(extractedFiles).to.contain(f.path); 68 | }); 69 | } 70 | 71 | /** 72 | * A test karma plugin, wrapping bro. 73 | */ 74 | function TestPlugin(bro, emitter, config, loggerFactory) { 75 | this.framework = bro.framework(emitter, config, loggerFactory); 76 | this.testFilePreprocessor = bro.testFilePreprocessor(config); 77 | this.bundlePreprocessor = bro.bundlePreprocessor(config); 78 | } 79 | 80 | /** 81 | * Preprocess bundle and test files, collect async results 82 | * and call the callback with all files in the order they have been processed. 83 | */ 84 | TestPlugin.prototype.preprocess = function preprocess(bundle, testFiles, done) { 85 | 86 | /*jshint validthis:true */ 87 | var plugin = this; 88 | 89 | var total = 1 + testFiles.length; 90 | 91 | var processed = []; 92 | 93 | function fileProcessed(file, result) { 94 | file.bundled = result; 95 | 96 | processed.push(file); 97 | 98 | if (processed.length == total) { 99 | done(); 100 | } 101 | } 102 | 103 | function process(preprocessor, file) { 104 | preprocessor(file.bundled || '', file, function(result) { 105 | fileProcessed(file, result); 106 | }); 107 | } 108 | 109 | process(plugin.bundlePreprocessor, bundle); 110 | 111 | // Karma does not necessarily preprocess test files in the order they are given. 112 | var shuffledTestFiles = testFiles.slice(0).reverse(); 113 | 114 | forEach(shuffledTestFiles, function(file) { 115 | process(plugin.testFilePreprocessor, file); 116 | }); 117 | }; 118 | 119 | 120 | describe('karma-browserify', function() { 121 | 122 | var emitter, loggerFactory, bundle, bro; 123 | 124 | beforeEach(function() { 125 | 126 | emitter = new events.EventEmitter(); 127 | loggerFactory = new LoggerFactory(); 128 | 129 | bundle = new BundleFile(); 130 | 131 | sinon.spy(bundle, 'update'); 132 | sinon.spy(bundle, 'touch'); 133 | sinon.spy(bundle, 'remove'); 134 | 135 | bro = new Bro(bundle); 136 | }); 137 | 138 | 139 | afterEach(function(done) { 140 | emitter.emit('exit', done); 141 | }); 142 | 143 | // increase timeout 144 | this.timeout(10000); 145 | 146 | 147 | function createPlugin(config) { 148 | config = createConfig(config); 149 | 150 | return new TestPlugin(bro, emitter, config, loggerFactory); 151 | } 152 | 153 | 154 | describe('framework', function() { 155 | 156 | describe('init', function() { 157 | 158 | it('should prepend and init bundle file', function() { 159 | 160 | // given 161 | var config = createConfig(); 162 | 163 | // when 164 | createPlugin(config); 165 | 166 | // then 167 | expect(bundle.touch).to.have.been.called; 168 | 169 | expect(config.files[0].pattern).to.eql(bundle.location); 170 | }); 171 | 172 | 173 | it('should insert bundle file right before first preprocessed file', function() { 174 | 175 | // given 176 | var config = createConfig({ 177 | files: [ 178 | { pattern: 'vendor/external.js' }, 179 | { pattern: 'foo/*Spec.js' } 180 | ], 181 | preprocessors: { 182 | 'foo/*Spec.js': [ 'browserify' ] 183 | } 184 | }); 185 | 186 | // when 187 | createPlugin(config); 188 | 189 | // expect bundle file to be inserted at pos=1 190 | expect(config.files).to.deep.eql([ 191 | { pattern : 'vendor/external.js' }, 192 | { pattern : bundle.location, served : true, included : true, watched : true }, 193 | { pattern : 'foo/*Spec.js' } 194 | ]); 195 | 196 | }); 197 | 198 | 199 | it('should insert bundle file before more-specific preprocessed pattern', function() { 200 | 201 | // given 202 | var config = createConfig({ 203 | files: [ 204 | { pattern: 'vendor/external.js' }, 205 | { pattern: 'foo/*Spec.js' } 206 | ], 207 | preprocessors: { 208 | 'foo/*.js': [ 'browserify' ] 209 | } 210 | }); 211 | 212 | // when 213 | createPlugin(config); 214 | 215 | // expect bundle file to be inserted at pos=1 216 | // since foo/*Spec.js matches the foo/*.js glob 217 | expect(config.files).to.deep.eql([ 218 | { pattern : 'vendor/external.js' }, 219 | { pattern : bundle.location, served : true, included : true, watched : true }, 220 | { pattern : 'foo/*Spec.js' } 221 | ]); 222 | 223 | }); 224 | 225 | 226 | it('should not insert bundle file before less-specific preprocessed pattern', function() { 227 | 228 | // given 229 | var config = createConfig({ 230 | files: [ 231 | { pattern: 'vendor/external.js' }, 232 | { pattern: 'foo/*.js' } 233 | ], 234 | preprocessors: { 235 | 'foo/*Spec.js': [ 'browserify' ] 236 | } 237 | }); 238 | 239 | // when 240 | createPlugin(config); 241 | 242 | // then 243 | expect(config.files[0].pattern).to.eql(bundle.location); 244 | 245 | }); 246 | 247 | }); 248 | 249 | 250 | describe('cleanup', function() { 251 | 252 | it('should remove bundle file on exit', function() { 253 | 254 | // given 255 | createPlugin(); 256 | 257 | // when 258 | emitter.emit('exit', function() { }); 259 | 260 | // then 261 | expect(bundle.remove).to.have.been.called; 262 | }); 263 | 264 | }); 265 | 266 | }); 267 | 268 | 269 | describe('preprocessing', function() { 270 | 271 | it('should create bundle', function(done) { 272 | 273 | // given 274 | var plugin = createPlugin(); 275 | 276 | var bundleFile = createFile(bundle.location); 277 | var testFileB = createFile('test/fixtures/b.js'); 278 | var testFileC = createFile('test/fixtures/c.js'); 279 | 280 | // when 281 | plugin.preprocess(bundleFile, [ testFileB, testFileC ], function() { 282 | 283 | // then 284 | // bundle got created 285 | expectBundleContainments(bundleFile, [ testFileB, testFileC ]); 286 | 287 | // test file stub got created 288 | expect(testFileB.bundled).to.eql(expectedBundle('test/fixtures/b.js')); 289 | expect(testFileC.bundled).to.eql(expectedBundle('test/fixtures/c.js')); 290 | 291 | done(); 292 | }); 293 | 294 | }); 295 | 296 | 297 | it('should pass through on updates', function(done) { 298 | 299 | // given 300 | var plugin = createPlugin(); 301 | 302 | var bundleFile = createFile(bundle.location); 303 | var testFile = createFile('test/fixtures/b.js'); 304 | 305 | // initial bundle creation 306 | plugin.preprocess(bundleFile, [ testFile ], function() { 307 | 308 | // when 309 | plugin.preprocess(bundleFile, [ testFile ], function() { 310 | 311 | // then 312 | 313 | // bundle got passed through 314 | expectBundleContainments(bundleFile, [ testFile ]); 315 | 316 | // test file got regenerated 317 | expect(testFile.bundled).to.eql(expectedBundle('test/fixtures/b.js')); 318 | 319 | done(); 320 | }); 321 | 322 | }); 323 | 324 | }); 325 | 326 | 327 | describe('automatic rebuild (autoWatch=true)', function() { 328 | 329 | // remember test files and restore them later 330 | // because they are going to be updated 331 | var aFile = new RestorableFile(__dirname + '/../fixtures/a.js'); 332 | var bFile = new RestorableFile(__dirname + '/../fixtures/b.js'); 333 | 334 | beforeEach(function() { 335 | aFile.load(); 336 | bFile.load(); 337 | }); 338 | 339 | afterEach(function() { 340 | aFile.restore(); 341 | bFile.restore(); 342 | }); 343 | 344 | 345 | it('should update on change', function(done) { 346 | 347 | // given 348 | var plugin = createPlugin({ autoWatch: true }); 349 | 350 | var bundleFile = createFile(bundle.location); 351 | var testFile = createFile('test/fixtures/b.js'); 352 | 353 | // initial bundle creation 354 | plugin.preprocess(bundleFile, [ testFile ], function() { 355 | 356 | // reset spy on bundle 357 | bundle.update.resetHistory(); 358 | 359 | // when 360 | // update bundle file 361 | delay(function() { 362 | aFile.update('module.exports = "UPDATED";'); 363 | }); 364 | 365 | // give watch a chance to trigger 366 | delay(function() { 367 | 368 | // then 369 | expect(bundle.update).to.have.been.called; 370 | 371 | done(); 372 | }, BUNDLE_UPDATE_CHECK_DELAY); 373 | 374 | }); 375 | 376 | }); 377 | 378 | 379 | it('should handle bundle error', function(done) { 380 | 381 | // given 382 | var plugin = createPlugin(); 383 | 384 | var bundleFile = createFile(bundle.location); 385 | var testFile = createFile('test/fixtures/error.js'); 386 | 387 | // when 388 | plugin.preprocess(bundleFile, [ testFile ], function() { 389 | 390 | // then 391 | // bundle reports error 392 | expect(bundleFile.bundled).to.eql('throw new Error("bundle error (see logs)");'); 393 | 394 | // test file stub got created anyway 395 | expect(testFile.bundled).to.eql(expectedBundle('test/fixtures/error.js')); 396 | 397 | done(); 398 | }); 399 | }); 400 | 401 | 402 | it('should handle bundle update error', function(done) { 403 | 404 | // given 405 | var plugin = createPlugin({ autoWatch: true }); 406 | 407 | var bundleFile = createFile(bundle.location); 408 | var testFile = createFile('test/fixtures/b.js'); 409 | 410 | // initial bundle creation 411 | plugin.preprocess(bundleFile, [ testFile ], function() { 412 | 413 | // reset spy on bundle 414 | bundle.update.resetHistory(); 415 | 416 | // when 417 | // update bundle file 418 | delay(function() { 419 | aFile.update('unpoarsable / {{ code'); 420 | }); 421 | 422 | // give watch a chance to trigger 423 | delay(function() { 424 | 425 | // then 426 | // no update on parse error 427 | expect(bundle.update).to.have.been.calledWith('throw new Error("bundle error (see logs)");'); 428 | 429 | done(); 430 | }, BUNDLE_UPDATE_CHECK_DELAY); 431 | 432 | }); 433 | 434 | }); 435 | 436 | 437 | it('should handle file remove', function(done) { 438 | 439 | // given 440 | var plugin = createPlugin({ autoWatch: true }); 441 | 442 | var bundleFile = createFile(bundle.location); 443 | var testFile = createFile('test/fixtures/b.js'); 444 | 445 | // initial bundle creation 446 | plugin.preprocess(bundleFile, [ testFile ], function() { 447 | 448 | // reset spy on bundle 449 | bundle.update.resetHistory(); 450 | 451 | // when 452 | // remove file 453 | delay(function() { 454 | bFile.remove(); 455 | }); 456 | 457 | // update a bundled file 458 | delay(function() { 459 | aFile.update('module.exports = "UPDATED";'); 460 | }, 500); 461 | 462 | // give watch a chance to trigger 463 | delay(function() { 464 | 465 | // then 466 | // update with file deleted 467 | expect(bundle.update).to.have.been.called; 468 | 469 | expect(bundleFile.realContents()).not.to.contain('/b.js'); 470 | done(); 471 | }, BUNDLE_UPDATE_CHECK_DELAY); 472 | 473 | }); 474 | 475 | }); 476 | 477 | }); 478 | 479 | }); 480 | 481 | 482 | describe('browserify', function() { 483 | 484 | it('should configure externalRequireName', function(done) { 485 | 486 | // given 487 | var plugin = createPlugin({ 488 | browserify: { 489 | externalRequireName: 'app_require' 490 | } 491 | }); 492 | 493 | var bundleFile = createFile(bundle.location); 494 | var testFile = createFile('test/fixtures/a.js'); 495 | 496 | // when 497 | plugin.preprocess(bundleFile, [ testFile ], function() { 498 | 499 | // then 500 | 501 | // bundle got created 502 | expect(bundleFile.bundled).to.contain('app_require='); 503 | expect(testFile.bundled).to.eql(expectedBundle('test/fixtures/a.js', 'app_require')); 504 | 505 | done(); 506 | }); 507 | }); 508 | 509 | 510 | it('should configure transform', function(done) { 511 | 512 | // given 513 | var plugin = createPlugin({ 514 | browserify: { 515 | transform: [ 'brfs' ] 516 | } 517 | }); 518 | 519 | var bundleFile = createFile(bundle.location); 520 | var testFile = createFile('test/fixtures/transform.js'); 521 | 522 | // when 523 | plugin.preprocess(bundleFile, [ testFile ], function() { 524 | 525 | // then 526 | 527 | // bundle got created 528 | expect(bundleFile.bundled).to.contain('module.exports.text = \'<\' + "HALLO" + \'>\''); 529 | 530 | done(); 531 | }); 532 | }); 533 | 534 | 535 | it('should configure transform with options', function(done) { 536 | 537 | // given 538 | var plugin = createPlugin({ 539 | browserify: { 540 | transform: [ ['brfs', { foo: 'bar' }] ] 541 | } 542 | }); 543 | 544 | var bundleFile = createFile(bundle.location); 545 | var testFile = createFile('test/fixtures/transform.js'); 546 | 547 | // when 548 | plugin.preprocess(bundleFile, [ testFile ], function() { 549 | 550 | // then 551 | 552 | // bundle got created 553 | expect(bundleFile.bundled).to.contain('module.exports.text = \'<\' + "HALLO" + \'>\''); 554 | 555 | done(); 556 | }); 557 | }); 558 | 559 | 560 | it('should support configure hook in the options', function(done) { 561 | 562 | // given 563 | var plugin = createPlugin({ 564 | browserify: { 565 | configure: function(bundle) { 566 | bundle.external('foobar'); 567 | } 568 | } 569 | }); 570 | 571 | var bundleFile = createFile(bundle.location); 572 | var testFile = createFile('test/fixtures/configure.js'); 573 | 574 | // when 575 | plugin.preprocess(bundleFile, [ testFile ], function() { 576 | 577 | // then 578 | // bundle got created 579 | expect(bundleFile.bundled).to.exist; 580 | expect(bundleFile.bundled).to.contain('require(\'foobar\')'); 581 | 582 | done(); 583 | }); 584 | }); 585 | 586 | 587 | it('should configure debug with source map support', function(done) { 588 | 589 | // given 590 | var plugin = createPlugin({ 591 | browserify: { 592 | debug: true 593 | } 594 | }); 595 | 596 | var bundleFile = createFile(bundle.location); 597 | var testFile = createFile('test/fixtures/a.js'); 598 | 599 | // when 600 | plugin.preprocess(bundleFile, [ testFile ], function() { 601 | 602 | // then 603 | 604 | // contains source map 605 | expect(bundleFile.bundled).to.contain('//# sourceMappingURL'); 606 | 607 | // and has the parsed mapping attached 608 | expect(bundleFile.sourceMap).to.exist; 609 | 610 | done(); 611 | }); 612 | }); 613 | 614 | 615 | it('should configure plugin with options', function(done) { 616 | 617 | // given 618 | var plugin = createPlugin({ 619 | browserify: { 620 | plugin: [ ['tsify', { removeComments: true } ] ] 621 | } 622 | }); 623 | 624 | var bundleFile = createFile(bundle.location); 625 | var testFile = createFile('test/fixtures/plugin.ts'); 626 | 627 | // when 628 | plugin.preprocess(bundleFile, [ testFile ], function() { 629 | 630 | // then 631 | 632 | // bundle file got processed via plug-in 633 | expect(bundleFile.bundled).to.contain('module.exports = plugin'); 634 | expect(bundleFile.bundled).not.to.contain('// typescript'); 635 | 636 | done(); 637 | }); 638 | }); 639 | 640 | 641 | it('should persist transforms', function(done) { 642 | 643 | // given 644 | var bundleFile = createFile(bundle.location); 645 | var testFile = createFile('test/fixtures/transform.js'); 646 | 647 | var plugin = createPlugin({ 648 | browserify: { 649 | transform: [ 'brfs' ], 650 | // Hook into bundler/pipeline events for success/error 651 | configure: function(bundle) { 652 | 653 | // after first bundle 654 | bundle.once('bundled', function(err) { 655 | 656 | // fail if there was an error 657 | if (err) { 658 | return done(err); 659 | } 660 | 661 | // set up error/success handlers 662 | bundle.on('bundled', function(err, contents) { 663 | if (err) { 664 | return done(err); 665 | } 666 | 667 | expect(bundleFile.bundled).to.contain('module.exports.text = \'<\' + "HALLO" + \'>\''); 668 | done(); 669 | }); 670 | 671 | // rebundle 672 | plugin.preprocess(bundleFile, [ testFile ], function() {}); 673 | }); 674 | } 675 | } 676 | }); 677 | 678 | // initial bundle 679 | plugin.preprocess(bundleFile, [ testFile ], function() {}); 680 | }); 681 | 682 | 683 | it('should persist plugins', function(done) { 684 | 685 | var bundleFile = createFile(bundle.location); 686 | var testFile = createFile('test/fixtures/plugin.ts'); 687 | var plugin = createPlugin({ 688 | browserify: { 689 | plugin: [ ['tsify', { removeComments: true } ] ], 690 | // Hook into bundler/pipeline events for success/error 691 | configure: function(bundle) { 692 | // After first bundle 693 | bundle.once('bundled', function(err) { 694 | // Fail if there was an error 695 | if (err) { 696 | return done(err); 697 | } 698 | 699 | // Set up error/success handlers 700 | bundle.once('bundled', done); 701 | 702 | // Rebundle 703 | plugin.preprocess(bundleFile, [ testFile ], function() {}); 704 | }); 705 | } 706 | } 707 | }); 708 | 709 | // Initial bundle 710 | plugin.preprocess(bundleFile, [ testFile ], function() {}); 711 | }); 712 | 713 | }); 714 | 715 | }); 716 | --------------------------------------------------------------------------------