Skip to content

Commit

Permalink
Merge bf42a5f into 6a377c1
Browse files Browse the repository at this point in the history
  • Loading branch information
ecowden committed Feb 11, 2017
2 parents 6a377c1 + bf42a5f commit d69bc18
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 47 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules
npm-debug.log
.nyc_output
coverage
111 changes: 92 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
# pluto-path

Create a Pluto dependency injection module from files in a path or paths.
Create and bootstrap an app from files in a path or paths using the [Pluto](https://github.com/ecowden/pluto.js) dependency injection package.

| Branch | Status |
| ------------- |:------------- |
| Master | [![Build Status](https://travis-ci.org/ecowden/pluto-path.png?branch=master)](https://travis-ci.org/ecowden/pluto-path) [![Coverage Status](https://coveralls.io/repos/github/ecowden/pluto-path/badge.svg?branch=master)](https://coveralls.io/github/ecowden/pluto-path?branch=master) |
| Master | [![Build Status](https://travis-ci.org/ecowden/pluto-path.png?branch=master)](https://travis-ci.org/ecowden/pluto-path) [![Coverage Status](https://coveralls.io/repos/github/ecowden/pluto-path/badge.svg?branch=master)](https://coveralls.io/github/ecowden/pluto-path?branch=master) [![NSP Status](https://nodesecurity.io/orgs/ecowden/projects/5cff7ae1-a34a-49f7-bf18-f2b816180930/badge)](https://nodesecurity.io/orgs/ecowden/projects/5cff7ae1-a34a-49f7-bf18-f2b816180930) |
| All | [![Build Status](https://travis-ci.org/ecowden/pluto-path.png)](https://travis-ci.org/ecowden/pluto-path) |

**Why?**

This package aims to solve two problems:

1. **Bootstrapping**. There are many example Express/Hapi/etc. applications where a chain of `index.js` files require a bunch of routes and other bits. I find this annoying. When I create a new part of my app, I want it to Just Work without extra boilerplate.
1. **Testing**. The key to good testing isn't writing tests: it's writing good, testable code. When unit testing, in particular, we want to test one thing at a time. But what about when our one thing uses other things? By injecting those other things, often called _collaborators_, we can pass mocks or fakes to our code under test.

## Usage

### Simplified Options

```js
var path = require('path');
'use strict'

const path = require('path')
const plutoPath = require('pluto-path')

requirePath(path.join(__dirname, 'my-directory')) // you can pass a single search path or array of them
.then(function (plutoModule) {
// Most of the time, you want to eagerly load all files.
// Alternately, use the plutoModule as desired and lazily load specific components.
plutoModule.eagerlyLoadAll();
plutoPath(path.join(__dirname, 'my-directory')) // you can pass a single search path or array of them
.then(function (app) {
// `app` holds a Map from filenames to their components.
// It's created by calling pluto's `bootstrap(...)` function.
// Use it if you want to do interact with components after they're
// wired together.
})
// Don't forget about errors!
.catch(err => {
console.error(err.stack) // eslint-disable-line no-console
process.exitCode = 1
})
// don't forget to handle errors!
.catch(handleError);
```

### Binding Types
Expand Down Expand Up @@ -50,18 +64,77 @@ you'd like to specify additional options beyond what pluto-path can find on the
filesystem. |

```js
var path = require('path');
'use strict'

const path = require('path')
const plutoPath = require('pluto-path')

requirePath({
plutoPath({
path: path.join(__dirname, 'my-directory'),
include: ['**/*.js', '**/*.json'],
exclude: ['**/*Spec.js']
exclude: ['**/*Spec.js'],
extraBindings: (bind) => {
bind('meaningOfLife').toInstance(42)
}
})
.then(function (app) {
// `app` holds a Map from filenames to their components.
// It's created by calling pluto's `bootstrap(...)` function.
})
// Don't forget about errors!
.catch(err => {
console.error(err.stack) // eslint-disable-line no-console
process.exitCode = 1
})
.then(function (plutoModule) {
// Most of the time, you want to eagerly load all files.
// Alternately, use the plutoModule as desired and lazily load specific components.
plutoModule.eagerlyLoadAll();
```

## Humble Opinionated Recommendations

### Project Organization

There's usually two different kinds of files in an app:

1. Long-lived components, like route handlers and server configuration. These need to be run exactly once and become a part of your app. Place these in an `app` folder.
1. Utilities and such that don't have a life of their own, and which you don't want subject to dependency injection. Place these in a `lib` folder.

```
/
/app Bootstrap long-lived components
/lib Utilities and such you don't want to dependency-inject
index.js `main` file with initial bootstrapping
```

Instruct `pluto-path` to bootstrap the `app` folder and leave the `lib` folder alone. If you're not doing any fancy startup stuff and you're fine with other defaults, your `index.js` file might look like:

```js
'use strict'

const path = require('path')
const plutoPath = require('pluto-path')

plutoPath(path.join(__dirname, 'app'))
// Don't forget about errors!
.catch(err => {
console.error(err.stack) // eslint-disable-line no-console
process.exitCode = 1
})
// don't forget to handle errors!
.catch(handleError);
```

### Tests

You might notice that there's not test path, event though one of the main motivations for dependency injection is testability. Rather than use a separate `test` tree, I like to put my tests right next to the thing they're testing, with a `Spec` suffix, like:

```
/
/app
myThing.js
myThingSpec.js
```

I am highly influenced by Uncle Bob's [Principles of Object Oriented Design](http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod). In this case:

* **Common Closure Principle**: Things that change together should be packaged together.

When it comes to unit testing, a test and the thing it tests unsurprisingly tend to change together, so I like to put them next to each other. Stepping back, this makes logical sense, too: why hunt deep down through two separate directory trees just to get to the two files you want to change? Putting tests next to the code they test reduces friction when writing tests and just makes life easier.

The default arguments to `pluto-path` assume this kind of organization. If you want to do something else, change the `include` and `exclude` options as you see fit.
32 changes: 15 additions & 17 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
'use strict'

const co = require('co')
const requirePath = require('require-path')
const pluto = require('pluto')
const standardNamingStrategy = require('./standardNamingStrategy')
const standardBindingStrategy = require('./standardBindingStrategy')
const lodash = require('lodash')

module.exports = function plutoPath(o) {
module.exports = co.wrap(function* plutoPath(o) {
const options = buildOptions(o)

return requirePath(options)
.then((components) => {
const plutoModule = pluto.createModule((bind) => {
for (let filename in components) {
const component = components[filename]
const components = yield requirePath(options)
const bind = pluto()
for (let filename in components) {
const component = components[filename]

const componentName = standardNamingStrategy(filename)
const bindingName = standardBindingStrategy(filename, component)
bind(componentName)[bindingName](component)
}
if (options.extraBindings) {
options.extraBindings(bind)
}
})
const componentName = standardNamingStrategy(filename)
const bindingName = standardBindingStrategy(filename, component)
bind(componentName)[bindingName](component)
}
if (options.extraBindings) {
options.extraBindings(bind)
}

return plutoModule
})
}
return bind.bootstrap()
})

function buildOptions(o) {
if (typeof o === 'string' || lodash.isArray(o)) {
Expand Down
15 changes: 7 additions & 8 deletions lib/indexSpec.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
'use strict'

const test = require('ava')
const co = require('co')
const path = require('path')

const plutoPath = require('./index')
const fixturesPath = path.join(__dirname, 'fixtures')
const simpleTestPath = path.join(fixturesPath, 'dir1')
const complexTextPath = path.join(fixturesPath, 'dir2')

function* simpleBindingMacro(t, input, expected) {
const simpleBindingMacro = co.wrap(function* (t, input, expected) {
const app = yield plutoPath(simpleTestPath)
const a = app.get('a')
t.is(typeof a, 'object')
t.deepEqual(a.b, require('./fixtures/dir1/b')())
}
})

simpleBindingMacro.title = (providedTitle, input, expected) => `The main function accepts ${providedTitle}`.trim()

Expand All @@ -33,22 +34,20 @@ test('The main function accepts `extraBindings` property in the config object',
t.is(app.get('theAnswer'), 42)
})

test('The main function throws a meaningful error if given a bad configuration type', function* (t) {
const error = t.throws(() => {
plutoPath(42)
}, Error)
test('The main function rejects with a meaningful error if given a bad configuration type', function* (t) {
const error = yield t.throws(plutoPath(42), Error)
t.is(error.message, 'options must be a string, array or strings or an object')
})

test('given complex filex, it binds a non-function object using `toInstance`', function* (t) {
const app = yield plutoPath(complexTextPath)
const app = yield plutoPath([complexTextPath, simpleTestPath])
const actual = app.get('myData')
const expected = require('./fixtures/dir2/innerDir/myData.json')
t.is(actual, expected)
})

test('given complex filex, it binds a factory function using `toFactory`', function* (t) {
const app = yield plutoPath(complexTextPath)
const app = yield plutoPath([complexTextPath, simpleTestPath])
const actual = app.get('myFactory')
const expected = require('./fixtures/dir2/myFactory')()
t.is(actual.name, expected.name)
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pluto-path",
"version": "0.4.0",
"version": "1.0.0",
"description": "Create a Pluto dependency injection module from files in a path or paths",
"main": "lib/index.js",
"scripts": {
Expand Down Expand Up @@ -35,9 +35,10 @@
}
},
"dependencies": {
"co": "^4.6.0",
"lodash": "^4.17.4",
"pluto": "^0.6.0",
"require-path": "^0.2.0"
"pluto": "^1.0.0",
"require-path": "^0.3.0"
},
"devDependencies": {
"ava": "^0.18.0",
Expand Down

0 comments on commit d69bc18

Please sign in to comment.