├── .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 | # ![Jpex](https://jpex-js.github.io/dist/jpex.svg) 2 | 3 | ## Easy Dependency Injection 4 | 5 | [![Build Status](https://travis-ci.org/jpex-js/jpex.svg?branch=master)](https://travis-ci.org/jackmellis/jpex) 6 | [![npm version](https://badge.fury.io/js/jpex.svg)](https://badge.fury.io/js/jpex) 7 | [![Code Climate](https://codeclimate.com/github/jackmellis/jpex/badges/gpa.svg)](https://codeclimate.com/github/jackmellis/jpex) 8 | [![Test Coverage](https://codeclimate.com/github/jackmellis/jpex/badges/coverage.svg)](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>, opts?: FactoryOpts): void; 64 | factoryAsync( 65 | name: string, 66 | deps: Dependency[], 67 | fn: AnyFunction>, 68 | opts?: FactoryOpts, 69 | ): void; 70 | 71 | service( 72 | name: string, 73 | deps: Dependency[], 74 | fn: AnyConstructor | AnyFunction, 75 | opts?: ServiceOpts, 76 | ): void; 77 | service(fn: AnyConstructor | AnyFunction, opts?: ServiceOpts): void; 78 | service( 79 | name: string, 80 | deps: Dependency[], 81 | fn: AnyConstructor | AnyFunction, 82 | opts?: ServiceOpts, 83 | ): void; 84 | 85 | alias(alias: string, name: string): void; 86 | alias(alias: string): void; 87 | alias(): void; 88 | alias(alias: string, name: string): void; 89 | alias(alias: string, name: string): void; 90 | 91 | resolve(name: Dependency, opts?: ResolveOpts): any; 92 | resolve(opts?: ResolveOpts): T; 93 | resolve(name: Dependency, opts?: ResolveOpts): T; 94 | 95 | resolveAsync(name: Dependency, opts?: ResolveOpts): Promise; 96 | resolveAsync(opts?: ResolveOpts): Promise; 97 | resolveAsync(name: Dependency, opts?: ResolveOpts): Promise; 98 | 99 | resolveWith( 100 | name: Dependency, 101 | namedParameters: NamedParameters, 102 | opts?: ResolveOpts, 103 | ): any; 104 | resolveWith(namedParameters: NamedParameters, opts?: ResolveOpts): T; 105 | resolveWith(namedParameters: NamedParameters, opts?: ResolveOpts): T; 106 | resolveWith(namedParameters: NamedParameters, opts?: ResolveOpts): T; 107 | resolveWith( 108 | namedParameters: NamedParameters, 109 | opts?: ResolveOpts, 110 | ): T; 111 | resolveWith( 112 | namedParameters: NamedParameters, 113 | opts?: ResolveOpts, 114 | ): T; 115 | resolveWith( 116 | namedParameters: NamedParameters, 117 | opts?: ResolveOpts, 118 | ): T; 119 | resolveWith( 120 | namedParameters: NamedParameters, 121 | opts?: ResolveOpts, 122 | ): T; 123 | resolveWith(namedParameters: NamedParameters, opts?: ResolveOpts): T; 124 | resolveWith(namedParameters: NamedParameters, opts?: ResolveOpts): T; 125 | resolveWith( 126 | namedParameters: NamedParameters, 127 | opts?: ResolveOpts, 128 | ): T; 129 | resolveWith( 130 | namedParameters: NamedParameters, 131 | opts?: ResolveOpts, 132 | ): T; 133 | resolveWith( 134 | namedParameters: NamedParameters, 135 | opts?: ResolveOpts, 136 | ): T; 137 | resolveWith( 138 | namedParameters: NamedParameters, 139 | opts?: ResolveOpts, 140 | ): T; 141 | 142 | resolveAsyncWith( 143 | name: Dependency, 144 | namedParameters: NamedParameters, 145 | opts?: ResolveOpts, 146 | ): any; 147 | resolveAsyncWith( 148 | namedParameters: NamedParameters, 149 | opts?: ResolveOpts, 150 | ): Promise; 151 | resolveAsyncWith( 152 | namedParameters: NamedParameters, 153 | opts?: ResolveOpts, 154 | ): Promise; 155 | resolveAsyncWith( 156 | namedParameters: NamedParameters, 157 | opts?: ResolveOpts, 158 | ): Promise; 159 | resolveAsyncWith( 160 | namedParameters: NamedParameters, 161 | opts?: ResolveOpts, 162 | ): Promise; 163 | resolveAsyncWith( 164 | namedParameters: NamedParameters, 165 | opts?: ResolveOpts, 166 | ): Promise; 167 | resolveAsyncWith( 168 | namedParameters: NamedParameters, 169 | opts?: ResolveOpts, 170 | ): Promise; 171 | resolveAsyncWith( 172 | namedParameters: NamedParameters, 173 | opts?: ResolveOpts, 174 | ): Promise; 175 | resolveAsyncWith( 176 | namedParameters: NamedParameters, 177 | opts?: ResolveOpts, 178 | ): Promise; 179 | resolveAsyncWith( 180 | namedParameters: NamedParameters, 181 | opts?: ResolveOpts, 182 | ): Promise; 183 | resolveAsyncWith( 184 | namedParameters: NamedParameters, 185 | opts?: ResolveOpts, 186 | ): Promise; 187 | resolveAsyncWith( 188 | namedParameters: NamedParameters, 189 | opts?: ResolveOpts, 190 | ): Promise; 191 | resolveAsyncWith( 192 | namedParameters: NamedParameters, 193 | opts?: ResolveOpts, 194 | ): Promise; 195 | resolveAsyncWith( 196 | namedParameters: NamedParameters, 197 | opts?: ResolveOpts, 198 | ): Promise; 199 | 200 | encase>( 201 | dependencies: Dependency[], 202 | fn: F, 203 | ): ReturnType & { encased: F }; 204 | encase>( 205 | fn: F, 206 | ): ReturnType & { encased: F }; 207 | 208 | defer(): T; 209 | defer(name: Dependency): T; 210 | 211 | raw(name: Dependency): AnyFunction; 212 | raw(): AnyFunction; 213 | raw(name: Dependency): AnyFunction; 214 | 215 | clearCache(): void; 216 | clearCache(...names: string[]): void; 217 | clearCache(...names: string[]): void; 218 | 219 | extend(config?: SetupConfig): JpexInstance; 220 | 221 | infer(): string; 222 | 223 | $$parent: JpexInstance; 224 | $$factories: { 225 | [key: string]: Factory; 226 | }; 227 | $$resolved: { 228 | [key: string]: Factory; 229 | }; 230 | $$alias: { 231 | [key: string]: string; 232 | }; 233 | $$deps: Dependency[]; 234 | $$config: { 235 | lifecycle: Lifecycle; 236 | precedence: Precedence; 237 | optional: boolean; 238 | nodeModules: boolean; 239 | globals: boolean; 240 | }; 241 | } 242 | -------------------------------------------------------------------------------- /src/types/base.ts: -------------------------------------------------------------------------------- 1 | export type Lifecycle = 'singleton' | 'container' | 'invocation' | 'none'; 2 | 3 | export type Precedence = 'active' | 'passive'; 4 | 5 | export type AnyFunction = (...args: any[]) => R; 6 | export interface AnyConstructor { 7 | new (...args: any[]): T; 8 | } 9 | 10 | export type Dependency = string; 11 | 12 | export interface Definition { 13 | dependencies?: Dependency[]; 14 | } 15 | 16 | export interface Factory extends Definition { 17 | fn: AnyFunction; 18 | lifecycle: Lifecycle; 19 | resolved?: boolean; 20 | value?: any; 21 | with?: Record; 22 | } 23 | -------------------------------------------------------------------------------- /src/types/custom.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | export type NodeModule = T; 3 | export type Global = T; 4 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | export * from './JpexInstance'; 3 | export * from './BuiltIns'; 4 | export * from './custom'; 5 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { Dependency, JpexInstance, Precedence } from '../types'; 2 | 3 | export const isString = (obj: any): obj is string => typeof obj === 'string'; 4 | export const isFunction = (obj: any): obj is (...args: any[]) => any => 5 | typeof obj === 'function'; 6 | 7 | export const validateName = (name: string) => { 8 | if (!isString(name)) { 9 | throw new Error(`Name must be a string, but recevied ${typeof name}`); 10 | } 11 | }; 12 | export const validateDependencies = (dependencies: Dependency[]) => { 13 | if (!Array.isArray(dependencies)) { 14 | throw new Error( 15 | `Expected an array of dependencies, but was called with [${typeof dependencies}]`, 16 | ); 17 | } 18 | }; 19 | export const validateFactory = (name: string, fn: (...args: any[]) => any) => { 20 | if (!isFunction(fn)) { 21 | throw new Error(`Factory ${name} must be a [Function]`); 22 | } 23 | }; 24 | export const validateArgs = ( 25 | name: string, 26 | dependencies: Dependency[], 27 | fn: (...args: any[]) => any, 28 | ) => { 29 | validateName(name); 30 | validateDependencies(dependencies); 31 | validateFactory(name, fn); 32 | }; 33 | 34 | export const isPassive = ( 35 | name: string, 36 | jpex: JpexInstance, 37 | precedence?: Precedence, 38 | ) => { 39 | return ( 40 | (precedence || jpex.$$config.precedence) === 'passive' && 41 | jpex.$$factories[name] != null 42 | ); 43 | }; 44 | 45 | export const instantiate = (context: any, args: any[]) => { 46 | return new (Function.prototype.bind.apply(context, args))(); 47 | }; 48 | 49 | export const unique = (arr: T[]) => [...new Set(arr)]; 50 | 51 | export const trackDeps = (jpex: JpexInstance, dependencies: Dependency[]) => { 52 | jpex.$$deps = unique([...jpex.$$deps, ...(dependencies || [])]); 53 | }; 54 | 55 | export const isNode = () => { 56 | let _process; // eslint-disable-line no-underscore-dangle 57 | 58 | try { 59 | // eslint-disable-next-line no-new-func 60 | _process = new Function('return process')(); 61 | } catch (e) { 62 | // No process 63 | } 64 | 65 | return ( 66 | typeof _process === 'object' && 67 | _process.toString && 68 | _process.toString() === '[object process]' 69 | ); 70 | }; 71 | 72 | // eslint-disable-next-line no-new-func 73 | const doUnsafeRequire = new Function( 74 | 'require', 75 | 'target', 76 | 'return require.main.require(target)', 77 | ); 78 | export const unsafeRequire = (target: string) => { 79 | // eslint-disable-next-line no-eval 80 | return doUnsafeRequire(eval('require'), target); 81 | }; 82 | 83 | export const ensureArray = (arr: T[] | T): T[] => { 84 | if (arr == null) { 85 | return []; 86 | } 87 | if (Array.isArray(arr)) { 88 | return arr; 89 | } 90 | return [arr]; 91 | }; 92 | 93 | export const hasLength = (arr: T[]) => arr != null && arr.length > 0; 94 | 95 | export const last = (arr: T[]) => arr[arr.length - 1]; 96 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "allowJs": false, 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "alwaysStrict": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "types": ["node", "jest"] 12 | }, 13 | "include": ["src", "@types"] 14 | } 15 | --------------------------------------------------------------------------------