Skip to content

Commit

Permalink
feat: Make auto-register also register named exports, and not just th…
Browse files Browse the repository at this point in the history
…e default export
  • Loading branch information
Iku-turso committed Sep 23, 2022
1 parent c7926ff commit 6af7f4c
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import isString from 'lodash/fp/isString';
import isFunction from 'lodash/fp/isFunction';
import { pipeline } from '@ogre-tools/fp';
import tap from 'lodash/fp/tap';
import { injectableSymbol } from '@ogre-tools/injectable';
import forEach from 'lodash/fp/forEach';
import flatMap from 'lodash/fp/flatMap';

Expand All @@ -11,33 +12,42 @@ const hasInjectableSignature = conforms({
instantiate: isFunction,
});

const getFileNameAndDefaultExport = requireContext =>
requireContext.keys().map(key => [key, requireContext(key).default]);
const getFileNameAndModule = requireContext =>
requireContext.keys().map(key => [key, requireContext(key)]);

const registerInjectableFor =
di =>
([, injectable]) =>
di.register(injectable);
([, module]) => {
di.register(...Object.values(module).filter(isInjectable));
};

const verifyInjectable = ([fileName, injectable]) => {
if (!injectable) {
throw new Error(
`Tried to register injectable from ${fileName}, but no default export`,
);
}
const verifyInjectables = ([[fileName, module]]) => {
const injectables = Object.entries(module).filter(([, exported]) =>
isInjectable(exported),
);

if (!hasInjectableSignature(injectable)) {
if (injectables.length === 0) {
throw new Error(
`Tried to register injectable from ${fileName}, but default export is of wrong shape`,
`Tried to register injectables from "${fileName}", but there were none"`,
);
}

injectables.forEach(([propertyName, injectable]) => {
if (!hasInjectableSignature(injectable)) {
throw new Error(
`Tried to register injectables from "${fileName}", but export "${propertyName}" is of wrong shape`,
);
}
});
};

export default ({ di, requireContexts }) => {
pipeline(
requireContexts,
flatMap(getFileNameAndDefaultExport),
tap(forEach(verifyInjectable)),
flatMap(getFileNameAndModule),
tap(verifyInjectables),
forEach(registerInjectableFor(di)),
);
};

const isInjectable = exported => exported?.aliasType === injectableSymbol;
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ describe('autoRegister', () => {

const di = createContainer('some-container');

const someRequireContext = getRequireContextStub(injectableStub);
const someRequireContext = getRequireContextStub({
default: injectableStub,
});

autoRegister({ di, requireContexts: [someRequireContext] });

Expand All @@ -25,10 +27,10 @@ describe('autoRegister', () => {
expect(actual).toBe('some-injected-instance');
});

it('given injectable file with no default export, when auto-registering, throws with name of faulty file', () => {
it('given injectable file with no injectables, when auto-registering, throws', () => {
const requireContextStub = Object.assign(
() => ({
notDefault: 'irrelevant',
someExport: 'irrelevant',
}),

{
Expand All @@ -41,14 +43,16 @@ describe('autoRegister', () => {
expect(() =>
autoRegister({ di, requireContexts: [requireContextStub] }),
).toThrowError(
'Tried to register injectable from ./some.injectable.js, but no default export',
'Tried to register injectables from "./some.injectable.js", but there were none',
);
});

it('given injectable file with default export without id, when auto-registering, throws with name of faulty file', () => {
it('given injectable file with default export without id, when auto-registering, throws', () => {
const injectable = getInjectable({ instantiate: () => {} });

const requireContextStub = Object.assign(
() => ({
default: 'irrelevant',
default: injectable,
}),
{
keys: () => ['./some.injectable.js'],
Expand All @@ -60,16 +64,16 @@ describe('autoRegister', () => {
expect(() =>
autoRegister({ di, requireContexts: [requireContextStub] }),
).toThrowError(
'Tried to register injectable from ./some.injectable.js, but default export is of wrong shape',
'Tried to register injectables from "./some.injectable.js", but export "default" is of wrong shape',
);
});

it('given injectable file with default export with in but without instantiate, when auto-registering, throws with name of faulty file', () => {
it('given injectable file with an id but without instantiate, when auto-registering, throws', () => {
const injectable = getInjectable({ id: 'irrelevant' });

const requireContextStub = Object.assign(
() => ({
default: {
id: 'irrelevant',
},
default: injectable,
}),
{
keys: () => ['./some.injectable.js'],
Expand All @@ -81,28 +85,8 @@ describe('autoRegister', () => {
expect(() =>
autoRegister({ di, requireContexts: [requireContextStub] }),
).toThrowError(
'Tried to register injectable from ./some.injectable.js, but default export is of wrong shape',
);
});

it('given injectable file with default export of correct shape, when auto-registering, does not throw', () => {
const requireContextStub = Object.assign(
() => ({
default: {
id: 'some-injectable-id',
instantiate: () => {},
},
}),
{
keys: () => ['./some.injectable.js'],
},
'Tried to register injectables from "./some.injectable.js", but export "default" is of wrong shape',
);

const di = createContainer('some-container');

expect(() =>
autoRegister({ di, requireContexts: [requireContextStub] }),
).not.toThrow();
});

it('injects auto-registered injectable with a another auto-registered child-injectable', () => {
Expand All @@ -122,25 +106,48 @@ describe('autoRegister', () => {
di,

requireContexts: [
getRequireContextStub(childInjectable),
getRequireContextStub(parentInjectable),
getRequireContextStub({ default: childInjectable }),
getRequireContextStub({ default: parentInjectable }),
],
});

const actual = di.inject(parentInjectable);

expect(actual).toBe('some-child-instance');
});

it('given file with both injectable and non-injectable exports, given auto-registered, when injecting one of the injectables, does so', () => {
const injectable = getInjectable({
id: 'some-injectable',
instantiate: () => 'some-instance',
});

const di = createContainer('some-container');

autoRegister({
di,

requireContexts: [
getRequireContextStub({
someExport: injectable,
someOtherExport: 'not-an-injectable',
}),
],
});

const actual = di.inject(injectable);

expect(actual).toBe('some-instance');
});
});

const getRequireContextStub = (...injectables) => {
const getRequireContextStub = (...modules) => {
const contextDictionary = pipeline(
injectables,
map(injectable => ({ default: injectable })),
modules,

nonCappedMap((file, index) => [
nonCappedMap((module, index) => [
`stubbed-require-context-key-${index}`,
file,
module,
]),

fromPairs,
Expand Down
5 changes: 4 additions & 1 deletion packages/injectable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import getInjectionToken, {
injectionTokenSymbol,
} from './src/getInjectionToken/getInjectionToken';

import getInjectable from './src/getInjectable/getInjectable';
import getInjectable, {
injectableSymbol,
} from './src/getInjectable/getInjectable';
import lifecycleEnum from './src/dependency-injection-container/lifecycleEnum';

import createContainer, {
Expand All @@ -21,5 +23,6 @@ export {
registrationCallbackToken,
deregistrationCallbackToken,
injectionTokenSymbol,
injectableSymbol,
lifecycleEnum,
};
3 changes: 3 additions & 0 deletions packages/injectable/src/getInjectable/getInjectable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import lifecycleEnum from '../dependency-injection-container/lifecycleEnum';

export const injectableSymbol = Symbol('injectable');

export default ({ lifecycle = lifecycleEnum.singleton, ...injectable }) => ({
aliasType: injectableSymbol,
lifecycle,
...injectable,
});

0 comments on commit 6af7f4c

Please sign in to comment.