Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow autoloading of non-default exports #115

Merged
merged 3 commits into from
Jan 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,29 @@ boring. You can automate this by using `loadModules`.
> * `module.exports = ...`
> * `module.exports.default = ...`
> * `export default ...`
>
> To load a non-default export, set the `[RESOLVER]` property on it:
>
> ```js
> const { RESOLVER } = require('awilix');
> export class ServiceClass {
> }
> ServiceClass[RESOLVER] = {}
> ```
>
> Or even more concise using TypeScript:
> ```typescript
> // TypeScript
> import { RESOLVER } from 'awilix'
> export class ServiceClass {
> static [RESOLVER] = {}
> }
> ```

Note that **multiple** services can be registered per file, i.e. it is
possible to have a file with a default export and named exports and for
all of them to be loaded. The named exports do require the `RESOLVER`
token to be recognized.

Imagine this app structure:

Expand Down
127 changes: 127 additions & 0 deletions src/__tests__/load-modules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,133 @@ describe('loadModules', function() {
expect(container.resolve('someClass')).toBeInstanceOf(SomeClass)
})

it('registers non-default export modules containing RESOLVER token with the container', function() {
const container = createContainer()

class SomeNonDefaultClass {
static [RESOLVER] = {}
}

const modules: any = {
'someIgnoredName.js': { SomeNonDefaultClass }
}
const moduleLookupResult = lookupResultFor(modules)
const deps = {
container,
listModules: jest.fn(() => moduleLookupResult),
require: jest.fn(path => modules[path])
}

const result = loadModules(deps, 'anything')
expect(result).toEqual({ loadedModules: moduleLookupResult })
expect(Object.keys(container.registrations).length).toBe(1)
// Note the capital first letter because the export key name is used instead of the filename
expect(container.resolve('SomeNonDefaultClass')).toBeInstanceOf(
SomeNonDefaultClass
)
})

it('does not register non-default modules without a RESOLVER token', function() {
const container = createContainer()

class SomeClass {}

const modules: any = {
'nopeClass.js': { SomeClass }
}
const moduleLookupResult = lookupResultFor(modules)
const deps = {
container,
listModules: jest.fn(() => moduleLookupResult),
require: jest.fn(path => modules[path])
}

const result = loadModules(deps, 'anything')
expect(result).toEqual({ loadedModules: moduleLookupResult })
expect(Object.keys(container.registrations).length).toBe(0)
})

it('registers multiple loaded modules from one file with the container', function() {
const container = createContainer()

class SomeClass {}
class SomeNonDefaultClass {
static [RESOLVER] = {}
}
class SomeNamedNonDefaultClass {
static [RESOLVER] = {
name: 'nameOverride'
}
}

const modules: any = {
'mixedFile.js': {
default: SomeClass,
nonDefault: SomeNonDefaultClass,
namedNonDefault: SomeNamedNonDefaultClass
}
}
const moduleLookupResult = lookupResultFor(modules)
const deps = {
container,
listModules: jest.fn(() => moduleLookupResult),
require: jest.fn(path => modules[path])
}

const result = loadModules(deps, 'anything')
expect(result).toEqual({ loadedModules: moduleLookupResult })
expect(Object.keys(container.registrations).length).toBe(3)
expect(container.resolve('mixedFile')).toBeInstanceOf(SomeClass)
expect(container.resolve('nonDefault')).toBeInstanceOf(SomeNonDefaultClass)
expect(container.resolve('nameOverride')).toBeInstanceOf(
SomeNamedNonDefaultClass
)
})

it('registers only the last module with a certain name with the container', function() {
const container = createContainer()

class SomeClass {}
class SomeNonDefaultClass {
static [RESOLVER] = {}
}
class SomeNamedNonDefaultClass {
static [RESOLVER] = {
name: 'nameOverride'
}
}

const modules: any = {
'mixedFileOne.js': {
default: SomeClass,
nameOverride: SomeNonDefaultClass,
// this will override the above named export with its specified name
namedNonDefault: SomeNamedNonDefaultClass
},
'mixedFileTwo.js': {
// this will override the default export from mixedFileOne
mixedFileOne: SomeNonDefaultClass
}
}

const moduleLookupResult = lookupResultFor(modules)
const deps = {
container,
listModules: jest.fn(() => moduleLookupResult),
require: jest.fn(path => modules[path])
}

const result = loadModules(deps, 'anything')
expect(result).toEqual({ loadedModules: moduleLookupResult })
expect(Object.keys(container.registrations).length).toBe(2)
expect(container.resolve('mixedFileOne')).toBeInstanceOf(
SomeNonDefaultClass
)
expect(container.resolve('nameOverride')).toBeInstanceOf(
SomeNamedNonDefaultClass
)
})

it('uses built-in formatter when given a formatName as a string', function() {
const container = createContainer()
const modules: any = {
Expand Down
67 changes: 50 additions & 17 deletions src/load-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,35 +90,68 @@ export function loadModules(
const modules = dependencies.listModules(globPatterns, opts)

const result = modules.map(m => {
const items: Array<{
name: string
path: string
opts: object
value: unknown
}> = []

const loaded = dependencies.require(m.path)

// Meh, it happens.
if (!loaded) {
return undefined
return items
}

if (isFunction(loaded)) {
// for module.exports = ...
items.push({
name: m.name,
path: m.path,
value: loaded,
opts: m.opts
})

return items
}

if (loaded.default && isFunction(loaded.default)) {
// ES6 default export
items.push({
name: m.name,
path: m.path,
value: loaded.default,
opts: m.opts
})
}

if (!isFunction(loaded)) {
if (loaded.default && isFunction(loaded.default)) {
// ES6 default export
return {
name: m.name,
// loop through non-default exports, but require the RESOLVER property set for
// it to be a valid service module export.
for (const key of Object.keys(loaded)) {
if (key === 'default') {
// default case handled separately due to its different name (file name)
continue
}

if (isFunction(loaded[key]) && RESOLVER in loaded[key]) {
jeffijoe marked this conversation as resolved.
Show resolved Hide resolved
items.push({
name: key,
path: m.path,
value: loaded.default,
value: loaded[key],
opts: m.opts
}
})
}

return undefined
}

return {
name: m.name,
path: m.path,
value: loaded,
opts: m.opts
}
return items
})
result.filter(x => x).forEach(registerDescriptor.bind(null, container, opts))

result
.reduce((acc, cur) => acc.concat(cur), [])
.filter(x => x)
.forEach(registerDescriptor.bind(null, container, opts))

return {
loadedModules: modules
}
Expand Down