decorator-dependency-injection 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,34 +1,73 @@
1
1
  {
2
2
  "name": "decorator-dependency-injection",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "A simple library for dependency injection using decorators",
5
5
  "author": "Ravi Gairola <mallox@pyxzl.net>",
6
6
  "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mallocator/decorator-dependency-injection.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/mallocator/decorator-dependency-injection/issues"
13
+ },
14
+ "homepage": "https://github.com/mallocator/decorator-dependency-injection#readme",
7
15
  "keywords": [
8
16
  "dependency-injection",
9
17
  "di",
10
18
  "decorators",
11
- "mocking"
19
+ "mocking",
20
+ "singleton",
21
+ "factory"
12
22
  ],
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
13
26
  "main": "index.js",
27
+ "types": "index.d.ts",
14
28
  "type": "module",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./index.d.ts",
32
+ "import": "./index.js"
33
+ }
34
+ },
15
35
  "scripts": {
16
36
  "start": "babel-node index.js",
17
- "test": "jest"
37
+ "test": "jest --no-watchman",
38
+ "test:coverage": "jest --no-watchman --coverage",
39
+ "typecheck": "tsc test/types.check.ts --noEmit --esModuleInterop --skipLibCheck",
40
+ "lint": "eslint .",
41
+ "lint:fix": "eslint . --fix"
18
42
  },
19
43
  "jest": {
20
44
  "transform": {
21
45
  "^.+\\.js$": "babel-jest"
22
- }
46
+ },
47
+ "coverageThreshold": {
48
+ "global": {
49
+ "branches": 80,
50
+ "functions": 80,
51
+ "lines": 80,
52
+ "statements": 80
53
+ }
54
+ },
55
+ "collectCoverageFrom": [
56
+ "index.js",
57
+ "src/**/*.js"
58
+ ]
23
59
  },
24
60
  "devDependencies": {
25
61
  "@babel/cli": "^7.26.4",
26
62
  "@babel/core": "^7.26.9",
63
+ "@babel/eslint-parser": "^7.28.6",
27
64
  "@babel/plugin-proposal-decorators": "^7.25.9",
28
- "@babel/polyfill": "^7.12.1",
29
65
  "@babel/preset-env": "^7.26.9",
30
66
  "@babel/register": "^7.25.9",
67
+ "@eslint/js": "^9.39.2",
31
68
  "@types/jest": "^29.5.14",
32
- "jest": "^29.7.0"
69
+ "eslint": "^9.39.2",
70
+ "jest": "^29.7.0",
71
+ "typescript": "^5.9.3"
33
72
  }
34
- }
73
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @typedef {Object} InstanceContext
3
+ * @property {'singleton'|'factory'} type - The type of the instance.
4
+ * @property {Function} clazz - The class constructor for the instance.
5
+ * @property {Function} [originalClazz] - The original class if this is a mock.
6
+ * @property {Object} [instance] - The singleton instance, if created.
7
+ * @property {Object} [originalInstance] - The original instance if this is a mock.
8
+ * @property {boolean} [proxy=false] - If true, the mock will proxy to the original class for undefined methods/properties.
9
+ */
10
+
11
+ import {createProxy} from './proxy.js'
12
+
13
+ /**
14
+ * A dependency injection container that manages singleton and factory instances.
15
+ * Supports mocking for testing purposes.
16
+ */
17
+ export class Container {
18
+ /** @type {Map<string|Function, InstanceContext>} */
19
+ #instances = new Map()
20
+
21
+ /**
22
+ * Register a class as a singleton.
23
+ * @param {Function} clazz The class constructor
24
+ * @param {string} [name] Optional name key
25
+ */
26
+ registerSingleton(clazz, name) {
27
+ this.#register(clazz, 'singleton', name)
28
+ }
29
+
30
+ /**
31
+ * Register a class as a factory.
32
+ * @param {Function} clazz The class constructor
33
+ * @param {string} [name] Optional name key
34
+ */
35
+ registerFactory(clazz, name) {
36
+ this.#register(clazz, 'factory', name)
37
+ }
38
+
39
+ /**
40
+ * Internal registration logic.
41
+ * @param {Function} clazz The class constructor
42
+ * @param {'singleton'|'factory'} type The registration type
43
+ * @param {string} [name] Optional name key
44
+ * @private
45
+ */
46
+ #register(clazz, type, name) {
47
+ const key = name ?? clazz
48
+ if (this.#instances.has(key)) {
49
+ throw new Error(
50
+ 'A different class is already registered under this name. ' +
51
+ 'This may be a circular dependency. Try using @InjectLazy'
52
+ )
53
+ }
54
+ this.#instances.set(key, {clazz, type})
55
+ }
56
+
57
+ /**
58
+ * Get the context for a given class or name.
59
+ * @param {string|Function} clazzOrName The class or name to look up
60
+ * @returns {InstanceContext}
61
+ * @throws {Error} If the context is not found
62
+ */
63
+ getContext(clazzOrName) {
64
+ if (this.#instances.has(clazzOrName)) {
65
+ return this.#instances.get(clazzOrName)
66
+ }
67
+ const available = Array.from(this.#instances.keys())
68
+ .map(k => typeof k === 'string' ? k : k.name)
69
+ .join(', ')
70
+ throw new Error(
71
+ `Cannot find injection source for "${clazzOrName?.name || clazzOrName}". ` +
72
+ `Available: [${available}]`
73
+ )
74
+ }
75
+
76
+ /**
77
+ * Check if a class or name is registered.
78
+ * @param {string|Function} clazzOrName The class or name to check
79
+ * @returns {boolean}
80
+ */
81
+ has(clazzOrName) {
82
+ return this.#instances.has(clazzOrName)
83
+ }
84
+
85
+ /**
86
+ * Get or create an instance based on the context.
87
+ * @param {InstanceContext} instanceContext The instance context
88
+ * @param {Array} params Constructor parameters
89
+ * @returns {Object} The instance
90
+ */
91
+ getInstance(instanceContext, params) {
92
+ if (instanceContext.type === 'singleton' && !instanceContext.originalClazz && instanceContext.instance) {
93
+ return instanceContext.instance
94
+ }
95
+
96
+ let instance
97
+ try {
98
+ instance = new instanceContext.clazz(...params)
99
+ } catch (err) {
100
+ if (err instanceof RangeError) {
101
+ throw new Error(
102
+ `Circular dependency detected for ${instanceContext.clazz.name || instanceContext.clazz}. ` +
103
+ `Use @InjectLazy to break the cycle.`
104
+ )
105
+ }
106
+ throw err
107
+ }
108
+
109
+ if (instanceContext.proxy && instanceContext.originalClazz) {
110
+ const originalInstance = new instanceContext.originalClazz(...params)
111
+ instance = createProxy(instance, originalInstance)
112
+ }
113
+
114
+ if (instanceContext.type === 'singleton') {
115
+ instanceContext.instance = instance
116
+ }
117
+
118
+ return instance
119
+ }
120
+
121
+ /**
122
+ * Register a mock for an existing class.
123
+ * @param {string|Function} targetClazzOrName The class or name to mock
124
+ * @param {Function} mockClazz The mock class
125
+ * @param {boolean} [useProxy=false] Whether to proxy unmocked methods to original
126
+ */
127
+ registerMock(targetClazzOrName, mockClazz, useProxy = false) {
128
+ const instanceContext = this.getContext(targetClazzOrName)
129
+ if (instanceContext.originalClazz) {
130
+ throw new Error('Mock already defined, reset before mocking again')
131
+ }
132
+ instanceContext.originalClazz = instanceContext.clazz
133
+ instanceContext.proxy = useProxy
134
+ instanceContext.clazz = mockClazz
135
+ }
136
+
137
+ /**
138
+ * Reset a specific mock to its original class.
139
+ * @param {string|Function} clazzOrName The class or name to reset
140
+ * @throws {Error} If the class or name is not registered
141
+ */
142
+ resetMock(clazzOrName) {
143
+ const key = typeof clazzOrName === 'string' ? clazzOrName : clazzOrName
144
+ this.#restoreOriginal(this.#instances.get(key), clazzOrName)
145
+ }
146
+
147
+ /**
148
+ * Reset all mocks to their original classes.
149
+ */
150
+ resetAllMocks() {
151
+ for (const instanceContext of this.#instances.values()) {
152
+ this.#restoreOriginal(instanceContext)
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Clear all registered instances and mocks.
158
+ * Useful for test isolation.
159
+ */
160
+ clear() {
161
+ this.#instances.clear()
162
+ }
163
+
164
+ /**
165
+ * Internal function to restore an instance context to its original.
166
+ * @param {InstanceContext} instanceContext The instance context to reset
167
+ * @param {string|Function} [clazzOrName] Optional identifier for error messages
168
+ * @throws {Error} If instanceContext is null or undefined
169
+ * @private
170
+ */
171
+ #restoreOriginal(instanceContext, clazzOrName) {
172
+ if (!instanceContext) {
173
+ const name = clazzOrName?.name || clazzOrName || 'unknown'
174
+ throw new Error(`Cannot reset mock for "${name}": not registered`)
175
+ }
176
+ if (instanceContext.originalClazz) {
177
+ instanceContext.clazz = instanceContext.originalClazz
178
+ delete instanceContext.instance
179
+ delete instanceContext.originalClazz
180
+ delete instanceContext.originalInstance
181
+ delete instanceContext.proxy
182
+ }
183
+ }
184
+ }
package/src/proxy.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Create a proxy that delegates to the mock first, then falls back to the original.
3
+ * This allows partial mocking where only specific methods are overridden.
4
+ *
5
+ * @param {Object} mock The mock instance
6
+ * @param {Object} original The original instance to fall back to
7
+ * @returns {Proxy} A proxy that delegates appropriately
8
+ */
9
+ export function createProxy(mock, original) {
10
+ return new Proxy(mock, {
11
+ get(target, prop, receiver) {
12
+ if (prop in target) {
13
+ return Reflect.get(target, prop, receiver)
14
+ }
15
+ return Reflect.get(original, prop, original)
16
+ },
17
+
18
+ set(target, prop, value, receiver) {
19
+ if (prop in target) {
20
+ return Reflect.set(target, prop, value, receiver)
21
+ }
22
+ return Reflect.set(original, prop, value, original)
23
+ },
24
+
25
+ has(target, prop) {
26
+ return prop in target || prop in original
27
+ },
28
+
29
+ ownKeys(target) {
30
+ const mockKeys = Reflect.ownKeys(target)
31
+ const originalKeys = Reflect.ownKeys(original)
32
+ return [...new Set([...mockKeys, ...originalKeys])]
33
+ },
34
+
35
+ getOwnPropertyDescriptor(target, prop) {
36
+ if (prop in target) {
37
+ return Reflect.getOwnPropertyDescriptor(target, prop)
38
+ }
39
+ return Reflect.getOwnPropertyDescriptor(original, prop)
40
+ }
41
+ })
42
+ }
@@ -1,129 +0,0 @@
1
- name: Release
2
-
3
- on:
4
- push:
5
- branches: [ "main" ]
6
- pull_request:
7
- branches: [ "main" ]
8
-
9
- jobs:
10
- build:
11
- runs-on: ubuntu-latest
12
- strategy:
13
- matrix:
14
- node-version: [22.x, 23.x]
15
- # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
16
- steps:
17
- - uses: actions/checkout@v4
18
- - name: Use Node.js ${{ matrix.node-version }}
19
- uses: actions/setup-node@v4
20
- with:
21
- node-version: ${{ matrix.node-version }}
22
- cache: 'npm'
23
- - run: npm ci
24
- - run: npm run build --if-present
25
- - run: npm test
26
-
27
- release:
28
- needs: build
29
- runs-on: ubuntu-latest
30
- permissions:
31
- contents: write
32
- steps:
33
- - name: Checkout Repository
34
- uses: actions/checkout@v4
35
-
36
- - name: Install Node.js
37
- uses: actions/setup-node@v4
38
- with:
39
- node-version: '22'
40
-
41
- - name: Get package.json version
42
- id: package_version
43
- run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
44
-
45
- - name: Check if tag exists
46
- id: check_tag
47
- run: |
48
- TAG_EXISTS=$(git ls-remote --tags origin "v${VERSION}" | wc -l)
49
- echo "TAG_EXISTS=${TAG_EXISTS}" >> $GITHUB_ENV
50
-
51
- - name: Get commit messages
52
- id: commit_messages
53
- run: |
54
- COMMITS=$(git log --pretty=format:"%h - %s" $(git describe --tags --abbrev=0 @^)..@ | while read -r hash msg; do echo "[${hash}](https://github.com/${{ github.repository }}/commit/${hash}) - ${msg}"; done)
55
- echo "COMMITS=${COMMITS}" >> $GITHUB_ENV
56
-
57
- - name: Create Tag and Release
58
- uses: actions/github-script@v7
59
- with:
60
- script: |
61
- const version = process.env.VERSION
62
- const tag = `v${version}`
63
- const commits = process.env.COMMITS
64
- const { data: tags } = await github.rest.repos.listTags({
65
- owner: context.repo.owner,
66
- repo: context.repo.repo,
67
- })
68
-
69
- if (!tags.find(t => t.name === tag)) {
70
- await github.rest.git.createRef({
71
- owner: context.repo.owner,
72
- repo: context.repo.repo,
73
- ref: `refs/tags/${tag}`,
74
- sha: context.sha,
75
- })
76
-
77
- await github.rest.repos.createRelease({
78
- owner: context.repo.owner,
79
- repo: context.repo.repo,
80
- tag_name: tag,
81
- name: `Release ${tag}`,
82
- body: `Automated release of version ${tag}\n\nCommits:\n${commits}`,
83
- draft: false,
84
- prerelease: false,
85
- })
86
- core.notice(`Release ${tag} created.`)
87
- } else {
88
- core.notice(`Release ${tag} already exists. No new release created.`)
89
- }
90
-
91
- publish:
92
- needs: release
93
- runs-on: ubuntu-latest
94
- steps:
95
- - name: Checkout Repository
96
- uses: actions/checkout@v4
97
-
98
- - name: Install Node.js
99
- uses: actions/setup-node@v4
100
- with:
101
- node-version: '22'
102
- registry-url: 'https://registry.npmjs.org/'
103
-
104
- - name: Get package.json version
105
- id: package_version
106
- run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
107
-
108
- - name: Check if version exists on npm
109
- id: check_npm_version
110
- run: |
111
- if npm show decorator-dependency-injection@${{ env.VERSION }} > /dev/null 2>&1; then
112
- echo "VERSION_EXISTS=true" >> $GITHUB_ENV
113
- else
114
- echo "VERSION_EXISTS=false" >> $GITHUB_ENV
115
- fi
116
-
117
- - name: Publish to npm
118
- if: env.VERSION_EXISTS == 'false'
119
- run: |
120
- npm publish --access public
121
- echo "::notice::Published new version ${{ env.VERSION }} to npm."
122
- env:
123
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
124
-
125
- - name: Notice Publish Status
126
- if: env.VERSION_EXISTS == 'true'
127
- run: echo "::notice::Version ${{ env.VERSION }} already exists on npm. No new version published."
128
-
129
-
package/babel.config.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "presets": ["@babel/preset-env"],
3
- "plugins": [
4
- ["@babel/plugin-proposal-decorators", { "version": "2023-11" }]
5
- ]
6
- }