@djodjonx/neo-syringe 1.1.5 → 1.2.0

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.
@@ -69,11 +69,12 @@ jobs:
69
69
 
70
70
  - name: Verify build output
71
71
  run: |
72
- test -f dist/index.js
73
- test -f dist/index.d.ts
74
- test -f dist/unplugin/index.js
75
- test -f dist/lsp/index.js
76
- test -f dist/cli/index.js
72
+ test -f dist/index.mjs
73
+ test -f dist/index.cjs
74
+ test -f dist/index.d.mts
75
+ test -f dist/unplugin/index.mjs
76
+ test -f dist/lsp/index.mjs
77
+ test -f dist/cli/index.mjs
77
78
 
78
79
  security:
79
80
  runs-on: ubuntu-latest
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [1.2.0](https://github.com/djodjonx/neo-syringe/compare/v1.1.5...v1.2.0) (2026-01-18)
6
+
7
+
8
+ ### Features
9
+
10
+ * rename 'scope' to 'lifecycle' to avoid confusion with 'scoped' ([b246121](https://github.com/djodjonx/neo-syringe/commit/b246121a5f71cbf26ce80ffba6ec6ea5b09017f8))
11
+ * **scoped:** add scoped injection support for parent container overrides ([170204c](https://github.com/djodjonx/neo-syringe/commit/170204c48f2c65dc7158adc11931fad4d2a91d6b))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * correct build output paths for ESM/CJS ([8ff7b74](https://github.com/djodjonx/neo-syringe/commit/8ff7b7476b3333540cb840cd21051ce935de88ac))
17
+
5
18
  ### [1.1.5](https://github.com/djodjonx/neo-syringe/compare/v1.1.4...v1.1.5) (2026-01-18)
6
19
 
7
20
  ### [1.1.4](https://github.com/djodjonx/neo-syringe/compare/v1.1.3...v1.1.4) (2026-01-18)
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  <p align="center">
6
6
  <a href="https://www.npmjs.com/package/@djodjonx/neo-syringe"><img src="https://img.shields.io/npm/v/@djodjonx/neo-syringe.svg?style=flat-square" alt="npm version"></a>
7
+ <a href="https://github.com/djodjonx/neo-syringe/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/djodjonx/neo-syringe/ci.yml?style=flat-square&label=tests" alt="Tests"></a>
7
8
  <a href="https://github.com/djodjonx/neo-syringe/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/djodjonx/neo-syringe/ci.yml?style=flat-square" alt="CI"></a>
8
9
  <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-blue.svg?style=flat-square" alt="TypeScript"></a>
9
10
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square" alt="License: MIT"></a>
@@ -71,20 +72,18 @@ const config = defineBuilderConfig({
71
72
  ```bash
72
73
  # With npm
73
74
  npm install @djodjonx/neo-syringe
75
+ npm install -D unplugin
74
76
 
75
77
  # With pnpm
76
78
  pnpm add @djodjonx/neo-syringe
79
+ pnpm add -D unplugin
77
80
  ```
78
81
 
79
- > **Note**: `typescript` (>=5.0.0) is a required peer dependency.
80
-
81
- ### Build System Integration (Recommended)
82
+ > **Note**: `typescript` (>=5.0.0) and `unplugin` are required peer dependencies.
82
83
 
83
- For compile-time code generation, install `unplugin`:
84
+ ### Build System Integration
84
85
 
85
- ```bash
86
- pnpm add -D unplugin
87
- ```
86
+ Configure the plugin in your bundler to enable compile-time code generation.
88
87
 
89
88
  <details>
90
89
  <summary><strong>Vite</strong></summary>
@@ -286,19 +285,61 @@ export const config = defineBuilderConfig({
286
285
  - ✅ LSP validation: Detects if parameter doesn't exist
287
286
  - ✅ Refactoring-friendly: Rename parameter → LSP error
288
287
 
289
- ### Scopes
288
+ ### Lifecycle
290
289
 
291
290
  ```typescript
292
- { token: UserService, scope: 'transient' } // Default is 'singleton'
291
+ { token: UserService, lifecycle: 'transient' } // Default is 'singleton'
293
292
 
294
- // Factories support scopes too
295
- {
296
- token: useInterface<IRequest>(),
293
+ // Factories support lifecycle too
294
+ {
295
+ token: useInterface<IRequest>(),
297
296
  provider: () => ({ id: crypto.randomUUID() }),
298
- scope: 'transient'
297
+ lifecycle: 'transient'
299
298
  }
300
299
  ```
301
300
 
301
+ ### Scoped Injections (`scoped: true`)
302
+
303
+ Override a token from a parent container **without causing a duplicate error**. Useful for testing or module-specific overrides.
304
+
305
+ ```typescript
306
+ // SharedKernel - Production logger
307
+ const sharedKernel = defineBuilderConfig({
308
+ name: 'SharedKernel',
309
+ injections: [
310
+ { token: useInterface<ILogger>(), provider: ConsoleLogger, lifecycle: 'singleton' }
311
+ ]
312
+ });
313
+
314
+ // TestModule - Override with MockLogger, scoped to this container
315
+ const testModule = defineBuilderConfig({
316
+ name: 'TestModule',
317
+ useContainer: sharedKernel,
318
+ injections: [
319
+ {
320
+ token: useInterface<ILogger>(),
321
+ provider: MockLogger,
322
+ lifecycle: 'transient', // Can use different lifecycle than parent
323
+ scoped: true // 👈 Allows override without duplicate error
324
+ },
325
+ { token: UserService } // Uses MockLogger from this container
326
+ ]
327
+ });
328
+ ```
329
+
330
+ **Behavior:**
331
+
332
+ | Scenario | Without `scoped` | With `scoped: true` |
333
+ |----------|------------------|---------------------|
334
+ | Token exists in parent | ❌ Duplicate error | ✅ Override locally |
335
+ | Token does not exist in parent | ✅ OK | ✅ OK |
336
+ | Resolution | Delegates to parent | Uses local instance |
337
+
338
+ **Use Cases:**
339
+ - 🧪 **Testing**: Override production services with mocks
340
+ - 🔧 **Module isolation**: Each module has its own instance
341
+ - ⚙️ **Different lifecycle**: Parent uses singleton, child uses transient
342
+
302
343
  ---
303
344
 
304
345
  ## Generated Code Example
@@ -204,16 +204,19 @@ var Analyzer = class {
204
204
  parseInjectionObject(obj, graph) {
205
205
  let tokenNode;
206
206
  let providerNode;
207
- let scope = "singleton";
207
+ let lifecycle = "singleton";
208
208
  let useFactory = false;
209
+ let isScoped = false;
209
210
  for (const prop of obj.properties) {
210
211
  if (!typescript.isPropertyAssignment(prop) || !typescript.isIdentifier(prop.name)) continue;
211
212
  if (prop.name.text === "token") tokenNode = prop.initializer;
212
213
  else if (prop.name.text === "provider") providerNode = prop.initializer;
213
- else if (prop.name.text === "scope" && typescript.isStringLiteral(prop.initializer)) {
214
- if (prop.initializer.text === "transient") scope = "transient";
214
+ else if (prop.name.text === "lifecycle" && typescript.isStringLiteral(prop.initializer)) {
215
+ if (prop.initializer.text === "transient") lifecycle = "transient";
215
216
  } else if (prop.name.text === "useFactory") {
216
217
  if (prop.initializer.kind === typescript.SyntaxKind.TrueKeyword) useFactory = true;
218
+ } else if (prop.name.text === "scoped") {
219
+ if (prop.initializer.kind === typescript.SyntaxKind.TrueKeyword) isScoped = true;
217
220
  }
218
221
  }
219
222
  if (!tokenNode) return;
@@ -250,17 +253,18 @@ var Analyzer = class {
250
253
  factorySource = providerNode.getText();
251
254
  type = "factory";
252
255
  if (tokenId) {
253
- if (graph.nodes.has(tokenId)) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
256
+ if (graph.nodes.has(tokenId) && !isScoped) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
254
257
  const definition = {
255
258
  tokenId,
256
259
  tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : void 0,
257
260
  registrationNode: obj,
258
261
  type: "factory",
259
- scope,
262
+ lifecycle,
260
263
  isInterfaceToken,
261
264
  isValueToken,
262
265
  isFactory: true,
263
- factorySource
266
+ factorySource,
267
+ isScoped
264
268
  };
265
269
  graph.nodes.set(tokenId, {
266
270
  service: definition,
@@ -282,15 +286,16 @@ var Analyzer = class {
282
286
  type = "autowire";
283
287
  }
284
288
  if (tokenId && implementationSymbol) {
285
- if (graph.nodes.has(tokenId)) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
289
+ if (graph.nodes.has(tokenId) && !isScoped) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
286
290
  const definition = {
287
291
  tokenId,
288
292
  implementationSymbol: this.resolveSymbol(implementationSymbol),
289
293
  tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : void 0,
290
294
  registrationNode: obj,
291
295
  type,
292
- scope,
293
- isInterfaceToken: isInterfaceToken || typescript.isCallExpression(tokenNode) && this.isUseInterfaceCall(tokenNode)
296
+ lifecycle,
297
+ isInterfaceToken: isInterfaceToken || typescript.isCallExpression(tokenNode) && this.isUseInterfaceCall(tokenNode),
298
+ isScoped
294
299
  };
295
300
  graph.nodes.set(tokenId, {
296
301
  service: definition,
@@ -433,7 +438,10 @@ var GraphValidator = class {
433
438
  const visited = /* @__PURE__ */ new Set();
434
439
  const recursionStack = /* @__PURE__ */ new Set();
435
440
  const parentTokens = graph.parentProvidedTokens ?? /* @__PURE__ */ new Set();
436
- for (const nodeId of graph.nodes.keys()) if (parentTokens.has(nodeId)) throw new Error(`Duplicate registration: '${nodeId}' is already registered in the parent container. Remove the local registration or use a different token.`);
441
+ for (const [nodeId, node] of graph.nodes) if (parentTokens.has(nodeId)) {
442
+ if (node.service.isScoped) continue;
443
+ throw new Error(`Duplicate registration: '${nodeId}' is already registered in the parent container. Use 'scoped: true' to override the parent's registration intentionally.`);
444
+ }
437
445
  for (const [nodeId, node] of graph.nodes) for (const depId of node.dependencies) {
438
446
  const isProvidedLocally = graph.nodes.has(depId);
439
447
  const isProvidedByParent = parentTokens.has(depId);
@@ -176,16 +176,19 @@ var Analyzer = class {
176
176
  parseInjectionObject(obj, graph) {
177
177
  let tokenNode;
178
178
  let providerNode;
179
- let scope = "singleton";
179
+ let lifecycle = "singleton";
180
180
  let useFactory = false;
181
+ let isScoped = false;
181
182
  for (const prop of obj.properties) {
182
183
  if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
183
184
  if (prop.name.text === "token") tokenNode = prop.initializer;
184
185
  else if (prop.name.text === "provider") providerNode = prop.initializer;
185
- else if (prop.name.text === "scope" && ts.isStringLiteral(prop.initializer)) {
186
- if (prop.initializer.text === "transient") scope = "transient";
186
+ else if (prop.name.text === "lifecycle" && ts.isStringLiteral(prop.initializer)) {
187
+ if (prop.initializer.text === "transient") lifecycle = "transient";
187
188
  } else if (prop.name.text === "useFactory") {
188
189
  if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) useFactory = true;
190
+ } else if (prop.name.text === "scoped") {
191
+ if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) isScoped = true;
189
192
  }
190
193
  }
191
194
  if (!tokenNode) return;
@@ -222,17 +225,18 @@ var Analyzer = class {
222
225
  factorySource = providerNode.getText();
223
226
  type = "factory";
224
227
  if (tokenId) {
225
- if (graph.nodes.has(tokenId)) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
228
+ if (graph.nodes.has(tokenId) && !isScoped) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
226
229
  const definition = {
227
230
  tokenId,
228
231
  tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : void 0,
229
232
  registrationNode: obj,
230
233
  type: "factory",
231
- scope,
234
+ lifecycle,
232
235
  isInterfaceToken,
233
236
  isValueToken,
234
237
  isFactory: true,
235
- factorySource
238
+ factorySource,
239
+ isScoped
236
240
  };
237
241
  graph.nodes.set(tokenId, {
238
242
  service: definition,
@@ -254,15 +258,16 @@ var Analyzer = class {
254
258
  type = "autowire";
255
259
  }
256
260
  if (tokenId && implementationSymbol) {
257
- if (graph.nodes.has(tokenId)) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
261
+ if (graph.nodes.has(tokenId) && !isScoped) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
258
262
  const definition = {
259
263
  tokenId,
260
264
  implementationSymbol: this.resolveSymbol(implementationSymbol),
261
265
  tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : void 0,
262
266
  registrationNode: obj,
263
267
  type,
264
- scope,
265
- isInterfaceToken: isInterfaceToken || ts.isCallExpression(tokenNode) && this.isUseInterfaceCall(tokenNode)
268
+ lifecycle,
269
+ isInterfaceToken: isInterfaceToken || ts.isCallExpression(tokenNode) && this.isUseInterfaceCall(tokenNode),
270
+ isScoped
266
271
  };
267
272
  graph.nodes.set(tokenId, {
268
273
  service: definition,
@@ -405,7 +410,10 @@ var GraphValidator = class {
405
410
  const visited = /* @__PURE__ */ new Set();
406
411
  const recursionStack = /* @__PURE__ */ new Set();
407
412
  const parentTokens = graph.parentProvidedTokens ?? /* @__PURE__ */ new Set();
408
- for (const nodeId of graph.nodes.keys()) if (parentTokens.has(nodeId)) throw new Error(`Duplicate registration: '${nodeId}' is already registered in the parent container. Remove the local registration or use a different token.`);
413
+ for (const [nodeId, node] of graph.nodes) if (parentTokens.has(nodeId)) {
414
+ if (node.service.isScoped) continue;
415
+ throw new Error(`Duplicate registration: '${nodeId}' is already registered in the parent container. Use 'scoped: true' to override the parent's registration intentionally.`);
416
+ }
409
417
  for (const [nodeId, node] of graph.nodes) for (const depId of node.dependencies) {
410
418
  const isProvidedLocally = graph.nodes.has(depId);
411
419
  const isProvidedByParent = parentTokens.has(depId);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_GraphValidator = require('../GraphValidator-G0F4QiLk.cjs');
2
+ const require_GraphValidator = require('../GraphValidator-CV4VoJl0.cjs');
3
3
  let typescript = require("typescript");
4
4
  typescript = require_GraphValidator.__toESM(typescript);
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as Analyzer, t as GraphValidator } from "../GraphValidator-C8ldJtNp.mjs";
2
+ import { n as Analyzer, t as GraphValidator } from "../GraphValidator-DXqqkNdS.mjs";
3
3
  import * as ts from "typescript";
4
4
 
5
5
  //#region src/cli/index.ts
package/dist/index.d.cts CHANGED
@@ -5,11 +5,11 @@
5
5
  */
6
6
  type Constructor<T = unknown> = new (...args: unknown[]) => T;
7
7
  /**
8
- * Defines the lifecycle scope of a service.
8
+ * Defines the lifecycle of a service.
9
9
  * - `singleton`: One instance per container.
10
10
  * - `transient`: A new instance every time it is resolved.
11
11
  */
12
- type Scope = 'singleton' | 'transient';
12
+ type Lifecycle = 'singleton' | 'transient';
13
13
  /**
14
14
  * The dependency injection container interface.
15
15
  * Provides access to the registered services.
@@ -74,7 +74,33 @@ interface Injection<T = any> {
74
74
  * Required when provider is a function, not a class.
75
75
  */
76
76
  useFactory?: boolean;
77
- scope?: Scope;
77
+ /**
78
+ * Lifecycle of the service.
79
+ * - `singleton`: One instance per container (default).
80
+ * - `transient`: A new instance every time it is resolved.
81
+ */
82
+ lifecycle?: Lifecycle;
83
+ /**
84
+ * If true, this injection is scoped to this container only.
85
+ * Allows overriding a token from a parent container without causing a duplicate error.
86
+ * The local instance will be used instead of delegating to the parent.
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * const parent = defineBuilderConfig({
91
+ * injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }]
92
+ * });
93
+ *
94
+ * const child = defineBuilderConfig({
95
+ * useContainer: parent,
96
+ * injections: [
97
+ * // Override parent's ILogger with a local FileLogger
98
+ * { token: useInterface<ILogger>(), provider: FileLogger, scoped: true }
99
+ * ]
100
+ * });
101
+ * ```
102
+ */
103
+ scoped?: boolean;
78
104
  }
79
105
  /**
80
106
  * Partial configuration that can be shared/extended.
@@ -139,4 +165,4 @@ declare function useProperty<T, C extends Constructor<any>>(targetClass: C, para
139
165
  */
140
166
  declare function declareContainerTokens<T extends Record<string, any>>(container: any): T & { [K in keyof T]: T[K] };
141
167
  //#endregion
142
- export { BuilderConfig, Constructor, Container, Factory, Injection, InterfaceToken, PartialConfig, PropertyToken, Provider, Scope, Token, declareContainerTokens, defineBuilderConfig, definePartialConfig, useInterface, useProperty };
168
+ export { BuilderConfig, Constructor, Container, Factory, Injection, InterfaceToken, Lifecycle, PartialConfig, PropertyToken, Provider, Token, declareContainerTokens, defineBuilderConfig, definePartialConfig, useInterface, useProperty };
package/dist/index.d.mts CHANGED
@@ -5,11 +5,11 @@
5
5
  */
6
6
  type Constructor<T = unknown> = new (...args: unknown[]) => T;
7
7
  /**
8
- * Defines the lifecycle scope of a service.
8
+ * Defines the lifecycle of a service.
9
9
  * - `singleton`: One instance per container.
10
10
  * - `transient`: A new instance every time it is resolved.
11
11
  */
12
- type Scope = 'singleton' | 'transient';
12
+ type Lifecycle = 'singleton' | 'transient';
13
13
  /**
14
14
  * The dependency injection container interface.
15
15
  * Provides access to the registered services.
@@ -74,7 +74,33 @@ interface Injection<T = any> {
74
74
  * Required when provider is a function, not a class.
75
75
  */
76
76
  useFactory?: boolean;
77
- scope?: Scope;
77
+ /**
78
+ * Lifecycle of the service.
79
+ * - `singleton`: One instance per container (default).
80
+ * - `transient`: A new instance every time it is resolved.
81
+ */
82
+ lifecycle?: Lifecycle;
83
+ /**
84
+ * If true, this injection is scoped to this container only.
85
+ * Allows overriding a token from a parent container without causing a duplicate error.
86
+ * The local instance will be used instead of delegating to the parent.
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * const parent = defineBuilderConfig({
91
+ * injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }]
92
+ * });
93
+ *
94
+ * const child = defineBuilderConfig({
95
+ * useContainer: parent,
96
+ * injections: [
97
+ * // Override parent's ILogger with a local FileLogger
98
+ * { token: useInterface<ILogger>(), provider: FileLogger, scoped: true }
99
+ * ]
100
+ * });
101
+ * ```
102
+ */
103
+ scoped?: boolean;
78
104
  }
79
105
  /**
80
106
  * Partial configuration that can be shared/extended.
@@ -139,4 +165,4 @@ declare function useProperty<T, C extends Constructor<any>>(targetClass: C, para
139
165
  */
140
166
  declare function declareContainerTokens<T extends Record<string, any>>(container: any): T & { [K in keyof T]: T[K] };
141
167
  //#endregion
142
- export { BuilderConfig, Constructor, Container, Factory, Injection, InterfaceToken, PartialConfig, PropertyToken, Provider, Scope, Token, declareContainerTokens, defineBuilderConfig, definePartialConfig, useInterface, useProperty };
168
+ export { BuilderConfig, Constructor, Container, Factory, Injection, InterfaceToken, Lifecycle, PartialConfig, PropertyToken, Provider, Token, declareContainerTokens, defineBuilderConfig, definePartialConfig, useInterface, useProperty };
@@ -1,4 +1,4 @@
1
- const require_GraphValidator = require('../GraphValidator-G0F4QiLk.cjs');
1
+ const require_GraphValidator = require('../GraphValidator-CV4VoJl0.cjs');
2
2
 
3
3
  //#region src/lsp/index.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { n as Analyzer, t as GraphValidator } from "../GraphValidator-C8ldJtNp.mjs";
1
+ import { n as Analyzer, t as GraphValidator } from "../GraphValidator-DXqqkNdS.mjs";
2
2
 
3
3
  //#region src/lsp/index.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- const require_GraphValidator = require('../GraphValidator-G0F4QiLk.cjs');
1
+ const require_GraphValidator = require('../GraphValidator-CV4VoJl0.cjs');
2
2
  let unplugin = require("unplugin");
3
3
  let typescript = require("typescript");
4
4
  typescript = require_GraphValidator.__toESM(typescript);
@@ -53,7 +53,7 @@ function ${factoryId}(container: NeoContainer) {
53
53
  return new ${className}(${args});
54
54
  }`);
55
55
  }
56
- const isTransient = node.service.scope === "transient";
56
+ const isTransient = node.service.lifecycle === "transient";
57
57
  let tokenKey;
58
58
  let tokenCheck;
59
59
  if (node.service.isInterfaceToken) {
@@ -1,4 +1,4 @@
1
- import { n as Analyzer, t as GraphValidator } from "../GraphValidator-C8ldJtNp.mjs";
1
+ import { n as Analyzer, t as GraphValidator } from "../GraphValidator-DXqqkNdS.mjs";
2
2
  import { createUnplugin } from "unplugin";
3
3
  import * as ts from "typescript";
4
4
 
@@ -52,7 +52,7 @@ function ${factoryId}(container: NeoContainer) {
52
52
  return new ${className}(${args});
53
53
  }`);
54
54
  }
55
- const isTransient = node.service.scope === "transient";
55
+ const isTransient = node.service.lifecycle === "transient";
56
56
  let tokenKey;
57
57
  let tokenCheck;
58
58
  if (node.service.isInterfaceToken) {
package/package.json CHANGED
@@ -1,29 +1,30 @@
1
1
  {
2
2
  "name": "@djodjonx/neo-syringe",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "Zero-Overhead, Compile-Time Dependency Injection for TypeScript",
5
5
  "type": "module",
6
- "main": "dist/index.js",
6
+ "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
8
- "types": "dist/index.d.ts",
8
+ "types": "dist/index.d.mts",
9
9
  "exports": {
10
10
  ".": {
11
- "types": "./dist/index.d.ts",
11
+ "types": "./dist/index.d.mts",
12
12
  "import": "./dist/index.mjs",
13
- "require": "./dist/index.js"
13
+ "require": "./dist/index.cjs"
14
14
  },
15
15
  "./plugin": {
16
- "types": "./dist/unplugin/index.d.ts",
16
+ "types": "./dist/unplugin/index.d.mts",
17
17
  "import": "./dist/unplugin/index.mjs",
18
- "require": "./dist/unplugin/index.js"
18
+ "require": "./dist/unplugin/index.cjs"
19
19
  },
20
20
  "./lsp": {
21
- "types": "./dist/lsp/index.d.ts",
22
- "require": "./dist/lsp/index.js"
21
+ "types": "./dist/lsp/index.d.mts",
22
+ "import": "./dist/lsp/index.mjs",
23
+ "require": "./dist/lsp/index.cjs"
23
24
  }
24
25
  },
25
26
  "bin": {
26
- "neo-syringe": "./dist/cli/index.js"
27
+ "neo-syringe": "./dist/cli/index.mjs"
27
28
  },
28
29
  "scripts": {
29
30
  "build": "tsdown",
@@ -283,11 +283,12 @@ export class Analyzer {
283
283
  }
284
284
 
285
285
  private parseInjectionObject(obj: ts.ObjectLiteralExpression, graph: DependencyGraph): void {
286
- // Extract properties: token, provider, scope, useFactory
286
+ // Extract properties: token, provider, lifecycle, useFactory, scoped
287
287
  let tokenNode: ts.Expression | undefined;
288
288
  let providerNode: ts.Expression | undefined;
289
- let scope: 'singleton' | 'transient' = 'singleton';
289
+ let lifecycle: 'singleton' | 'transient' = 'singleton';
290
290
  let useFactory = false;
291
+ let isScoped = false;
291
292
 
292
293
  for (const prop of obj.properties) {
293
294
  if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
@@ -296,13 +297,18 @@ export class Analyzer {
296
297
  tokenNode = prop.initializer;
297
298
  } else if (prop.name.text === 'provider') {
298
299
  providerNode = prop.initializer;
299
- } else if (prop.name.text === 'scope' && ts.isStringLiteral(prop.initializer)) {
300
- if (prop.initializer.text === 'transient') scope = 'transient';
300
+ } else if (prop.name.text === 'lifecycle' && ts.isStringLiteral(prop.initializer)) {
301
+ if (prop.initializer.text === 'transient') lifecycle = 'transient';
301
302
  } else if (prop.name.text === 'useFactory') {
302
303
  // Check if useFactory: true
303
304
  if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
304
305
  useFactory = true;
305
306
  }
307
+ } else if (prop.name.text === 'scoped') {
308
+ // Check if scoped: true
309
+ if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
310
+ isScoped = true;
311
+ }
306
312
  }
307
313
  }
308
314
 
@@ -362,7 +368,8 @@ export class Analyzer {
362
368
  type = 'factory';
363
369
 
364
370
  if (tokenId) {
365
- if (graph.nodes.has(tokenId)) {
371
+ // Check for duplicate - allow if scoped: true (intentional override)
372
+ if (graph.nodes.has(tokenId) && !isScoped) {
366
373
  throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
367
374
  }
368
375
 
@@ -371,11 +378,12 @@ export class Analyzer {
371
378
  tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : undefined,
372
379
  registrationNode: obj,
373
380
  type: 'factory',
374
- scope: scope,
381
+ lifecycle: lifecycle,
375
382
  isInterfaceToken,
376
383
  isValueToken,
377
384
  isFactory: true,
378
- factorySource
385
+ factorySource,
386
+ isScoped
379
387
  };
380
388
  graph.nodes.set(tokenId, { service: definition, dependencies: [] });
381
389
  }
@@ -406,7 +414,8 @@ export class Analyzer {
406
414
  }
407
415
 
408
416
  if (tokenId && implementationSymbol) {
409
- if (graph.nodes.has(tokenId)) {
417
+ // Check for duplicate - allow if scoped: true (intentional override)
418
+ if (graph.nodes.has(tokenId) && !isScoped) {
410
419
  throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
411
420
  }
412
421
 
@@ -416,8 +425,9 @@ export class Analyzer {
416
425
  tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : undefined,
417
426
  registrationNode: obj,
418
427
  type: type,
419
- scope: scope,
420
- isInterfaceToken: isInterfaceToken || (ts.isCallExpression(tokenNode) && this.isUseInterfaceCall(tokenNode))
428
+ lifecycle: lifecycle,
429
+ isInterfaceToken: isInterfaceToken || (ts.isCallExpression(tokenNode) && this.isUseInterfaceCall(tokenNode)),
430
+ isScoped
421
431
  };
422
432
  graph.nodes.set(tokenId, { service: definition, dependencies: [] });
423
433
  }
@@ -24,7 +24,7 @@ export interface ServiceDefinition {
24
24
  registrationNode: Node;
25
25
 
26
26
  type: RegistrationType;
27
- scope: 'singleton' | 'transient';
27
+ lifecycle: 'singleton' | 'transient';
28
28
 
29
29
  /**
30
30
  * True if the token is an Interface (requires string literal key).
@@ -47,6 +47,12 @@ export interface ServiceDefinition {
47
47
  * The raw source text of the factory function (for code generation).
48
48
  */
49
49
  factorySource?: string;
50
+
51
+ /**
52
+ * True if this injection is scoped to the local container.
53
+ * Allows overriding a token from a parent container without duplicate error.
54
+ */
55
+ isScoped?: boolean;
50
56
  }
51
57
 
52
58
  export interface DependencyNode {
@@ -78,7 +78,7 @@ function ${factoryId}(container: NeoContainer) {
78
78
  }
79
79
 
80
80
  // 2. Generate Resolve Switch Case
81
- const isTransient = node.service.scope === 'transient';
81
+ const isTransient = node.service.lifecycle === 'transient';
82
82
 
83
83
  // Determine key for instances Map and token check
84
84
  let tokenKey: string;
@@ -19,11 +19,15 @@ export class GraphValidator {
19
19
  const parentTokens = graph.parentProvidedTokens ?? new Set<TokenId>();
20
20
 
21
21
  // 1. Check for Duplicate Registrations (local token already in parent)
22
- for (const nodeId of graph.nodes.keys()) {
22
+ for (const [nodeId, node] of graph.nodes) {
23
23
  if (parentTokens.has(nodeId)) {
24
+ // Allow if scoped: true (intentional override)
25
+ if (node.service.isScoped) {
26
+ continue;
27
+ }
24
28
  throw new Error(
25
29
  `Duplicate registration: '${nodeId}' is already registered in the parent container. ` +
26
- `Remove the local registration or use a different token.`
30
+ `Use 'scoped: true' to override the parent's registration intentionally.`
27
31
  );
28
32
  }
29
33
  }