├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── kitchen │ ├── .eslintrc │ ├── app │ ├── kitchen │ │ ├── createKitchen.js │ │ └── data.json │ ├── logger.js │ └── stove │ │ ├── turnOn.js │ │ └── undo.js │ ├── index.js │ └── package.json ├── index.js ├── lib ├── autoMock.js ├── createDepsProxy.js ├── getAppModules.js ├── getDependencyModules.js ├── getModuleKey.js └── getNativeModules.js ├── package.json └── spec ├── fixtures ├── animation.swf ├── broken │ └── package.json ├── fakeBreadboardEntryModule.js ├── fakeBreadboardJsonFile.json ├── fakeBreadboardModule.js ├── fakeConfig.json ├── fakeModule.js ├── fakeNativeModule.js ├── package.json └── packageJsonWithoutDependencies │ └── package.json ├── index.spec.js └── lib ├── createDepsProxy.spec.js ├── getAppModules.spec.js ├── getDependecyModules.spec.js ├── getModuleKey.spec.js └── getNativeModule.spec.js /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | - javascript 9 | - python 10 | - php 11 | eslint: 12 | enabled: true 13 | fixme: 14 | enabled: true 15 | ratings: 16 | paths: 17 | - "**.inc" 18 | - "**.js" 19 | - "**.jsx" 20 | - "**.module" 21 | - "**.php" 22 | - "**.py" 23 | - "**.rb" 24 | exclude_paths: 25 | - spec/ 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | indent_style = space 5 | trim_trailing_whitespace = true 6 | 7 | [*.js] 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "node": true, 6 | "es6": true 7 | }, 8 | "parserOptions": { 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "quotes": [2, "single"], 13 | "eqeqeq": 2, 14 | "curly": 2, 15 | "no-multiple-empty-lines": [2, {"max": 1}], 16 | "no-console": 2, 17 | "camelcase": 2, 18 | "consistent-this": [2, "self"], 19 | "brace-style": [2, "stroustrup"], 20 | "eol-last": 2, 21 | "comma-spacing": 2, 22 | "space-before-function-paren": [2, "never"], 23 | "indent": [2, 2], 24 | "no-shadow": 2, 25 | "space-before-blocks": 2, 26 | "new-cap": 0, 27 | "semi": [2, "always"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | npm-debug.log 5 | .idea 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: npm run lint ; npm run test 3 | after_success: 4 | - npm run codeclimate 5 | deploy: 6 | provider: npm 7 | email: tech.team@notonthehighstreet.com 8 | api_key: 9 | secure: k6wmHDg3OVI1z8zEAbNKaWGyREODuvMzWAKz/XQxUacci1Jdvj6sZVWkSpJouczodxVwa5qRUk3s5QeHJm7NMR4F+kym6KTOPpNOzarse13Oorkda7viEhlB5jKI2dYxVsBB0ccTyb2Bsw6VrnwuEVLVoi5sZ58xEhEwfikUbObwGZvF78Apsu6V4ssZyEQZyvfZR3p5bGWHY9n8V48/s8OK93tXj7CCa+dsITzxnPGHu/MYnCud4iJdoxDhQQkxY4pys9Wh4TofovANx8jq73kQNvPHwpWqOvIiKv+OjV3mZDsaedxJZOAmNu3w3vyMygiHNyidPMVCLpYYZC/SWwaRFoJGJ2ouNszTnFIlWyKqd3o2dW7xm1Ta7ScDc4+jg30d/Du3HqiiZdij1Uf9gOxKCWu9JXr6gURMDDVmzSOujhA9C0ws9JPW1UAxH9n9SUGEivV39ZsYvST7wHNogEocZfubNxjKW+D/2+CSvnQuKYM+1PNgzduHlNH9NHb3wcr8e+iIbGKR71LBc+1cpkLNn5XXA6jhjemoloUB3MoK4YojLZjX6seDVgo8ezVSEc2W4enWONb1xXYtSIrl5N0rWOGrVR/4d6V25/DeVUWNSQiG1utRP26J4A4lbQsMGE3xOTCVaJlsWrqDRI0MQidxVC0ob3NWFS6Yudi/2AM= 10 | on: 11 | tags: true 12 | repo: notonthehighstreet/breadboard 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 notonthehighstreet 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](http://i.imgur.com/3lFs20I.png) 2 | 3 | # Breadboard 4 | [![Code Climate](https://codeclimate.com/github/notonthehighstreet/breadboard/badges/gpa.svg)](https://codeclimate.com/github/notonthehighstreet/breadboard) 5 | [![Test Coverage](https://codeclimate.com/github/notonthehighstreet/breadboard/badges/coverage.svg)](https://codeclimate.com/github/notonthehighstreet/breadboard/coverage) 6 | [![Build Status](https://travis-ci.org/notonthehighstreet/breadboard.svg?branch=master)](https://travis-ci.org/notonthehighstreet/breadboard) 7 | 8 | Breadboard is an opinionated inversion of control container for Node.js applications. 9 | 10 | ## Motivation 11 | * Working with `require` is less than ideal. 12 | * The same module will have a different key depending on the path of the requiring module. 13 | * Testing a module in isolation, ie. mocking its dependencies, requires hacky solutions that hijack `require` calls. This approach is indeterministic, depending on various seemingly unrelated conditions around how the the module you want to mock was defined. 14 | * Discouraging managing state of the app through side effects when `require`ing. 15 | * Single function call to auto-mock a module's dependencies in your tests. 16 | 17 | Breadboard will lazily require all your application's dependencies defined in `package.json`, all Node native modules and all of your application's modules and store them in a `dependencies` object. The object is exposed to your application's modules by calling the modules as functions, passing the dependencies as an argument. As such, your modules are expected to be wrapped in an extra function returning the desired export value, which Breadboard then calls on application start. 18 | 19 | ## Install 20 | ``` 21 | npm install breadboard 22 | ``` 23 | 24 | ## Example of a module in your application 25 | 26 | Consider this CommonJS module: 27 | ```js 28 | //startServer.js 29 | 30 | const d = require('debug')('myApp'); 31 | const createServer = require('./lib/createServer'); 32 | 33 | module.exports = () => { 34 | const server = createServer(); 35 | 36 | server.listen(80, () => { 37 | d('Server listening on port 80'); 38 | }); 39 | 40 | return server; 41 | }; 42 | ``` 43 | 44 | The Breadboard equivalent would be: 45 | ```js 46 | //startServer.js 47 | 48 | // wrap module in factory function 49 | module.exports = ({ // destructure dependencies to get the modules needed 50 | debug, 51 | '/lib/createServer': createServer 52 | }) => { 53 | // return the core functionality of the module as a function 54 | return () => { 55 | const server = createServer(); 56 | const d = debug('myApp'); 57 | 58 | server.listen(80, () => { 59 | d('Server listening on port 80'); 60 | }); 61 | 62 | return server; 63 | }; 64 | }; 65 | ``` 66 | 67 | To start your application: 68 | ```js 69 | 70 | const breadboard = require('breadboard'); 71 | 72 | breadboard({ 73 | entry: '/index', 74 | containerRoot: 'app', 75 | initialState: { 76 | arbitrary: 'state data' 77 | }, 78 | blacklist: ['newrelic'] 79 | }).then(({deps, entryResolveValue}) => { 80 | console.log('Application started', deps, entryResolveValue); 81 | }); 82 | ``` 83 | 84 | ## Further examples 85 | 86 | Take a look at `examples/kitchen`. To run, `npm install` then `npm start`. 87 | 88 | ## Module keys 89 | Module keys in a Breadboard app are static, ie. are always relative to the container's root folder, starting with `/`, and always using `/` as path separators, no matter the platform. Consider these example module keys: 90 | ``` 91 | /lib/getUser 92 | /middleware/getUser 93 | /logger 94 | ``` 95 | Keys for native Node.js modules and 3rd party modules remain the same as if you were using `require`. 96 | Breadboard also loads all JSON files. To access them, append `.json` to the end of the key, eg. `/data/userPasswords.json`. 97 | 98 | ## API 99 | `breadboard(options)` 100 | 101 | Returns a promise chained to the return value of your application's entry point, which might be another promise or a concrete value. 102 | ### `options` 103 | #### `options.entry` (String | Function<Object>) 104 | ##### String 105 | Module key for the entry point of the application. 106 | ##### Function<Object> 107 | Will be called as the entry point module, with resolved dependencies as the argument. 108 | #### `options.containerRoot` (String) 109 | Path relative to the current working directory, from which all module keys will be resolved 110 | #### `options.initialState` (Object) 111 | The argument the `entry` function will be called with. 112 | #### `options.blacklist` (Array<String>) 113 | List of modules from your `package.json` which you wish Breadboard not to load. If you want to defer a `require` call to a 3rd party module, put it in the `blacklist` and `require` manually in your code. 114 | #### `options.substitutes` (Object) 115 | A Breadboard module-key to module mapping to indicate which modules you want to substitute across the whole application with your custom implementation. Useful when testing integration of multiple modules. You could substitute eg. a database connector with a stub to remove a running database as a dependency of your tests. 116 | 117 | ## Testing 118 | In tests require your Breadboard modules as if they were CommonJS modules. You can then supply your own stubs and spies as test doubles. Consider the following example: 119 | ### Test subject module `/index` 120 | ```js 121 | module.exports = (deps) => { 122 | return () => { 123 | const { 124 | '/widgets/createDough': createDough, 125 | '/pasta/createPapardelle': createPapardelle, 126 | 'debug': debug 127 | } = deps; 128 | const d = debug('pasta'); 129 | 130 | d(createPapardelle(createDough())); 131 | }; 132 | }; 133 | ``` 134 | ### Test for `/index` 135 | ```js 136 | import { expect } from 'chai'; 137 | import mainFactory from '../../app/index'; 138 | import sinon from 'sinon'; 139 | 140 | describe('Main', () => { 141 | const sandbox = sinon.sandbox.create(); 142 | const debugSpy = sandbox.spy(); 143 | const mockDough = 'dough'; 144 | const mockDependencies = { 145 | debug: sandbox.stub().returns(debugSpy), 146 | '/widgets/createDough': sandbox.stub().returns(mockDough), 147 | '/pasta/createPapardelle': sandbox.spy() 148 | }; 149 | let main; 150 | 151 | beforeEach(() => { 152 | main = mainFactory(mockDependencies); 153 | }); 154 | afterEach(() => { 155 | sandbox.reset(); 156 | }); 157 | it('calls debug', () => { 158 | main(); 159 | expect(mockDependencies.debug.calledOnce).to.be.true; 160 | }); 161 | it('calls createDough', () => { 162 | main(); 163 | expect(mockDependencies['/widgets/createDough'].calledOnce).to.be.true; 164 | }); 165 | it('calls createPapardelle with createDough return value', () => { 166 | main(); 167 | expect(mockDependencies['/pasta/createPapardelle'].calledWith(mockDough)).to.be.true; 168 | }); 169 | }); 170 | ``` 171 | 172 | ### `autoMock` 173 | `autoMock` automatically replaces every dependency of a given Breadboard module with a [Sinon.JS stub](http://sinonjs.org/docs/#stubs). 174 | 175 | ### `autoMock` API 176 | 177 | `autoMock(factory, options)` 178 | #### `factory` (Function) 179 | The Breadboard factory function to build the subject to test. 180 | #### `options` (Object) 181 | ##### `options.mocks` (Object) 182 | A Breadboard module-key to module mapping to indicate which modules you want to mock manually. 183 | #### returns Object<subject, deps, sandbox> 184 | ##### `subject` 185 | The module returned by `factory`. 186 | ##### `deps` 187 | The mock dependencies injected into `subject`. 188 | ##### `sandbox` 189 | Instance of a [Sinon.JS sandbox](http://sinonjs.org/docs/#sandbox). 190 | 191 | ### `autoMock` example 192 | #### Test subject module `/index` 193 | ```js 194 | module.exports = (deps) => { 195 | return function main() { 196 | const { 197 | '/widgets/createDough': createDough, 198 | '/pasta/createPapardelle': createPapardelle, 199 | 'debug': debug 200 | } = deps; 201 | const d = debug('pasta'); 202 | 203 | d(createPapardelle(createDough())); 204 | }; 205 | }; 206 | ``` 207 | #### Test for module `/index` 208 | ```js 209 | import { expect } from 'chai'; 210 | import mainFactory from '../../app/index'; 211 | import autoMock from 'breadboard/lib/autoMock'; 212 | 213 | const mockDough = 'dough'; 214 | const {subject: main, sandbox, deps} = autoMock(mainFactory); 215 | const debugSpy = sandbox.spy(); 216 | 217 | deps.debug.returns(debugSpy); 218 | deps['/widgets/createDough'].returns(mockDough); 219 | describe('Main', () => { 220 | afterEach(() => { 221 | sandbox.reset(); 222 | }); 223 | it('calls debug', () => { 224 | main(); 225 | expect(deps.debug.calledOnce).to.be.true; 226 | }); 227 | it('calls createDough', () => { 228 | main(); 229 | expect(deps['/widgets/createDough'].calledOnce).to.be.true; 230 | }); 231 | it('calls createPapardelle with createDough return value', () => { 232 | main(); 233 | expect(deps['/pasta/createPapardelle'].calledWith(mockDough)).to.be.true; 234 | }); 235 | }); 236 | ``` 237 | -------------------------------------------------------------------------------- /examples/kitchen/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["react"], 3 | "rules": { 4 | "react/jsx-uses-react": 2 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/kitchen/app/kitchen/createKitchen.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ 2 | debug, 3 | fs, 4 | '/stove/turnOn': turnOn, 5 | '/kitchen/data.json': data, 6 | '/logger': logger, 7 | 'react-dom/server': ReactDOMServer, 8 | 'react': React 9 | }) => { 10 | return goodWhat => { 11 | setTimeout(() => { 12 | const d = debug('kitchen'); 13 | 14 | fs.readFile('../../package.json', (err, packageJsonContents) => { 15 | d(JSON.parse(packageJsonContents.toString()).dependencies); 16 | d(ReactDOMServer.renderToString({data.name})); 17 | logger('Have a good %s!', goodWhat); 18 | turnOn(data.name); 19 | }); 20 | }, 200); 21 | 22 | return Promise.resolve('DINNER TIME'); 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /examples/kitchen/app/kitchen/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Simek" 3 | } 4 | -------------------------------------------------------------------------------- /examples/kitchen/app/logger.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0*/ 2 | module.exports = () => { 3 | return (...args) => { 4 | console.log.apply(console, args); 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /examples/kitchen/app/stove/turnOn.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ 2 | '/logger': logger, 3 | '/stove/undo': undoStove 4 | }) => { 5 | return name => { 6 | logger(`Turning the stove on, ${name}`); 7 | undoStove(name); 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /examples/kitchen/app/stove/undo.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ 2 | '/logger': logger 3 | }) => name => logger(`Undoing the stove, ${name}`); 4 | -------------------------------------------------------------------------------- /examples/kitchen/index.js: -------------------------------------------------------------------------------- 1 | import breadboard from 'breadboard'; 2 | 3 | breadboard({ 4 | entry: ({'/kitchen/createKitchen': createKitchen}) => { 5 | return createKitchen('day'); 6 | }, 7 | containerRoot: 'app' 8 | }) 9 | .then(({deps, entryResolveValue}) => { 10 | const logger = deps['/logger']; 11 | 12 | logger(entryResolveValue); 13 | }) 14 | .catch((e) => { 15 | process.stderr.write(e.stack); 16 | process.exit(1); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/kitchen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kitchen", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "babel-node index.js", 9 | "start-debug": "env DEBUG=\"breadboard:*,kitchen\" babel-node.js index.js" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "breadboard": "file:../../", 15 | "debug": "2.2.0", 16 | "react": "^15.1.0", 17 | "react-dom": "^15.1.0" 18 | }, 19 | "devDependencies": { 20 | "babel-cli": "6.7.7", 21 | "babel-preset-es2015": "6.6.0", 22 | "babel-preset-react": "^6.5.0" 23 | }, 24 | "babel": { 25 | "presets": [ 26 | "es2015", 27 | "react" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug'); 4 | const join = require('path').join; 5 | const getNativeModules = require('./lib/getNativeModules'); 6 | const getAppModules = require('./lib/getAppModules'); 7 | const getDependencyModules = require('./lib/getDependencyModules'); 8 | const createDepsProxy = require('./lib/createDepsProxy'); 9 | const isFunction = require('lodash/isFunction'); 10 | const d = debug('breadboard:setup'); 11 | const e = debug('breadboard:error'); 12 | 13 | module.exports = (options) => { 14 | const containerRoot = options.containerRoot; 15 | const blacklist = options.blacklist || []; 16 | const substitutes = options.substitutes || {}; 17 | const substituteKeys = Object.keys(substitutes); 18 | const entry = options.entry; 19 | const initialState = options.initialState; 20 | 21 | if (!entry) { 22 | throw new Error('Expected application entry point to be specified'); 23 | } 24 | d('Starting bootstrap'); 25 | 26 | return Promise 27 | .all([ 28 | getNativeModules(substituteKeys), 29 | getDependencyModules(join(__dirname, '..', '..'), blacklist, substituteKeys), 30 | getAppModules(join(__dirname, '..', '..', containerRoot), substituteKeys) 31 | ]) 32 | .then((moduleGroups) => { 33 | const depsProxy = createDepsProxy(moduleGroups, { 34 | substitutes: substitutes 35 | }); 36 | let entryPointReturn; 37 | 38 | if (isFunction(entry)) { 39 | entryPointReturn = entry(depsProxy); 40 | } 41 | else { 42 | entryPointReturn = depsProxy[entry](initialState); 43 | } 44 | 45 | return Promise 46 | .resolve(entryPointReturn) 47 | .then((entryPointResolveValue) => { 48 | return { 49 | deps: depsProxy, 50 | entryResolveValue: entryPointResolveValue 51 | }; 52 | }); 53 | }) 54 | .catch((err) => { 55 | e(err); 56 | 57 | return Promise.reject(err); 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /lib/autoMock.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon'); 2 | 3 | module.exports = (breadboardModule, options) => { 4 | const sandbox = sinon.sandbox.create(); 5 | const mocks = options && options.mocks || {}; 6 | const deps = Object.assign({}, mocks); 7 | const depsProxy = new Proxy(deps, { 8 | get(target, name) { 9 | if (!(name in target)) { 10 | deps[name] = sandbox.stub(); 11 | } 12 | return deps[name]; 13 | } 14 | }); 15 | 16 | return { 17 | subject: breadboardModule(depsProxy), 18 | deps: depsProxy, 19 | sandbox: sandbox 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/createDepsProxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isFunction = require('lodash/isFunction'); 4 | const debug = require('debug'); 5 | const d = debug('breadboard:setup:injector'); 6 | const moduleGroupContainsKey = 7 | (moduleGroup, key) => Object.keys(moduleGroup).some(moduleKey => moduleKey === key); 8 | 9 | module.exports = (moduleGroups, options) => { 10 | const nativeModules = moduleGroups[0]; 11 | const dependencyModules = moduleGroups[1]; 12 | const appModules = moduleGroups[2]; 13 | let mergedDepPaths; 14 | let finalDeps; 15 | 16 | options = options || {}; 17 | mergedDepPaths = Object.assign( 18 | {}, 19 | appModules, 20 | nativeModules, 21 | dependencyModules, 22 | options.substitutes 23 | ); 24 | finalDeps = new Proxy(mergedDepPaths, { 25 | get(target, moduleKey) { 26 | let resolvedModule; 27 | 28 | d(`Calling getter for ${moduleKey}`); 29 | if (options.substitutes && moduleGroupContainsKey(options.substitutes, moduleKey)) { 30 | resolvedModule = options.substitutes[moduleKey]; 31 | } 32 | else if (moduleGroupContainsKey(appModules, moduleKey)) { 33 | let breadboardModule = require(target[moduleKey]); 34 | 35 | if (isFunction(breadboardModule)) { 36 | resolvedModule = breadboardModule(finalDeps); 37 | } 38 | else { 39 | resolvedModule = breadboardModule; 40 | } 41 | } 42 | else if (moduleKey[0] === '/') { 43 | throw new Error(`Cannot resolve app module ${moduleKey}`); 44 | } 45 | else { 46 | resolvedModule = require(moduleKey); 47 | } 48 | 49 | return resolvedModule; 50 | }, 51 | set() { 52 | throw new Error('Runtime changes to dependencies not supported'); 53 | } 54 | }); 55 | 56 | return finalDeps; 57 | }; 58 | -------------------------------------------------------------------------------- /lib/getAppModules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug'); 4 | const walk = require('walk').walk; 5 | const parse = require('path').parse; 6 | const join = require('path').join; 7 | const relative = require('path').relative; 8 | const getModuleKey = require('./getModuleKey'); 9 | const d = debug('breadboard:setup:appModules'); 10 | 11 | module.exports = (containerRoot, substitutes) => { 12 | if (!containerRoot) { 13 | return Promise.reject(new Error('Expected container root to be specified')); 14 | } 15 | const walker = walk(containerRoot); 16 | let appModules = {}; 17 | 18 | substitutes = substitutes || []; 19 | walker.on('file', (dir, fileStat, next) => { 20 | const parsedModulePath = parse(join(dir, fileStat.name)); 21 | const relativePath = relative(containerRoot, dir); 22 | const moduleKey = getModuleKey(relativePath.replace(/\\/g, '/'), parsedModulePath); 23 | 24 | if (substitutes.every(substitute => substitute !== moduleKey)) { 25 | appModules[moduleKey] = join(dir, fileStat.name); 26 | } 27 | next(); 28 | }); 29 | 30 | return new Promise((resolve, reject) => { 31 | walker.on('errors', (root, stats) => { 32 | reject(stats.map(stat => stat.error)); 33 | }); 34 | walker.on('end', () => { 35 | d('App modules', Object.keys(appModules)); 36 | resolve(appModules); 37 | }); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /lib/getDependencyModules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug'); 4 | const stat = require('fs').statSync; 5 | const resolvePath = require('path').resolve; 6 | const includes = require('lodash/includes'); 7 | const d = debug('breadboard:setup:dependencyModules'); 8 | 9 | module.exports = (packageDir, blacklist, substitutes) => { 10 | const absolutePackageJsonPath = resolvePath(process.cwd(), `${packageDir}/package.json`); 11 | 12 | return new Promise((resolve, reject) => { 13 | let dependencyModules = {}; 14 | let packageJsonModuleNames; 15 | 16 | try { 17 | stat(absolutePackageJsonPath); 18 | packageJsonModuleNames = Object.keys(require(absolutePackageJsonPath).dependencies); 19 | } 20 | catch (e) { 21 | d('Error getting module dependencies: ', e); 22 | return reject(e); 23 | } 24 | 25 | packageJsonModuleNames = packageJsonModuleNames 26 | .filter((moduleName) => { 27 | return !includes(blacklist, moduleName); 28 | }) 29 | .filter((moduleName) => { 30 | return !includes(substitutes, moduleName); 31 | }); 32 | dependencyModules = packageJsonModuleNames.reduce((modules, moduleName) => { 33 | modules[moduleName] = moduleName; 34 | 35 | return modules; 36 | }, dependencyModules); 37 | 38 | d('package.json modules', Object.keys(dependencyModules)); 39 | 40 | return resolve(dependencyModules); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /lib/getModuleKey.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getModuleFilename = (parsedFileName) => { 4 | return parsedFileName[parsedFileName.ext === '.js' ? 'name' : 'base']; 5 | }; 6 | 7 | const buildModulePath = (relativeModulePath) => { 8 | return '/' + (relativeModulePath ? relativeModulePath + '/' : ''); 9 | }; 10 | 11 | module.exports = (relativeModulePath, parsedModulePath) => { 12 | return buildModulePath(relativeModulePath) + getModuleFilename(parsedModulePath); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/getNativeModules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug'); 4 | const d = debug('breadboard:setup:nativeModules'); 5 | const loadSanitisedNativeModules = (modules, nativeModuleName) => { 6 | const excludedNativesRegExp = /^internal\/|_|v8\//; 7 | 8 | if (!excludedNativesRegExp.test(nativeModuleName)) { 9 | modules[nativeModuleName] = nativeModuleName; 10 | } 11 | 12 | return modules; 13 | }; 14 | const excludeSubstitutes = (modules, substitutes) => { 15 | return modules.filter(module => !substitutes.some(substitute => substitute === module)); 16 | }; 17 | 18 | module.exports = (substitutes) => { 19 | const natives = excludeSubstitutes(Object.keys(process.binding('natives')), substitutes); 20 | 21 | return new Promise((resolve) => { 22 | const nativeModules = natives.reduce(loadSanitisedNativeModules, {}); 23 | 24 | d('Native modules', Object.keys(nativeModules)); 25 | 26 | return resolve(nativeModules); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "breadboard", 3 | "version": "9.0.1", 4 | "description": "Lightweight IOC container for Node.js", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=6.2" 8 | }, 9 | "repository": "notonthehighstreet/breadboard", 10 | "scripts": { 11 | "test": "./node_modules/ava/cli.js --serial", 12 | "coverage": "./node_modules/.bin/nyc ./node_modules/ava/cli.js --serial --all", 13 | "report": "npm run coverage && ./node_modules/.bin/nyc report --reporter=html", 14 | "codeclimate": "npm run coverage -- --reporter=lcov --reporter=text-lcov | ./node_modules/codeclimate-test-reporter/bin/codeclimate.js", 15 | "lint": "./node_modules/eslint/bin/eslint.js ." 16 | }, 17 | "pre-commit": [ 18 | "lint", 19 | "test" 20 | ], 21 | "author": "notonthehighstreet.com ", 22 | "license": "MIT", 23 | "dependencies": { 24 | "debug": "^2.6.0", 25 | "lodash": "^4.13.1", 26 | "walk": "2.3.9" 27 | }, 28 | "ava": { 29 | "files": [ 30 | "**/*.spec.js" 31 | ] 32 | }, 33 | "nyc": { 34 | "include": [ 35 | "lib/**/*.js", 36 | "index.js" 37 | ], 38 | "exclude": [ 39 | "lib/autoMock.js" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "ava": "^0.15.2", 44 | "babel-eslint": "^7.1.1", 45 | "chance": "^1.0.3", 46 | "codeclimate-test-reporter": "^0.3.3", 47 | "eslint": "^3.13.1", 48 | "eslint-plugin-react": "^6.9.0", 49 | "mock-require": "^1.3.0", 50 | "nyc": "^6.6.1", 51 | "pre-commit": "^1.1.3", 52 | "sinon": "^1.17.7" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /spec/fixtures/animation.swf: -------------------------------------------------------------------------------- 1 | BADGER BADGER BADGER BADGER 2 | -------------------------------------------------------------------------------- /spec/fixtures/broken/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "./broken/path/to/module": "1.0.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/fakeBreadboardEntryModule.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return () => {}; 3 | }; 4 | -------------------------------------------------------------------------------- /spec/fixtures/fakeBreadboardJsonFile.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/fakeBreadboardModule.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return (stuff) => { 3 | return stuff; 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /spec/fixtures/fakeConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "world" 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/fakeModule.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return 'foo'; 3 | }; 4 | -------------------------------------------------------------------------------- /spec/fixtures/fakeNativeModule.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return 'foo'; 3 | }; 4 | -------------------------------------------------------------------------------- /spec/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "fakeDependency": "1.0.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/packageJsonWithoutDependencies/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /spec/index.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | import mock from 'mock-require'; 4 | import Chance from 'chance'; 5 | 6 | const chance = Chance(); 7 | const sandbox = sinon.sandbox.create(); 8 | const getNativeModulesMock = sandbox.stub(); 9 | const getDependencyModulesMock = sandbox.stub(); 10 | const getAppModulesMock = sandbox.stub(); 11 | const createDepsProxyMock = sandbox.stub(); 12 | const fakeEntryModule = sandbox.stub(); 13 | let fakeContainerRoot; 14 | let fakeEntryModuleKey; 15 | let fakeDeps; 16 | let subject; 17 | 18 | test.after(() => { 19 | sandbox.restore(); 20 | }); 21 | test.beforeEach(() => { 22 | fakeEntryModuleKey = chance.word(); 23 | fakeContainerRoot = chance.word(); 24 | fakeDeps = { 25 | [fakeEntryModuleKey]: fakeEntryModule 26 | }; 27 | getNativeModulesMock.returns(Promise.resolve()); 28 | getDependencyModulesMock.returns(Promise.resolve()); 29 | getAppModulesMock.returns(Promise.resolve()); 30 | createDepsProxyMock.returns(fakeDeps); 31 | mock('../lib/getNativeModules', getNativeModulesMock); 32 | mock('../lib/getDependencyModules', getDependencyModulesMock); 33 | mock('../lib/getAppModules', getAppModulesMock); 34 | mock('../lib/createDepsProxy', createDepsProxyMock); 35 | subject = require('../index'); 36 | }); 37 | test.afterEach(() => { 38 | mock.stopAll(); 39 | sandbox.reset(); 40 | }); 41 | test('throws if no entry point given', t => { 42 | t.throws(() => subject({ 43 | containerRoot: fakeContainerRoot 44 | }), 'Expected application entry point to be specified'); 45 | }); 46 | test('bootstraps container with entry as custom function', async t => { 47 | const customEntryFunction = sandbox.stub(); 48 | const fakeCustomEntryFunctionReturnValue = chance.word(); 49 | 50 | customEntryFunction.returns(fakeCustomEntryFunctionReturnValue); 51 | t.deepEqual( 52 | await subject({ 53 | containerRoot: fakeContainerRoot, 54 | entry: customEntryFunction 55 | }), 56 | {deps: fakeDeps, entryResolveValue: fakeCustomEntryFunctionReturnValue}, 57 | 'expected bootstrapping to resolve with array of dependencies and return value of entry point' 58 | ); 59 | t.is( 60 | customEntryFunction.args[0][0], 61 | fakeDeps, 62 | 'expected to call custom entry function with app dependencies' 63 | ); 64 | }); 65 | test('bootstraps container with entry as module key', async t => { 66 | const fakeAppReturnValue = chance.word(); 67 | const fakeInitialState = {}; 68 | 69 | fakeEntryModule.returns(fakeAppReturnValue); 70 | t.deepEqual( 71 | await subject({ 72 | containerRoot: fakeContainerRoot, 73 | entry: fakeEntryModuleKey, 74 | initialState: fakeInitialState 75 | }), 76 | {deps: fakeDeps, entryResolveValue: fakeAppReturnValue}, 77 | 'expected bootstrapping to resolve with array of dependencies and return value of entry point' 78 | ); 79 | t.is(fakeEntryModule.args[0][0], fakeInitialState, 'expected to call entry module with passed in initial state'); 80 | }); 81 | test('rejects promise on entry module errors', t => { 82 | const fakeErrorMessage = chance.word(); 83 | const fakeError = new Error(fakeErrorMessage); 84 | 85 | fakeEntryModule.throws(fakeError); 86 | t.throws(subject({ 87 | containerRoot: fakeContainerRoot, 88 | entry: fakeEntryModuleKey 89 | }), fakeErrorMessage); 90 | }); 91 | test('resolves with entry point returned promise resolve value', async t => { 92 | const fakeEntryModuleResolveValue = {}; 93 | let resolveValue; 94 | 95 | fakeEntryModule.returns(Promise.resolve(fakeEntryModuleResolveValue)); 96 | resolveValue = await subject({ 97 | containerRoot: fakeContainerRoot, 98 | entry: fakeEntryModuleKey 99 | }); 100 | 101 | t.is(resolveValue.entryResolveValue, fakeEntryModuleResolveValue); 102 | }); 103 | test.cb('rejects with entry point returned promise reject value', t => { 104 | const fakeEntryModuleRejectValue = chance.word(); 105 | 106 | fakeEntryModule.returns(Promise.reject(fakeEntryModuleRejectValue)); 107 | subject({ 108 | containerRoot: fakeContainerRoot, 109 | entry: fakeEntryModuleKey 110 | }) 111 | .catch((e) => { 112 | t.is(e, fakeEntryModuleRejectValue); 113 | t.end(); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /spec/lib/createDepsProxy.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | import subject from '../../lib/createDepsProxy'; 4 | import mock from 'mock-require'; 5 | import chanceFactory from 'chance'; 6 | 7 | const chance = chanceFactory(); 8 | const sandbox = sinon.sandbox.create(); 9 | 10 | test.afterEach(() => { 11 | sandbox.reset(); 12 | }); 13 | test('native modules are added to deps', t => { 14 | const nativeModules = {'fs': require('fs')}; 15 | const moduleGroups = [{}, nativeModules, {}]; 16 | const appDeps = subject(moduleGroups); 17 | 18 | t.is(appDeps['fs'], require('fs')); 19 | }); 20 | test('dependency modules are added to deps', t => { 21 | const nodeModules = {'chance': require('chance')}; 22 | const moduleGroups = [nodeModules, {}, {}]; 23 | const appDeps = subject(moduleGroups); 24 | 25 | t.is(appDeps['chance'], require('chance')); 26 | }); 27 | test('app modules as factories are added to deps', t => { 28 | const fakeAppModuleKey = `/${chance.word()}`; 29 | const fakeAppModuleFactory = sandbox.stub(); 30 | const fakeAppModule = {}; 31 | const appModules = {[fakeAppModuleKey]: '../spec/fixtures/fakeBreadboardModule'}; 32 | const moduleGroups = [{}, {}, appModules]; 33 | let appDeps; 34 | 35 | fakeAppModuleFactory.returns(fakeAppModule); 36 | mock('../fixtures/fakeBreadboardModule', fakeAppModuleFactory); 37 | appDeps = subject(moduleGroups); 38 | t.is(appDeps[fakeAppModuleKey], fakeAppModule); 39 | }); 40 | test('app modules as values are added to deps', t => { 41 | const fakeAppModuleKey = `/${chance.word()}`; 42 | const fakeAppModuleValue = {}; 43 | const appModules = {[fakeAppModuleKey]: '../spec/fixtures/fakeBreadboardModule'}; 44 | const moduleGroups = [{}, {}, appModules]; 45 | let appDeps; 46 | 47 | mock('../fixtures/fakeBreadboardModule', fakeAppModuleValue); 48 | appDeps = subject(moduleGroups); 49 | t.is(appDeps[fakeAppModuleKey], fakeAppModuleValue); 50 | mock.stopAll(); 51 | }); 52 | test('throws if requiring non-existing app module', t => { 53 | const fakeAppModuleKey = `/${chance.word()}`; 54 | const moduleGroups = [{}, {}, {}]; 55 | const appDeps = subject(moduleGroups); 56 | 57 | t.throws(() => appDeps[fakeAppModuleKey], `Cannot resolve app module ${fakeAppModuleKey}`); 58 | }); 59 | test('deps are frozen', t => { 60 | const moduleGroups = [{}, {}, {}]; 61 | const deps = subject(moduleGroups); 62 | 63 | t.throws(() => { 64 | deps.foo = 'update'; 65 | }, 'Runtime changes to dependencies not supported'); 66 | }); 67 | test('deps are required only when accessed', t => { 68 | const moduleGroups = [{}, {}, { 69 | '/entry': '../spec/fixtures/fakeBreadboardEntryModule.js', 70 | '/fakeBreadboardModule': '../spec/fixtures/fakeBreadboardModule.js' 71 | }]; 72 | const isFakeBreadboardModule = modulePath => /fakeBreadboardModule\.js$/.test(modulePath); 73 | const fakeBreadboardModuleKey = Object.keys(require.cache).filter(isFakeBreadboardModule); 74 | let fakeBreadboardModuleCached; 75 | 76 | delete require.cache[fakeBreadboardModuleKey]; 77 | subject(moduleGroups); 78 | fakeBreadboardModuleCached = Object.keys(require.cache).some(isFakeBreadboardModule); 79 | t.false(fakeBreadboardModuleCached); 80 | }); 81 | test('accepts explicit substitutes for modules', t => { 82 | const fakeFs = {}; 83 | const fakeAppModule = {}; 84 | const fakeDepModule = {}; 85 | const moduleGroups = [{'fs': 'fs'}, {'debug': 'debug'}, {'/foo': '../spec/fixtures/fakeBreadboardModule.js'}]; 86 | const deps = subject(moduleGroups, { 87 | substitutes: { 88 | 'fs': fakeFs, 89 | 'debug': fakeDepModule, 90 | '/foo': fakeAppModule 91 | } 92 | }); 93 | 94 | t.is(deps['fs'], fakeFs); 95 | t.is(deps['debug'], fakeDepModule); 96 | t.is(deps['/foo'], fakeAppModule); 97 | }); 98 | -------------------------------------------------------------------------------- /spec/lib/getAppModules.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import mock from 'mock-require'; 3 | import createChance from 'chance'; 4 | import sinon from 'sinon'; 5 | 6 | const chance = createChance(); 7 | const sandbox = sinon.sandbox.create(); 8 | const walkerStub = { 9 | on: sandbox.stub() 10 | }; 11 | const walkMock = { 12 | walk: sandbox.stub().returns(walkerStub) 13 | }; 14 | const getModuleKeyMock = sandbox.stub(); 15 | const fakeFileStat = { 16 | name: chance.word() 17 | }; 18 | let subject; 19 | let fakeContainerRoot; 20 | let fakeWalkerDir; 21 | 22 | test.beforeEach(() => { 23 | fakeWalkerDir = chance.word(); 24 | walkerStub.on.withArgs('file').yields(fakeWalkerDir, fakeFileStat, () => { 25 | }); 26 | mock('walk', walkMock); 27 | mock('../../lib/getModuleKey', getModuleKeyMock); 28 | fakeContainerRoot = chance.word(); 29 | subject = require('../../lib/getAppModules'); 30 | }); 31 | test.afterEach(() => { 32 | mock.stopAll(); 33 | sandbox.reset(); 34 | }); 35 | 36 | test('throws if container root not specified', t => { 37 | t.throws(subject()); 38 | }); 39 | test('creates a directory walker', t => { 40 | subject(fakeContainerRoot); 41 | t.truthy(walkMock.walk.calledWithExactly(fakeContainerRoot)); 42 | }); 43 | test('does not throw when no substitutes defined', t => { 44 | t.notThrows(() => { 45 | subject(fakeContainerRoot); 46 | }); 47 | }); 48 | test('when walker encounters a file it continues the walk', t => { 49 | const nextSpy = sandbox.spy(); 50 | 51 | walkerStub.on.withArgs('file').yields(chance.word(), fakeFileStat, nextSpy); 52 | subject(fakeContainerRoot); 53 | t.truthy(nextSpy.calledOnce); 54 | }); 55 | test('when walker encounters a file that has a specified substitute', async t => { 56 | const fakeModuleName = chance.word(); 57 | 58 | getModuleKeyMock.returns(fakeModuleName); 59 | walkerStub.on.withArgs('end').yields(); 60 | t.deepEqual(await subject(fakeContainerRoot, [fakeModuleName]), {}); 61 | }); 62 | test('when walker encounters a file that has no specified substitute. a module getter is registered', async t => { 63 | const fakeModuleName = chance.word(); 64 | let customModules; 65 | 66 | getModuleKeyMock.returns(fakeModuleName); 67 | walkerStub.on.withArgs('end').yields(); 68 | customModules = await subject(fakeContainerRoot); 69 | t.deepEqual(customModules[fakeModuleName], `${fakeWalkerDir}/${fakeFileStat.name}`); 70 | }); 71 | test('when walker encounters errors, promise gets rejected', t => { 72 | const fakeStatError = {}; 73 | 74 | walkerStub.on.withArgs('errors').yields(null, [{ 75 | error: fakeStatError 76 | }]); 77 | 78 | return subject(fakeContainerRoot) 79 | .catch(errors => { 80 | t.deepEqual(errors[0], fakeStatError); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /spec/lib/getDependecyModules.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const subject = require('../../lib/getDependencyModules'); 3 | const packageDir = '../../spec/fixtures'; 4 | const chance = new require('chance')(); 5 | 6 | test('registers modules specified in package.json', async t => { 7 | t.deepEqual(await subject(packageDir), { 8 | 'fakeDependency': 'fakeDependency' 9 | }); 10 | }); 11 | test('does not load modules specified in both package.json and blacklist', async t => { 12 | const blacklist = ['fakeDependency']; 13 | 14 | t.deepEqual(await subject(packageDir, blacklist), {}); 15 | }); 16 | test('throws if specified module is not found', t => { 17 | const brokenPackageDir = `../../spec/fixtures/${chance.word()}`; 18 | 19 | t.throws(subject(brokenPackageDir)); 20 | }); 21 | test('throws if package.json doesn\'t exist', t => { 22 | const nonExistentPackageDir = `../spec/fixtures/${chance.word()}`; 23 | 24 | return subject(nonExistentPackageDir) 25 | .catch((e) => { 26 | t.truthy(e instanceof Error); 27 | }); 28 | }); 29 | test('throws if package.json doesn\'t include dependencies', t => { 30 | const packageWithoutDepsDir = '../spec/fixtures/packageJsonWithoutDependencies'; 31 | 32 | return subject(packageWithoutDepsDir) 33 | .catch((e) => { 34 | t.truthy(e instanceof Error); 35 | }); 36 | }); 37 | test('does not load modules specified in both package.json and substitutes', async t => { 38 | const substitutes = ['fakeDependency']; 39 | 40 | t.deepEqual(await subject(packageDir, [], substitutes), {}); 41 | }); 42 | -------------------------------------------------------------------------------- /spec/lib/getModuleKey.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const subject = require('../../lib/getModuleKey'); 3 | 4 | test('module is a JavaScript file, it does not include the file extension', t => { 5 | const relativeModulePath = 'containerRoot/lib/modules'; 6 | const parsedModulePath = { 7 | base: 'file.js', 8 | name: 'file', 9 | ext: '.js' 10 | }; 11 | const moduleKey = subject(relativeModulePath, parsedModulePath); 12 | t.is(moduleKey, '/containerRoot/lib/modules/file'); 13 | }); 14 | test('module is not a JavaScript file, it includes the file extension', t => { 15 | const relativeModulePath = 'containerRoot/lib/modules'; 16 | const parsedModulePath = { 17 | base: 'file.json', 18 | name: 'file', 19 | ext: '.json' 20 | }; 21 | const moduleKey = subject(relativeModulePath, parsedModulePath); 22 | t.is(moduleKey, '/containerRoot/lib/modules/file.json'); 23 | }); 24 | test('relative path is empty, return filename', t => { 25 | const relativeModulePath = ''; 26 | const parsedModulePath = { 27 | base: 'file.js', 28 | name: 'file', 29 | ext: '.js' 30 | }; 31 | const moduleKey = subject(relativeModulePath, parsedModulePath); 32 | t.is(moduleKey, '/file'); 33 | }); 34 | -------------------------------------------------------------------------------- /spec/lib/getNativeModule.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | import subject from '../../lib/getNativeModules'; 4 | 5 | const sandbox = sinon.sandbox.create(); 6 | const fakeInternalModuleKey = 'internal/fakeNativeModule'; 7 | const fakeNativeModuleKey = 'fakeNativeModule'; 8 | const modules = { 9 | [fakeNativeModuleKey]: {}, 10 | [fakeInternalModuleKey]: {} 11 | }; 12 | 13 | test.before(() => { 14 | sandbox.stub(process, 'binding').withArgs('natives').returns(modules); 15 | }); 16 | test.after(() => { 17 | sandbox.restore(); 18 | }); 19 | test.afterEach(() => { 20 | sandbox.reset(); 21 | }); 22 | test('non-internal native modules are returned', async t => { 23 | const nativeModules = await subject([]); 24 | 25 | t.is(nativeModules[fakeNativeModuleKey], fakeNativeModuleKey); 26 | }); 27 | test('when only internal native modules are provided, no modules are returned', async t => { 28 | const nativeModules = await subject([]); 29 | 30 | t.is(nativeModules[fakeInternalModuleKey], undefined); 31 | }); 32 | test('does not add substitutes to deps', async t => { 33 | const substitutes = [fakeNativeModuleKey]; 34 | const nativeModules = await subject(substitutes); 35 | 36 | t.is(nativeModules[fakeNativeModuleKey], undefined); 37 | }); 38 | --------------------------------------------------------------------------------