├── .codeclimate.yml
├── .eslintrc.js
├── .github
└── workflows
│ └── deploy.yml
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
└── pre-commit
├── .npmignore
├── .nycrc.yaml
├── .prettierignore
├── .prettierrc.js
├── .travis.yml
├── .vscode
└── settings.json
├── @types
└── ava
│ └── index.d.ts
├── CHANGELOG.md
├── README.md
├── babel-plugin.js
├── babel.config.js
├── commitlint.config.js
├── package.json
├── postbuild-checks.js
├── rollup.config.es5.js
├── rollup.config.js
├── rollup.config.src.js
├── src
├── __tests__
│ ├── clearCache.test.ts
│ ├── defer.test.ts
│ ├── encase.test.ts
│ ├── extend.test.ts
│ └── raw.test.ts
├── clearCache.ts
├── constants
│ └── index.ts
├── defer.ts
├── encase.ts
├── index.ts
├── makeJpex.ts
├── registers
│ ├── __tests__
│ │ ├── alias.test.ts
│ │ ├── constant.test.ts
│ │ ├── factory
│ │ │ ├── decorators.test.ts
│ │ │ ├── lifecycle.test.ts
│ │ │ ├── precedence.test.ts
│ │ │ └── validate.test.ts
│ │ └── service.test.ts
│ ├── alias.ts
│ ├── constant.ts
│ ├── factory.ts
│ ├── factoryAsync.ts
│ ├── index.ts
│ └── service.ts
├── resolver
│ ├── __tests__
│ │ ├── async.test.ts
│ │ ├── default.test.ts
│ │ ├── global.test.ts
│ │ ├── node-module.test.ts
│ │ ├── optional.test.ts
│ │ ├── resolve.test.ts
│ │ └── resolveWith.test.ts
│ ├── getFactory.ts
│ ├── index.ts
│ └── resolve.ts
├── types
│ ├── BuiltIns.ts
│ ├── JpexInstance.ts
│ ├── base.ts
│ ├── custom.ts
│ └── index.ts
└── utils
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | engines:
2 | csslint:
3 | enabled: true
4 | duplication:
5 | enabled: true
6 | config:
7 | languages:
8 | javascript:
9 | mass_threshold : 70
10 | checks:
11 | Similar code:
12 | enabled : false
13 | eslint:
14 | enabled: true
15 | checks :
16 | global-require:
17 | enabled: false
18 | no-eq-null:
19 | enabled : false
20 | complexity:
21 | enabled : false
22 | fixme:
23 | enabled: true
24 | ratings:
25 | paths:
26 | - "src/**/*.ts"
27 | - "plugin/**/*.ts"
28 | exclude_paths:
29 | - "**/*.test.ts"
30 | - "**/*.js"
31 | ecmaFeatures:
32 | modules: true
33 | jsx: true
34 | env:
35 | amd: true
36 | browser: true
37 | es6: true
38 | jquery: true
39 | node: true
40 |
41 | # http://eslint.org/docs/rules/
42 | rules:
43 | # Possible Errors
44 | comma-dangle: [2, never]
45 | no-cond-assign: 2
46 | no-console: 0
47 | no-constant-condition: 2
48 | no-control-regex: 2
49 | no-debugger: 2
50 | no-dupe-args: 2
51 | no-dupe-keys: 2
52 | no-duplicate-case: 2
53 | no-empty: 2
54 | no-empty-character-class: 2
55 | no-ex-assign: 2
56 | no-extra-boolean-cast: 2
57 | no-extra-parens: 0
58 | no-extra-semi: 2
59 | no-func-assign: 2
60 | no-inner-declarations: [2, functions]
61 | no-invalid-regexp: 2
62 | no-irregular-whitespace: 2
63 | no-negated-in-lhs: 2
64 | no-obj-calls: 2
65 | no-regex-spaces: 2
66 | no-sparse-arrays: 2
67 | no-unexpected-multiline: 2
68 | no-unreachable: 2
69 | use-isnan: 2
70 | valid-jsdoc: 0
71 | valid-typeof: 2
72 | eqeqeq: [2, allow-null]
73 |
74 | # Best Practices
75 | accessor-pairs: 2
76 | block-scoped-var: 0
77 | complexity: [2, 8]
78 | consistent-return: 0
79 | curly: 0
80 | default-case: 0
81 | dot-location: 0
82 | dot-notation: 0
83 | eqeqeq: 2
84 | guard-for-in: 2
85 | no-alert: 2
86 | no-caller: 2
87 | no-case-declarations: 2
88 | no-div-regex: 2
89 | no-else-return: 0
90 | no-empty-label: 2
91 | no-empty-pattern: 2
92 | no-eq-null: 2
93 | no-eval: 2
94 | no-extend-native: 2
95 | no-extra-bind: 2
96 | no-fallthrough: 2
97 | no-floating-decimal: 0
98 | no-implicit-coercion: 0
99 | no-implied-eval: 2
100 | no-invalid-this: 0
101 | no-iterator: 2
102 | no-labels: 0
103 | no-lone-blocks: 2
104 | no-loop-func: 2
105 | no-magic-number: 0
106 | no-multi-spaces: 0
107 | no-multi-str: 0
108 | no-native-reassign: 2
109 | no-new-func: 2
110 | no-new-wrappers: 2
111 | no-new: 2
112 | no-octal-escape: 2
113 | no-octal: 2
114 | no-proto: 2
115 | no-redeclare: 2
116 | no-return-assign: 2
117 | no-script-url: 2
118 | no-self-compare: 2
119 | no-sequences: 0
120 | no-throw-literal: 0
121 | no-unused-expressions: 2
122 | no-useless-call: 2
123 | no-useless-concat: 2
124 | no-void: 2
125 | no-warning-comments: 0
126 | no-with: 2
127 | radix: 2
128 | vars-on-top: 0
129 | wrap-iife: 2
130 | yoda: 0
131 |
132 | # Strict
133 | strict: 0
134 |
135 | # Variables
136 | init-declarations: 0
137 | no-catch-shadow: 2
138 | no-delete-var: 2
139 | no-label-var: 2
140 | no-shadow-restricted-names: 2
141 | no-shadow: 0
142 | no-undef-init: 2
143 | no-undef: 0
144 | no-undefined: 0
145 | no-unused-vars: 0
146 | no-use-before-define: 0
147 |
148 | # Node.js and CommonJS
149 | callback-return: 2
150 | global-require: 2
151 | handle-callback-err: 2
152 | no-mixed-requires: 0
153 | no-new-require: 0
154 | no-path-concat: 2
155 | no-process-exit: 2
156 | no-restricted-modules: 0
157 | no-sync: 0
158 |
159 | # Stylistic Issues
160 | array-bracket-spacing: 0
161 | block-spacing: 0
162 | brace-style: 0
163 | camelcase: 0
164 | comma-spacing: 0
165 | comma-style: 0
166 | computed-property-spacing: 0
167 | consistent-this: 0
168 | eol-last: 0
169 | func-names: 0
170 | func-style: 0
171 | id-length: 0
172 | id-match: 0
173 | indent: 0
174 | jsx-quotes: 0
175 | key-spacing: 0
176 | linebreak-style: 0
177 | lines-around-comment: 0
178 | max-depth: 0
179 | max-len: 0
180 | max-nested-callbacks: 0
181 | max-params: 0
182 | max-statements: [2, 30]
183 | new-cap: 0
184 | new-parens: 0
185 | newline-after-var: 0
186 | no-array-constructor: 0
187 | no-bitwise: 0
188 | no-continue: 0
189 | no-inline-comments: 0
190 | no-lonely-if: 0
191 | no-mixed-spaces-and-tabs: 0
192 | no-multiple-empty-lines: 0
193 | no-negated-condition: 0
194 | no-nested-ternary: 0
195 | no-new-object: 0
196 | no-plusplus: 0
197 | no-restricted-syntax: 0
198 | no-spaced-func: 0
199 | no-ternary: 0
200 | no-trailing-spaces: 0
201 | no-underscore-dangle: 0
202 | no-unneeded-ternary: 0
203 | object-curly-spacing: 0
204 | one-var: 0
205 | operator-assignment: 0
206 | operator-linebreak: 0
207 | padded-blocks: 0
208 | quote-props: 0
209 | quotes: 0
210 | require-jsdoc: 0
211 | semi-spacing: 0
212 | semi: 0
213 | sort-vars: 0
214 | space-after-keywords: 0
215 | space-before-blocks: 0
216 | space-before-function-paren: 0
217 | space-before-keywords: 0
218 | space-in-parens: 0
219 | space-infix-ops: 0
220 | space-return-throw-case: 0
221 | space-unary-ops: 0
222 | spaced-comment: 0
223 | wrap-regex: 0
224 |
225 | # ECMAScript 6
226 | arrow-body-style: 0
227 | arrow-parens: 0
228 | arrow-spacing: 0
229 | constructor-super: 0
230 | generator-star-spacing: 0
231 | no-arrow-condition: 0
232 | no-class-assign: 0
233 | no-const-assign: 0
234 | no-dupe-class-members: 0
235 | no-this-before-super: 0
236 | no-var: 0
237 | object-shorthand: 0
238 | prefer-arrow-callback: 0
239 | prefer-const: 0
240 | prefer-reflect: 0
241 | prefer-spread: 0
242 | prefer-template: 0
243 | require-yield: 0
244 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['airbnb-typescript-prettier', 'plugin:jest/recommended'],
3 | plugins: ['jest'],
4 | rules: {
5 | 'import/no-named-as-default': 'off',
6 | 'consistent-return': 'off',
7 | 'no-void': 'off',
8 | 'jest/no-disabled-tests': 'warn',
9 | 'jest/no-focused-tests': 'error',
10 | 'jest/no-identical-title': 'error',
11 | 'jest/prefer-to-have-length': 'warn',
12 | 'jest/valid-expect': 'error',
13 | '@typescript-eslint/explicit-module-boundary-types': 'off',
14 | '@typescript-eslint/no-shadow': 'off',
15 | '@typescript-eslint/ban-ts-comment': 'warn',
16 | '@typescript-eslint/no-explicit-any': 'off',
17 | '@typescript-eslint/no-unused-vars': [
18 | 'error',
19 | {
20 | ignoreRestSiblings: true,
21 | argsIgnorePattern: '^_',
22 | },
23 | ],
24 | 'import/prefer-default-export': 'off',
25 | 'jsx-a11y/label-has-associated-control': ['error', { assert: 'either' }],
26 | 'no-param-reassign': 'off',
27 | },
28 | parserOptions: {
29 | project: 'tsconfig.json',
30 | tsconfigRootDir: __dirname,
31 | sourceType: 'module',
32 | },
33 | overrides: [
34 | {
35 | files: ['**/*.test.ts'],
36 | env: {
37 | jest: true,
38 | browser: true,
39 | es6: true,
40 | },
41 | },
42 | ],
43 | settings: {
44 | 'import/resolver': {
45 | node: {
46 | extensions: ['.ts', '.js'],
47 | },
48 | extensions: ['.ts', '.js'],
49 | },
50 | },
51 | };
52 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | # https://github.com/Live-GM/api/blob/fd26d8efa46b101deba47d45061542ac0d76142f/.github/workflows/main.yml
2 | name: CI
3 |
4 | on:
5 | push:
6 | branches: [master]
7 | workflow_dispatch:
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - uses: actions/setup-node@v2
15 | with:
16 | node-version: '14'
17 | - name: 'Build'
18 | run: 'yarn ci'
19 | - name: 'Deploy'
20 | env:
21 | GH_TOKEN: ${{secrets.GH_TOKEN}}
22 | NPM_TOKEN: ${{secrets.NPM_TOKEN}}
23 | run: 'yarn semantic-release'
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .rpt2_cache/
2 | dist/
3 | tmp/
4 | lcov.info
5 |
6 | # Code Coverage
7 | coverage
8 | # Logs
9 | logs
10 | *.log
11 | npm-debug.log*
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 |
24 | # nyc test coverage
25 | .nyc_output
26 |
27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
28 | .grunt
29 |
30 | # node-waf configuration
31 | .lock-wscript
32 |
33 | # Compiled binary addons (http://nodejs.org/api/addons.html)
34 | build/Release
35 |
36 | # Dependency directories
37 | node_modules
38 | jspm_packages
39 |
40 | # Optional npm cache directory
41 | .npm
42 |
43 | # Optional REPL history
44 | .node_repl_history
45 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn commitlint --edit "$1"
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn lint-staged;
5 | yarn test --watchAll=false --onlyChanged;
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | .*
3 |
4 | !dist/**
5 | !plugin/**
6 | !babel-plugin.js
7 |
--------------------------------------------------------------------------------
/.nycrc.yaml:
--------------------------------------------------------------------------------
1 | lines: 91
2 | statements: 91
3 | branches: 82
4 | functions: 95
5 | all: true
6 | include:
7 | - 'src/**/*.*'
8 | exclude:
9 | - 'src/**/*.test.*'
10 | - '**/__tests__/**/*'
11 | report-dir: coverage
12 | check-coverage: true
13 | reporter:
14 | - 'html'
15 | - 'text-summary'
16 | - 'lcov'
17 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | @types
3 | coverage
4 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | singleQuote: true,
3 | trailingComma: 'all',
4 | };
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 10
4 | install:
5 | - yarn install
6 |
7 | - npm install codeclimate-test-reporter -g
8 | script:
9 | - yarn build
10 | - yarn lint
11 | - yarn test
12 | - yarn coverage
13 |
14 | after_script:
15 | - codeclimate-test-reporter < coverage/lcov.info
16 |
17 | deploy:
18 | provider: script
19 | skip_cleanup: true
20 | script:
21 | - yarn semantic-release
22 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "editor.formatOnSave": true
4 | }
5 |
--------------------------------------------------------------------------------
/@types/ava/index.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | declare global {
4 | interface SymbolConstructor {
5 | readonly observable: symbol;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | Further change logs can be found on the [releases](https://github.com/jpex-js/jpex/releases) page
4 |
5 | ### 4.0.0
6 |
7 | - global dependencies such as `Window` and `Document` are now automatically resolved (unless you register your own dependency of the same name)
8 | - you can now control dependency resolution with config flags `nodeModules` and `globals`
9 | - you can also specify whether dependencies should be optional-by-default with an `optional` flag
10 | - dependencies are no longer determined by reading the factory function. Either use `TS` inference, or explicitly pass an array of deps
11 | - changed format of `.factory` `.service` and `.resolve`
12 | - you can now pass an `opts` parameter when registering a factory i.e. `.factory(fn, { lifecycle: 'none' })`
13 | - you can now pass an `opts` parameter when resolving i.e. `.resolve({ optional: true })`
14 | - `resolveWith` now has a nicer syntax for ts inference: `.resolveWith([ 'val1', 'val2' ])`. The original syntax i.e. `.resolveWith({ dep1: 'val1' })` is still valid.
15 | - removed the built-in dependency `$options`. You can no longer do `.resolve({ foo: 'someValue' })`
16 | - removed the built-in dependency `$resolve`
17 | - `precedence` option lets you determine if a factory should overwrite an existing factory or not
18 | - Support for IE11 has been dropped by default. If you want a fully ES5-compatible version, you can import `jpex/dist/es5.js`
19 | - You can now alias 2 types i.e. `jpex.alias()`
20 |
21 | #### Breaking Changes
22 |
23 | - if you attempt to resolve a global like `Window` without registering it first, rather than throw an error, you will now get the global variable
24 | - You can no longer do `jpex.factory('foo', (depA, depB) => { ... })` as we no longer parse the function and extract the dependencies.
25 | - rather than calling `.factory(fn).lifecycle.application()` you must now do `.factory(fn, { lifecycle: 'application' })`
26 | - clearCache now takes an arity of names, i.e. `clearCache('a', 'b', 'c')` whereas previous it took an array
27 | - you can no longer mix ts and js modes i.e. you cannot do `.factory([ 'b' ], fn)`
28 | - `Lifecycle` is now a type rather than an enum
29 | - wrapping a name in `__` will no longer make it optional, you must explicitly pass the optional flag
30 | - `$options` and `$resolve` functionality have been removed
31 | - If you want to support IE11 you will need to import `jpex/dist/es5.js` or create an alias for it
32 |
33 | ### 3.5.1
34 |
35 | - building with webpack was giving warnings about `require` being used which meant it couldn't make optimizations
36 |
37 | ### 3.5.0
38 |
39 | - add some deprecation warnings for pre-4.0.0 changes
40 |
41 | ### 3.4.0
42 |
43 | - clearCache now supports type inference
44 | - you can now pass `publicPath: true` and it will use the `name` property of your app's `package.json` as the public path
45 | - built in deps `$options` `$namedParameters` and `$resolve` now have corresponding type exports `Options` `NamedParameters` and `Resolve`
46 |
47 | ### 3.3.3
48 |
49 | - array dependencies were being incorrectly flattened
50 |
51 | ### 3.3.1
52 |
53 | - publicPath relative imports was checking the incorrect path property
54 |
55 | ### 3.3.0
56 |
57 | - add `jpex.extend` option: `inherit` (defaults to `true`). Determines if the extended container should inherit factories
58 |
59 | ### 3.2.3
60 |
61 | - publicPath should be operate on relative `.` imports
62 |
63 | ### 3.2.2
64 |
65 | - publicPath option was not working correctly for complex relative imports
66 |
67 | ### 3.2.1
68 |
69 | - support `useResolve` taking a dependency array
70 |
71 | ## 3.2.0
72 |
73 | - add `jpex.raw` function for extracting a factory function
74 | - add publicPath babel config option
75 | - add support for react-jpex's `useResolve` method
76 |
77 | ## 3.1.0
78 |
79 | - global types like `Window` and `Document` can now be used to register dependencies
80 |
81 | ## 3.0.1
82 |
83 | - encase now caches the wrapped function for better performance
84 | - alias is now bidirectional, so it determines which is the alias and which is the original
85 | - default lifecycle should be inherited from the parent
86 | - removed decorators in favour of self-invoking factories. Decorators were not intended to reach v3. You can decorate a factory from a parent container by doing `jpex.factory('foo', [ 'foo' ], (foo) => { /* decorate here */ })`
87 |
88 | ## 3.0.0
89 |
90 | - Complete rewrite of the entire library
91 | - Typescript support
92 | - Jpex is no longer a class emulator as native classes have reached a pretty stable level. It should now be considered as purely a container IOC tool
93 | - Things like `jpex.$resolve` and `jpex.$encase` have been renamed to `jpex.resolve` and `jpex.encase`. This is the tip of the iceberg in terms of changes. Jpex is no longer a constructor and can't be instantiated. `jpex.extend` no longer accepts class-related properties, etc.
94 | - Set factory dependencies via chaining i.e. `jpex.factory('foo', fn).dependencies('bah', 'baz')`
95 | - Available plugin hooks are exported i.e. `import jpex, { Hook } from 'jpex'`
96 | - Alias option lets you create a dependency that returns another dependency
97 | - Plugin functionality has been removed as 90% of its use was for intercepting class lifecycle steps
98 | - `resolve` now only returns a single dependency and doesn't accept named parameters. For named params you must call `resolveWith`
99 | - Type inference can now be used to register and resolve dependencies i.e. `jpex.factory((log: ILog) => { ... })` and `jpex.resolve()` `jpex.encase((thing: IThing) => () => { ... })` etc.
100 | - ^ requires a babel plugin to work, which can be imported from `jpex/babel-plugin`
101 | - You can get a concrete name of an inferred dependency using `infer`: `const name = jpex.infer()`
102 |
103 | ## 2.1.0
104 |
105 | - if an option in the `properties` config is null, jpex will no longer throw an error
106 | - Passing `$options` into a `Jpex as a Service` service now works
107 | - `Jpex.register.service().bindToInstance()` allows you to bind dependencies to a service instance
108 | - `Jpex.$encase` method allos you to wrap a function with a number of dependencies
109 |
110 | ## 2.0.0
111 |
112 | ### Features
113 |
114 | - Can now pass in a `config` option when extending a class. Any properties of the config option will be used as default values for that class and its descendants.
115 | - The default lifecycle for factories registered against a class can now be configured using the `defaultLifecycle` option.
116 | - Methods option has been added (which replaces the _prototype_ option from v1).
117 | - Properties option has been added, allowing you to predefine getters, setters, and watchers on any instance properties.
118 | - the `bindToInstance` option can now accept a nested property name, i.e. `bindToInstance : 'foo.bah'`
119 | - Node-specific code has been isolated so the core _jpex_ module can be included in any webpack/browserify build. (_see depcrecation of jpex-web below_)
120 | - Added a pre-compiled build of Jpex at `jpex/dist/jpex.js` and `jpex/dist/jpex.min.js`
121 | - Default factories (`$timeout`, `$promise`, etc.) have been separated from the core module. They now must be installed separately from the **jpex-defaults**, **jpex-node**, and **jpex-web** packages.
122 | - The `$resolve` method is now available as a static method on every class, so dependencies can be resolved with `Class.$resolve(name)`. This allows for **Jpex** to be used as a container rather than forcing the class instantiation pattern.
123 | - `$resolve` can be called with an array of dependencies to resolve instead of just one.
124 | - Cached factories (i.e. with a `class` or `application` lifecycle) can be cleared with `Class.$clearCache()`.
125 | - Added `decorators` that allow a factory to be altered before being resolved. Can be registered like normal factories i.e. `Class.register.decorator(name, fn)`
126 | - A complete plugin API has been created that allows plugins to hook into a number of lifecycle events.
127 |
128 | ### Breaking Changes
129 |
130 | - The `prototype` option has been replaced with `methods`
131 | - The **jpex-web** version of Jpex has been deprecated. Instead, Jpex can be `required`'d with _webpack/browserify_, or a web-safe js file can be found at `jpex/dist/jpex.js/`
132 | - Internal variables have been renamed. e.g. `Class._factories` is now `Class.$$factories`.
133 | - Default factories (`$timeout`, `$promise`, etc.) have been separated from the core module. They now must be installed separately from the **jpex-defaults**, **jpex-node**, and **jpex-web** packages.
134 | - After deprecating its use in v1.3.0, the `singleton` option has been removed from factory registration. `Class.register.factory(name, fn, true/false)` should now be written as `Class.register.factory(name, fn).lifecycle.application()`
135 | - Following depcrecation in v1.4.0, the static methods `Typeof` and `Copy` have been removed.
136 | - Factory registration methods have been renamed to camelCase: `Jpex.Register.Factory` becomes `Jpex.register.factory`, for example.
137 | - `Interfaces` have been completely removed from the module. This was an experimental feature that in the end was more overhead than it was worth.
138 | - A number of spurious factory types have been removed: _enum, errorType, file, folder, interface, nodeModule_ - although the _nodeModule_ factory type is still available via the **jpex-node** package as `Class.register.node_module`.
139 | - Ancestoral dependencies have been removed so depending on `["^someParentFactory"]` will no longer work. The equivalent can now be achieved with _decorators_.
140 |
141 | ## 1.4.1
142 |
143 | ### Bugs
144 |
145 | - `$copy.extend` no longer combines arrays, but instead replaces the previous array value.
146 | - `$timeout $immediate $interval $tick` bug fixed when attaching to a class instance.
147 | - Added a `clear()` method to the timer factories that clear the respective timeouts.
148 |
149 | ## 1.4.0
150 |
151 | ### Features
152 |
153 | - $typeof factory is available which returns the type of an object.
154 | - $copy factory allows you create a deep or shallow copy of an object, or combine multiple objects.
155 | - $itypeof and $icopy interfaces
156 | - The static methods Jpex.Typeof and Jpex.Copy have been deprecated and will be removed in a future release.
157 | - $resolve factory which allows lazy loading of dependencies.
158 |
159 | ### Breaking Changes
160 |
161 | - Calling `Class()` is now the same as calling `new Class()` so calls like `Class.call(obj)===obj` will no longer work.
162 |
163 | ## 1.3.1
164 |
165 | ### Bugs
166 |
167 | - Fixed issues where `require`-based functions were not requiring from the correct location.
168 |
169 | ## 1.3.0
170 |
171 | ### Features
172 |
173 | - Interfaces functionality added
174 | - Registering a factory returns an object with additional option methods (currently only contains the _interface()_ method)
175 | - It is now possible to specify the life cycle of a factory or service using the `.lifecycle.x()` syntax. Possible options are `application`, `class`, `instance`, `none`
176 | - Due to the introduction of life cycles, the _singleton_ parameter has been deprecated.
177 |
178 | ### Breaking Changes
179 |
180 | - All $ factories now have interfaces (i.e. _$ipromise_). If you have overwritten a default factory that is used by another default factory, it will need to include the interface in order to work. i.e. _$fs_ used to depend on _$promise_ but it now depends on $ipromise.
181 |
182 | ## 1.2.0
183 |
184 | ### Features
185 |
186 | - Added detailed documentation
187 | - $error factory and $errorFactory factory
188 | - ErrorType Factory i.e. `jpex.Register.ErrorType('Custom')`
189 | - Ancestoral dependencies i.e. `['^$errorFactory']`
190 | - Deprecated jpex-fs as it is now included in the standard jpex build
191 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 
2 |
3 | ## Easy Dependency Injection
4 |
5 | [](https://travis-ci.org/jackmellis/jpex)
6 | [](https://badge.fury.io/js/jpex)
7 | [](https://codeclimate.com/github/jackmellis/jpex)
8 | [](https://codeclimate.com/github/jackmellis/jpex/coverage)
9 |
10 | Jpex is an Inversion of Control framework. Register dependencies on a container, then resolve them anywhere in your application. The real magic of jpex is its ability to infer dependencies using the magic of babel and typescript...
11 |
12 | ## Contents
13 |
14 | - [Getting Started](#getting-started)
15 | - [Registering Dependencies](#registering-dependencies)
16 | - [Consuming Dependencies](#consuming-dependencies)
17 | - [API](#api)
18 | - [jpex](#jpex)
19 | - [constant](#jpexconstant)
20 | - [factory](#jpexfactory)
21 | - [lifecycle](#lifecycle)
22 | - [precedence](#precedence)
23 | - [bindToInstance](#bindtoinstance)
24 | - [alias](#alias)
25 | - [service](#jpexservice)
26 | - [factoryAsync](#jpexfactoryAsync)
27 | - [alias](#jpexalias)
28 | - [resolve](#jpexresolve)
29 | - [optional](#optional)
30 | - [with](#with)
31 | - [resolveAsync][#jpexresolveasync]
32 | - [resolveWith](#jpexresolvewith)
33 | - [encase](#jpexencase)
34 | - [defer](#jpexdefer)
35 | - [extend](#jpexextend)
36 | - [inherit](#inherit)
37 | - [lifecycle](#lifecycle-1)
38 | - [precedence](#precedence-1)
39 | - [optional](#optional-1)
40 | - [nodeModules](#nodemodules)
41 | - [globals](#globals)
42 | - [raw](#jpexraw)
43 | - [clearCache](#jpexclearcache)
44 | - [infer](#jpexinfer)
45 | - [Types](#types)
46 | - [Jpex](#jpex)
47 | - [NodeModule](#nodemodule)
48 | - [Global](#global)
49 | - [caveats](#caveats)
50 | - [react](#react)
51 | - [Vanilla JS mode](#vanilla-js-mode)
52 |
53 | ## Getting Started
54 |
55 | ### Install
56 |
57 | ```
58 | npm install jpex
59 | ```
60 |
61 | ### Plugin
62 |
63 | Jpex uses babel to infer type interfaces at build time. You can do this with one of several methods:
64 | [@jpex-js/babel-plugin](https://github.com/jpex-js/babel-plugin)
65 | [@jpex-js/rollup-plugin](https://github.com/jpex-js/rollup-plugin)
66 | [@jpex-js/webpack-plugin](https://github.com/jpex-js/webpack-loader)
67 |
68 | Jpex comes bundled with the `@jpex-js/babel-plugin` so you can easily get started with a `.babelrc` like this:
69 |
70 | ```js
71 | // .bablerc
72 | {
73 | presets: [ '@babel/preset-typescript' ],
74 | plugins: [ 'jpex/babel-plugin' ]
75 | }
76 | ```
77 |
78 | ### Usage
79 |
80 | ```ts
81 | import jpex from 'jpex';
82 | import { Foo, Bah } from './types';
83 |
84 | jpex.factory((bah: Bah) => bah.baz);
85 |
86 | const foo = jpex.resolve();
87 | ```
88 |
89 | ---
90 |
91 | ## Registering Dependencies
92 |
93 | Services and factories are small modules or functions that provide a common piece of functionality.
94 |
95 | ### factories
96 |
97 | ```ts
98 | type MyFactory = {};
99 |
100 | jpex.factory(() => {
101 | return {};
102 | });
103 | ```
104 |
105 | ### services
106 |
107 | ```ts
108 | class MyService = {
109 | method: (): any {
110 | // ...
111 | }
112 | };
113 |
114 | jpex.service(MyService);
115 | ```
116 |
117 | ### constants
118 |
119 | ```ts
120 | type MyConstant = string;
121 | jpex.constant('foo');
122 | ```
123 |
124 | ---
125 |
126 | ## Consuming Dependencies
127 |
128 | ### resolve
129 |
130 | You can then resolve a dependency anywhere in your app:
131 |
132 | ```ts
133 | const value = jpex.resolve();
134 | ```
135 |
136 | ### dependent factories
137 |
138 | A factory can request another dependency and jpex will resolve it on the fly:
139 |
140 | ```ts
141 | jpex.constant('foo');
142 |
143 | jpex.factory((myConstant: MyConstant) => {
144 | return `my constant is ${myConstant}`;
145 | });
146 |
147 | jpex.resolve(); // "my constant is foo"
148 | ```
149 |
150 | ### encase
151 |
152 | Or you can _encase_ a regular function so that dependencies are injected into it when called:
153 |
154 | ```ts
155 | const fn = jpex.encase((value: MyFactory) => (arg1, arg2) => {
156 | return value + arg1 + arg2;
157 | });
158 |
159 | fn(1, 2);
160 | ```
161 |
162 | ---
163 |
164 | ## API
165 |
166 | ### jpex
167 |
168 | #### jpex.constant
169 |
170 | ```ts
171 | (obj: T): void
172 | ```
173 |
174 | Registers a constant value.
175 |
176 | #### jpex.factory
177 |
178 | ```ts
179 | (fn: (...deps: any[] => T), opts?: object): void
180 | ```
181 |
182 | Registers a factory function against the given type. Jpex works out the types of `deps` and injects them at resolution time, then returns the resulting value `T`.
183 |
184 | ```ts
185 | type GetStuff = () => Promise;
186 |
187 | jpex.factory((window: Window) => () => window.fetch('/stuff));
188 | ```
189 |
190 | > By default jpex will automatically resolve global types like Window or Document. In a node environment it will also be able to resolve node_modules.
191 |
192 | The following options can be provided for both factories and services:
193 |
194 | ##### lifecycle
195 |
196 | ```ts
197 | 'singleton' | 'container' | 'invocation' | 'none';
198 | ```
199 |
200 | Determines how long the factory is cached for once resolved.
201 |
202 | - `singleton` is resolved forever across all containers
203 | - `container` is resolved for the current jpex container, if you `.extend()` the new container will resolve it again
204 | - `invocation` if you request the same dependency multiple times in the same `resolve` call, this will use the same value, but the next time you call `resolve` it will start again
205 | - `none` never caches anything
206 |
207 | The default lifecycle is `container`
208 |
209 | ##### precedence
210 |
211 | ```ts
212 | 'active' | 'passive';
213 | ```
214 |
215 | Determines the behavior when the same factory is registered multiple times.
216 |
217 | - `active` overwrites the existing factory
218 | - `passive` prefers the existing factory
219 |
220 | Defaults to `active`
221 |
222 | ##### bindToInstance
223 |
224 | ```ts
225 | boolean;
226 | ```
227 |
228 | Specifically for services, automatically binds all of the dependencies to the service instance.
229 |
230 | ##### alias
231 |
232 | ```ts
233 | string | string[]
234 | ```
235 |
236 | Creates aliases for the factory. This is essentially just shorthand for writing `jpex.factory(...); jpex.alias(...);`
237 |
238 | #### jpex.service
239 |
240 | ```ts
241 | (class: ClassWithConstructor, opts?: object): void
242 | ```
243 |
244 | Registers a service. A service is like a factory but instantiates a class instead.
245 |
246 | ```ts
247 | class Foo {
248 | constructor(window: Window) {
249 | // ...
250 | }
251 | }
252 |
253 | jpex.service(Foo);
254 | ```
255 |
256 | If a class `implements` an interface, you can actually use it to resolve the class:
257 |
258 | ```ts
259 | interface IFoo {}
260 |
261 | class Foo implements IFoo {}
262 |
263 | jpex.service(Foo);
264 |
265 | const foo = jpex.resolve();
266 | ```
267 |
268 | #### jpex.factoryAsync
269 |
270 | ```ts
271 | (fn: (...deps: any[] => Promise), opts?: object): void
272 | ```
273 |
274 | Registers an asynchronous factory. The factory should return a promise that resolves to type `T`. If you are using async factories, you should ensure you are using `resolveAsync`, this will wait for asynchronous factories to resolve before passing them to their dependents.
275 |
276 | #### jpex.alias
277 |
278 | ```ts
279 | (alias: string): void
280 | ```
281 |
282 | Creates an alias to another factory
283 |
284 | #### jpex.resolve
285 |
286 | ```ts
287 | (opts?: object): T
288 | ```
289 |
290 | Locates and resolves the desired factory.
291 |
292 | ```ts
293 | const foo = jpex.resolve();
294 | ```
295 |
296 | The following options can be provided for both `resolve` and `resolveWith`:
297 |
298 | ##### optional
299 |
300 | ```ts
301 | boolean;
302 | ```
303 |
304 | When `true` if the dependency cannot be found or resolved, it will just return `undefined` rather than throwing an error.
305 |
306 | ### default
307 |
308 | ```ts
309 | any;
310 | ```
311 |
312 | Provide a fallback value if the dependency cannot be found.
313 |
314 | #### jpex.resolveAsync
315 |
316 | ```ts
317 | (opts?: object): Promise
318 | ```
319 |
320 | Locates and resolves the desired factory. Unlike `resolve`, this method returns a promise and allows all asynchronous dependents to resolve before returning the final value.
321 |
322 | #### jpex.resolveWith
323 |
324 | ```ts
325 | (values: Rest, opts?: object): T
326 | ```
327 |
328 | Resolves a factory while substituting dependencies for the given values
329 |
330 | ```ts
331 | const foo = jpex.resolveWith(['bah', 'baz']);
332 | ```
333 |
334 | #### jpex.resolveWithAsync
335 |
336 | ```ts
337 | (values: Rest, opts?: object): Promise
338 | ```
339 |
340 | This is an asynchronous version of `resolveWith` and returns a promise that will resolve all dependent factories.
341 |
342 | #### jpex.encase
343 |
344 | ```ts
345 | (...deps: any[]): (...args: any[]) => any
346 | ```
347 |
348 | Wraps a function and injects values into it, it then returns the inner function for use.
349 |
350 | ```ts
351 | const getStuff = jpex.encase((http: Http) => (thing: string) => {
352 | return http(`api/app/${thing}`);
353 | });
354 |
355 | await getStuff('my-thing');
356 | ```
357 |
358 | The dependencies are only resolved at call time and are then cached and reused on subsequent calls (based on their lifecycles).
359 |
360 | To help with testing, the returned function also has an `encased` property containng the outer function
361 |
362 | ```ts
363 | getStuff.encased(fakeHttp)('my-thing');
364 | ```
365 |
366 | > If you include any factoryAsync dependencies, jpex will ensure the encased function returns a promise as well.
367 |
368 | #### jpex.defer
369 |
370 | ```ts
371 | (): T
372 | ```
373 |
374 | Provided `T` is a function type, returns `T` but only resolves its dependencies at call time.
375 |
376 | ```ts
377 | const getStuff = jpex.defer();
378 |
379 | await getStuff('my-thing'); // will only resolve the GetStuff factory here
380 | ```
381 |
382 | #### jpex.extend
383 |
384 | ```ts
385 | (config?: object): Jpex
386 | ```
387 |
388 | creates a new container, using the current one as a base.
389 |
390 | This is useful for creating isolated contexts or not poluting the global container.
391 |
392 | The default behavior is to pass down all config options and factories to the new container.
393 |
394 | ##### inherit
395 |
396 | `boolean`
397 |
398 | Whether or not to inherit config and factories from its parent
399 |
400 | ##### lifecycle
401 |
402 | `'singleton' | 'container' | 'invocation' | 'none'`
403 |
404 | The default lifecycle for factories. `container` by default
405 |
406 | ##### precedence
407 |
408 | `'active' | 'passive'`
409 |
410 | The default precedence for factories. `active` by default
411 |
412 | ##### optional
413 |
414 | `boolean`
415 |
416 | Whether factories should be optional by default
417 |
418 | ##### nodeModules
419 |
420 | `boolean`
421 |
422 | When trying to resolve a dependency, should it attempt to import the it from node modules?
423 |
424 | ##### globals
425 |
426 | `boolean`
427 |
428 | When trying to resolve a dependency, should it check for it on the global object?
429 |
430 | #### jpex.raw
431 |
432 | ```ts
433 | () => (...deps: any[]) => T;
434 | ```
435 |
436 | Returns the raw factory function, useful for testing.
437 |
438 | #### jpex.clearCache
439 |
440 | ```ts
441 | () => void
442 | () => void
443 | ```
444 |
445 | Clears the cache of resolved factories. If you provide a type, that specific factory will be cleared, otherwise it will clear all factories.
446 |
447 | #### jpex.infer
448 |
449 | ```ts
450 | () => string;
451 | ```
452 |
453 | 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`
454 |
455 | ### Types
456 |
457 | #### Jpex
458 |
459 | This is the type definition for the jpex container
460 |
461 | #### NodeModule
462 |
463 | This is a special type that lets you automatically inject a node module with type inference.
464 |
465 | For example:
466 |
467 | ```ts
468 | import jpex, { NodeModule } from 'jpex';
469 |
470 | // this will resolve to the fs module without you having to explicitly register it as a dependency
471 | const fs = jpex.resolve>();
472 | ```
473 |
474 | The default return type will be `any` but you can specify one explicitly with the second type parameter:
475 |
476 | ```ts
477 | import type fstype from 'fs';
478 | import jpex, { NodeModule } from 'jpex';
479 |
480 | const fs = jpex.resolve>();
481 | ```
482 |
483 | #### Global
484 |
485 | This is another special type that lets you automatically inject a global property with type inference.
486 |
487 | For built-in types you can do this without any helpers:
488 |
489 | ```ts
490 | import jpex from 'jpex';
491 |
492 | const navigator = jpex.resolve();
493 | ```
494 |
495 | But for custom globals, or properties that don't have built-in types, you can use the `Global` type:
496 |
497 | ```ts
498 | import jpex, { Global } from 'jpex';
499 |
500 | const analytics = jpex.resolve>();
501 | ```
502 |
503 | ## caveats
504 |
505 | There are a few caveats to be aware of:
506 |
507 | - Only named types/interfaces are supported so you can't do `jpex.factory<{}>()`
508 | - There is not yet a concept of extending types, so if you do `interface Bah extends Foo {}` you can't then try to resolve `Foo` and expect to be given `Bah`, they are treated as 2 separate things
509 | - The check for a jpex instance is based on the variable name, so you can't do `const jpex2 = jpex; jpex2.constant(foo);` without explicitly adding `jpex2` to the plugin config
510 | - Similiarly you can't do `const { factory } = jpex`
511 |
512 | ## react
513 |
514 | Jpex is a really good fit with React as it offers a good way to inject impure effects into pure components. There is a `react-jpex` library that exposes a few hooks.
515 |
516 | ```tsx
517 | import React from 'react';
518 | import { useResolve } from 'react-jpex';
519 | import { SaveData } from '../types';
520 |
521 | const MyComponent = (props) => {
522 | const saveData = useResolve();
523 |
524 | const onSubmit = () => saveData(props.values);
525 |
526 | return (
527 |
528 |
529 |
530 |
531 | );
532 | };
533 | ```
534 |
535 | And this pattern also makes it really easy to isolate a component from its side effects when writing tests:
536 |
537 | ```tsx
538 | import { Provider } from 'react-jpex';
539 | // create a stub for the SaveData dependency
540 | const saveData = stub();
541 |
542 | render(
543 | jpex.constant(saveData)}
547 | >
548 | {/* when we render MyComponent, it will be given our stubbed dependency */}
549 |
550 | ,
551 | );
552 |
553 | // trigger the compnent's onClick
554 | doOnClick();
555 |
556 | expect(saveData.called).to.be.true;
557 | ```
558 |
559 | ## node
560 |
561 | Jpex was originally written for node and works out of the box in a node environment. However, the `@jpex-js/node` library was created to provide similar benefits to `react-jpex`. It can be used to create a provider/context pattern where the top level application (or testing environment) can choose which dependencies to inject into the app.
562 |
563 | ```ts
564 | import { encase } from '@jpex-js/node';
565 |
566 | const myFunction = encase((dep: MyDep) => () => {
567 | const someValue = dep();
568 | });
569 | ```
570 |
571 | ```ts
572 | import { provide } from '@jpex-js/node';
573 |
574 | provide((jpex) => {
575 | jpex.constant(mockDep);
576 |
577 | myFunction(); // will be called with mockDep
578 | });
579 | ```
580 |
581 | ## Vanilla JS mode
582 |
583 | 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:
584 |
585 | ```ts
586 | const { jpex } = require('jpex');
587 |
588 | jpex.constant('foo', 'foo');
589 | jpex.factory('bah', ['foo'], (foo) => foo + 'bah');
590 |
591 | const value = jpex.resolve('bah');
592 | ```
593 |
594 | Jpex uses language features supported by the latest browsers, but if you need to support IE11 et al. you can import from 'jpex/dist/es5` (or create an alias in your build process)
595 |
--------------------------------------------------------------------------------
/babel-plugin.js:
--------------------------------------------------------------------------------
1 | module.exports = require('@jpex-js/babel-plugin');
2 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const isTest = process.env.NODE_ENV === 'test';
2 |
3 | module.exports = {
4 | presets: [
5 | '@babel/preset-typescript',
6 | [
7 | '@babel/preset-env',
8 | {
9 | targets: {
10 | browsers: ['> 2%'],
11 | },
12 | modules: isTest ? 'commonjs' : false,
13 | useBuiltIns: false,
14 | loose: true,
15 | },
16 | ],
17 | ],
18 | plugins: [
19 | '@babel/plugin-proposal-class-properties',
20 | ['@babel/plugin-proposal-optional-chaining', { loose: true }],
21 | ['@babel/plugin-proposal-nullish-coalescing-operator', { loose: true }],
22 | [
23 | '@jpex-js/babel-plugin',
24 | {
25 | publicPath: true,
26 | identifier: ['jpex', 'jpex2', 'jpex3', 'base', 'base2'],
27 | },
28 | ],
29 | ],
30 | };
31 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | };
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jpex",
3 | "version": "0.0.0",
4 | "description": "Javascript Prototype Extension",
5 | "main": "dist/cjs/jpex.js",
6 | "module": "dist/es/jpex.js",
7 | "types": "dist/ts/index.d.ts",
8 | "scripts": {
9 | "clear-cache": "rm -rf node_modules/.cache",
10 | "test": "jest",
11 | "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
12 | "coverage": "jest --coverage",
13 | "lint": "eslint './src/**/*.ts' --fix && tsc --noEmit",
14 | "build:prepare": "rm -rf dist",
15 | "build:js": "rollup --config ./rollup.config.js",
16 | "build:ts": "tsc -d --outDir dist/ts --emitDeclarationOnly --downlevelIteration ./src/index.ts",
17 | "build:post": "node ./postbuild-checks.js",
18 | "build": "yarn build:prepare && yarn build:js && yarn build:ts && yarn build:post",
19 | "prepublishOnly": "yarn build",
20 | "semantic-release": "semantic-release",
21 | "ci": "yarn install && yarn lint && yarn test && yarn build",
22 | "prepare": "husky install"
23 | },
24 | "lint-staged": {
25 | "src/**/*.{js,ts}": [
26 | "eslint",
27 | "prettier --write --ignore-unknown"
28 | ]
29 | },
30 | "repository": {
31 | "type": "git",
32 | "url": "https://github.com/jpex-js/jpex.git"
33 | },
34 | "author": "Jack Ellis",
35 | "license": "ISC",
36 | "bugs": {
37 | "url": "https://github.com/jpex-js/jpex/issues"
38 | },
39 | "homepage": "https://github.com/jpex-js/jpex",
40 | "devDependencies": {
41 | "@babel/core": "^7.7.7",
42 | "@babel/plugin-proposal-class-properties": "^7.8.3",
43 | "@babel/preset-env": "^7.7.7",
44 | "@babel/preset-typescript": "^7.8.3",
45 | "@commitlint/cli": "^8.3.4",
46 | "@commitlint/config-conventional": "^8.3.4",
47 | "@types/jest": "^26.0.20",
48 | "@types/node": "^14.0.26",
49 | "@typescript-eslint/eslint-plugin": "^4.2.0",
50 | "@typescript-eslint/parser": "^4.2.0",
51 | "eslint": "^7.5.0",
52 | "eslint-config-airbnb-typescript-prettier": "^4.1.0",
53 | "eslint-plugin-import": "^2.22.1",
54 | "eslint-plugin-jest": "^24.1.5",
55 | "husky": "^5.1.3",
56 | "jest": "^26.6.3",
57 | "lint-staged": "^10.5.4",
58 | "prettier": "^2.2.1",
59 | "rollup": "^2.23.0",
60 | "rollup-plugin-babel": "^4.3.3",
61 | "rollup-plugin-cleanup": "^3.2.1",
62 | "rollup-plugin-node-resolve": "^5.2.0",
63 | "semantic-release": "^17.1.1",
64 | "typescript": "^4.0.3"
65 | },
66 | "dependencies": {
67 | "@jpex-js/babel-plugin": "^1.3.0"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/postbuild-checks.js:
--------------------------------------------------------------------------------
1 | const { promises: fs } = require('fs');
2 | const jpex = require('./dist/cjs/jpex');
3 |
4 | const expectedFiles = [
5 | 'dist/cjs/jpex.js',
6 | 'dist/es/jpex.js',
7 | 'dist/ts/index.d.ts',
8 | 'dist/ts/types/index.d.ts',
9 | ];
10 | const expectedExports = [
11 | ['default', '[object Object]'],
12 | ['jpex', '[object Object]'],
13 | ];
14 |
15 | const run = async () => {
16 | for (let i = 0; i < expectedFiles.length; i++) {
17 | const target = expectedFiles[i];
18 |
19 | try {
20 | await fs.stat(target);
21 | } catch (e) {
22 | throw new Error(`Unable to verify file ${target}`);
23 | }
24 | }
25 |
26 | for (let i = 0; i < expectedExports.length; i++) {
27 | const [key, type] = expectedExports[i];
28 | if (jpex[key] === void 0) {
29 | throw new Error(`${key} was not exported`);
30 | }
31 | const actualType = Object.prototype.toString.call(jpex[key]);
32 | if (actualType !== type) {
33 | throw new Error(
34 | `Expected type of ${key} to be ${type} but it was ${actualType}`,
35 | );
36 | }
37 | }
38 |
39 | console.log('Everything looks good to me');
40 | };
41 |
42 | run();
43 |
--------------------------------------------------------------------------------
/rollup.config.es5.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import localResolve from 'rollup-plugin-node-resolve';
3 | import cleanup from 'rollup-plugin-cleanup';
4 |
5 | export default {
6 | input: 'src/index.ts',
7 | output: [
8 | {
9 | file: 'dist/es5.js',
10 | format: 'cjs',
11 | exports: 'named',
12 | },
13 | ],
14 | plugins: [
15 | localResolve({
16 | extensions: ['.js', '.ts'],
17 | }),
18 | babel({
19 | exclude: 'node_modules/**',
20 | extensions: ['.js', '.ts'],
21 | presets: [
22 | [
23 | '@babel/preset-env',
24 | {
25 | targets: {
26 | browsers: ['last 2 versions', 'safari >= 7'],
27 | },
28 | modules: false,
29 | useBuiltIns: false,
30 | loose: true,
31 | },
32 | ],
33 | ],
34 | }),
35 | cleanup({
36 | extensions: [ 'js','ts' ],
37 | sourcemap: false,
38 | }),
39 | ],
40 | };
41 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import src from './rollup.config.src';
2 | import es5 from './rollup.config.es5';
3 |
4 | export default [src, es5];
5 |
--------------------------------------------------------------------------------
/rollup.config.src.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import localResolve from 'rollup-plugin-node-resolve';
3 | import cleanup from 'rollup-plugin-cleanup';
4 |
5 | export default {
6 | input: 'src/index.ts',
7 | output: [
8 | {
9 | file: 'dist/es/jpex.js',
10 | format: 'es',
11 | exports: 'named',
12 | },
13 | {
14 | file: 'dist/cjs/jpex.js',
15 | format: 'cjs',
16 | exports: 'named',
17 | },
18 | ],
19 | plugins: [
20 | localResolve({
21 | extensions: ['.js', '.ts'],
22 | }),
23 | babel({
24 | exclude: 'node_modules/**',
25 | extensions: ['.js', '.ts'],
26 | }),
27 | cleanup({
28 | extensions: [ 'js','ts' ],
29 | sourcemap: false,
30 | }),
31 | ],
32 | };
33 |
--------------------------------------------------------------------------------
/src/__tests__/clearCache.test.ts:
--------------------------------------------------------------------------------
1 | import { jpex } from '..';
2 |
3 | type Instance = Record;
4 | const instance = jpex.infer();
5 |
6 | const setup = () => ({
7 | jpex: jpex.extend(),
8 | });
9 |
10 | it('sets a factory to resolved once resolved', () => {
11 | const { jpex } = setup();
12 |
13 | jpex.factory(() => ({}), { lifecycle: 'singleton' });
14 | expect(jpex.$$factories[instance].resolved).toBe(void 0);
15 |
16 | jpex.resolve();
17 | expect(jpex.$$factories[instance].resolved).toBe(true);
18 | });
19 |
20 | it('clears the cache', () => {
21 | const { jpex } = setup();
22 |
23 | jpex.factory(() => ({}), { lifecycle: 'singleton' });
24 |
25 | expect(jpex.$$factories[instance].resolved).toBe(void 0);
26 | jpex.resolve();
27 |
28 | expect(jpex.$$factories[instance].resolved).toBe(true);
29 |
30 | jpex.clearCache();
31 | expect(jpex.$$factories[instance].resolved).toBe(false);
32 | });
33 |
34 | it('returns a new instance once the cache is cleared', () => {
35 | const { jpex } = setup();
36 | jpex.factory(() => ({}), { lifecycle: 'singleton' });
37 |
38 | const a = jpex.resolve();
39 | const b = jpex.resolve();
40 | jpex.clearCache();
41 | const c = jpex.resolve();
42 |
43 | expect(a).toBe(b);
44 | expect(a).not.toBe(c);
45 | });
46 |
47 | it('clears specific factories', () => {
48 | const { jpex } = setup();
49 | type A = string;
50 | type B = string;
51 | jpex.factory(() => 'a', { lifecycle: 'singleton' });
52 | jpex.factory(() => 'b', { lifecycle: 'singleton' });
53 | jpex.resolve();
54 | jpex.resolve();
55 |
56 | expect(jpex.$$factories[jpex.infer()].resolved).toBe(true);
57 | expect(jpex.$$factories[jpex.infer()].resolved).toBe(true);
58 |
59 | jpex.clearCache();
60 |
61 | expect(jpex.$$factories[jpex.infer()].resolved).not.toBe(true);
62 | expect(jpex.$$factories[jpex.infer()].resolved).toBe(true);
63 | });
64 |
65 | it('should clear container-based caches', () => {
66 | const { jpex } = setup();
67 | type A = any;
68 | type B = any;
69 | jpex.factory(() => ({}), { lifecycle: 'container' });
70 | jpex.factory(() => ({}), { lifecycle: 'container' });
71 | jpex.resolve();
72 | jpex.resolve();
73 |
74 | expect(typeof jpex.$$resolved[jpex.infer()]).toBe('object');
75 |
76 | jpex.clearCache();
77 |
78 | expect(jpex.$$resolved[jpex.infer()]).toBe(void 0);
79 | });
80 |
81 | it('skips unregistered dependencies', () => {
82 | const { jpex } = setup();
83 | type A = any;
84 |
85 | expect(() => jpex.clearCache()).not.toThrow();
86 | });
87 |
--------------------------------------------------------------------------------
/src/__tests__/defer.test.ts:
--------------------------------------------------------------------------------
1 | import _jpex, { Jpex } from '..';
2 |
3 | let jpex: Jpex;
4 | type Foo = (v: string) => string;
5 | type FooAsync = (v: string) => Promise;
6 | type Bar = string;
7 |
8 | beforeEach(() => {
9 | jpex = _jpex.extend();
10 |
11 | jpex.factory((bar: Bar) => (v: string) => `${v}foo${bar}`);
12 | jpex.factory(() => 'bar');
13 | jpex.factory((foo: Foo) => async (v: string) => `${foo(v)}async`);
14 | });
15 |
16 | it('returns a function', () => {
17 | const foo = jpex.defer();
18 |
19 | expect(foo).toBeInstanceOf(Function);
20 | });
21 |
22 | it('does not resolve any dependencies at creation time', () => {
23 | jpex.defer();
24 |
25 | expect(jpex.$$factories[jpex.infer()]?.resolved).toBeFalsy();
26 | expect(jpex.$$resolved[jpex.infer()]).toBeFalsy();
27 | expect(jpex.$$factories[jpex.infer()]?.resolved).toBeFalsy();
28 | expect(jpex.$$resolved[jpex.infer()]).toBeFalsy();
29 | });
30 |
31 | it('resolves and calls the factory at call time', () => {
32 | const foo = jpex.defer();
33 |
34 | const result = foo('provided');
35 |
36 | expect(result).toBe('providedfoobar');
37 | });
38 |
39 | it('works with async factories', async () => {
40 | const foo = jpex.defer();
41 |
42 | const result = await foo('provided');
43 |
44 | expect(result).toBe('providedfoobarasync');
45 | });
46 |
47 | it('caches the inner function', () => {
48 | const spyFactory = jest.fn(() => () => 'spy');
49 |
50 | jpex.factory(spyFactory);
51 |
52 | const foo = jpex.defer();
53 |
54 | expect(spyFactory).not.toHaveBeenCalled();
55 |
56 | foo('provided');
57 |
58 | expect(spyFactory).toHaveBeenCalledTimes(1);
59 |
60 | foo('provided');
61 |
62 | expect(spyFactory).toHaveBeenCalledTimes(1);
63 |
64 | jpex.clearCache();
65 |
66 | foo('provided');
67 | expect(spyFactory).toHaveBeenCalledTimes(2);
68 | });
69 |
70 | it('keeps a list of deferred dependencies', () => {
71 | jpex.defer();
72 |
73 | expect(jpex.$$deps).toContain(jpex.infer());
74 | expect(jpex.$$deps).toContain(jpex.infer());
75 | });
76 |
--------------------------------------------------------------------------------
/src/__tests__/encase.test.ts:
--------------------------------------------------------------------------------
1 | import jpex from '..';
2 |
3 | const setup = () => ({
4 | jpex: jpex.extend(),
5 | });
6 |
7 | it('wraps a method with specified dependencies', () => {
8 | const { jpex } = setup();
9 |
10 | type Foo = string;
11 | const fn = jpex.encase((foo: Foo) => (bah: string) => foo + bah);
12 |
13 | jpex.constant('injected');
14 |
15 | const result = fn('provided');
16 |
17 | expect(result).toBe('injectedprovided');
18 | });
19 |
20 | it('works with global interfaces', () => {
21 | const { jpex } = setup();
22 |
23 | jpex.constant(window);
24 |
25 | const fn = jpex.encase((window: Window) => () => window);
26 |
27 | const result = fn();
28 |
29 | expect(result).toBe(window);
30 | });
31 |
32 | it('works with async factories', async () => {
33 | const { jpex } = setup();
34 |
35 | type AsyncFactory = string;
36 |
37 | jpex.factoryAsync(async () => 'async');
38 |
39 | const fn = jpex.encase((x: AsyncFactory) => async () => `${x}!`);
40 |
41 | const result = await fn();
42 |
43 | expect(result).toBe('async!');
44 | });
45 |
46 | it('exposes the inner function', () => {
47 | const { jpex } = setup();
48 | type Foo = string;
49 |
50 | const fn = jpex.encase((foo: Foo) => (bah: string) => foo + bah);
51 | const fn2 = fn.encased;
52 |
53 | const result = fn2('injected')('provided');
54 |
55 | expect(result).toBe('injectedprovided');
56 | });
57 |
58 | it('caches the inner function', () => {
59 | const { jpex } = setup();
60 | type Foo = string;
61 |
62 | jpex.constant('injected');
63 |
64 | const spy = jest.fn((foo) => {
65 | return (bah: string) => {
66 | return foo + bah;
67 | };
68 | });
69 | const inner = (foo: Foo) => spy(foo);
70 | const fn = jpex.encase(inner);
71 |
72 | fn('provided');
73 | expect(spy).toBeCalledTimes(1);
74 |
75 | fn('xxx');
76 | expect(spy).toBeCalledTimes(1);
77 |
78 | jpex.clearCache();
79 |
80 | fn('yyy');
81 | expect(spy).toBeCalledTimes(2);
82 | });
83 |
84 | it('keeps a list of encased dependencies', () => {
85 | const { jpex } = setup();
86 |
87 | type A = string;
88 | type B = string;
89 | type C = string;
90 | type D = string;
91 |
92 | const a = jpex.infer();
93 | const b = jpex.infer();
94 | const c = jpex.infer();
95 | const d = jpex.infer();
96 |
97 | // All initially undefined
98 | expect(jpex.$$deps).not.toContain(a);
99 | expect(jpex.$$deps).not.toContain(b);
100 | expect(jpex.$$deps).not.toContain(c);
101 | expect(jpex.$$deps).not.toContain(d);
102 |
103 | // Register a factory, since it has no dependencies, nothing should change
104 | jpex.factory(() => 'a');
105 |
106 | expect(jpex.$$deps).not.toContain(a);
107 | expect(jpex.$$deps).not.toContain(b);
108 | expect(jpex.$$deps).not.toContain(c);
109 | expect(jpex.$$deps).not.toContain(d);
110 |
111 | // Create an encased function, both its dependencies should be added to deps
112 | jpex.encase((a: A, b: B) => () => a + b);
113 |
114 | expect(jpex.$$deps).toContain(a);
115 | expect(jpex.$$deps).toContain(b);
116 | expect(jpex.$$deps).not.toContain(c);
117 | expect(jpex.$$deps).not.toContain(d);
118 |
119 | // Create a factory with a dependency, the dependency should be added to deps
120 | jpex.factory((c: C) => c);
121 |
122 | expect(jpex.$$deps).toContain(c);
123 | expect(jpex.$$deps).not.toContain(d);
124 |
125 | // Attempt to resolve a dependency directly, it should be added to deps
126 | jpex.resolve({ optional: true });
127 |
128 | expect(jpex.$$deps).toContain(d);
129 | });
130 |
--------------------------------------------------------------------------------
/src/__tests__/extend.test.ts:
--------------------------------------------------------------------------------
1 | import { jpex as base } from '..';
2 |
3 | type Foo = string;
4 |
5 | const setup = () => {
6 | const jpex = base.extend();
7 | jpex.constant('foo');
8 |
9 | return { jpex };
10 | };
11 |
12 | it('returns a new jpex instance', () => {
13 | const { jpex } = setup();
14 | const jpex2 = jpex.extend();
15 |
16 | expect(typeof jpex2).toBe('object');
17 | expect(typeof jpex.resolve).toBe('function');
18 | });
19 |
20 | it('inherits dependencies', () => {
21 | const { jpex } = setup();
22 | const jpex2 = jpex.extend();
23 | const value = jpex2.resolve();
24 |
25 | expect(value).toBe('foo');
26 | });
27 |
28 | it('overrides inherited dependencies', () => {
29 | const { jpex } = setup();
30 | const jpex2 = jpex.extend();
31 | jpex2.constant('bah');
32 | const value = jpex2.resolve();
33 |
34 | expect(value).toBe('bah');
35 | });
36 |
37 | it('does not inherit dependencies', () => {
38 | const { jpex } = setup();
39 | const jpex2 = jpex.extend({ inherit: false });
40 | const value = jpex2.resolve({ optional: true });
41 |
42 | expect(value).toBe(void 0);
43 | });
44 |
--------------------------------------------------------------------------------
/src/__tests__/raw.test.ts:
--------------------------------------------------------------------------------
1 | import base from '..';
2 |
3 | const setup = () => ({
4 | jpex: base.extend(),
5 | });
6 |
7 | it('returns the raw factory by name', () => {
8 | const { jpex } = setup();
9 | type Constant = string;
10 | type Factory = string;
11 |
12 | jpex.constant('foo');
13 | jpex.factory((v: Constant) => {
14 | return v.split('').reverse().join('');
15 | });
16 |
17 | const factory = jpex.raw();
18 | const result = factory('bah');
19 |
20 | expect(result).toBe('hab');
21 | });
22 |
23 | it('throws when not found', () => {
24 | const { jpex } = setup();
25 | type NotFound = any;
26 |
27 | expect(() => jpex.raw()).toThrow();
28 | });
29 |
--------------------------------------------------------------------------------
/src/clearCache.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | // ^ this is disabled because we need to iterate over all prototype keys in the object
3 | import { JpexInstance } from './types';
4 | import { ensureArray, hasLength } from './utils';
5 |
6 | export default function clearCache(this: JpexInstance, ..._names: any[]): any {
7 | const names = ensureArray(_names);
8 |
9 | for (const key in this.$$factories) {
10 | if (!hasLength(names) || names.includes(key)) {
11 | this.$$factories[key].resolved = false;
12 | }
13 | }
14 |
15 | for (const key in this.$$resolved) {
16 | if (!hasLength(names) || names.includes(key)) {
17 | delete this.$$resolved[key];
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const GLOBAL_TYPE_PREFIX = 'type:global:';
2 | export const NAMED_PARAMS = '$namedParameters';
3 | export const VOID = 'undefined';
4 |
--------------------------------------------------------------------------------
/src/defer.ts:
--------------------------------------------------------------------------------
1 | import { Dependency, JpexInstance } from './types';
2 |
3 | export default function defer(this: JpexInstance, name: Dependency) {
4 | return this.encase([name], (fn) => fn);
5 | }
6 |
--------------------------------------------------------------------------------
/src/encase.ts:
--------------------------------------------------------------------------------
1 | import { JpexInstance, Dependency, AnyFunction } from './types';
2 | import { allResolved, resolveDependencies } from './resolver';
3 | import { trackDeps } from './utils';
4 |
5 | export default function encase>(
6 | this: JpexInstance,
7 | dependencies: Dependency[],
8 | fn: F,
9 | ): any {
10 | // We want to alias this here because we'll end up with 2 this contexts
11 | // 1 for the outer function that resolves its dependencies, then another for the inner function
12 | // that runs the original method. The inner function uses both this contexts
13 | // eslint-disable-next-line @typescript-eslint/no-this-alias
14 | const jpex = this;
15 | let result: AnyFunction;
16 |
17 | trackDeps(jpex, dependencies);
18 |
19 | const invokeFn = (deps: any[], args: any[]) => {
20 | result = fn.apply(jpex, deps);
21 |
22 | return result.apply(this, args);
23 | };
24 |
25 | const encased = function encased(...args: Parameters) {
26 | if (result && allResolved.call(jpex, dependencies)) {
27 | return result.apply(this, args);
28 | }
29 | const deps = resolveDependencies.call(
30 | jpex,
31 | { dependencies },
32 | { async: true },
33 | );
34 |
35 | if (deps instanceof Promise) {
36 | return deps.then((deps) => invokeFn(deps, args));
37 | }
38 |
39 | return invokeFn(deps, args);
40 | };
41 | encased.encased = fn;
42 |
43 | return encased;
44 | }
45 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import makeJpex from './makeJpex';
2 | import type {
3 | JpexInstance,
4 | SetupConfig,
5 | NamedParameters,
6 | Lifecycle,
7 | Precedence,
8 | FactoryOpts,
9 | ResolveOpts,
10 | ServiceOpts,
11 | NodeModule,
12 | Global,
13 | } from './types';
14 |
15 | const jpex = makeJpex();
16 |
17 | export { jpex };
18 |
19 | export type {
20 | Lifecycle,
21 | JpexInstance,
22 | JpexInstance as Jpex,
23 | SetupConfig,
24 | NamedParameters,
25 | Precedence,
26 | FactoryOpts,
27 | ServiceOpts,
28 | ResolveOpts,
29 | NodeModule,
30 | Global,
31 | };
32 |
33 | export default jpex;
34 |
--------------------------------------------------------------------------------
/src/makeJpex.ts:
--------------------------------------------------------------------------------
1 | import { JpexInstance as IJpex, SetupConfig } from './types';
2 | import { constant, factory, service, alias, factoryAsync } from './registers';
3 | import { resolve, getFactory, resolveAsync } from './resolver';
4 | import encase from './encase';
5 | import clearCache from './clearCache';
6 | import defer from './defer';
7 |
8 | const defaultConfig = {
9 | lifecycle: 'container' as const,
10 | precedence: 'active' as const,
11 | globals: true,
12 | nodeModules: true,
13 | optional: false,
14 | };
15 |
16 | export default function makeJpex(
17 | { inherit = true, ...config }: SetupConfig = {},
18 | parent?: IJpex,
19 | ) {
20 | const jpex = {
21 | $$parent: parent,
22 | $$config: {
23 | ...defaultConfig,
24 | ...(inherit ? parent?.$$config : {}),
25 | ...config,
26 | },
27 | $$factories: parent && inherit ? Object.create(parent.$$factories) : {},
28 | $$resolved: {},
29 | $$alias: parent && inherit ? Object.create(parent.$$alias) : {},
30 | $$deps: parent && inherit ? parent.$$deps : [],
31 | constant,
32 | factory,
33 | factoryAsync,
34 | service,
35 | alias,
36 | resolve,
37 | resolveAsync,
38 | encase,
39 | defer,
40 | clearCache,
41 | extend(config?: SetupConfig): IJpex {
42 | return makeJpex(config, this);
43 | },
44 | resolveWith(name: any, namedParameters?: any, opts?: any): any {
45 | return this.resolve(name, {
46 | with: namedParameters,
47 | ...opts,
48 | });
49 | },
50 | resolveAsyncWith(name: any, namedParameters?: any, opts?: any): any {
51 | return this.resolveAsync(name, {
52 | with: namedParameters,
53 | ...opts,
54 | });
55 | },
56 | raw(name?: any): any {
57 | return getFactory(this, name, {}).fn;
58 | },
59 | infer: () => '',
60 | };
61 |
62 | return jpex as IJpex;
63 | }
64 |
--------------------------------------------------------------------------------
/src/registers/__tests__/alias.test.ts:
--------------------------------------------------------------------------------
1 | import { jpex } from '../..';
2 |
3 | const setup = () => {
4 | return {
5 | jpex: jpex.extend(),
6 | };
7 | };
8 |
9 | test('it aliases a factory to a type', () => {
10 | const { jpex } = setup();
11 | type Bah = string;
12 |
13 | jpex.factory('foo', [], () => 'foo');
14 | jpex.alias('foo');
15 |
16 | const result = jpex.resolve();
17 |
18 | expect(result).toBe('foo');
19 | });
20 |
21 | test('it aliases a factory to a string', () => {
22 | const { jpex } = setup();
23 | type Foo = any;
24 |
25 | jpex.factory(() => 'foo');
26 | jpex.alias('bah');
27 |
28 | const result = jpex.resolve('bah');
29 |
30 | expect(result).toBe('foo');
31 | });
32 |
33 | test('it aliases two types', () => {
34 | const { jpex } = setup();
35 | type Foo = any;
36 | type Bah = any;
37 |
38 | jpex.factory(() => 'foo');
39 | jpex.alias();
40 |
41 | const result = jpex.resolve();
42 |
43 | expect(result).toBe('foo');
44 | });
45 |
46 | test('aliases a factory at registration', () => {
47 | const { jpex } = setup();
48 | type Foo = any;
49 | type Bah = any;
50 |
51 | jpex.factory(() => 'foo', { alias: [jpex.infer()] });
52 |
53 | const result = jpex.resolve();
54 |
55 | expect(result).toBe('foo');
56 | });
57 |
58 | test('respects precedence', () => {
59 | const { jpex } = setup();
60 | const jpex2 = jpex.extend({ precedence: 'passive' });
61 |
62 | type Foo = any;
63 | type Bah = any;
64 |
65 | jpex2.factory(() => 'foo');
66 | jpex2.factory(() => 'bah');
67 | jpex2.alias();
68 |
69 | expect(jpex2.resolve()).toBe('foo');
70 | expect(jpex2.resolve()).toBe('bah');
71 | });
72 |
--------------------------------------------------------------------------------
/src/registers/__tests__/constant.test.ts:
--------------------------------------------------------------------------------
1 | import { jpex } from '../..';
2 |
3 | const setup = () => {
4 | return {
5 | jpex: jpex.extend(),
6 | };
7 | };
8 |
9 | test('registers a constant', () => {
10 | const { jpex } = setup();
11 |
12 | type Foo = string;
13 | jpex.constant('foo');
14 |
15 | expect(jpex.resolve()).toBe('foo');
16 | });
17 |
18 | test('always resolves the same value', () => {
19 | const { jpex } = setup();
20 | const jpex2 = jpex.extend();
21 | const jpex3 = jpex.extend();
22 |
23 | type Foo = any;
24 | jpex.constant({});
25 |
26 | const a = jpex3.resolve();
27 | const b = jpex.resolve();
28 | const c = jpex2.resolve();
29 |
30 | expect(a).toBe(b);
31 | expect(b).toBe(c);
32 | });
33 |
--------------------------------------------------------------------------------
/src/registers/__tests__/factory/decorators.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable newline-per-chained-call */
2 | /* eslint-disable no-invalid-this */
3 | import { jpex as base } from '../../..';
4 |
5 | interface Voice {
6 | shout(str: string): string;
7 | }
8 |
9 | const setup = () => {
10 | const base2 = base.extend();
11 | base2.service(function voice() {
12 | this.shout = (str: string) => {
13 | return `${str}!`;
14 | };
15 | });
16 | const jpex = base2.extend();
17 |
18 | return {
19 | base,
20 | base2,
21 | jpex,
22 | };
23 | };
24 |
25 | test('decorates a factory', () => {
26 | const { jpex } = setup();
27 | jpex.factory((voice: Voice) => {
28 | const original = voice.shout;
29 | // eslint-disable-next-line no-param-reassign
30 | voice.shout = (str: string) => original(str.toUpperCase());
31 | return voice;
32 | });
33 | const voice = jpex.resolve();
34 | const result = voice.shout('hello');
35 |
36 | expect(result).toBe('HELLO!');
37 | });
38 |
39 | test('decorators do not propogate up', () => {
40 | const { jpex, base2 } = setup();
41 | jpex.factory((voice: Voice) => {
42 | const original = voice.shout;
43 | // eslint-disable-next-line no-param-reassign
44 | voice.shout = (str: string) => original(str.toUpperCase());
45 | return voice;
46 | });
47 | const voice = base2.resolve();
48 | const result = voice.shout('hello');
49 |
50 | expect(result).toBe('hello!');
51 | });
52 |
--------------------------------------------------------------------------------
/src/registers/__tests__/factory/lifecycle.test.ts:
--------------------------------------------------------------------------------
1 | import { jpex as base } from '../../..';
2 |
3 | type Foo = any;
4 | type Factory = any;
5 | type Test = any;
6 |
7 | const setup = () => {
8 | const jpex = base.extend();
9 | const jpex2 = jpex.extend();
10 | const jpex3 = jpex2.extend();
11 |
12 | jpex.constant('jpex');
13 | jpex2.constant('jpex2');
14 | jpex3.constant('jpex3');
15 |
16 | return {
17 | jpex,
18 | jpex2,
19 | jpex3,
20 | };
21 | };
22 |
23 | test('singleton returns the same instance for all classes', () => {
24 | const { jpex, jpex2, jpex3 } = setup();
25 |
26 | jpex.factory((foo: Foo) => ({ foo }), { lifecycle: 'singleton' });
27 |
28 | const a = jpex.resolve();
29 | const b = jpex2.resolve();
30 | const c = jpex3.resolve();
31 |
32 | expect(a).toBe(b);
33 | expect(b).toBe(c);
34 | expect(c.foo).toBe('jpex');
35 | });
36 |
37 | test('singleton uses the first resolution forever', () => {
38 | const { jpex, jpex2, jpex3 } = setup();
39 |
40 | jpex.factory((foo: Foo) => ({ foo }), { lifecycle: 'singleton' });
41 |
42 | const c = jpex3.resolve();
43 | const a = jpex.resolve();
44 | const b = jpex2.resolve();
45 |
46 | expect(a).toBe(b);
47 | expect(b).toBe(c);
48 | expect(c.foo).toBe('jpex3');
49 | });
50 |
51 | test('class returns different instances for each class', () => {
52 | const { jpex, jpex2, jpex3 } = setup();
53 |
54 | jpex.factory((foo: Foo) => ({ foo }), { lifecycle: 'container' });
55 |
56 | const a = jpex.resolve();
57 | const b = jpex2.resolve();
58 | const c = jpex3.resolve();
59 |
60 | expect(a).not.toBe(b);
61 | expect(b).not.toBe(c);
62 | expect(a.foo).toBe('jpex');
63 | expect(b.foo).toBe('jpex2');
64 | expect(c.foo).toBe('jpex3');
65 | });
66 |
67 | test('class returns the same instance within a single class', () => {
68 | const { jpex } = setup();
69 |
70 | jpex.factory((foo: Foo) => ({ foo }), { lifecycle: 'container' });
71 |
72 | const a = jpex.resolve();
73 | const b = jpex.resolve();
74 | const c = jpex.resolve();
75 |
76 | expect(a).toBe(b);
77 | expect(b).toBe(c);
78 | });
79 |
80 | test('instance returns a new instance for each separate call', () => {
81 | const { jpex } = setup();
82 |
83 | jpex.factory((foo: Foo) => ({ foo }), { lifecycle: 'invocation' });
84 |
85 | const a = jpex.resolve();
86 | const b = jpex.resolve();
87 | const c = jpex.resolve();
88 |
89 | expect(a).not.toBe(b);
90 | expect(b).not.toBe(c);
91 | });
92 |
93 | test('instance returns a single instance within a single call', () => {
94 | const { jpex } = setup();
95 |
96 | jpex.factory((foo: Foo) => ({ foo }), { lifecycle: 'invocation' });
97 | jpex.factory((a: Factory, b: Factory) => {
98 | expect(a).toBe(b);
99 | });
100 |
101 | jpex.resolve();
102 | });
103 |
104 | test('none should return a different instance within a single call', () => {
105 | const { jpex } = setup();
106 |
107 | jpex.factory((foo: Foo) => ({ foo }), { lifecycle: 'none' });
108 | jpex.factory((a: Factory, b: Factory) => {
109 | expect(a).not.toBe(b);
110 | expect(a).toEqual(b);
111 | });
112 |
113 | jpex.resolve();
114 | });
115 |
--------------------------------------------------------------------------------
/src/registers/__tests__/factory/precedence.test.ts:
--------------------------------------------------------------------------------
1 | import { jpex as base } from '../../..';
2 |
3 | const setup = () => {
4 | const jpex = base.extend();
5 | const jpex2 = jpex.extend();
6 |
7 | return {
8 | jpex,
9 | jpex2,
10 | };
11 | };
12 |
13 | test('active overwrites an existing factory', () => {
14 | const { jpex } = setup();
15 |
16 | type A = string;
17 | jpex.factory(() => 'a');
18 | jpex.factory(() => 'A', { precedence: 'active' });
19 |
20 | const result = jpex.resolve();
21 |
22 | expect(result).toBe('A');
23 | });
24 |
25 | test('active overwrites an inherited factory', () => {
26 | const { jpex, jpex2 } = setup();
27 |
28 | type A = string;
29 | jpex.factory(() => 'a');
30 | jpex2.factory(() => 'A', { precedence: 'active' });
31 |
32 | const result = jpex2.resolve();
33 |
34 | expect(result).toBe('A');
35 | });
36 |
37 | test('defaults to active', () => {
38 | const { jpex } = setup();
39 |
40 | type A = string;
41 | jpex.factory(() => 'a');
42 | jpex.factory(() => 'A');
43 |
44 | const result = jpex.resolve();
45 |
46 | expect(result).toBe('A');
47 | });
48 |
49 | test('passive is ignored over an existing factory', () => {
50 | const { jpex } = setup();
51 |
52 | type A = string;
53 | jpex.factory(() => 'a');
54 | jpex.factory(() => 'A', { precedence: 'passive' });
55 |
56 | const result = jpex.resolve();
57 |
58 | expect(result).toBe('a');
59 | });
60 |
61 | test('passive is ignored over an inherited factory', () => {
62 | const { jpex, jpex2 } = setup();
63 |
64 | type A = string;
65 | jpex.factory(() => 'a');
66 | jpex2.factory(() => 'A', { precedence: 'passive' });
67 |
68 | const result = jpex2.resolve();
69 |
70 | expect(result).toBe('a');
71 | });
72 |
73 | test('passive is used if it does not exist', () => {
74 | const { jpex2 } = setup();
75 |
76 | type A = string;
77 | jpex2.factory(() => 'A', { precedence: 'passive' });
78 |
79 | const result = jpex2.resolve();
80 |
81 | expect(result).toBe('A');
82 | });
83 |
84 | test('inherits passive from config', () => {
85 | const { jpex: base } = setup();
86 | const jpex = base.extend({ precedence: 'passive' });
87 | type A = string;
88 | jpex.factory(() => 'a');
89 | jpex.factory(() => 'A');
90 |
91 | const result = jpex.resolve();
92 |
93 | expect(result).toBe('a');
94 | });
95 |
--------------------------------------------------------------------------------
/src/registers/__tests__/factory/validate.test.ts:
--------------------------------------------------------------------------------
1 | import base from '../../..';
2 |
3 | const setup = () => {
4 | const jpex = base.extend();
5 |
6 | return {
7 | jpex,
8 | };
9 | };
10 |
11 | test('throws is name is not provided', () => {
12 | const { jpex } = setup();
13 | // @ts-expect-error intentionally missing the name parameter
14 | expect(() => jpex.factory([], () => null)).toThrow();
15 | });
16 |
17 | test('throws if dependencies not provided', () => {
18 | const { jpex } = setup();
19 |
20 | expect(() => jpex.factory('foo', void 0, () => null)).toThrow();
21 | });
22 |
23 | test('throws if factory is not provided', () => {
24 | const { jpex } = setup();
25 |
26 | // @ts-expect-error intentionally missing fn
27 | expect(() => jpex.factory('foo', [])).toThrow();
28 | });
29 |
--------------------------------------------------------------------------------
/src/registers/__tests__/service.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-classes-per-file */
2 | import base from '../..';
3 |
4 | const setup = () => {
5 | const jpex = base.extend();
6 |
7 | return {
8 | jpex,
9 | };
10 | };
11 |
12 | test('registers a class service', () => {
13 | const { jpex } = setup();
14 |
15 | class Service {
16 | val = 'SERVICE';
17 | }
18 |
19 | jpex.service(Service);
20 |
21 | expect(jpex.resolve().val).toBe('SERVICE');
22 | });
23 |
24 | test('registers a function service', () => {
25 | const { jpex } = setup();
26 |
27 | type Service = { val: string };
28 | function service() {
29 | this.val = 'SERVICE';
30 | }
31 |
32 | jpex.service(service);
33 |
34 | expect(jpex.resolve().val).toBe('SERVICE');
35 | });
36 |
37 | test('infers services from class names', () => {
38 | const { jpex } = setup();
39 |
40 | class Service1 {
41 | val = 'SERVICE1';
42 | }
43 | jpex.service(Service1);
44 | jpex.service(
45 | class Service2 {
46 | val = 'SERVICE2';
47 | },
48 | );
49 | class Service3 {
50 | val = 'SERVICE3';
51 | }
52 | jpex.service(Service3, { precedence: 'passive' });
53 |
54 | jpex.resolve();
55 | jpex.resolve();
56 |
57 | expect(true).toBe(true);
58 | });
59 |
60 | test('infers services from class interfaces', () => {
61 | const { jpex } = setup();
62 |
63 | type IService = Record;
64 | type IService1 = Record;
65 | type IService2 = Record;
66 | type IService3 = Record;
67 |
68 | class Service1 implements IService, IService1 {
69 | [x: string]: unknown;
70 |
71 | val = 'SERVICE1';
72 | }
73 | jpex.service(Service1);
74 | jpex.service(
75 | class Service2 implements IService, IService2 {
76 | [x: string]: unknown;
77 |
78 | val = 'SERVICE2';
79 | },
80 | );
81 | class Service3 implements IService, IService3 {
82 | [x: string]: unknown;
83 |
84 | val = 'SERVICE3';
85 | }
86 | jpex.service(Service3, { precedence: 'passive' });
87 |
88 | jpex.resolve();
89 | jpex.resolve();
90 | jpex.resolve();
91 | jpex.resolve();
92 |
93 | expect(true).toBe(true);
94 | });
95 |
--------------------------------------------------------------------------------
/src/registers/alias.ts:
--------------------------------------------------------------------------------
1 | import { JpexInstance } from '../types';
2 |
3 | export default function alias(this: JpexInstance, alias: any, name: any) {
4 | if (this.$$alias[alias] == null || this.$$config.precedence === 'active') {
5 | this.$$alias[alias] = name;
6 | }
7 | if (this.$$alias[name] == null || this.$$config.precedence === 'active') {
8 | this.$$alias[name] = alias;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/registers/constant.ts:
--------------------------------------------------------------------------------
1 | import { JpexInstance } from '../types';
2 | import { isPassive, validateName } from '../utils';
3 |
4 | export default function constant(this: JpexInstance, name: string, obj: any) {
5 | validateName(name);
6 |
7 | if (isPassive(name, this)) {
8 | return;
9 | }
10 |
11 | this.$$factories[name] = {
12 | fn: () => obj,
13 | lifecycle: 'singleton',
14 | value: obj,
15 | resolved: true,
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/src/registers/factory.ts:
--------------------------------------------------------------------------------
1 | import { JpexInstance, Dependency, AnyFunction, FactoryOpts } from '../types';
2 | import {
3 | hasLength,
4 | ensureArray,
5 | isPassive,
6 | validateArgs,
7 | trackDeps,
8 | } from '../utils';
9 |
10 | export default function factory(
11 | this: JpexInstance,
12 | name: string,
13 | dependencies: Dependency[],
14 | fn: AnyFunction,
15 | opts: FactoryOpts = {},
16 | ) {
17 | validateArgs(name, dependencies, fn);
18 |
19 | if (!hasLength(dependencies)) {
20 | dependencies = null;
21 | }
22 |
23 | if (isPassive(name, this, opts.precedence)) {
24 | return;
25 | }
26 |
27 | this.$$factories[name] = {
28 | fn,
29 | dependencies,
30 | lifecycle: opts.lifecycle,
31 | };
32 |
33 | trackDeps(this, dependencies);
34 |
35 | if (opts.alias) {
36 | ensureArray(opts.alias).forEach((alias) => this.alias(alias, name));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/registers/factoryAsync.ts:
--------------------------------------------------------------------------------
1 | export { default } from './factory';
2 |
--------------------------------------------------------------------------------
/src/registers/index.ts:
--------------------------------------------------------------------------------
1 | export { default as constant } from './constant';
2 | export { default as factory } from './factory';
3 | export { default as service } from './service';
4 | export { default as alias } from './alias';
5 | export { default as factoryAsync } from './factoryAsync';
6 |
--------------------------------------------------------------------------------
/src/registers/service.ts:
--------------------------------------------------------------------------------
1 | import { JpexInstance, Dependency, ServiceOpts } from '../types';
2 | import { instantiate, validateArgs } from '../utils';
3 |
4 | export default function service(
5 | this: JpexInstance,
6 | name: string,
7 | dependencies: Dependency[],
8 | fn: any,
9 | opts: ServiceOpts = {},
10 | ) {
11 | validateArgs(name, dependencies, fn);
12 |
13 | function factory(...args: any[]) {
14 | const context = {} as any;
15 |
16 | if (opts.bindToInstance) {
17 | dependencies.forEach((key, i) => {
18 | context[key] = args[i];
19 | });
20 | }
21 |
22 | return instantiate(fn, [context, ...args]);
23 | }
24 |
25 | return this.factory(name, dependencies, factory, opts);
26 | }
27 |
--------------------------------------------------------------------------------
/src/resolver/__tests__/async.test.ts:
--------------------------------------------------------------------------------
1 | import base from '../..';
2 |
3 | test('default async factories', async () => {
4 | const jpex = base.extend();
5 |
6 | type FactoryA = Promise;
7 | type FactoryB = () => Promise<{ value: string }>;
8 |
9 | jpex.factory(async () => 'A');
10 | jpex.factory((a: FactoryA) => async () => {
11 | return { value: await a };
12 | });
13 |
14 | const a = jpex.resolve();
15 | const b = await a();
16 | expect(b).toEqual({ value: 'A' });
17 | });
18 |
19 | test('resolves asynchronous factories', async () => {
20 | const jpex = base.extend();
21 |
22 | type FactoryA = string;
23 | type FactoryB = { value: string };
24 | type FactoryC = FactoryB;
25 |
26 | jpex.factoryAsync(async () => 'A');
27 | jpex.factoryAsync(async (a: FactoryA) => {
28 | return { value: a };
29 | });
30 | jpex.factoryAsync(async (b: FactoryB) => b);
31 |
32 | const a = await jpex.resolveAsync();
33 | const b = await jpex.resolveAsync();
34 | const c = await jpex.resolveAsync();
35 | const d = await jpex.resolveAsync();
36 |
37 | expect(a).toEqual({ value: 'A' });
38 | expect(b).toEqual({ value: 'A' });
39 | expect(c).toEqual('A');
40 | expect(d).toEqual({ value: 'A' });
41 | });
42 |
43 | test('mixed default/async factories', async () => {
44 | const jpex = base.extend();
45 |
46 | type FactoryA = Promise;
47 | type FactoryB = { value: string };
48 | type FactoryC = FactoryB;
49 |
50 | jpex.factory(async () => 'A');
51 | jpex.factoryAsync(async (a: FactoryA) => {
52 | return { value: await a };
53 | });
54 | jpex.factory((b: FactoryB) => b);
55 |
56 | const a = await jpex.resolve();
57 |
58 | expect(a).toEqual({ value: 'A' });
59 | });
60 |
--------------------------------------------------------------------------------
/src/resolver/__tests__/default.test.ts:
--------------------------------------------------------------------------------
1 | import base from '../..';
2 |
3 | const setup = () => {
4 | const jpex = base.extend();
5 |
6 | return {
7 | jpex,
8 | };
9 | };
10 |
11 | test('if factory exists, it should be resolved', () => {
12 | const { jpex } = setup();
13 | type Foo = string;
14 | jpex.constant('foo');
15 |
16 | const result = jpex.resolve({ default: 'bar' });
17 |
18 | expect(result).toBe('foo');
19 | });
20 |
21 | test('if factory does not exist, it should return the default', () => {
22 | const { jpex } = setup();
23 | type Foo = string;
24 |
25 | const result = jpex.resolve({ default: 'bar' });
26 |
27 | expect(result).toBe('bar');
28 | });
29 |
30 | test('if factory does not exist and default not provided, it should throw an error', () => {
31 | const { jpex } = setup();
32 | type Foo = string;
33 |
34 | expect(() => jpex.resolve()).toThrow();
35 | });
36 |
37 | test('if factory does not exist and default is undefined, it should return undefined', () => {
38 | const { jpex } = setup();
39 | type Foo = string;
40 |
41 | const result = jpex.resolve({ default: undefined });
42 |
43 | expect(result).toBe(undefined);
44 | });
45 |
--------------------------------------------------------------------------------
/src/resolver/__tests__/global.test.ts:
--------------------------------------------------------------------------------
1 | /* global global */
2 | /* eslint-disable no-invalid-this */
3 | import base, { Global } from '../..';
4 |
5 | const setup = () => {
6 | const jpex = base.extend();
7 |
8 | return {
9 | jpex,
10 | };
11 | };
12 |
13 | afterEach(() => {
14 | delete (global as any).foo;
15 | delete (global as any).Foo;
16 | });
17 |
18 | test('resolves a global property', () => {
19 | const { jpex } = setup();
20 |
21 | const value = jpex.resolve();
22 |
23 | expect(value).toBe(window);
24 | });
25 |
26 | test('prefers a registered dependency over a global', () => {
27 | const { jpex } = setup();
28 | const fakeWindow = {};
29 | jpex.factory(() => fakeWindow as any);
30 |
31 | const value = jpex.resolve();
32 |
33 | expect(value).not.toBe(window);
34 | expect(value).toBe(fakeWindow);
35 | });
36 |
37 | test('allows a custom global variable', () => {
38 | const { jpex } = setup();
39 | (global as any).foo = 'hello';
40 |
41 | const value = jpex.resolve>();
42 |
43 | expect(value).toBe('hello');
44 | });
45 |
46 | test('allows a custom global class', () => {
47 | const { jpex } = setup();
48 | class Foo {}
49 | (global as any).Foo = Foo;
50 |
51 | const value = jpex.resolve>();
52 |
53 | expect(value).toBe(Foo);
54 | });
55 |
--------------------------------------------------------------------------------
/src/resolver/__tests__/node-module.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-invalid-this */
2 | import fs from 'fs';
3 | import base, { NodeModule } from '../..';
4 |
5 | const setup = () => {
6 | const jpex = base.extend();
7 |
8 | return {
9 | jpex,
10 | };
11 | };
12 |
13 | test('resolves a node module', () => {
14 | const { jpex } = setup();
15 |
16 | const value = jpex.resolve>();
17 |
18 | expect(value).toBe(fs);
19 | });
20 |
21 | test('prefers a registered dependency over a node module', () => {
22 | const { jpex } = setup();
23 | const fakeFs = {};
24 | jpex.factory>(() => fakeFs);
25 |
26 | const value = jpex.resolve>();
27 |
28 | expect(value).not.toBe(fs);
29 | expect(value).toBe(fakeFs);
30 | });
31 |
32 | test('does not resolve a node module when disabled', () => {
33 | const { jpex: base } = setup();
34 | const jpex = base.extend({
35 | nodeModules: false,
36 | optional: true,
37 | });
38 |
39 | const value = jpex.resolve>();
40 |
41 | expect(value).not.toBe(fs);
42 | expect(value).toBe(void 0);
43 | });
44 |
--------------------------------------------------------------------------------
/src/resolver/__tests__/optional.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-invalid-this */
2 | import base from '../..';
3 |
4 | const setup = () => {
5 | const jpex = base.extend();
6 |
7 | return {
8 | jpex,
9 | };
10 | };
11 |
12 | test('throws if dependency does not exist', () => {
13 | const { jpex } = setup();
14 | type Doesnotexist = any;
15 |
16 | expect(() => jpex.resolve()).toThrow();
17 | });
18 |
19 | test('does not throw if dependency is optional', () => {
20 | const { jpex } = setup();
21 | type Doesnotexist = any;
22 |
23 | expect(() => jpex.resolve({ optional: true })).not.toThrow();
24 | });
25 |
26 | test("does not throw if optional dependency's dependencies fail", () => {
27 | const { jpex } = setup();
28 | type Doesnotexist = any;
29 | type Exists = any;
30 | jpex.factory((x: Doesnotexist) => x);
31 |
32 | expect(() => jpex.resolve({ optional: true })).not.toThrow();
33 | });
34 |
35 | test('does not throw if the default optional is set', () => {
36 | const { jpex: base } = setup();
37 | type Doesnotexist = any;
38 | const jpex = base.extend({ optional: true });
39 |
40 | expect(() => jpex.resolve()).not.toThrow();
41 | });
42 |
43 | test('resolves an optional dependency', () => {
44 | const { jpex } = setup();
45 | type Exists = any;
46 | jpex.factory(() => 'foo');
47 | const result = jpex.resolve({ optional: true });
48 |
49 | expect(result).toBe('foo');
50 | });
51 |
--------------------------------------------------------------------------------
/src/resolver/__tests__/resolve.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-classes-per-file */
2 | /* eslint-disable no-invalid-this */
3 | import base from '../..';
4 |
5 | const setup = () => {
6 | const jpex = base.extend();
7 |
8 | return {
9 | jpex,
10 | };
11 | };
12 |
13 | test('resolves factories and services', () => {
14 | const { jpex } = setup();
15 | type Factory = string;
16 | type Service = { val: string };
17 | type Dependent = { val: string };
18 | type Master = { val: string; sub: string };
19 | type Constant = string;
20 | class ComplexConcrete {
21 | val: string;
22 |
23 | constructor(dep: Dependent) {
24 | this.val = dep.val;
25 | }
26 | }
27 |
28 | jpex.factory(() => 'FACTORY');
29 | jpex.service(
30 | class {
31 | val = 'SERVICE';
32 | },
33 | );
34 | jpex.service(function dependent() {
35 | this.val = 'DEPENDENT';
36 | });
37 | jpex.service(function master(d: Dependent) {
38 | this.val = 'MASTER';
39 | this.sub = d.val;
40 | });
41 | jpex.service(ComplexConcrete);
42 | jpex.constant('CONSTANT');
43 |
44 | const f = jpex.resolve();
45 | const s = jpex.resolve();
46 | const m = jpex.resolve();
47 | const cc = jpex.resolve();
48 | const c = jpex.resolve(jpex.infer());
49 |
50 | expect(f).toBe('FACTORY');
51 | expect(s.val).toBe('SERVICE');
52 | expect(m.val).toBe('MASTER');
53 | expect(m.sub).toBe('DEPENDENT');
54 | expect(cc.val).toBe('DEPENDENT');
55 | expect(c).toBe('CONSTANT');
56 | });
57 |
58 | test('resolves named dependencies', () => {
59 | const { jpex } = setup();
60 | type Foo = string;
61 | type Named = string;
62 |
63 | jpex.factory((named: Named) => named);
64 | const result = jpex.resolve({
65 | with: {
66 | [jpex.infer()]: 'pop',
67 | },
68 | });
69 |
70 | expect(result).toBe('pop');
71 | });
72 |
73 | test('throws if dependency is recurring', () => {
74 | const { jpex } = setup();
75 | type A = any;
76 | type B = any;
77 | jpex.factory((b: B) => b);
78 | jpex.factory((a: A) => a);
79 |
80 | expect(() => jpex.resolve('a')).toThrow();
81 | });
82 |
83 | test('resolves array-like dependencies', () => {
84 | const { jpex } = setup();
85 | type Keys = string[];
86 | type Value = string;
87 | jpex.constant(['hello', 'world']);
88 | jpex.factory((keys: Keys) => keys[0]);
89 |
90 | const value = jpex.resolve();
91 |
92 | expect(value).toBe('hello');
93 | });
94 |
--------------------------------------------------------------------------------
/src/resolver/__tests__/resolveWith.test.ts:
--------------------------------------------------------------------------------
1 | import base from '../..';
2 |
3 | const setup = () => {
4 | const jpex = base.extend();
5 |
6 | return {
7 | jpex,
8 | };
9 | };
10 |
11 | it('resolves with given values', () => {
12 | const { jpex } = setup();
13 |
14 | type A = string;
15 | type B = string;
16 | type C = string;
17 | type D = string;
18 |
19 | jpex.factory((b: B, c: C, d: D) => b + c + d);
20 |
21 | const result = jpex.resolveWith({
22 | [jpex.infer()]: 'b',
23 | [jpex.infer()]: 'c',
24 | [jpex.infer()]: 'd',
25 | });
26 |
27 | expect(result).toBe('bcd');
28 | });
29 |
30 | it('resolves using type inference (1)', () => {
31 | const { jpex } = setup();
32 | type A = string;
33 | type B = string;
34 |
35 | jpex.factory((b: B) => `a${b}`);
36 |
37 | const result = jpex.resolveWith(['b']);
38 |
39 | expect(result).toBe('ab');
40 | });
41 |
42 | it('resolves with type inference (6)', () => {
43 | const { jpex } = setup();
44 | type A = string;
45 | type B = string;
46 | type C = string;
47 | type D = string;
48 | type E = string;
49 | type F = string;
50 | type G = string;
51 |
52 | jpex.factory(
53 | (b: B, c: C, d: D, e: E, f: F, g: G) => `a${b}${c}${d}${e}${f}${g}`,
54 | );
55 |
56 | const result = jpex.resolveWith([
57 | 'b',
58 | 'c',
59 | 'd',
60 | 'e',
61 | 'f',
62 | 'g',
63 | ]);
64 |
65 | expect(result).toBe('abcdefg');
66 | });
67 |
68 | test('it resolves different values for different arguments', () => {
69 | const { jpex } = setup();
70 | type A = string;
71 | type B = string;
72 |
73 | jpex.factory((b: B) => `a${b}`);
74 | jpex.factory(() => 'z');
75 |
76 | const result1 = jpex.resolveWith(['b']);
77 | const result2 = jpex.resolveWith(['c']);
78 | const result3 = jpex.resolveWith(['d']);
79 | const result4 = jpex.resolve();
80 |
81 | expect(result1).toBe('ab');
82 | expect(result2).toBe('ac');
83 | expect(result3).toBe('ad');
84 | expect(result4).toBe('az');
85 | });
86 |
--------------------------------------------------------------------------------
/src/resolver/getFactory.ts:
--------------------------------------------------------------------------------
1 | import { Factory, JpexInstance, ResolveOpts } from '../types';
2 | import { isNode, unsafeRequire, validateName } from '../utils';
3 | import { GLOBAL_TYPE_PREFIX, VOID } from '../constants';
4 |
5 | const getFromNodeModules = (jpex: JpexInstance, target: string): Factory => {
6 | // in order to stop webpack environments from including every possible
7 | // import source in the bundle, we have to stick all node require stuff
8 | // inside an eval setup
9 | if (!jpex.$$config.nodeModules || !isNode()) {
10 | return;
11 | }
12 |
13 | try {
14 | const value = unsafeRequire(target);
15 | jpex.constant(target, value);
16 | return jpex.$$factories[target];
17 | } catch (e) {
18 | if (e.message?.includes?.(`Cannot find module '${target}'`)) {
19 | // not found in node modules, just continue
20 | return;
21 | }
22 |
23 | throw e;
24 | }
25 | };
26 |
27 | const getGlobalObject = (): any => {
28 | if (typeof global !== VOID) {
29 | return global;
30 | }
31 | if (typeof globalThis !== VOID) {
32 | return globalThis;
33 | }
34 | if (typeof window !== VOID) {
35 | return window;
36 | }
37 | return {};
38 | };
39 |
40 | const getGlobalProperty = (name: string) => {
41 | const global = getGlobalObject();
42 | if (global[name] !== void 0) {
43 | return global[name];
44 | }
45 | // we need to handle inferred types as well
46 | // this gets a little bit hacky...
47 | if (name.startsWith(GLOBAL_TYPE_PREFIX)) {
48 | // most global types will just be the name of the property in pascal case
49 | // i.e. window = Window / document = Document
50 | // sometimes though, like classes, the concrete name and type name are the same
51 | // i.e. the URL class
52 | const len = GLOBAL_TYPE_PREFIX.length;
53 | const inferred = name.substring(len);
54 | const inferredLower =
55 | inferred.charAt(0).toLowerCase() + inferred.substring(1);
56 | return global[inferredLower] ?? global[inferred];
57 | }
58 | };
59 |
60 | const getFromGlobal = (jpex: JpexInstance, name: string): Factory => {
61 | if (!jpex.$$config.globals) {
62 | return;
63 | }
64 |
65 | const value = getGlobalProperty(name);
66 |
67 | if (value !== void 0) {
68 | jpex.constant(name, value);
69 | return jpex.$$factories[name];
70 | }
71 | };
72 |
73 | const getFromAlias = (jpex: JpexInstance, alias: string) => {
74 | const name = jpex.$$alias[alias];
75 | if (name != null) {
76 | return jpex.$$factories[name];
77 | }
78 | };
79 |
80 | const getFromResolved = (jpex: JpexInstance, name: string) => {
81 | return jpex.$$resolved[name];
82 | };
83 |
84 | const getFromRegistry = (jpex: JpexInstance, name: string) => {
85 | return jpex.$$factories[name];
86 | };
87 |
88 | const getFactory = (
89 | jpex: JpexInstance,
90 | name: string,
91 | opts: ResolveOpts = {},
92 | ): Factory | undefined => {
93 | validateName(name);
94 | const fns = [
95 | getFromResolved,
96 | getFromRegistry,
97 | getFromAlias,
98 | getFromGlobal,
99 | getFromNodeModules,
100 | ];
101 | while (fns.length) {
102 | const factory = fns.shift()(jpex, name);
103 | if (factory != null) {
104 | return factory;
105 | }
106 | }
107 |
108 | if (opts.optional ?? jpex.$$config.optional) {
109 | return;
110 | }
111 |
112 | if ('default' in opts) {
113 | return {
114 | fn: () => opts.default,
115 | lifecycle: jpex.$$config.lifecycle,
116 | resolved: true,
117 | value: opts.default,
118 | };
119 | }
120 |
121 | throw new Error(`Unable to find required dependency [${name}]`);
122 | };
123 |
124 | export default getFactory;
125 |
--------------------------------------------------------------------------------
/src/resolver/index.ts:
--------------------------------------------------------------------------------
1 | import { JpexInstance, Dependency, ResolveOpts, Factory } from '../types';
2 | import { resolveMany, resolveOne } from './resolve';
3 | import { isString, trackDeps } from '../utils';
4 |
5 | export { default as getFactory } from './getFactory';
6 |
7 | export function resolve(
8 | this: JpexInstance,
9 | name: Dependency,
10 | opts?: ResolveOpts,
11 | ) {
12 | trackDeps(this, [name]);
13 | return resolveOne(this, name, void 0, opts, []);
14 | }
15 |
16 | export function resolveAsync(
17 | this: JpexInstance,
18 | name: Dependency,
19 | opts?: ResolveOpts,
20 | ) {
21 | trackDeps(this, [name]);
22 | return resolveOne(this, name, void 0, { ...opts, async: true }, []);
23 | }
24 |
25 | export function resolveDependencies(
26 | this: JpexInstance,
27 | definition: Factory,
28 | opts?: ResolveOpts,
29 | ) {
30 | trackDeps(this, definition.dependencies);
31 | return resolveMany(this, definition, void 0, opts, []);
32 | }
33 |
34 | export function isResolved(this: JpexInstance, dependency: Dependency) {
35 | if (!isString(dependency)) {
36 | return false;
37 | }
38 | if (this.$$resolved[dependency] != null) {
39 | return true;
40 | }
41 | if (this.$$factories[dependency]) {
42 | return this.$$factories[dependency].resolved === true;
43 | }
44 | return false;
45 | }
46 |
47 | export function allResolved(this: JpexInstance, dependencies: Dependency[]) {
48 | return dependencies.every(isResolved.bind(this));
49 | }
50 |
--------------------------------------------------------------------------------
/src/resolver/resolve.ts:
--------------------------------------------------------------------------------
1 | import {
2 | JpexInstance,
3 | Dependency,
4 | NamedParameters,
5 | ResolveOpts,
6 | Factory,
7 | } from '../types';
8 | import getFactory from './getFactory';
9 | import { ensureArray, hasLength, last, unique } from '../utils';
10 | import { NAMED_PARAMS } from '../constants';
11 |
12 | // Ensure we're not stuck in a recursive loop
13 | const checkStack = (
14 | jpex: JpexInstance,
15 | name: Dependency,
16 | stack: string[],
17 | ): 'new' | 'inherit' | 'recursive' => {
18 | if (!hasLength(stack)) {
19 | // This is the first loop
20 | return 'new';
21 | }
22 | if (!stack.includes(name)) {
23 | // We've definitely not tried to resolve this one before
24 | return 'new';
25 | }
26 | if (last(stack) === name) {
27 | // We've tried to resolve this one before, but...
28 | // if this factory has overridden a parent factory
29 | // we should assume it actually wants to resolve the parent
30 | const parent = jpex.$$parent?.$$factories[name];
31 | if (parent != null) {
32 | return 'inherit';
33 | }
34 | }
35 | return 'recursive';
36 | };
37 |
38 | // Cache the result of resolving a factory
39 | export const cacheResult = (
40 | jpex: JpexInstance,
41 | name: string,
42 | factory: Factory,
43 | value: any,
44 | namedParameters: NamedParameters,
45 | withArg: Record,
46 | ) => {
47 | switch (factory.lifecycle || jpex.$$config.lifecycle) {
48 | case 'singleton':
49 | // Cache the result against the factory itself
50 | // so it is shared across all instances that use that factory
51 | factory.resolved = true;
52 | factory.value = value;
53 | factory.with = withArg;
54 | // Also store the result in the namedParameters for a quick look-up
55 | namedParameters[name] = value;
56 | break;
57 | case 'container':
58 | // Cache the result against the current instance
59 | // so it is shared across this instance and all child instances, but not parent instances
60 | jpex.$$resolved[name] = {
61 | ...factory,
62 | resolved: true,
63 | value,
64 | with: withArg,
65 | } as Factory;
66 | // Also store the result in the namedParameters for a quick look-up
67 | namedParameters[name] = value;
68 | break;
69 | case 'none':
70 | // Do not cache the result at all
71 | break;
72 | case 'invocation':
73 | default:
74 | // Cache the result for the duration of the current resolution
75 | // so if two dependencies share the same dependency it will re-use it
76 | // but if the same dependency is resolved again later it will be re-resolved
77 | namedParameters[name] = value;
78 | break;
79 | }
80 | };
81 |
82 | // Get named parameters, these will either be custom dependencies passed in at resolve time,
83 | // or dependencies that were resolved during the current resolution
84 | const getNamedParameters = (
85 | namedParameters: NamedParameters,
86 | opts: ResolveOpts = {},
87 | ) => {
88 | if (namedParameters) {
89 | // Use existing named parameters
90 | return namedParameters;
91 | }
92 | if (opts.with) {
93 | // Use custom named parameters
94 | return { ...opts.with };
95 | }
96 | // Create a new parameters object to use just for this resolution
97 | return {};
98 | };
99 |
100 | // Check if the factory has already been resolved with the same parameters
101 | // If it has, we can re-use the reoslved value, otherwise we need to re-resolve and re-cache with the new parameters
102 | const isResolvedWithParams = (factory: Factory, opts: ResolveOpts = {}) => {
103 | if (!factory.with && !opts.with) {
104 | return true;
105 | }
106 | const keys = unique([
107 | ...Object.keys(opts.with || {}),
108 | ...Object.keys(factory.with || {}),
109 | ]);
110 | return keys.every((key) => opts.with?.[key] === factory.with?.[key]);
111 | };
112 |
113 | const invokeFactory = (
114 | jpex: JpexInstance,
115 | name: string,
116 | factory: Factory,
117 | namedParameters: NamedParameters,
118 | opts: ResolveOpts,
119 | args: any[],
120 | ) => {
121 | // Invoke the factory
122 | const value = factory.fn.apply(jpex, args);
123 | // Cache the result
124 | cacheResult(jpex, name, factory, value, namedParameters, opts?.with);
125 | return value;
126 | };
127 |
128 | const resolveFactory = (
129 | jpex: JpexInstance,
130 | name: string,
131 | factory: Factory,
132 | namedParameters: NamedParameters,
133 | opts: ResolveOpts,
134 | stack: string[],
135 | ) => {
136 | if (factory == null) {
137 | return;
138 | }
139 |
140 | // Check if it's already been resolved
141 | if (factory.resolved && isResolvedWithParams(factory, opts)) {
142 | return factory.value;
143 | }
144 |
145 | // Work out dependencies
146 | let args: any[] | Promise = [];
147 |
148 | if (hasLength(factory.dependencies)) {
149 | // eslint-disable-next-line @typescript-eslint/no-use-before-define
150 | args = resolveMany(jpex, factory, namedParameters, opts, [...stack, name]);
151 | }
152 |
153 | // Handle async factories by waiting for the dependencies to resolve
154 | if (args instanceof Promise) {
155 | return args.then((args) => {
156 | return invokeFactory(jpex, name, factory, namedParameters, opts, args);
157 | });
158 | }
159 |
160 | return invokeFactory(jpex, name, factory, namedParameters, opts, args);
161 | };
162 |
163 | export const resolveOne = (
164 | jpex: JpexInstance,
165 | name: Dependency,
166 | initialParameters: NamedParameters,
167 | opts: ResolveOpts,
168 | stack: string[],
169 | ): any | Promise => {
170 | const namedParameters = getNamedParameters(initialParameters, opts);
171 |
172 | // Check named parameters
173 | // if we have a named parameter for this dependency
174 | // we don't need to do any resolution, we can just return the value
175 | if (Object.hasOwnProperty.call(namedParameters, name)) {
176 | return namedParameters[name];
177 | }
178 |
179 | // Special keys
180 | if (name === NAMED_PARAMS || name === jpex.infer()) {
181 | return namedParameters;
182 | }
183 |
184 | switch (checkStack(jpex, name, stack)) {
185 | case 'inherit':
186 | return resolveOne(jpex.$$parent, name, namedParameters, opts, []);
187 | case 'recursive':
188 | throw new Error(`Recursive loop for dependency ${name} encountered`);
189 | case 'new':
190 | default:
191 | // All good
192 | break;
193 | }
194 |
195 | // Get the factory
196 | // This will either return the factory,
197 | // return null (meaning it's an optional dependency)
198 | // or throw an error
199 | const factory = getFactory(jpex, name, opts);
200 |
201 | return resolveFactory(jpex, name, factory, namedParameters, opts, stack);
202 | };
203 |
204 | export const resolveMany = (
205 | jpex: JpexInstance,
206 | definition: Factory,
207 | namedParameters: NamedParameters,
208 | opts: ResolveOpts,
209 | stack: string[] = [],
210 | ): any[] | Promise => {
211 | if (!hasLength(definition.dependencies)) {
212 | return [];
213 | }
214 | let isAsync = false;
215 | const dependencies: Dependency[] = ensureArray(definition.dependencies);
216 |
217 | const values = dependencies.map((dependency) => {
218 | const value = resolveOne(jpex, dependency, namedParameters, opts, stack);
219 | if (opts && opts.async && value instanceof Promise) {
220 | isAsync = true;
221 | }
222 | return value;
223 | });
224 |
225 | if (isAsync) {
226 | return Promise.all(values);
227 | }
228 |
229 | return values;
230 | };
231 |
--------------------------------------------------------------------------------
/src/types/BuiltIns.ts:
--------------------------------------------------------------------------------
1 | export interface NamedParameters {
2 | [key: string]: any;
3 | }
4 |
--------------------------------------------------------------------------------
/src/types/JpexInstance.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | // ^ we have functions that have several generic types that don't get used in the runtime code, but are necessary for the build process to extract type info
3 | import type {
4 | Lifecycle,
5 | AnyFunction,
6 | Dependency,
7 | AnyConstructor,
8 | Factory,
9 | Precedence,
10 | } from './base';
11 | import { NamedParameters } from './BuiltIns';
12 |
13 | export interface SetupConfig {
14 | inherit?: boolean;
15 | lifecycle?: Lifecycle;
16 | precedence?: Precedence;
17 | optional?: boolean;
18 | nodeModules?: boolean;
19 | globals?: boolean;
20 | }
21 |
22 | export interface FactoryOpts {
23 | lifecycle?: Lifecycle;
24 | precedence?: Precedence;
25 | alias?: string | string[];
26 | }
27 | export interface ServiceOpts extends FactoryOpts {
28 | bindToInstance?: boolean;
29 | }
30 | export interface ResolveOpts {
31 | optional?: boolean;
32 | with?: NamedParameters;
33 | async?: boolean;
34 | default?: any;
35 | }
36 |
37 | export interface JpexInstance {
38 | constant(name: string, obj: any): void;
39 | constant(obj: T): void;
40 | // we need to include these mixed variants so that typescript doesn't get mad after we do our transformation
41 | constant(name: string, obj: T): void;
42 |
43 | factory(
44 | name: string,
45 | deps: Dependency[],
46 | fn: AnyFunction,
47 | opts?: FactoryOpts,
48 | ): void;
49 | factory(fn: AnyFunction, opts?: FactoryOpts): void;
50 | factory(
51 | name: string,
52 | deps: Dependency[],
53 | fn: AnyFunction,
54 | opts?: FactoryOpts,
55 | ): void;
56 |
57 | factoryAsync(
58 | name: string,
59 | deps: Dependency[],
60 | fn: AnyFunction,
61 | opts?: FactoryOpts,
62 | ): void;
63 | factoryAsync(fn: AnyFunction