Skip to content

Commit a677ee7

Browse files
whefterjeffijoe
authored andcommitted
feat: allow autoloading of non-default exports (#115)
Add support for auto-loading named exports that expose an object on the `RESOLVER` symbol property.
1 parent dc67d7a commit a677ee7

File tree

3 files changed

+200
-17
lines changed

3 files changed

+200
-17
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,29 @@ boring. You can automate this by using `loadModules`.
451451
> * `module.exports = ...`
452452
> * `module.exports.default = ...`
453453
> * `export default ...`
454+
>
455+
> To load a non-default export, set the `[RESOLVER]` property on it:
456+
>
457+
> ```js
458+
> const { RESOLVER } = require('awilix');
459+
> export class ServiceClass {
460+
> }
461+
> ServiceClass[RESOLVER] = {}
462+
> ```
463+
>
464+
> Or even more concise using TypeScript:
465+
> ```typescript
466+
> // TypeScript
467+
> import { RESOLVER } from 'awilix'
468+
> export class ServiceClass {
469+
> static [RESOLVER] = {}
470+
> }
471+
> ```
472+
473+
Note that **multiple** services can be registered per file, i.e. it is
474+
possible to have a file with a default export and named exports and for
475+
all of them to be loaded. The named exports do require the `RESOLVER`
476+
token to be recognized.
454477
455478
Imagine this app structure:
456479

src/__tests__/load-modules.test.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,133 @@ describe('loadModules', function() {
3737
expect(container.resolve('someClass')).toBeInstanceOf(SomeClass)
3838
})
3939

40+
it('registers non-default export modules containing RESOLVER token with the container', function() {
41+
const container = createContainer()
42+
43+
class SomeNonDefaultClass {
44+
static [RESOLVER] = {}
45+
}
46+
47+
const modules: any = {
48+
'someIgnoredName.js': { SomeNonDefaultClass }
49+
}
50+
const moduleLookupResult = lookupResultFor(modules)
51+
const deps = {
52+
container,
53+
listModules: jest.fn(() => moduleLookupResult),
54+
require: jest.fn(path => modules[path])
55+
}
56+
57+
const result = loadModules(deps, 'anything')
58+
expect(result).toEqual({ loadedModules: moduleLookupResult })
59+
expect(Object.keys(container.registrations).length).toBe(1)
60+
// Note the capital first letter because the export key name is used instead of the filename
61+
expect(container.resolve('SomeNonDefaultClass')).toBeInstanceOf(
62+
SomeNonDefaultClass
63+
)
64+
})
65+
66+
it('does not register non-default modules without a RESOLVER token', function() {
67+
const container = createContainer()
68+
69+
class SomeClass {}
70+
71+
const modules: any = {
72+
'nopeClass.js': { SomeClass }
73+
}
74+
const moduleLookupResult = lookupResultFor(modules)
75+
const deps = {
76+
container,
77+
listModules: jest.fn(() => moduleLookupResult),
78+
require: jest.fn(path => modules[path])
79+
}
80+
81+
const result = loadModules(deps, 'anything')
82+
expect(result).toEqual({ loadedModules: moduleLookupResult })
83+
expect(Object.keys(container.registrations).length).toBe(0)
84+
})
85+
86+
it('registers multiple loaded modules from one file with the container', function() {
87+
const container = createContainer()
88+
89+
class SomeClass {}
90+
class SomeNonDefaultClass {
91+
static [RESOLVER] = {}
92+
}
93+
class SomeNamedNonDefaultClass {
94+
static [RESOLVER] = {
95+
name: 'nameOverride'
96+
}
97+
}
98+
99+
const modules: any = {
100+
'mixedFile.js': {
101+
default: SomeClass,
102+
nonDefault: SomeNonDefaultClass,
103+
namedNonDefault: SomeNamedNonDefaultClass
104+
}
105+
}
106+
const moduleLookupResult = lookupResultFor(modules)
107+
const deps = {
108+
container,
109+
listModules: jest.fn(() => moduleLookupResult),
110+
require: jest.fn(path => modules[path])
111+
}
112+
113+
const result = loadModules(deps, 'anything')
114+
expect(result).toEqual({ loadedModules: moduleLookupResult })
115+
expect(Object.keys(container.registrations).length).toBe(3)
116+
expect(container.resolve('mixedFile')).toBeInstanceOf(SomeClass)
117+
expect(container.resolve('nonDefault')).toBeInstanceOf(SomeNonDefaultClass)
118+
expect(container.resolve('nameOverride')).toBeInstanceOf(
119+
SomeNamedNonDefaultClass
120+
)
121+
})
122+
123+
it('registers only the last module with a certain name with the container', function() {
124+
const container = createContainer()
125+
126+
class SomeClass {}
127+
class SomeNonDefaultClass {
128+
static [RESOLVER] = {}
129+
}
130+
class SomeNamedNonDefaultClass {
131+
static [RESOLVER] = {
132+
name: 'nameOverride'
133+
}
134+
}
135+
136+
const modules: any = {
137+
'mixedFileOne.js': {
138+
default: SomeClass,
139+
nameOverride: SomeNonDefaultClass,
140+
// this will override the above named export with its specified name
141+
namedNonDefault: SomeNamedNonDefaultClass
142+
},
143+
'mixedFileTwo.js': {
144+
// this will override the default export from mixedFileOne
145+
mixedFileOne: SomeNonDefaultClass
146+
}
147+
}
148+
149+
const moduleLookupResult = lookupResultFor(modules)
150+
const deps = {
151+
container,
152+
listModules: jest.fn(() => moduleLookupResult),
153+
require: jest.fn(path => modules[path])
154+
}
155+
156+
const result = loadModules(deps, 'anything')
157+
expect(result).toEqual({ loadedModules: moduleLookupResult })
158+
expect(Object.keys(container.registrations).length).toBe(2)
159+
expect(container.resolve('mixedFileOne')).toBeInstanceOf(
160+
SomeNonDefaultClass
161+
)
162+
expect(container.resolve('nameOverride')).toBeInstanceOf(
163+
SomeNamedNonDefaultClass
164+
)
165+
})
166+
40167
it('uses built-in formatter when given a formatName as a string', function() {
41168
const container = createContainer()
42169
const modules: any = {

src/load-modules.ts

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,35 +90,68 @@ export function loadModules(
9090
const modules = dependencies.listModules(globPatterns, opts)
9191

9292
const result = modules.map(m => {
93+
const items: Array<{
94+
name: string
95+
path: string
96+
opts: object
97+
value: unknown
98+
}> = []
99+
93100
const loaded = dependencies.require(m.path)
94101

95102
// Meh, it happens.
96103
if (!loaded) {
97-
return undefined
104+
return items
105+
}
106+
107+
if (isFunction(loaded)) {
108+
// for module.exports = ...
109+
items.push({
110+
name: m.name,
111+
path: m.path,
112+
value: loaded,
113+
opts: m.opts
114+
})
115+
116+
return items
117+
}
118+
119+
if (loaded.default && isFunction(loaded.default)) {
120+
// ES6 default export
121+
items.push({
122+
name: m.name,
123+
path: m.path,
124+
value: loaded.default,
125+
opts: m.opts
126+
})
98127
}
99128

100-
if (!isFunction(loaded)) {
101-
if (loaded.default && isFunction(loaded.default)) {
102-
// ES6 default export
103-
return {
104-
name: m.name,
129+
// loop through non-default exports, but require the RESOLVER property set for
130+
// it to be a valid service module export.
131+
for (const key of Object.keys(loaded)) {
132+
if (key === 'default') {
133+
// default case handled separately due to its different name (file name)
134+
continue
135+
}
136+
137+
if (isFunction(loaded[key]) && RESOLVER in loaded[key]) {
138+
items.push({
139+
name: key,
105140
path: m.path,
106-
value: loaded.default,
141+
value: loaded[key],
107142
opts: m.opts
108-
}
143+
})
109144
}
110-
111-
return undefined
112145
}
113146

114-
return {
115-
name: m.name,
116-
path: m.path,
117-
value: loaded,
118-
opts: m.opts
119-
}
147+
return items
120148
})
121-
result.filter(x => x).forEach(registerDescriptor.bind(null, container, opts))
149+
150+
result
151+
.reduce((acc, cur) => acc.concat(cur), [])
152+
.filter(x => x)
153+
.forEach(registerDescriptor.bind(null, container, opts))
154+
122155
return {
123156
loadedModules: modules
124157
}

0 commit comments

Comments
 (0)