Skip to content

Commit 757c8dd

Browse files
Jack Ellisjackmellis
authored andcommitted
feat: nodemodule and global types
NodeModule type lets you resolve a NodeModule using just a type Global type lets you resolve a global variable using just a type closes #90
1 parent e34ce23 commit 757c8dd

8 files changed

Lines changed: 289 additions & 210 deletions

File tree

README.md

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Jpex is an Inversion of Control framework. Register dependencies on a container,
1717
- [Consuming Dependencies](#consuming-dependencies)
1818
- [API](#api)
1919
- [jpex](#jpex)
20+
- [types](#types)
2021
- [caveats](#caveats)
2122
- [react](#react)
2223
- [Vanilla JS mode](#vanilla-js-mode)
@@ -303,6 +304,46 @@ Clears the cache of resolved factories. If you provide a type, that specific fac
303304
304305
Under the hood jpex converts types into strings for runtime resolution. If you want to get that calculated string for whatever reason, you can use `jpex.infer`
305306
307+
### Types
308+
#### Jpex
309+
This is the type definition for the jpex container
310+
311+
#### NodeModule
312+
This is a special type that lets you automatically inject a node module with type inference.
313+
314+
For example:
315+
```ts
316+
import jpex, { NodeModule } from 'jpex';
317+
318+
// this will resolve to the fs module without you having to explicitly register it as a dependency
319+
const fs = jpex.resolve<NodeModule<'fs'>>();
320+
```
321+
322+
The default return type will be `any` but you can specify one explicitly with the second type parameter:
323+
```ts
324+
import type fstype from 'fs';
325+
import jpex, { NodeModule } from 'jpex';
326+
327+
const fs = jpex.resolve<NodeModule<'fs', typeof fstype>>();
328+
```
329+
330+
#### Global
331+
This is another special type that lets you automatically inject a global property with type inference.
332+
333+
For built-in types you can do this without any helpers:
334+
```ts
335+
import jpex from 'jpex';
336+
337+
const navigator = jpex.resolve<Navigator>();
338+
```
339+
340+
But for custom globals, or properties that don't have built-in types, you can use the `Global` type:
341+
```ts
342+
import jpex, { Global } from 'jpex';
343+
344+
const analytics = jpex.resolve<Global<'ga', Function>>();
345+
```
346+
306347
## caveats
307348
There are a few caveats to be aware of:
308349
- Only named types/interfaces are supported so you can't do `jpex.factory<{}>()`
@@ -335,22 +376,18 @@ const MyComponent = (props) => {
335376
And this pattern also makes it really easy to isolate a component from its side effects when writing tests:
336377
337378
```tsx
338-
import { Provider, useJpex } from 'jpex';
379+
import base, { Provider, useJpex } from 'jpex';
339380
// create a stub for the SaveData dependency
340381
const saveData = stub();
382+
// create a new container
383+
const jpex = base.extend();
384+
// register our stub dependency
385+
jpex.constant<SaveData>(saveData);
341386

342387
render(
343-
// the Provider component will create a new jpex instance
344-
<Provider>
345-
{() => {
346-
// grab jpex - it will be isolated to this context only
347-
const jpex = useJpex();
348-
// register our stub dependency
349-
jpex.constant<SaveData>(saveData);
350-
351-
// when we render MyComponent, it will be given our stubbed dependency
352-
return (<MyComponent/>);
353-
}}
388+
<Provider value={jpex}>
389+
{/* when we render MyComponent, it will be given our stubbed dependency */}
390+
<MyComponent/>
354391
</Provider>
355392
);
356393

@@ -364,6 +401,8 @@ expect(saveData.called).to.be.true;
364401
Perhaps you hate typescript, or babel, or both. Or perhaps you don't have the luxury of a build pipeline in your application. That's fine because jpex supports vanilla js as well, you just have to explicitly state your dependencies up front:
365402
366403
```ts
404+
const { jpex } = require('jpex');
405+
367406
jpex.constant('foo', 'foo');
368407
jpex.factory('bah', [ 'foo' ], (foo) => foo + 'bah');
369408

src/__tests__/js/resolve.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ test('prefers a registered dependency over a node module', (t) => {
127127
t.is(value, fakeFs);
128128
});
129129

130+
test('does not resolve a node module when disabled', (t) => {
131+
const { jpex: base } = t.context;
132+
const jpex = base.extend({
133+
nodeModules: false,
134+
optional: true,
135+
});
136+
137+
const value = jpex.resolve('fs');
138+
139+
t.not(value, fs);
140+
t.is(value, void 0);
141+
});
142+
130143
test('resolves a global property', (t) => {
131144
const { jpex } = t.context;
132145

src/__tests__/ts/resolve.test.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@
22
/* eslint-disable no-invalid-this */
33
import anyTest, { TestInterface } from 'ava';
44
import fs from 'fs';
5-
import base, { JpexInstance } from '../..';
6-
7-
/* eslint-disable @typescript-eslint/no-unused-vars*/
8-
type NodeModuleType<S extends string, T = any> = T;
9-
type GlobalType<S extends string, T = any> = T;
10-
/* eslint-enable @typescript-eslint/no-unused-vars*/
5+
import base, { JpexInstance, NodeModule, Global } from '../..';
116

127
const test: TestInterface<{
138
jpex: JpexInstance,
@@ -139,26 +134,38 @@ test('resolves array-like dependencies', (t) => {
139134
t.is(value, 'hello');
140135
});
141136

142-
test.failing('resolves a node module', (t) => {
137+
test('resolves a node module', (t) => {
143138
const { jpex } = t.context;
144-
type Fs = NodeModuleType<'fs', typeof fs>;
145-
const value = jpex.resolve<Fs>();
139+
140+
const value = jpex.resolve<NodeModule<'fs'>>();
146141

147142
t.is(value, fs);
148143
});
149144

150145
test('prefers a registered dependency over a node module', (t) => {
151146
const { jpex } = t.context;
152-
type Fs = NodeModuleType<'fs', typeof fs>;
153147
const fakeFs = {};
154-
jpex.factory<Fs>(() => fakeFs as any);
148+
jpex.factory<NodeModule<'fs'>>(() => fakeFs);
155149

156-
const value = jpex.resolve<Fs>();
150+
const value = jpex.resolve<NodeModule<'fs'>>();
157151

158152
t.not(value, fs);
159153
t.is(value, fakeFs);
160154
});
161155

156+
test('does not resolve a node module when disabled', (t) => {
157+
const { jpex: base } = t.context;
158+
const jpex = base.extend({
159+
nodeModules: false,
160+
optional: true,
161+
});
162+
163+
const value = jpex.resolve<NodeModule<'fs'>>();
164+
165+
t.not(value, fs);
166+
t.is(value, void 0);
167+
});
168+
162169
test('resolves a global property', (t) => {
163170
const { jpex } = t.context;
164171

@@ -178,13 +185,12 @@ test('prefers a registered dependency over a global', (t) => {
178185
t.is(value, fakeWindow);
179186
});
180187

181-
test.failing('allows a custom global variable', (t) => {
188+
test('allows a custom global variable', (t) => {
182189
const { jpex } = t.context;
183190
// @ts-ignore
184191
global.foo = 'hello';
185-
type Foo = GlobalType<'foo', any>;
186192

187-
const value = jpex.resolve<Foo>();
193+
const value = jpex.resolve<Global<'foo'>>();
188194

189195
t.is(value, 'hello');
190196
});

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type {
88
FactoryOpts,
99
ResolveOpts,
1010
ServiceOpts,
11+
NodeModule,
12+
Global,
1113
} from './types';
1214

1315
const jpex = new Jpex() as JpexInstance;
@@ -19,12 +21,15 @@ export {
1921
export type {
2022
Lifecycle,
2123
JpexInstance,
24+
JpexInstance as Jpex,
2225
SetupConfig,
2326
NamedParameters,
2427
Precedence,
2528
FactoryOpts,
2629
ServiceOpts,
2730
ResolveOpts,
31+
NodeModule,
32+
Global,
2833
};
2934

3035
export default jpex;

src/resolver/utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import {
1111
isString,
1212
hasLength,
1313
} from '../utils';
14-
import { GLOBAL_TYPE_PREFIX, VOID } from '../constants';
14+
import {
15+
GLOBAL_TYPE_PREFIX,
16+
VOID,
17+
} from '../constants';
1518

1619
const getFromNodeModules = (jpex: JpexInstance, target: string): Factory => {
1720
// in order to stop webpack environments from including every possible
@@ -63,7 +66,8 @@ const getGlobalProperty = (name: string) => {
6366
if (name.startsWith(GLOBAL_TYPE_PREFIX)) {
6467
// most global types will just be the name of the property in pascal case
6568
// i.e. window = Window / document = Document
66-
const inferredName = name.charAt(12).toLowerCase() + name.substr(13);
69+
const len = GLOBAL_TYPE_PREFIX.length;
70+
const inferredName = name.charAt(len).toLowerCase() + name.substr(len + 1);
6771
return global[inferredName];
6872
}
6973
};

src/types/custom.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
export type NodeModule<S extends string, T = any> = T;
3+
export type Global<S extends string, T = any> = T;

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './JpexInstance';
22
export * from './BuiltIns';
3+
export * from './custom';
34

45
export type Lifecycle = 'application' | 'class' | 'instance' | 'none';
56

0 commit comments

Comments
 (0)