@ts-ioc-container/bundler 1.5.0 → 2.1.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.
Files changed (122) hide show
  1. package/AGENTS.md +47 -31
  2. package/README.md +157 -280
  3. package/cjm/app.js +39 -0
  4. package/cjm/bin.js +2 -2
  5. package/cjm/cli/Application.js +58 -0
  6. package/cjm/cli/IErrorHandler.js +5 -0
  7. package/cjm/cli/decorators.js +9 -0
  8. package/cjm/cli/index.js +17 -0
  9. package/cjm/cli/options.js +25 -0
  10. package/cjm/cli/usage.js +19 -0
  11. package/cjm/domain/CliIo.js +7 -0
  12. package/cjm/domain/GlobalConfig.js +7 -0
  13. package/cjm/exceptions/DomainException.js +54 -0
  14. package/cjm/exceptions/ExceptionHandler.js +38 -0
  15. package/cjm/features/build/BuildConfig.js +183 -0
  16. package/cjm/features/build/BuildController.js +91 -0
  17. package/cjm/features/build/BuildModule.js +16 -0
  18. package/cjm/{ImportPaths.js → features/build/domain/ImportPaths.js} +7 -29
  19. package/cjm/features/build/domain/configFiles.js +35 -0
  20. package/cjm/{emit.js → features/build/domain/emit.js} +10 -9
  21. package/cjm/{scan.js → features/build/domain/scan.js} +6 -3
  22. package/cjm/features/build/domain/tokenCollisions.js +29 -0
  23. package/cjm/features/build/domain/tsconfig.js +78 -0
  24. package/cjm/features/build/services/BundleBuilder.js +116 -0
  25. package/cjm/features/build/services/TicConfigService.js +68 -0
  26. package/cjm/features/help/HelpController.js +39 -0
  27. package/cjm/features/help/HelpModule.js +12 -0
  28. package/cjm/features/help/VersionController.js +45 -0
  29. package/cjm/index.js +87 -28
  30. package/cjm/modules/CommonModule.js +28 -0
  31. package/cjm/schema/ticConfigSchema.js +24 -0
  32. package/cjm/services/ConsoleLogger.js +36 -0
  33. package/cjm/services/HandlebarsRenderService.js +33 -0
  34. package/cjm/services/NodeFileSystemService.js +32 -0
  35. package/cjm/services/OutputService.js +33 -0
  36. package/package.json +10 -7
  37. package/tic.schema.json +118 -147
  38. package/tpl/index.cjs +36 -16
  39. package/typings/app.d.ts +24 -0
  40. package/typings/cli/Application.d.ts +16 -0
  41. package/typings/cli/Application.d.ts.map +1 -0
  42. package/typings/cli/IErrorHandler.d.ts +7 -0
  43. package/typings/cli/IErrorHandler.d.ts.map +1 -0
  44. package/typings/cli/decorators.d.ts +10 -0
  45. package/typings/cli/decorators.d.ts.map +1 -0
  46. package/typings/cli/index.d.ts +6 -0
  47. package/typings/cli/index.d.ts.map +1 -0
  48. package/typings/cli/options.d.ts +23 -0
  49. package/typings/cli/options.d.ts.map +1 -0
  50. package/typings/cli/usage.d.ts +2 -0
  51. package/typings/cli/usage.d.ts.map +1 -0
  52. package/typings/domain/CliIo.d.ts +10 -0
  53. package/typings/domain/CliIo.d.ts.map +1 -0
  54. package/typings/domain/GlobalConfig.d.ts +10 -0
  55. package/typings/domain/GlobalConfig.d.ts.map +1 -0
  56. package/typings/exceptions/DomainException.d.ts +37 -0
  57. package/typings/exceptions/DomainException.d.ts.map +1 -0
  58. package/typings/exceptions/ExceptionHandler.d.ts +12 -0
  59. package/typings/exceptions/ExceptionHandler.d.ts.map +1 -0
  60. package/typings/features/build/BuildConfig.d.ts +179 -0
  61. package/typings/features/build/BuildConfig.d.ts.map +1 -0
  62. package/typings/features/build/BuildController.d.ts +32 -0
  63. package/typings/features/build/BuildController.d.ts.map +1 -0
  64. package/typings/features/build/BuildModule.d.ts +5 -0
  65. package/typings/features/build/BuildModule.d.ts.map +1 -0
  66. package/typings/{ImportPaths.d.ts → features/build/domain/ImportPaths.d.ts} +4 -16
  67. package/typings/features/build/domain/ImportPaths.d.ts.map +1 -0
  68. package/typings/features/build/domain/configFiles.d.ts +13 -0
  69. package/typings/features/build/domain/configFiles.d.ts.map +1 -0
  70. package/typings/features/build/domain/emit.d.ts +42 -0
  71. package/typings/features/build/domain/emit.d.ts.map +1 -0
  72. package/typings/features/build/domain/glob.d.ts.map +1 -0
  73. package/typings/{scan.d.ts → features/build/domain/scan.d.ts} +4 -2
  74. package/typings/features/build/domain/scan.d.ts.map +1 -0
  75. package/typings/features/build/domain/tokenCollisions.d.ts +14 -0
  76. package/typings/features/build/domain/tokenCollisions.d.ts.map +1 -0
  77. package/typings/features/build/domain/tsconfig.d.ts +25 -0
  78. package/typings/features/build/domain/tsconfig.d.ts.map +1 -0
  79. package/typings/features/build/services/BundleBuilder.d.ts +68 -0
  80. package/typings/features/build/services/BundleBuilder.d.ts.map +1 -0
  81. package/typings/features/build/services/TicConfigService.d.ts +37 -0
  82. package/typings/features/build/services/TicConfigService.d.ts.map +1 -0
  83. package/typings/features/help/HelpController.d.ts +8 -0
  84. package/typings/features/help/HelpController.d.ts.map +1 -0
  85. package/typings/features/help/HelpModule.d.ts +5 -0
  86. package/typings/features/help/HelpModule.d.ts.map +1 -0
  87. package/typings/features/help/VersionController.d.ts +8 -0
  88. package/typings/features/help/VersionController.d.ts.map +1 -0
  89. package/typings/index.d.ts +25 -11
  90. package/typings/modules/CommonModule.d.ts +9 -0
  91. package/typings/modules/CommonModule.d.ts.map +1 -0
  92. package/typings/schema/ticConfigSchema.d.ts +7 -0
  93. package/typings/schema/ticConfigSchema.d.ts.map +1 -0
  94. package/typings/services/ConsoleLogger.d.ts +20 -0
  95. package/typings/services/ConsoleLogger.d.ts.map +1 -0
  96. package/typings/services/HandlebarsRenderService.d.ts +23 -0
  97. package/typings/services/HandlebarsRenderService.d.ts.map +1 -0
  98. package/typings/services/NodeFileSystemService.d.ts +21 -0
  99. package/typings/services/NodeFileSystemService.d.ts.map +1 -0
  100. package/typings/services/OutputService.d.ts +13 -0
  101. package/typings/services/OutputService.d.ts.map +1 -0
  102. package/cjm/build.js +0 -132
  103. package/cjm/cli.js +0 -84
  104. package/cjm/config.js +0 -162
  105. package/cjm/errors.js +0 -15
  106. package/cjm/exportPredicate.js +0 -13
  107. package/cjm/inclusion.js +0 -24
  108. package/cjm/predicateFile.js +0 -30
  109. package/cjm/render.js +0 -15
  110. package/cjm/utils.js +0 -2
  111. package/typings/build.d.ts +0 -45
  112. package/typings/cli.d.ts +0 -8
  113. package/typings/config.d.ts +0 -123
  114. package/typings/emit.d.ts +0 -18
  115. package/typings/errors.d.ts +0 -13
  116. package/typings/exportPredicate.d.ts +0 -36
  117. package/typings/inclusion.d.ts +0 -42
  118. package/typings/predicateFile.d.ts +0 -16
  119. package/typings/render.d.ts +0 -8
  120. package/typings/utils.d.ts +0 -3
  121. /package/cjm/{glob.js → features/build/domain/glob.js} +0 -0
  122. /package/typings/{glob.d.ts → features/build/domain/glob.d.ts} +0 -0
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @ts-ioc-container/bundler
2
2
 
3
3
  **Bundles your dependencies into a single container module.** `tic build` scans
4
- your folders — by path or tsconfig alias — and generates one typed bundle that
5
- registers every class in them, ready for
4
+ what your tsconfig compiles — or the folders you name, by path or tsconfig alias —
5
+ and generates one typed bundle that registers every class in them, ready for
6
6
  [`ts-ioc-container`](https://www.npmjs.com/package/ts-ioc-container):
7
7
 
8
8
  ```ts
@@ -14,9 +14,8 @@ Adding a service means writing the class — no hand-maintained list of
14
14
  TypeScript, generated at build time, so it type-checks, bundles and tree-shakes
15
15
  like code you wrote yourself.
16
16
 
17
- In TypeScript a namespace is an import path, so folders are named the way
18
- imports are: relative to the config file (`./src/services`) or through a
19
- `tsconfig.json` `paths` alias (`@app/services`).
17
+ A bundle is configured like a tsconfig that `extends` yours: by default it
18
+ takes the files your tsconfig compiles, and narrows or overrides them.
20
19
 
21
20
  ## Install
22
21
 
@@ -30,347 +29,225 @@ below use the shortcut.
30
29
 
31
30
  `typescript` (>= 5) is a peer dependency.
32
31
 
33
- ## Configure
32
+ ## Zero config
34
33
 
35
- `tic.config.json`, next to your `tsconfig.json`:
34
+ With a `tsconfig.json`, there is nothing to configure:
36
35
 
37
- ```json
38
- {
39
- "$schema": "./node_modules/@ts-ioc-container/bundler/tic.schema.json",
40
- "bundles": [
41
- {
42
- "output": "src/di/app.bundle.ts",
43
- "name": "AppBundle",
44
- "namespaces": ["@app/services", { "path": "./src/infra", "recursive": false }]
45
- }
46
- ]
47
- }
36
+ ```bash
37
+ tic build
48
38
  ```
49
39
 
50
- | Field | Default | Meaning |
51
- | ---------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------- |
52
- | `tsconfig` | `tsconfig.json` (may be absent) | Source of `paths` aliases and of the import extension |
53
- | `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
54
- | `bundles[].output` | — | The bundle file (convention: `*.bundle.ts`) |
55
- | `bundles[].name` | `Bundle` | Name of the generated class, an `IContainerModule` |
56
- | `bundles[].namespaces` | — | Folders to scan; `{ path, recursive }` to stop at the folder itself |
57
- | `bundles[].select` | every exported class | Which classes of a file are registered — see [Selecting classes](#selecting-classes) |
58
- | `bundles[].exclude` | test files, `__tests__/`, `node_modules/` | Globs (relative to the config) never scanned; replaces the default |
59
- | `bundles[].additionalExclude`| — | Globs never scanned, added on top of `exclude` (or the defaults) — see [Excluding files](#excluding-files) |
60
- | `bundles[].filterExports` | `tic.exports.*` next to the config | File exporting an `ExportPredicate` — see [Filtering classes](#filtering-classes-after-parsing) |
61
- | `bundles[].include` | `tic.include.*` next to the config | File exporting an `InclusionPredicate` — see [Including files](#including-files-with-a-predicate) |
62
-
63
- ## Excluding files
64
-
65
- By default test files, `__tests__/` and `node_modules/` are never scanned. To
66
- add one exclusion without restating those defaults, use `additionalExclude`:
67
-
68
- ```json
69
- {
70
- "bundles": [
71
- {
72
- "output": "src/di/app.bundle.ts",
73
- "namespaces": ["@app/services"],
74
- "additionalExclude": ["frontend/api/generated/**", "**/Mock*.ts"]
75
- }
76
- ]
77
- }
40
+ ```text
41
+ wrote src/base.bundle.ts (12 registrations)
78
42
  ```
79
43
 
80
- A non-empty `exclude` replaces the defaults; if it omits a default glob the
81
- build warns, since test classes usually carry the same `@register` decorators as
82
- production ones. `exclude: []` deliberately scans everything, including tests —
83
- combine it with `include` when only some tests should join.
44
+ The bundle registers every exported class decorated with `@register` among the
45
+ files your `tsconfig.json` compiles, test files excluded, and lands at the root
46
+ of those sources — `rootDir`, else their common folder (`src/`), so `tsc`
47
+ compiles it too. It exports `BaseBundle`:
84
48
 
85
- ## Selecting classes
86
-
87
- By default every exported, non-abstract class of a scanned file is registered.
88
- `select` narrows that; a class must meet every criterion that is set:
49
+ ```ts
50
+ import { BaseBundle } from './base.bundle';
89
51
 
90
- ```json
91
- "select": {
92
- "export": "named",
93
- "decorators": ["register", "repository"],
94
- "nameGlob": "*Service"
95
- }
52
+ const container = new Container().useModule(new BaseBundle());
96
53
  ```
97
54
 
98
- | Criterion | Default | Meaning |
99
- | ------------------ | ------- | ----------------------------------------------------------------------------------------------------------- |
100
- | `export` | `any` | `any`, `named` or `default` — which exports count |
101
- | `decorators` | — | The class must carry one of these, by name — also renamed imports and `@ioc.register()`; list composed ones |
102
- | `nameGlob` | — | Glob on the class name; an anonymous default export is named after its file (`user-service.ts` → `UserService`) |
103
- | `excludeClasses` | — | Class names to drop, e.g. `["MockDashboardRepository"]` |
104
- | `excludeNameGlob` | — | Glob the class name must **not** match, e.g. `"*Mock"` |
105
- | `excludeAliases` | — | tsconfig `paths` aliases to drop, e.g. `["services"]` for a `services/*` or `@services/*` alias |
55
+ `tic build` works on the package it is invoked in: it walks up from the working
56
+ directory to the nearest `package.json` — the project root, or in a monorepo
57
+ the package — and uses the `tsconfig.json` there. It never falls back to a
58
+ workspace root's tsconfig.
106
59
 
107
- Abstract and non-exported classes are never registered.
108
-
109
- ### Excluding a single class
60
+ ## Configure
110
61
 
111
- When a real implementation and a test-only stand-in are both decorated and both
112
- bind the same token, registration is last-wins and the stand-in silently wins. A
113
- `tic.exports.*` predicate works, but forces a hand-maintained name list in code;
114
- `excludeClasses` and `excludeNameGlob` drop a class right in the config:
62
+ To change a default, add a config file next to your `tsconfig.json`. One config
63
+ file describes one bundle and is named `<name>.bundle.json` — `app.bundle.json`:
115
64
 
116
65
  ```json
117
- "select": {
118
- "decorators": ["repository", "service"],
119
- "excludeClasses": ["MockDashboardRepository"],
120
- "excludeNameGlob": "*Fake"
66
+ {
67
+ "$schema": "./node_modules/@ts-ioc-container/bundler/tic.schema.json",
68
+ "output": "src/di/app.bundle.ts"
121
69
  }
122
70
  ```
123
71
 
124
- Both apply after every other criterion, so they can be combined with
125
- `decorators` and `nameGlob` freely. Reach for `filterExports` when the rule
126
- needs more than the class name.
127
-
128
- `excludeAliases` drops classes by the tsconfig `paths` alias their file resolves
129
- through — handy for excluding a whole namespace without naming its classes:
72
+ or, in YAML, `<name>.bundle.yaml` / `<name>.bundle.yml` — same fields, same schema:
130
73
 
131
- ```json
132
- "select": {
133
- "excludeAliases": ["services"]
134
- }
74
+ ```yaml
75
+ # yaml-language-server: $schema=./node_modules/@ts-ioc-container/bundler/tic.schema.json
76
+ output: src/di/app.bundle.ts
77
+ files:
78
+ include: ['**/*.service.ts']
135
79
  ```
136
80
 
137
- An entry matches a `paths` pattern such as `services/*` or `@services/*`; a
138
- leading `@` and a trailing `/*` may be omitted.
81
+ An empty YAML file is a bundle with every setting at its default. Describing
82
+ one bundle in two formats (`app.bundle.json` and `app.bundle.yaml`) is an error.
139
83
 
140
- As a safety net, the build warns when two selected classes pass the same
141
- plain-identifier first argument to a decorator — usually the binding token, as in
142
- `@repository(IDashboardRepositoryToken)`. The check is syntactic (aliased imports
143
- are not resolved), so it can only be a heuristic:
84
+ Every field is optional; `{}` is the zero-config bundle.
144
85
 
145
- ```text
146
- tic: warning: bundles[0]: decorator token "IDashboardRepositoryToken" is passed by
147
- HttpDashboardRepository, MockDashboardRepository; registration is last-wins,
148
- exclude one with select.excludeClasses
149
- ```
86
+ | Field | Default | Meaning |
87
+ | ----------------- | -------------------------------------- | --------------------------------------------------------------------------------------------- |
88
+ | `output` | `<root>/<name>.bundle.ts` | The bundle file; `<root>` as in [Zero config](#zero-config). Required without a tsconfig |
89
+ | `name` | the config's stem, else `base` | The bundle's name (`production.bundle.json` → `production`): names the default output and the class, `ProductionBundle` |
90
+ | `tags` | `[]` | Tags associated with the bundle |
91
+ | `extends` | `./tsconfig.json` (may be absent) | The tsconfig to build on: its file set, `paths` aliases and import extension; one named here must exist |
92
+ | `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
93
+ | `files` | what the tsconfig compiles, no tests | Which files are parsed — see [Selecting files](#selecting-files) |
94
+ | `classes` | exported classes with `@register` | Which classes of a parsed file are registered — see [Selecting classes](#selecting-classes) |
150
95
 
151
- Classes that are scope-gated are not last-wins, so they do not warn: when the
152
- colliding classes share a decorator called with different arguments, e.g.
153
- `@perPage('stations')` vs `@perPage('sessions')`, each scope registers its own
154
- class and nothing is lost.
96
+ An unknown field is an error, so a misspelled or removed option never goes
97
+ unnoticed.
155
98
 
156
- ## Including files with a predicate
99
+ ## Several bundles
157
100
 
158
- When globs aren't enough, decide in code which files take part:
101
+ Need more than one bundle — per environment, per app? Add a config per bundle.
102
+ `tic build` builds every `*.bundle.json`, `*.bundle.yaml` and `*.bundle.yml` at
103
+ the package root:
159
104
 
160
- ```ts
161
- type InclusionPredicate = (context: { filename: string }) => boolean;
105
+ ```text
106
+ production.bundle.json → src/di/production.bundle.ts
107
+ development.bundle.json → src/di/development.bundle.ts
108
+ test.bundle.json → src/di/test.bundle.ts
162
109
  ```
163
110
 
164
- `filename` is the path relative to the config file, `/`-separated
165
- (`src/services/Logger.ts`). A file takes part only when it matches no `exclude`
166
- glob **and** the predicate returns `true`.
167
-
168
- By convention there is nothing to configure: put a `tic.include.ts` (or `.cjs`,
169
- `.js`, `.mjs`, `.cts`, `.mts`) next to `tic.config.json` and its default export
170
- applies to every bundle.
111
+ Each one can extend its own tsconfig and select its own files:
171
112
 
172
- ```ts
173
- // tic.include.ts
174
- import type { InclusionPredicate } from '@ts-ioc-container/bundler';
175
-
176
- const include: InclusionPredicate = ({ filename }) => !filename.includes('/legacy/');
177
- export default include;
113
+ ```jsonc
114
+ // production.bundle.json
115
+ {
116
+ "extends": "./tsconfig.production.json",
117
+ "files": { "exclude": ["**/*.development.ts", "**/*.test.ts", "**/*.spec.ts", "**/__tests__/**", "**/node_modules/**"] }
118
+ }
178
119
  ```
179
120
 
180
- | Where the predicate comes from | Applies to | Wins over |
181
- | ------------------------------------ | ------------- | ------------------------------ |
182
- | `build({ include })` (programmatic) | every bundle | everything |
183
- | `bundles[].include: "./path/file"` | that bundle | the conventional file |
184
- | `tic.include.*` next to the config | every bundle | — (used when nothing is named) |
121
+ It writes `src/production.bundle.ts`, exporting `ProductionBundle`.
185
122
 
186
- Predicate files are loaded synchronously with `require`: `.cjs` / `.js`
187
- everywhere, `.mjs` on Node 22.12+, `.ts` on Node versions that strip types
188
- (22.18+, 23.6+). Like any `require`d module a file is loaded **once per
189
- process**, so read environment variables inside the predicate, not at the top
190
- of the file.
123
+ A bundle generated by one config is never registered by another, even when it
124
+ sits in a folder the other scans.
191
125
 
192
- ### Tags: `TagInclusionPredicate`
126
+ ## Selection in two stages
193
127
 
194
- A file's **tags** are the dot-separated parts of its name between the base name
195
- and the extension — `StripeGateway.production.ts` → `['production']`,
196
- `Report.production.eu.ts` → `['production', 'eu']`, `Shared.ts` → `[]`. Write
197
- a predicate over them and wrap it with `byTags`:
128
+ A bundle picks its registrations in two stages:
198
129
 
199
- ```ts
200
- type TagInclusionPredicate = (tags: string[], context: { filename: string }) => boolean;
201
- ```
130
+ 1. **`files`** — decided by path alone, before anything is read. Only files that
131
+ pass are parsed.
132
+ 2. **`classes`** — decided per class, on the files stage 1 let through.
202
133
 
203
- ```ts
204
- // tic.include.ts
205
- import { byTags } from '@ts-ioc-container/bundler';
134
+ Parsing is where the time goes, so when your project names files by convention
135
+ (`user.service.ts`, `user.repository.ts`), say so in `files.include` and the
136
+ bundler never reads anything else:
206
137
 
207
- export default byTags((tags) => !tags.includes('manual')); // Scheduler.manual.ts is never generated
208
- ```
209
-
210
- `byTags` returns an ordinary `InclusionPredicate`, so it goes anywhere one
211
- does. `fileTags(filename)` is exported too. More examples — folders, name
212
- conventions, environments, environment × region — run as tests in
213
- [`__tests__/examples/inclusion-predicates.spec.ts`](__tests__/examples/inclusion-predicates.spec.ts).
214
-
215
- ## Filtering classes after parsing
216
-
217
- `include` decides on whole files before they are read. To decide class by
218
- class, right after a file is parsed, use an `ExportPredicate`. It runs on every
219
- exported, non-abstract class that passes `select`:
220
-
221
- ```ts
222
- type FilterPredicate<Target> = (value: Target) => boolean;
223
- type ExportPredicate = FilterPredicate<ExportContext>;
224
-
225
- interface ExportContext {
226
- filename: string; // relative to the config: 'src/Mailer.ts'
227
- exportName: string; // 'default' for a default export
228
- className: string; // an anonymous default export is named after its file
229
- isDefault: boolean;
230
- decorators: string[]; // renamed imports resolved: `register as reg` -> 'register'
231
- tags: string[]; // file-name tags, as byTags reads them
138
+ ```json
139
+ {
140
+ "output": "src/di/app.bundle.ts",
141
+ "files": {
142
+ "paths": ["@app/services"],
143
+ "include": ["**/*.service.ts", "**/*.repository.ts"]
144
+ },
145
+ "classes": { "decorators": ["register"] }
232
146
  }
233
147
  ```
234
148
 
235
- ```ts
236
- // tic.exports.ts, next to tic.config.json: picked up by convention
237
- import type { ExportPredicate } from '@ts-ioc-container/bundler';
238
-
239
- const filterExports: ExportPredicate = ({ exportName }) => !/(Stub|Mock|Fake)$/.test(exportName);
240
- export default filterExports;
241
- ```
242
-
243
- It is found and loaded like an inclusion predicate: `build({ filterExports })`,
244
- else the bundle's `filterExports` file, else a `tic.exports.*` next to the
245
- config. Examples (test doubles sharing a file, decorator combinations, tags
246
- with class names) run as tests in
247
- [`__tests__/examples/inclusion-predicates.spec.ts`](__tests__/examples/inclusion-predicates.spec.ts).
248
-
249
- ## Recipe: generate per environment
250
-
251
- Keep environment-specific implementations side by side, named by convention:
252
- `*.production.ts` joins only production, `*.development.ts` only development,
253
- and every other file is shared.
254
-
255
- ```text
256
- src/services/
257
- ├── Shared.ts → every environment
258
- ├── StripeGateway.production.ts → production only
259
- └── FakeGateway.development.ts → development only
260
- ```
261
-
262
- One helper turns the convention into a predicate — the environment tags a file
263
- carries must all be the wanted one, and a file without any is shared:
149
+ ## Selecting files
264
150
 
265
- ```js
266
- // tic/for-env.cjs
267
- const { byTags } = require('@ts-ioc-container/bundler');
268
- const ENVS = ['development', 'production', 'test'];
151
+ The candidates are the files the extended tsconfig compiles (its `files` /
152
+ `include` / `exclude`, following its own `extends`). `paths` overrides that set
153
+ the way a child tsconfig's `include` overrides its parent's: the bundle then scans
154
+ those folders instead — relative to the config file (`./src/services`) or tsconfig
155
+ `paths` aliases (`@app/services`), recursively unless an entry says
156
+ `{ "path": "...", "recursive": false }`. Without a tsconfig, `paths` is required.
269
157
 
270
- module.exports = (env) => byTags((tags) => tags.filter((tag) => ENVS.includes(tag)).every((tag) => tag === env));
271
- ```
158
+ A candidate is parsed when it matches **one of** `include` and **none of**
159
+ `exclude`. Globs are relative to the config file and `/`-separated; `**` spans
160
+ folders.
272
161
 
273
- ### A. One bundle per environment (recommended)
162
+ | Rule | Default | Meaning |
163
+ | --------- | ----------------------------------------- | ---------------------------------------------------------- |
164
+ | `paths` | what the tsconfig compiles | Folders to scan instead |
165
+ | `include` | every source file | Globs a file must match one of, e.g. `"**/*.service.ts"` |
166
+ | `exclude` | test files, `__tests__/`, `node_modules/` | Globs of files never read; replaces the default when given |
274
167
 
275
- Every environment gets its own bundle, and both are committed. The files don't
276
- depend on who ran `tic build`, so `tic build --check` stays stable in CI.
168
+ A non-empty `exclude` replaces the defaults, so restate the ones you still want:
277
169
 
278
170
  ```json
279
- {
280
- "bundles": [
281
- {
282
- "output": "src/di/app.production.bundle.ts",
283
- "name": "AppBundle",
284
- "namespaces": ["@app/services"],
285
- "include": "./tic/production.cjs"
286
- },
287
- {
288
- "output": "src/di/app.development.bundle.ts",
289
- "name": "AppBundle",
290
- "namespaces": ["@app/services"],
291
- "include": "./tic/development.cjs"
292
- }
171
+ "files": {
172
+ "exclude": [
173
+ "**/*.spec.ts",
174
+ "**/*.test.ts",
175
+ "**/*.spec.tsx",
176
+ "**/*.test.tsx",
177
+ "**/__tests__/**",
178
+ "**/node_modules/**",
179
+ "frontend/api/generated/**"
293
180
  ]
294
181
  }
295
182
  ```
296
183
 
297
- ```js
298
- // tic/production.cjs
299
- module.exports = require('./for-env.cjs')('production');
300
- // tic/development.cjs
301
- module.exports = require('./for-env.cjs')('development');
302
- ```
303
-
304
- To keep development code out of the production build, the production entry
305
- point must import **only** the production bundle — a bundler includes what is
306
- imported, and a registration keeps its class alive, so nothing can be dropped
307
- after the fact. Give each environment its own entry:
184
+ If it omits a default glob the build warns, since test classes usually carry
185
+ the same `@register` decorators as production ones. `"exclude": []` deliberately
186
+ parses everything, including tests. Giving only `include` keeps the default
187
+ `exclude`, so `**/*.service.ts` still skips `user.service.spec.ts`.
308
188
 
309
- ```ts
310
- // src/main.production.ts — production build entry
311
- import { Container } from 'ts-ioc-container';
312
- import { AppBundle } from './di/app.production.bundle';
313
-
314
- export const container = new Container().useModule(new AppBundle());
315
- ```
189
+ ## Selecting classes
316
190
 
317
- ```ts
318
- // src/main.development.ts — development entry
319
- import { Container } from 'ts-ioc-container';
320
- import { AppBundle } from './di/app.development.bundle';
191
+ By default every exported, non-abstract class of a parsed file that carries
192
+ `@register` is registered. `classes` changes that; a class must meet every
193
+ criterion that is set:
321
194
 
322
- export const container = new Container().useModule(new AppBundle());
195
+ ```json
196
+ "classes": {
197
+ "export": "named",
198
+ "decorators": ["register", "repository"],
199
+ "name": "*Service"
200
+ }
323
201
  ```
324
202
 
325
- With a single entry, pick the bundle with a dynamic `import()`: the bundler
326
- splits each into its own chunk and only the chosen one is loaded — the other
327
- chunk is still emitted, just never fetched.
328
-
329
- ```ts
330
- const { AppBundle } =
331
- process.env.NODE_ENV === 'production'
332
- ? await import('./di/app.production.bundle')
333
- : await import('./di/app.development.bundle');
334
- ```
203
+ | Criterion | Default | Meaning |
204
+ | ------------------ | ------- | ----------------------------------------------------------------------------------------------------------- |
205
+ | `export` | `any` | `any`, `named` or `default` — which exports count |
206
+ | `decorators` | `["register"]` | The class must carry one of these, by name — also renamed imports and `@ioc.register()`; list composed ones. `[]` requires none |
207
+ | `name` | — | Glob the class name must match, e.g. `"*Service"`; an anonymous default export is named after its file (`user-service.ts` → `UserService`) |
208
+ | `excludeClasses` | — | Class names to drop, e.g. `["MockDashboardRepository"]` |
209
+ | `excludeName` | — | Glob the class name must **not** match, e.g. `"*Mock"` |
335
210
 
336
- | How the app picks the bundle | Development classes in the production build |
337
- | ---------------------------------------------------------------- | ------------------------------------------- |
338
- | One entry per environment, importing its own bundle | ✅ none |
339
- | Recipe B below — only the production bundle exists at build time | ✅ none |
340
- | Dynamic `import()` of the bundle | ⚠️ emitted as a separate chunk, never loaded |
341
- | Static imports of both bundles plus a runtime condition | ❌ both bundles, in full |
211
+ Abstract and non-exported classes are never registered.
342
212
 
343
- Don't rely on the bundler dropping a statically imported bundle behind
344
- `process.env.NODE_ENV === 'production'`: the classes it imports run decorators
345
- at load time, which bundlers treat as side effects and keep.
213
+ ### Excluding a single class
346
214
 
347
- ### B. One output, environment chosen when `tic build` runs
215
+ When a real implementation and a test-only stand-in are both decorated and both
216
+ bind the same token, registration is last-wins and the stand-in silently wins.
217
+ `excludeClasses` and `excludeName` drop a class right in the config:
348
218
 
349
- The smallest production build: only the production bundle ever exists while it
350
- is built. For pipelines that generate at deploy time instead of committing the
351
- output:
219
+ ```json
220
+ "classes": {
221
+ "decorators": ["repository", "service"],
222
+ "excludeClasses": ["MockDashboardRepository"],
223
+ "excludeName": "*Fake"
224
+ }
225
+ ```
352
226
 
353
- ```js
354
- // tic.include.cjs — picked up by convention
355
- const forEnv = require('./tic/for-env.cjs');
227
+ Both apply after every other criterion, so they can be combined with
228
+ `decorators` and `name` freely.
356
229
 
357
- // Read TIC_ENV per call: the file is loaded once per process.
358
- module.exports = (context) => forEnv(process.env.TIC_ENV ?? 'development')(context);
359
- ```
230
+ As a safety net, the build warns when two selected classes pass the same
231
+ plain-identifier first argument to a decorator — usually the binding token, as in
232
+ `@repository(IDashboardRepositoryToken)`. The check is syntactic (aliased imports
233
+ are not resolved), so it can only be a heuristic:
360
234
 
361
- ```bash
362
- TIC_ENV=production tic build
235
+ ```text
236
+ tic: warning: app.bundle.json: decorator token "IDashboardRepositoryToken" is passed by
237
+ HttpDashboardRepository, MockDashboardRepository; registration is last-wins,
238
+ exclude one with classes.excludeClasses
363
239
  ```
364
240
 
365
- The output now depends on `TIC_ENV`. Either don't commit it (generate it in
366
- every pipeline), or run `tic build --check` with the same `TIC_ENV` it was
367
- generated with.
241
+ Classes that are scope-gated are not last-wins, so they do not warn: when the
242
+ colliding classes share a decorator called with different arguments, e.g.
243
+ `@perPage('stations')` vs `@perPage('sessions')`, each scope registers its own
244
+ class and nothing is lost.
368
245
 
369
246
  ## Build
370
247
 
371
248
  ```bash
372
- tic build # tic.config.json in the working directory
373
- tic build -c path/to/tic.config.json
249
+ tic build # every *.bundle.{json,yaml,yml} of this package, else its tsconfig.json
250
+ tic build -c app.bundle.json -c admin/admin.bundle.json # only these
374
251
  tic build --check # CI: write nothing, exit 1 if a bundle is out of date
375
252
  ```
376
253
 
@@ -378,8 +255,8 @@ The output is plain TypeScript over the public `ts-ioc-container` API:
378
255
 
379
256
  ```ts
380
257
  // src/di/app.bundle.ts
381
- // Generated by `tic build` from ../../tic.config.json. Do not edit by hand.
382
- // Namespaces: @app/services, @app/infra
258
+ // Generated by `tic build` from ../../app.bundle.json. Do not edit by hand.
259
+ // Paths: @app/services, @app/infra
383
260
  import { type IContainer, type IContainerModule, type IRegistration, Registration } from 'ts-ioc-container';
384
261
  import { MemoryLogger } from '@app/infra/logging/MemoryLogger';
385
262
  import { Greeter } from '@app/services/Greeter';
package/cjm/app.js ADDED
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createContainer = createContainer;
4
+ exports.run = run;
5
+ exports.build = build;
6
+ exports.loadConfig = loadConfig;
7
+ require("reflect-metadata");
8
+ const ts_ioc_container_1 = require("ts-ioc-container");
9
+ const Application_1 = require("./cli/Application");
10
+ const CliIo_1 = require("./domain/CliIo");
11
+ const BuildModule_1 = require("./features/build/BuildModule");
12
+ const BundleBuilder_1 = require("./features/build/services/BundleBuilder");
13
+ const TicConfigService_1 = require("./features/build/services/TicConfigService");
14
+ const HelpModule_1 = require("./features/help/HelpModule");
15
+ const CommonModule_1 = require("./modules/CommonModule");
16
+ function createContainer(io = (0, CliIo_1.processIo)()) {
17
+ return new ts_ioc_container_1.Container({ tags: ['root'] })
18
+ .useModule(new CommonModule_1.CommonModule(io))
19
+ .useModule(new BuildModule_1.BuildModule())
20
+ .useModule(new HelpModule_1.HelpModule());
21
+ }
22
+ function withContainer(io, fn) {
23
+ const container = createContainer(io);
24
+ try {
25
+ return fn(container);
26
+ }
27
+ finally {
28
+ container.dispose();
29
+ }
30
+ }
31
+ function run(argv, io = (0, CliIo_1.processIo)()) {
32
+ return Application_1.Application.bootstrap(createContainer(io)).run(...argv);
33
+ }
34
+ function build({ cwd = process.cwd(), ...request } = {}) {
35
+ return withContainer({ ...(0, CliIo_1.processIo)(), cwd }, (container) => BundleBuilder_1.IBundleBuilderKey.resolve(container).build(request));
36
+ }
37
+ function loadConfig(file) {
38
+ return withContainer((0, CliIo_1.processIo)(), (container) => TicConfigService_1.ITicConfigServiceKey.resolve(container).load(file));
39
+ }
package/cjm/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- const cli_1 = require("./cli");
5
- process.exitCode = (0, cli_1.run)(process.argv.slice(2));
4
+ const app_1 = require("./app");
5
+ process.exitCode = (0, app_1.run)(process.argv.slice(2));