@thomasrepnik/ts-implscan 0.0.0-stage → 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thomas Repnik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,293 @@
1
- # Temporary Holding Version
1
+ # ts-implscan
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/@thomasrepnik/ts-implscan)](https://www.npmjs.com/package/@thomasrepnik/ts-implscan)
4
+ [![CI](https://github.com/thomasrepnik/ts-implscan/actions/workflows/ci.yml/badge.svg)](https://github.com/thomasrepnik/ts-implscan/actions/workflows/ci.yml)
5
+
6
+ **Spring-style `List<A>` injection for TypeScript.** Find every `implements A` and generate the registry.
7
+
8
+ ## Installation
9
+
10
+ ```sh
11
+ npm install --save-dev @thomasrepnik/ts-implscan
12
+ ```
13
+
14
+ The package contains two things:
15
+
16
+ | What | How you use it |
17
+ |---|---|
18
+ | The `ImplScan` marker interface | `import type { ImplScan } from '@thomasrepnik/ts-implscan'` |
19
+ | The `ts-implscan` CLI (the generator) | `npx ts-implscan`, or in a `package.json` script |
20
+
21
+ It's a dev dependency because it only runs at build time. The `ImplScan` import is type-only and
22
+ disappears when compiled, so nothing from this package ends up in your production bundle.
23
+ You need Node 22 or newer.
24
+
25
+ ## Quick start
26
+
27
+ ### 1. Mark an interface
28
+
29
+ Extend `ImplScan` to tell the generator to collect the interface's implementations:
30
+
31
+ ```ts
32
+ // payment-handler.ts
33
+ import type { ImplScan } from '@thomasrepnik/ts-implscan';
34
+
35
+ export interface PaymentHandler extends ImplScan {
36
+ name: string;
37
+ pay(amount: number): string;
38
+ }
39
+ ```
40
+
41
+ ### 2. Implement it
42
+
43
+ Write implementations as usual, anywhere under `src/`. They must be exported:
44
+
45
+ ```ts
46
+ // card-handler.ts
47
+ export class CardHandler implements PaymentHandler {
48
+ name = 'card';
49
+ pay(amount: number) {
50
+ return `charged ${amount} to credit card`;
51
+ }
52
+ }
53
+
54
+ // paypal-handler.ts
55
+ export class PaypalHandler implements PaymentHandler { ... }
56
+ ```
57
+
58
+ ### 3. Generate the registry and use it
59
+
60
+ Pick the variant for your project.
61
+
62
+ #### With Angular
63
+
64
+ Add the generator to your scripts, so it runs before every `ng serve` and `ng build`:
65
+
66
+ ```json
67
+ "scripts": {
68
+ "generate": "ts-implscan --target angular --project tsconfig.app.json --out src/app/generated/implscan-registry.ts",
69
+ "prestart": "npm run generate",
70
+ "prebuild": "npm run generate"
71
+ }
72
+ ```
73
+
74
+ Register all implementations once:
75
+
76
+ ```ts
77
+ // app.config.ts
78
+ import { provideImplscan } from './generated/implscan-registry';
79
+
80
+ export const appConfig: ApplicationConfig = {
81
+ providers: [provideImplscan()],
82
+ };
83
+ ```
84
+
85
+ Inject the list anywhere. This is the equivalent of Spring's `List<PaymentHandler>`:
86
+
87
+ ```ts
88
+ import { PAYMENT_HANDLER_LIST } from './generated/implscan-registry';
89
+
90
+ @Injectable({ providedIn: 'root' })
91
+ export class PaymentService {
92
+ private readonly handlers = inject(PAYMENT_HANDLER_LIST); // PaymentHandler[]
93
+
94
+ payWithAll(amount: number) {
95
+ return this.handlers.map((handler) => handler.pay(amount));
96
+ }
97
+ }
98
+ ```
99
+
100
+ Implementations can use DI themselves, for example `private readonly http = inject(HttpClient)`.
101
+ Mark them with `@Injectable()`, but **without** `providedIn: 'root'`. `provideImplscan()` already
102
+ creates one instance per implementation, so a root-provided implementation would exist twice.
103
+
104
+ <details>
105
+ <summary>What gets generated</summary>
106
+
107
+ ```ts
108
+ // src/app/generated/implscan-registry.ts
109
+ // GENERATED by ts-implscan – do not edit
110
+ import { InjectionToken, type Provider } from '@angular/core';
111
+ import type { PaymentHandler } from '../payment/payment-handler';
112
+ import { CardHandler } from '../payment/card-handler';
113
+ import { PaypalHandler } from '../payment/paypal-handler';
114
+
115
+ export const PaymentHandlerImplementations: Array<new (...args: any[]) => PaymentHandler> = [CardHandler, PaypalHandler];
116
+ export const PAYMENT_HANDLER_LIST = new InjectionToken<PaymentHandler[]>('PAYMENT_HANDLER_LIST');
117
+
118
+ export function provideImplscan(): Provider[] {
119
+ return [
120
+ { provide: PAYMENT_HANDLER_LIST, useClass: CardHandler, multi: true },
121
+ { provide: PAYMENT_HANDLER_LIST, useClass: PaypalHandler, multi: true },
122
+ ];
123
+ }
124
+ ```
125
+
126
+ There is one token per marked interface, and `provideImplscan()` registers all of them.
127
+ </details>
128
+
129
+ The [example](example/main-angular.ts) in this repository resolves the generated providers
130
+ through Angular's real injector (`npm run start:angular`).
131
+
132
+ #### Without Angular
133
+
134
+ The default target generates plain arrays of classes, with no framework dependency. It works for
135
+ Node, other frameworks, or plain TypeScript:
136
+
137
+ ```json
138
+ "scripts": {
139
+ "generate": "ts-implscan",
140
+ "prestart": "npm run generate",
141
+ "prebuild": "npm run generate"
142
+ }
143
+ ```
144
+
145
+ ```ts
146
+ import { PaymentHandlerImplementations } from './generated/implscan-registry';
147
+
148
+ const handlers = PaymentHandlerImplementations.map((Handler) => new Handler()); // PaymentHandler[]
149
+
150
+ for (const handler of handlers) {
151
+ console.log(handler.pay(42));
152
+ }
153
+ ```
154
+
155
+ If your implementations have dependencies, register the classes from the array with your DI
156
+ container instead of calling `new`.
157
+
158
+ <details>
159
+ <summary>What gets generated</summary>
160
+
161
+ ```ts
162
+ // src/generated/implscan-registry.ts
163
+ // GENERATED by ts-implscan – do not edit
164
+ import type { PaymentHandler } from '../payment/payment-handler';
165
+ import { CardHandler } from '../payment/card-handler';
166
+ import { PaypalHandler } from '../payment/paypal-handler';
167
+
168
+ export const PaymentHandlerImplementations: Array<new (...args: any[]) => PaymentHandler> = [CardHandler, PaypalHandler];
169
+ ```
170
+ </details>
171
+
172
+ The [example](example/main.ts) in this repository does exactly this (`npm start`).
173
+
174
+ **That's it.** A new implementation is picked up the next time the generator runs. A new
175
+ interface only needs `extends ImplScan`, and with Angular its token is generated too.
176
+
177
+ ## Good to know
178
+
179
+ - **Check what would be collected** without writing anything: `npx ts-implscan --dry-run`.
180
+ - **Replace a whole list in Angular tests** with one provider:
181
+ `providers: [{ provide: PAYMENT_HANDLER_LIST, useValue: [fakeHandler] }]`.
182
+ - **A marked interface without implementations** gives an empty array, and in Angular a
183
+ `useValue: []` provider, so injecting it doesn't throw.
184
+ - **Commit the generated file**, so the IDE and type checks work right after a checkout. Let CI
185
+ catch a forgotten regeneration with `npm run generate && git diff --exit-code`.
186
+ - **In CI**, `prebuild` only runs through `npm run build`. If your pipeline calls `ng build`
187
+ directly, run `npm run generate` first.
188
+
189
+ ## Why
190
+
191
+ In Spring you can inject `List<PaymentHandler>`, and the container hands you every bean that
192
+ implements the interface. TypeScript can't do this out of the box, for two reasons:
193
+
194
+ 1. **Interfaces are erased.** At runtime nothing records that a class implements `PaymentHandler`.
195
+ 2. **There is no classpath scanning.** JavaScript only runs files that something imports, so a
196
+ new implementation that nobody imports doesn't exist at runtime.
197
+
198
+ DI containers such as Angular, tsyringe or InversifyJS solve this with injection tokens, but
199
+ then you have to register every implementation by hand, and it's easy to forget one.
200
+
201
+ `ts-implscan` moves the scan to build time. It uses [ts-morph](https://ts-morph.com) and the
202
+ TypeScript type checker to find every exported class that `implements` a marked interface. The
203
+ generated file is ordinary TypeScript, so `tsc` fails if a listed class doesn't actually satisfy
204
+ its interface.
205
+
206
+ **Why a marker interface rather than a comment tag like `/** @implscan */`?** The marker is
207
+ checked by the compiler, so a typo doesn't silently drop the interface. Your IDE's "Find usages"
208
+ on `ImplScan` lists every collected interface, and renames are safe. It's the same idea as a Java
209
+ marker interface such as `Serializable`, and it keeps out the interfaces you implement for other
210
+ reasons, such as Angular's `OnInit`.
211
+
212
+ ## When it's helpful
213
+
214
+ - **Plugin and strategy patterns**, such as payment providers, exporters, validators, notification
215
+ channels or feature handlers, where the set of implementations keeps growing.
216
+ - **Teams coming from Spring or Java** who want the open/closed principle that `List<A>` injection
217
+ gives them: adding behaviour means adding a class, not editing a central list.
218
+ - **Angular apps** with many multi-provider tokens, so nobody forgets to register a new
219
+ implementation, or to write the token.
220
+
221
+ It's probably overkill if you have two implementations that rarely change. A hand-written array
222
+ is fine then.
223
+
224
+ ## CLI options
225
+
226
+ ```sh
227
+ ts-implscan [--project tsconfig.json] [--src src] [--out src/generated/implscan-registry.ts] [--target plain|angular] [--dry-run]
228
+ ```
229
+
230
+ | Option | Default | Meaning |
231
+ |--------------|--------------------------------------|----------------------------------------------------------|
232
+ | `--project` | `tsconfig.json` | tsconfig to read compiler options from (paths, resolution) |
233
+ | `--src` | `src` | Folder to scan. `*.spec.ts` and `*.d.ts` are skipped. |
234
+ | `--out` | `src/generated/implscan-registry.ts` | File to generate |
235
+ | `--target` | `plain` | `angular` adds an `InjectionToken` per interface and `provideImplscan()` |
236
+ | `--dry-run` | off | Print the result to stdout instead of writing it |
237
+ | `-h, --help` | | Show the help |
238
+
239
+ The files to scan come from `--src`, not from the tsconfig, because a new implementation isn't
240
+ imported anywhere yet. The tsconfig is only read for compiler options. The import extension in
241
+ the generated file follows it too: `.ts` with `allowImportingTsExtensions`, `.js` with `NodeNext`,
242
+ and no extension with `bundler`, which is what Angular uses.
243
+
244
+ ## Limits
245
+
246
+ - Only interfaces that extend `ImplScan` **directly** are collected. The CLI detects the marker by
247
+ its name, so a local `interface ImplScan {}` works too.
248
+ - Only explicit `implements X` clauses count. Classes that only match the interface structurally,
249
+ and subclasses of an implementing class, are not found.
250
+ - Implementations must be exported, non-abstract, named-export classes. A non-exported
251
+ implementation is skipped with a warning.
252
+
253
+ ## Development
254
+
255
+ This repository contains the library in `src/` and a runnable demo in `example/`. The demo has
256
+ two marked interfaces, `PaymentHandler` and `Notifier`.
257
+
258
+ ```sh
259
+ npm install
260
+ npm run generate:dry # print the plain registry for the example, write nothing
261
+ npm run generate:dry:angular # the same with Angular tokens and providers
262
+ npm start # regenerate, then run the example with plain arrays
263
+ npm run start:angular # regenerate, then resolve everything through Angular's real Injector
264
+ npm run typecheck
265
+ npm run test:package # pack, install into an empty project, run the CLI from there
266
+ ```
267
+
268
+ `@angular/core` and `rxjs` are dev dependencies only so that `start:angular` can run Angular's
269
+ injector in Node. The published package only depends on `ts-morph`.
270
+
271
+ ### Releasing
272
+
273
+ Releases are uploaded to npm by `.github/workflows/release.yml`, using npm
274
+ [trusted publishing](https://docs.npmjs.com/trusted-publishers). It doesn't need an npm token.
275
+
276
+ ```sh
277
+ npm version minor # or patch / major: bumps package.json, commits and tags vX.Y.Z
278
+ git push --follow-tags
279
+ gh release create v0.2.0 --generate-notes # or click "Publish release" on GitHub
280
+ ```
281
+
282
+ Publishing the GitHub release starts the workflow. It checks that the tag matches
283
+ `package.json`, type-checks, runs the package smoke test and **stages** the version on npm.
284
+ A staged version isn't public until a maintainer approves it with 2FA:
285
+
286
+ ```sh
287
+ npm stage list @thomasrepnik/ts-implscan # shows the stage id
288
+ npm stage approve <stage-id>
289
+ ```
290
+
291
+ ## License
292
+
293
+ [MIT](LICENSE)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ // Build-time "component scan": finds every exported interface that extends the
3
+ // `ImplScan` marker, then every exported class that implements one of them, and
4
+ // writes one array of implementations per interface. With `--target angular` it
5
+ // also writes an InjectionToken per interface and `provideImplscan()`.
6
+ import { mkdirSync, writeFileSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { parseArgs } from 'node:util';
9
+ import { Node, Project, ts } from 'ts-morph';
10
+ const MARKER = 'ImplScan';
11
+ const { values: args } = parseArgs({
12
+ options: {
13
+ project: { type: 'string', default: 'tsconfig.json' },
14
+ src: { type: 'string', default: 'src' },
15
+ out: { type: 'string', default: 'src/generated/implscan-registry.ts' },
16
+ target: { type: 'string', default: 'plain' },
17
+ 'dry-run': { type: 'boolean', default: false },
18
+ help: { type: 'boolean', short: 'h', default: false },
19
+ },
20
+ });
21
+ if (args.help) {
22
+ console.log(`Usage: ts-implscan [options]
23
+
24
+ --project <file> tsconfig to read compiler options from
25
+ (default: tsconfig.json)
26
+ --src <dir> folder to scan (default: src)
27
+ --out <file> file to generate
28
+ (default: src/generated/implscan-registry.ts)
29
+ --target <target> plain | angular (default: plain)
30
+ --dry-run print the result instead of writing it
31
+ -h, --help show this help`);
32
+ process.exit(0);
33
+ }
34
+ if (args.target !== 'plain' && args.target !== 'angular') {
35
+ console.error(`unknown --target '${args.target}', expected 'plain' or 'angular'`);
36
+ process.exit(1);
37
+ }
38
+ const outFile = path.resolve(args.out);
39
+ const outDir = path.dirname(outFile);
40
+ // Use the tsconfig only for compiler options (paths, module resolution). Files come from
41
+ // a glob, because new implementations are not imported anywhere yet (Angular's
42
+ // tsconfig.app.json only lists main.ts).
43
+ const project = new Project({ tsConfigFilePath: args.project, skipAddingFilesFromTsConfig: true });
44
+ project.addSourceFilesAtPaths([`${args.src}/**/*.ts`, `!${args.src}/**/*.spec.ts`, `!${args.src}/**/*.d.ts`]);
45
+ const sourceFiles = project.getSourceFiles().filter((f) => f.getFilePath() !== outFile);
46
+ // Pick the import extension the tsconfig expects: `.ts` (Node type stripping),
47
+ // `.js` (NodeNext) or none (Angular's `bundler` resolution).
48
+ const options = project.getCompilerOptions();
49
+ const importExt = options.allowImportingTsExtensions
50
+ ? '.ts'
51
+ : options.moduleResolution === ts.ModuleResolutionKind.Node16 ||
52
+ options.moduleResolution === ts.ModuleResolutionKind.NodeNext
53
+ ? '.js'
54
+ : '';
55
+ // resolve through the type checker so aliased/renamed imports still match
56
+ function resolveInterface(clause) {
57
+ let symbol = clause.getExpression().getSymbol();
58
+ if (symbol?.isAlias())
59
+ symbol = symbol.getAliasedSymbol();
60
+ return symbol?.getDeclarations().find(Node.isInterfaceDeclaration);
61
+ }
62
+ const keyOf = (decl) => `${decl.getSourceFile().getFilePath()}#${decl.getName()}`;
63
+ const registry = new Map();
64
+ // 1. interfaces that opt in by extending the marker directly
65
+ for (const sourceFile of sourceFiles) {
66
+ for (const iface of sourceFile.getInterfaces()) {
67
+ if (!iface.isExported())
68
+ continue;
69
+ if (!iface.getExtends().some((clause) => resolveInterface(clause)?.getName() === MARKER))
70
+ continue;
71
+ registry.set(keyOf(iface), { iface: { name: iface.getName(), file: sourceFile.getFilePath() }, impls: [] });
72
+ }
73
+ }
74
+ // 2. classes implementing one of those interfaces
75
+ for (const sourceFile of sourceFiles) {
76
+ for (const cls of sourceFile.getClasses()) {
77
+ const name = cls.getName();
78
+ if (!name || cls.isAbstract())
79
+ continue;
80
+ for (const clause of cls.getImplements()) {
81
+ const decl = resolveInterface(clause);
82
+ const entry = decl && registry.get(keyOf(decl));
83
+ if (!entry)
84
+ continue;
85
+ if (!cls.isExported() || cls.isDefaultExport()) {
86
+ console.warn(`skipped ${name} (${path.relative(process.cwd(), sourceFile.getFilePath())}): needs a named export`);
87
+ continue;
88
+ }
89
+ entry.impls.push({ name, file: sourceFile.getFilePath() });
90
+ }
91
+ }
92
+ }
93
+ function importPath(file) {
94
+ const rel = path.relative(outDir, file).split(path.sep).join('/').replace(/\.ts$/, importExt);
95
+ return rel.startsWith('.') ? rel : `./${rel}`;
96
+ }
97
+ // PaymentHandler -> PAYMENT_HANDLER_LIST
98
+ function tokenName(interfaceName) {
99
+ const snake = interfaceName.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/([A-Z])([A-Z][a-z])/g, '$1_$2');
100
+ return `${snake.toUpperCase()}_LIST`;
101
+ }
102
+ const angular = args.target === 'angular';
103
+ const imports = angular ? [`import { InjectionToken, type Provider } from '@angular/core';`] : [];
104
+ const exports = [];
105
+ const providers = [];
106
+ const entries = [...registry.values()].sort((a, b) => a.iface.name.localeCompare(b.iface.name));
107
+ for (const { iface, impls } of entries) {
108
+ impls.sort((a, b) => a.name.localeCompare(b.name));
109
+ imports.push(`import type { ${iface.name} } from '${importPath(iface.file)}';`);
110
+ for (const impl of impls)
111
+ imports.push(`import { ${impl.name} } from '${importPath(impl.file)}';`);
112
+ // same shape as Angular's Type<T>, so it can be used directly as `useClass`
113
+ exports.push(`export const ${iface.name}Implementations: Array<new (...args: any[]) => ${iface.name}> = [${impls.map((i) => i.name).join(', ')}];`);
114
+ if (angular) {
115
+ const token = tokenName(iface.name);
116
+ exports.push(`export const ${token} = new InjectionToken<${iface.name}[]>('${token}');`);
117
+ // a multi token without any provider would throw on inject(), so fall back to []
118
+ if (impls.length === 0)
119
+ providers.push(` { provide: ${token}, useValue: [] },`);
120
+ for (const impl of impls)
121
+ providers.push(` { provide: ${token}, useClass: ${impl.name}, multi: true },`);
122
+ }
123
+ }
124
+ if (angular) {
125
+ exports.push('', 'export function provideImplscan(): Provider[] {', ' return [', ...providers, ' ];', '}');
126
+ }
127
+ const content = ['// GENERATED by ts-implscan – do not edit', ...imports, '', ...exports, ''].join('\n');
128
+ if (args['dry-run']) {
129
+ console.log(content);
130
+ }
131
+ else {
132
+ mkdirSync(outDir, { recursive: true });
133
+ writeFileSync(outFile, content);
134
+ for (const { iface, impls } of entries)
135
+ console.log(`${iface.name}: ${impls.map((i) => i.name).join(', ') || '(none)'}`);
136
+ console.log(`wrote ${path.relative(process.cwd(), outFile)}`);
137
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Marker interface: every interface that extends `ImplScan` gets its implementations
3
+ * collected by the `ts-implscan` CLI. It has no members and no runtime cost.
4
+ *
5
+ * @example
6
+ * export interface PaymentHandler extends ImplScan {
7
+ * pay(amount: number): string;
8
+ * }
9
+ */
10
+ export interface ImplScan {
11
+ }
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "@thomasrepnik/ts-implscan",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "description": "Spring-style List<A> injection for TypeScript: find every class implementing a marked interface and generate a typed registry, plus Angular InjectionTokens and providers.",
5
+ "keywords": [
6
+ "typescript",
7
+ "dependency-injection",
8
+ "angular",
9
+ "multi-provider",
10
+ "injection-token",
11
+ "code-generation",
12
+ "ts-morph",
13
+ "spring",
14
+ "component-scan"
15
+ ],
16
+ "homepage": "https://github.com/thomasrepnik/ts-implscan#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/thomasrepnik/ts-implscan/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/thomasrepnik/ts-implscan.git"
23
+ },
24
+ "license": "MIT",
25
+ "author": "Thomas Repnik",
26
+ "type": "module",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "types": "./dist/index.d.ts",
34
+ "bin": {
35
+ "ts-implscan": "dist/cli.js"
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "engines": {
41
+ "node": ">=22"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.build.json",
48
+ "prepack": "npm run build",
49
+ "typecheck": "tsc --noEmit",
50
+ "generate": "node src/cli.ts --src example --out example/generated/implscan-registry.ts && node src/cli.ts --src example --target angular --out example/generated/implscan-registry.angular.ts",
51
+ "generate:dry": "node src/cli.ts --src example --dry-run",
52
+ "generate:dry:angular": "node src/cli.ts --src example --target angular --dry-run",
53
+ "prestart": "npm run generate",
54
+ "start": "node example/main.ts",
55
+ "prestart:angular": "npm run generate",
56
+ "start:angular": "node example/main-angular.ts",
57
+ "test:package": "bash scripts/smoke-test.sh"
58
+ },
59
+ "dependencies": {
60
+ "ts-morph": "^28.0.0"
61
+ },
62
+ "devDependencies": {
63
+ "@angular/core": "^22.2.4",
64
+ "@types/node": "^26.6.6",
65
+ "rxjs": "^7.8.2",
66
+ "typescript": "^7.0.2"
67
+ }
68
+ }