@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.
- package/.github/workflows/ci.yml +6 -5
- package/CHANGELOG.md +13 -0
- package/README.md +54 -13
- package/dist/{GraphValidator-G0F4QiLk.cjs → GraphValidator-CV4VoJl0.cjs} +18 -10
- package/dist/{GraphValidator-C8ldJtNp.mjs → GraphValidator-DXqqkNdS.mjs} +18 -10
- package/dist/cli/index.cjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.d.cts +30 -4
- package/dist/index.d.mts +30 -4
- package/dist/lsp/index.cjs +1 -1
- package/dist/lsp/index.mjs +1 -1
- package/dist/unplugin/index.cjs +2 -2
- package/dist/unplugin/index.mjs +2 -2
- package/package.json +11 -10
- package/src/analyzer/Analyzer.ts +20 -10
- package/src/analyzer/types.ts +7 -1
- package/src/generator/Generator.ts +1 -1
- package/src/generator/GraphValidator.ts +6 -2
- package/src/types.ts +29 -3
- package/tests/analyzer/Analyzer.test.ts +4 -4
- package/tests/analyzer/Factory.test.ts +2 -2
- package/tests/analyzer/Scoped.test.ts +434 -0
- package/tests/cli/cli.test.ts +91 -0
- package/tests/e2e/container-integration.test.ts +2 -2
- package/tests/e2e/generated-code.test.ts +5 -5
- package/tests/e2e/scoped.test.ts +370 -0
- package/tests/e2e/snapshots.test.ts +2 -2
- package/tests/e2e/standalone.test.ts +2 -2
- package/tests/generator/ExternalGenerator.test.ts +1 -1
- package/tests/generator/FactoryGenerator.test.ts +6 -6
- package/tests/generator/Generator.test.ts +2 -2
- package/tests/generator/GeneratorDeclarative.test.ts +1 -1
- package/tests/generator/GraphValidator.test.ts +1 -1
package/.github/workflows/ci.yml
CHANGED
|
@@ -69,11 +69,12 @@ jobs:
|
|
|
69
69
|
|
|
70
70
|
- name: Verify build output
|
|
71
71
|
run: |
|
|
72
|
-
test -f dist/index.
|
|
73
|
-
test -f dist/index.
|
|
74
|
-
test -f dist/
|
|
75
|
-
test -f dist/
|
|
76
|
-
test -f dist/
|
|
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)
|
|
80
|
-
|
|
81
|
-
### Build System Integration (Recommended)
|
|
82
|
+
> **Note**: `typescript` (>=5.0.0) and `unplugin` are required peer dependencies.
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
### Build System Integration
|
|
84
85
|
|
|
85
|
-
|
|
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
|
-
###
|
|
288
|
+
### Lifecycle
|
|
290
289
|
|
|
291
290
|
```typescript
|
|
292
|
-
{ token: UserService,
|
|
291
|
+
{ token: UserService, lifecycle: 'transient' } // Default is 'singleton'
|
|
293
292
|
|
|
294
|
-
// Factories support
|
|
295
|
-
{
|
|
296
|
-
token: useInterface<IRequest>(),
|
|
293
|
+
// Factories support lifecycle too
|
|
294
|
+
{
|
|
295
|
+
token: useInterface<IRequest>(),
|
|
297
296
|
provider: () => ({ id: crypto.randomUUID() }),
|
|
298
|
-
|
|
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
|
|
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 === "
|
|
214
|
-
if (prop.initializer.text === "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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 === "
|
|
186
|
-
if (prop.initializer.text === "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
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
package/dist/cli/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_GraphValidator = require('../GraphValidator-
|
|
2
|
+
const require_GraphValidator = require('../GraphValidator-CV4VoJl0.cjs');
|
|
3
3
|
let typescript = require("typescript");
|
|
4
4
|
typescript = require_GraphValidator.__toESM(typescript);
|
|
5
5
|
|
package/dist/cli/index.mjs
CHANGED
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
168
|
+
export { BuilderConfig, Constructor, Container, Factory, Injection, InterfaceToken, Lifecycle, PartialConfig, PropertyToken, Provider, Token, declareContainerTokens, defineBuilderConfig, definePartialConfig, useInterface, useProperty };
|
package/dist/lsp/index.cjs
CHANGED
package/dist/lsp/index.mjs
CHANGED
package/dist/unplugin/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_GraphValidator = require('../GraphValidator-
|
|
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.
|
|
56
|
+
const isTransient = node.service.lifecycle === "transient";
|
|
57
57
|
let tokenKey;
|
|
58
58
|
let tokenCheck;
|
|
59
59
|
if (node.service.isInterfaceToken) {
|
package/dist/unplugin/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as Analyzer, t as GraphValidator } from "../GraphValidator-
|
|
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.
|
|
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.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Zero-Overhead, Compile-Time Dependency Injection for TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
8
|
-
"types": "dist/index.d.
|
|
8
|
+
"types": "dist/index.d.mts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"types": "./dist/index.d.
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
12
|
"import": "./dist/index.mjs",
|
|
13
|
-
"require": "./dist/index.
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
14
|
},
|
|
15
15
|
"./plugin": {
|
|
16
|
-
"types": "./dist/unplugin/index.d.
|
|
16
|
+
"types": "./dist/unplugin/index.d.mts",
|
|
17
17
|
"import": "./dist/unplugin/index.mjs",
|
|
18
|
-
"require": "./dist/unplugin/index.
|
|
18
|
+
"require": "./dist/unplugin/index.cjs"
|
|
19
19
|
},
|
|
20
20
|
"./lsp": {
|
|
21
|
-
"types": "./dist/lsp/index.d.
|
|
22
|
-
"
|
|
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.
|
|
27
|
+
"neo-syringe": "./dist/cli/index.mjs"
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
30
|
"build": "tsdown",
|
package/src/analyzer/Analyzer.ts
CHANGED
|
@@ -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,
|
|
286
|
+
// Extract properties: token, provider, lifecycle, useFactory, scoped
|
|
287
287
|
let tokenNode: ts.Expression | undefined;
|
|
288
288
|
let providerNode: ts.Expression | undefined;
|
|
289
|
-
let
|
|
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 === '
|
|
300
|
-
if (prop.initializer.text === '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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
}
|
package/src/analyzer/types.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface ServiceDefinition {
|
|
|
24
24
|
registrationNode: Node;
|
|
25
25
|
|
|
26
26
|
type: RegistrationType;
|
|
27
|
-
|
|
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.
|
|
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
|
|
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
|
-
`
|
|
30
|
+
`Use 'scoped: true' to override the parent's registration intentionally.`
|
|
27
31
|
);
|
|
28
32
|
}
|
|
29
33
|
}
|