@ts-ioc-container/bundler 1.4.0 → 2.0.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/AGENTS.md +35 -30
- package/README.md +157 -280
- package/cjm/ImportPaths.js +5 -27
- package/cjm/build.js +53 -79
- package/cjm/cli.js +38 -13
- package/cjm/config.js +121 -81
- package/cjm/emit.js +5 -2
- package/cjm/index.js +12 -12
- package/cjm/scan.js +6 -3
- package/cjm/tsconfig.js +78 -0
- package/package.json +4 -3
- package/tic.schema.json +120 -136
- package/tpl/index.cjs +36 -16
- package/typings/ImportPaths.d.ts +4 -16
- package/typings/build.d.ts +13 -16
- package/typings/config.d.ts +104 -62
- package/typings/emit.d.ts +7 -2
- package/typings/errors.d.ts +1 -1
- package/typings/index.d.ts +5 -7
- package/typings/scan.d.ts +4 -2
- package/typings/tsconfig.d.ts +25 -0
- package/cjm/exportPredicate.js +0 -13
- package/cjm/inclusion.js +0 -24
- package/cjm/predicateFile.js +0 -30
- package/cjm/utils.js +0 -2
- package/typings/exportPredicate.d.ts +0 -36
- package/typings/inclusion.d.ts +0 -42
- package/typings/predicateFile.d.ts +0 -16
- package/typings/utils.d.ts +0 -3
package/AGENTS.md
CHANGED
|
@@ -7,40 +7,45 @@ nothing here runs inside the container, and the core package is not changed by
|
|
|
7
7
|
it.
|
|
8
8
|
|
|
9
9
|
- The CLI is `ts-ioc-container`, with `tic` as a shortcut (both `bin` entries,
|
|
10
|
-
same program).
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
same program). One config file describes one bundle: `<name>.bundle.json`, or the same in
|
|
11
|
+
YAML as `<name>.bundle.yaml` / `.yml` (an empty YAML file = all defaults)
|
|
12
|
+
(schema: `tic.schema.json`), flat — `output`, `name`, `tags`, `extends`,
|
|
13
|
+
`importExtension`, `files`, `classes`, all optional. `tic build` works on
|
|
14
|
+
the package it is invoked in (nearest `package.json` up from the working
|
|
15
|
+
directory; never a workspace root): it builds every `*.bundle.{json,yaml,yml}` there
|
|
16
|
+
(or the ones named with repeatable `-c`), else — zero config — one bundle
|
|
17
|
+
from that package's `tsconfig.json` with every default. Each writes one class — `export class AppBundle implements IContainerModule`,
|
|
18
|
+
applied with `container.useModule(new AppBundle())`. Several bundles (e.g.
|
|
19
|
+
`production.bundle.json`, `development.bundle.json`) mean several configs.
|
|
20
|
+
`--check` writes nothing and exits 1 when an output is stale — run it in CI.
|
|
21
|
+
- A config `extends` a tsconfig (default `./tsconfig.json`, may be absent; one
|
|
22
|
+
named explicitly must exist), like a child tsconfig: what it compiles is the
|
|
23
|
+
bundle's file set, and its `paths` aliases and module resolution shape the
|
|
24
|
+
generated imports (most specific alias, else a relative path). `output`
|
|
25
|
+
defaults to `<root>/<name>.bundle.ts`: root = tsconfig `rootDir`, else the
|
|
26
|
+
common folder of its files. `name` defaults to the config's stem
|
|
27
|
+
(`production.bundle.json` → `production`), else `base`; the class is
|
|
28
|
+
`toClassName(name)` — `ProductionBundle`, `BaseBundle`.
|
|
29
|
+
- Selection has two stages. `files: { paths, include, exclude }` decides by path
|
|
30
|
+
which files are read and parsed at all. `paths` override the tsconfig's file
|
|
31
|
+
set with folders (relative to the config, or tsconfig aliases); required when
|
|
32
|
+
there is no tsconfig. A candidate must match one of `include` (default: all)
|
|
33
|
+
and none of `exclude` (default: test files, `__tests__/`, `node_modules/`; a
|
|
34
|
+
non-empty list replaces it, and the build warns if it drops a default). Globs
|
|
35
|
+
are relative to the config. With file naming conventions, `include` is the
|
|
36
|
+
speed lever — nothing else is read. Files `tic build` generated are never input.
|
|
37
|
+
- `classes: { export, decorators, name, excludeClasses, excludeName }`
|
|
38
|
+
then picks classes of the parsed files; default is every exported,
|
|
39
|
+
non-abstract class decorated with `@register` (`decorators` defaults to
|
|
40
|
+
`["register"]`; `[]` requires none); positive criteria must all hold and then `excludeClasses` (exact
|
|
41
|
+
names) and `excludeName` (glob) drop classes.
|
|
20
42
|
Decorators match by name, so composed ones must be listed. The build warns when
|
|
21
43
|
two selected classes pass the same plain-identifier first decorator argument (a
|
|
22
44
|
same-token heuristic), unless a decorator they share has differing arguments
|
|
23
45
|
(scope-gated, e.g. `@perPage('a')` vs `@perPage('b')`).
|
|
24
46
|
- Never edit a `*.bundle.ts` by hand — change the classes or the config and
|
|
25
47
|
rerun `tic build`.
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
bundle's `include` file, else a conventional `tic.include.*` next to the
|
|
30
|
-
config. Predicate files are `require`d once per process — read env vars
|
|
31
|
-
inside the predicate. README "Recipe: generate per environment" shows the
|
|
32
|
-
per-environment setup.
|
|
33
|
-
- Right after parsing, an `ExportPredicate` (`FilterPredicate<ExportContext>`:
|
|
34
|
-
filename, exportName, className, isDefault, decorators, tags) filters class by
|
|
35
|
-
class, after `select`; found via `build({ filterExports })`, a bundle's
|
|
36
|
-
`filterExports` file, or `tic.exports.*` next to the config.
|
|
37
|
-
- Prefer `byTags((tags, context) => ...)` (a `TagInclusionPredicate`) over parsing
|
|
38
|
-
file names: tags are the dot-parts between base name and extension
|
|
39
|
-
(`Report.production.eu.ts` → `['production', 'eu']`). Usage examples:
|
|
40
|
-
`__tests__/examples/inclusion-predicates.spec.ts`.
|
|
41
|
-
- For a lean production build, the production entry must import only the
|
|
42
|
-
production bundle (README "One bundle per environment"); statically importing
|
|
43
|
-
both bundles ships both.
|
|
44
|
-
- Programmatic API: `build({ config, cwd, check })`, `run(argv, io)`,
|
|
45
|
-
`loadConfig(file)`; errors are `TicConfigError` / `NamespaceNotFoundError`
|
|
48
|
+
- Unknown config fields are errors, at every level.
|
|
49
|
+
- Programmatic API: `build({ config, cwd, check })` (one config, one output),
|
|
50
|
+
`findConfigFiles(dir)`, `run(argv, io)`, `loadConfig(file)`; errors are `TicConfigError` / `NamespaceNotFoundError`
|
|
46
51
|
(both `TicError`).
|
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
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
##
|
|
32
|
+
## Zero config
|
|
34
33
|
|
|
35
|
-
`
|
|
34
|
+
With a `tsconfig.json`, there is nothing to configure:
|
|
36
35
|
|
|
37
|
-
```
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
### Excluding a single class
|
|
60
|
+
## Configure
|
|
110
61
|
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
"
|
|
119
|
-
"
|
|
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
|
-
|
|
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
|
-
```
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
##
|
|
99
|
+
## Several bundles
|
|
157
100
|
|
|
158
|
-
|
|
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
|
-
```
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
126
|
+
## Selection in two stages
|
|
193
127
|
|
|
194
|
-
A
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
195
|
+
```json
|
|
196
|
+
"classes": {
|
|
197
|
+
"export": "named",
|
|
198
|
+
"decorators": ["register", "repository"],
|
|
199
|
+
"name": "*Service"
|
|
200
|
+
}
|
|
323
201
|
```
|
|
324
202
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
219
|
+
```json
|
|
220
|
+
"classes": {
|
|
221
|
+
"decorators": ["repository", "service"],
|
|
222
|
+
"excludeClasses": ["MockDashboardRepository"],
|
|
223
|
+
"excludeName": "*Fake"
|
|
224
|
+
}
|
|
225
|
+
```
|
|
352
226
|
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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
|
-
```
|
|
362
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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 #
|
|
373
|
-
tic build -c
|
|
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 ../../
|
|
382
|
-
//
|
|
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';
|