Skip to content

Commit

Permalink
chore!: Extract auto-registration as extension
Browse files Browse the repository at this point in the history
In addition to keeping the injectable core minimal, with this we have control over
order of registrations in extensions and normal injectables. Eg. decorators for
registration need to register before they are expected to do anything.
  • Loading branch information
Iku-turso committed Apr 7, 2022
1 parent eea1c0a commit 8395a7a
Show file tree
Hide file tree
Showing 16 changed files with 306 additions and 153 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@babel/preset-react": "^7.14.5",
"@types/react": "^17.0.37",
"@types/jest": "^27.0.3",
"@types/webpack-env": "^1.16.3",
"babel-loader": "^8.1.0",
"jest": "^27.0.6",
"lerna": "^4.0.0",
Expand Down
26 changes: 26 additions & 0 deletions packages/injectable-extension-for-auto-registration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Auto-registration for Injectable in Ogre Tools

Auto register injectables from default exports of files that match a require.context.

## Usage

```
$ npm install @ogre-tools/injectable
$ npm install @ogre-tools/injectable-extension-for-auto-registration
...
const di = createContainer();
autoRegister({
di,
requireContexts: [
require.context("./some-directory", true, /\.injectable\.(ts|tsx)$/),
require.context("./some-other-directory", true, /\.injectable\.(ts|tsx)$/),
],
});
```

## Documentation

Check unit tests for documentation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],

'@babel/react',
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { DiContainer } from '@ogre-tools/injectable';

declare module '@ogre-tools/injectable-extension-for-mobx' {
export function autoRegister(arg: {
di: DiContainer;
requireContexts: __WebpackModuleApi.RequireContext[];
}): void;
}
3 changes: 3 additions & 0 deletions packages/injectable-extension-for-auto-registration/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import autoRegister from './src/autoRegister';

export { autoRegister };

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions packages/injectable-extension-for-auto-registration/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@ogre-tools/injectable-extension-for-auto-registration",
"private": false,
"version": "6.0.1",
"description": "Auto-registration of injectables for Injectable in Ogre Tools",
"repository": {
"type": "git",
"url": "https://github.com/ogre-works/ogre-tools"
},
"main": "build/index.js",
"types": "./index.d.ts",
"keywords": [
"js"
],
"author": "Ogre Works",
"license": "MIT",
"dependencies": {
"@ogre-tools/fp": "^6.0.1",
"@ogre-tools/injectable": "^6.0.1",
"lodash": "^4.17.21"
},
"bugs": {
"url": "https://github.com/ogre-works/ogre-tools/issues"
},
"homepage": "https://github.com/ogre-works/ogre-tools#readme",
"publishConfig": {
"access": "public"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import conforms from 'lodash/fp/conforms';
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 forEach from 'lodash/fp/forEach';
import flatMap from 'lodash/fp/flatMap';

const hasInjectableSignature = conforms({
id: isString,
instantiate: isFunction,
});

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

const registerInjectableFor =
di =>
([, injectable]) =>
di.register(injectable);

const verifyInjectable = ([fileName, injectable]) => {
if (!injectable) {
throw new Error(
`Tried to register injectable from ${fileName}, but no default export`,
);
}

if (!hasInjectableSignature(injectable)) {
throw new Error(
`Tried to register injectable from ${fileName}, but default export is of wrong shape`,
);
}
};

export default ({ di, requireContexts }) => {
pipeline(
requireContexts,
flatMap(getFileNameAndDefaultExport),
tap(forEach(verifyInjectable)),
forEach(registerInjectableFor(di)),
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { createContainer, getInjectable } from '@ogre-tools/injectable';
import { pipeline } from '@ogre-tools/fp';
import map from 'lodash/fp/map';
import fromPairs from 'lodash/fp/fromPairs';
import keys from 'lodash/fp/keys';
import autoRegister from './autoRegister';

const nonCappedMap = map.convert({ cap: false });

describe('autoRegister', () => {
it('injects auto-registered injectable without sub-injectables', () => {
const injectableStub = getInjectable({
id: 'irrelevant',
instantiate: () => 'some-injected-instance',
});

const di = createContainer();

const someRequireContext = getRequireContextStub(injectableStub);

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

const actual = di.inject(injectableStub);

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

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

{
keys: () => ['./some.injectable.js'],
},
);

const di = createContainer();

expect(() =>
autoRegister({ di, requireContexts: [requireContextStub] }),
).toThrowError(
'Tried to register injectable from ./some.injectable.js, but no default export',
);
});

it('given injectable file with default export without id, when auto-registering, throws with name of faulty file', () => {
const requireContextStub = Object.assign(
() => ({
default: 'irrelevant',
}),
{
keys: () => ['./some.injectable.js'],
},
);

const di = createContainer();

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 with in but without instantiate, when auto-registering, throws with name of faulty file', () => {
const requireContextStub = Object.assign(
() => ({
default: {
id: 'irrelevant',
},
}),
{
keys: () => ['./some.injectable.js'],
},
);

const di = createContainer();

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'],
},
);

const di = createContainer();

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

it('injects auto-registered injectable with a another auto-registered child-injectable', () => {
const childInjectable = getInjectable({
id: 'some-injectable',
instantiate: () => 'some-child-instance',
});

const parentInjectable = getInjectable({
id: 'some-other-injectable',
instantiate: di => di.inject(childInjectable),
});

const di = createContainer();

autoRegister({
di,

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

const actual = di.inject(parentInjectable);

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

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

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

fromPairs,
);

return Object.assign(contextKey => contextDictionary[contextKey], {
keys: () => keys(contextDictionary),
});
};
1 change: 1 addition & 0 deletions packages/injectable-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { withInjectables } from '@ogre-tools/injectable-react';
it('given a Component is registered, when Inject is rendered for the Component, renders with dependencies', () => {
const di = createContainer();
const NonInjectedTestComponent = ({ someDependency, ...props }) => (
<div {...props}>Some content: "{someDependency}"</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,7 @@ describe('withInjectables', () => {
instantiate: di => di.inject(injectable),
});

di.register(injectable);
di.register(otherInjectable);
di.register(injectable, otherInjectable);

const DumbTestComponent = scenario.getComponent();

Expand Down
1 change: 1 addition & 0 deletions packages/injectable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { createContainer } from '@ogre-tools/injectable';
```
it('given an injectable is registered, when injected, injects', () => {
const di = createContainer();
const someInjectable = getInjectable({
id: 'some-id',
Expand Down
9 changes: 1 addition & 8 deletions packages/injectable/ogre-tools-injectable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,5 @@ declare module '@ogre-tools/injectable' {
};
};

export interface RequireContext {
keys(): string[];
(key: string): any;
}

export function createContainer(
...getRequireContexts: (() => RequireContext)[]
): DiContainer;
export function createContainer(): DiContainer;
}
Loading

0 comments on commit 8395a7a

Please sign in to comment.