@ts-ioc-container/bundler 3.0.0 → 4.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.
Files changed (54) hide show
  1. package/AGENTS.md +45 -40
  2. package/README.md +181 -129
  3. package/cjm/app.js +2 -2
  4. package/cjm/cli/Application.js +4 -5
  5. package/cjm/cli/options.js +4 -2
  6. package/cjm/cli/usage.js +11 -8
  7. package/cjm/domain/CliIo.js +7 -1
  8. package/cjm/exceptions/DomainException.js +7 -7
  9. package/cjm/features/build/BuildConfig.js +108 -59
  10. package/cjm/features/build/BuildController.js +36 -46
  11. package/cjm/features/build/domain/ImportPaths.js +30 -8
  12. package/cjm/features/build/domain/emit.js +2 -2
  13. package/cjm/features/build/domain/scan.js +20 -4
  14. package/cjm/features/build/domain/tokenCollisions.js +1 -1
  15. package/cjm/features/build/domain/tsconfig.js +10 -35
  16. package/cjm/features/build/services/BundleBuilder.js +36 -29
  17. package/cjm/features/build/services/TicConfigService.js +10 -31
  18. package/cjm/index.js +9 -10
  19. package/cjm/schema/ticConfigSchema.js +5 -1
  20. package/package.json +2 -2
  21. package/tic.schema.json +117 -73
  22. package/tpl/index.cjs +15 -15
  23. package/typings/app.d.ts +7 -6
  24. package/typings/cli/Application.d.ts.map +1 -1
  25. package/typings/cli/options.d.ts +7 -1
  26. package/typings/cli/options.d.ts.map +1 -1
  27. package/typings/cli/usage.d.ts.map +1 -1
  28. package/typings/domain/CliIo.d.ts +2 -0
  29. package/typings/domain/CliIo.d.ts.map +1 -1
  30. package/typings/domain/GlobalConfig.d.ts +1 -1
  31. package/typings/domain/GlobalConfig.d.ts.map +1 -1
  32. package/typings/exceptions/DomainException.d.ts +4 -4
  33. package/typings/exceptions/DomainException.d.ts.map +1 -1
  34. package/typings/features/build/BuildConfig.d.ts +107 -94
  35. package/typings/features/build/BuildConfig.d.ts.map +1 -1
  36. package/typings/features/build/BuildController.d.ts +23 -17
  37. package/typings/features/build/BuildController.d.ts.map +1 -1
  38. package/typings/features/build/domain/ImportPaths.d.ts +15 -4
  39. package/typings/features/build/domain/ImportPaths.d.ts.map +1 -1
  40. package/typings/features/build/domain/emit.d.ts +4 -4
  41. package/typings/features/build/domain/emit.d.ts.map +1 -1
  42. package/typings/features/build/domain/scan.d.ts +2 -0
  43. package/typings/features/build/domain/scan.d.ts.map +1 -1
  44. package/typings/features/build/domain/tsconfig.d.ts +9 -16
  45. package/typings/features/build/domain/tsconfig.d.ts.map +1 -1
  46. package/typings/features/build/services/BundleBuilder.d.ts +25 -29
  47. package/typings/features/build/services/BundleBuilder.d.ts.map +1 -1
  48. package/typings/features/build/services/TicConfigService.d.ts +11 -15
  49. package/typings/features/build/services/TicConfigService.d.ts.map +1 -1
  50. package/typings/index.d.ts +6 -7
  51. package/typings/schema/ticConfigSchema.d.ts.map +1 -1
  52. package/cjm/features/build/domain/configFiles.js +0 -35
  53. package/typings/features/build/domain/configFiles.d.ts +0 -13
  54. package/typings/features/build/domain/configFiles.d.ts.map +0 -1
package/AGENTS.md CHANGED
@@ -1,62 +1,67 @@
1
1
  # AGENTS.md — @ts-ioc-container/bundler
2
2
 
3
3
  Bundles an application's dependencies into a single container module for
4
- `ts-ioc-container`: the `tic` CLI scans what a tsconfig compiles and generates a typed bundle
4
+ `ts-ioc-container`: the `tic` CLI scans folders and generates a typed bundle
5
5
  (`useModule(new AppBundle())`). Discovery happens at build time (ADR 0022);
6
6
  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). 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`), shaped like a tsconfig — `extends`, `include`,
13
- `exclude`, `compilerOptions` — plus the bundler's `output` and `name` at
14
- the top level and `compilerOptions.importExtension` / `compilerOptions.classes`;
15
- all optional. `tic build` works on
16
- the package it is invoked in (nearest `package.json` up from the working
17
- directory; never a workspace root): it builds every `*.bundle.{json,yaml,yml}` there
18
- (or the ones named with repeatable `-c`), else — zero config — one bundle
19
- from that package's `tsconfig.json` with every default. Each writes one class — `export class AppBundle implements IContainerModule`,
20
- applied with `container.useModule(new AppBundle())`. Several bundles (e.g.
21
- `production.bundle.json`, `development.bundle.json`) mean several configs.
22
- `--check` writes nothing and exits 1 when an output is stale — run it in CI.
23
- - A config is a tsconfig that `extends` another (default `./tsconfig.json`, may
24
- be absent; one named explicitly must exist): TypeScript parses it with the
25
- bundler's own fields removed, so the files it compiles (its `include` /
26
- `exclude` replacing the parent's) are the bundle's file set, and the merged
27
- `paths` aliases and module resolution shape the generated imports (most
28
- specific alias, else a relative path). Every compiler option but `classes`
29
- and `importExtension` goes to TypeScript, which reports unknown ones.
30
- `output` defaults to `<root>/<name>.bundle.ts`: root = compiled `rootDir`,
31
- else the common folder of the files compiled. `name` defaults to the config's
32
- stem (`production.bundle.json` → `production`), else `base`; the class is
33
- `toClassName(name)` — `ProductionBundle`, `BaseBundle`.
34
- - Test files, `__tests__/` and `node_modules/` (`DEFAULT_EXCLUDE`) are never
35
- scanned, whatever is compiled; neither are files `tic build` generated. With
36
- file naming conventions, a narrow `include` is the speed lever — nothing else
37
- is read.
38
- - `compilerOptions.classes: { export, decorators, name, excludeClasses, excludeName }`
39
- picks classes of the scanned files; default is every exported,
10
+ same program): `tic build [<config>]` is a Unix filter. It reads the config
11
+ file `<config>` (JSON for `.json`, YAML for `.yaml` / `.yml`) — or, without
12
+ one or with `-`, config text from stdin (parsed as YAML, which covers JSON) —
13
+ and prints the bundle to **stdout**; it writes nothing. Saving is the shell's
14
+ job: `tic build app.bundle.json > src/di/app.bundle.ts`,
15
+ `cat dev.bundle.json | tic build > src/di/dev.bundle.ts`; CI checks with
16
+ `tic build app.bundle.json | diff - src/di/app.bundle.ts`. Warnings and errors
17
+ go to stderr. The bundle is `export class AppBundle implements
18
+ IContainerModule`, applied with `container.useModule(new AppBundle())`.
19
+ - Every import in a bundle is written through a tsconfig `paths` alias, so the
20
+ bundle works wherever it is saved; a selected class no alias covers fails the
21
+ build naming its file. There are no relative imports.
22
+ - Config fields (schema: `tic.schema.json`), flat: `glob` with `glob.paths`
23
+ required; `className`, `name`, `tsconfig`, `importExtension` optional.
24
+ Relative paths resolve against the config file; for piped text, against the
25
+ working directory. `name` defaults to the file's stem (`prod.bundle.json` →
26
+ `prod`) and is required for piped text or a file named otherwise; the class
27
+ is `toClassName(name)` — `ProdBundle`.
28
+ - A config does **not** extend its tsconfig and never takes files from it. The
29
+ `tsconfig` field (default `./tsconfig.json`, may be absent; one named
30
+ explicitly must exist) only supplies `paths` aliases — which `glob.paths` may
31
+ name and generated imports are written in (most specific alias, else a
32
+ relative path) — and the module resolution that sets the import extension.
33
+ - Selection has two stages. `glob: { paths, include, exclude }` decides by path
34
+ which files are read and parsed at all. `paths` (required, non-empty) are the
35
+ folders scanned — relative to the config file (`./src/services`) or
36
+ tsconfig aliases (`@app/services`). A file in them must match one of `include`
37
+ (default: all) and none of `exclude` (default: test files, `__tests__/`,
38
+ `node_modules/`; a non-empty list replaces it, and the build warns if it drops
39
+ a default). With file naming conventions, `include` is the speed lever —
40
+ nothing else is read. Files `tic build` generated are never input.
41
+ - `className: { export, decorators, glob, exclude }`
42
+ then picks classes of the parsed files; default is every exported,
40
43
  non-abstract class decorated with `@register` (`decorators` defaults to
41
- `["register"]`; `[]` requires none); positive criteria must all hold and then `excludeClasses` (exact
42
- names) and `excludeName` (glob) drop classes.
44
+ `["register"]`; `[]` requires none); positive criteria must all hold — `glob` is one glob
45
+ or a list the class name must match one of — and then `exclude` (one glob or a
46
+ list; a plain class name matches only itself) drops classes.
43
47
  Decorators match by name, so composed ones must be listed. The build warns when
44
48
  two selected classes pass the same plain-identifier first decorator argument (a
45
49
  same-token heuristic), unless a decorator they share has differing arguments
46
50
  (scope-gated, e.g. `@perPage('a')` vs `@perPage('b')`).
47
51
  - Never edit a `*.bundle.ts` by hand — change the classes or the config and
48
52
  rerun `tic build`.
49
- - Unknown config fields are errors, except inside `compilerOptions`, where
50
- TypeScript reports unknown compiler options.
51
- - Programmatic API: `build({ config, cwd, check })` (one config, one output),
52
- `findConfigFiles(dir)`, `run(argv, io)`, `loadConfig(file)`; every error the
53
+ - Unknown config fields are errors, at every level.
54
+ - Programmatic API: `build({ config, cwd })` or `build({ text, cwd })` returns
55
+ `{ content, bundle, registrations, warnings }` and writes nothing;
56
+ `loadConfig(file)`, `run(argv, io)` (`io.stdin` backs piped configs); every error the
53
57
  bundler raises on purpose is a `TicError` with a stable `code`
54
- (`TicConfigError` = `INVALID_CONFIG`, `UsageError`,
55
- `StaleBundlesError`, …).
58
+ (`TicConfigError` = `INVALID_CONFIG`, `NamespaceNotFoundError`, `UsageError`,
59
+ …).
56
60
  - Internals follow `release-monorepo-semantically` (ADR 0023): `createContainer(io)`
57
61
  in `lib/app.ts` is the composition root; `Application` resolves the controller
58
62
  registered under the command name (`@register('build')`) and runs its
59
- `@onDefault(invoke)` action, which parses its own options with
63
+ `@onDefault(invoke)` action (a word in the action position that names no
64
+ declared action is the default action's argument: `tic build <config>`), which parses its own options with
60
65
  `@inject(pipe(commandArgs, parseOptions(spec), validate(SCHEMA)))`. Work lives in
61
66
  services behind tokens (`IBundleBuilderKey`, `ITicConfigServiceKey`, …); pure
62
67
  logic in `lib/features/build/domain/`. A new command is a controller plus a module.
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
- what your tsconfig compiles and generates one typed bundle that registers every
5
- class in it, ready for
4
+ the folders you name — by path or tsconfig alias — and generates one typed
5
+ 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,9 @@ 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
- A bundle config **is** a tsconfig that `extends` yours — `include`, `exclude`
18
- and `compilerOptions` mean what they mean to `tsc` — plus a few fields of the
19
- bundler's own.
17
+ Everything is explicit, and `tic build` is a plain Unix filter: a config goes in
18
+ — a file, or text on stdin — and the bundle comes out on stdout. Where it is saved
19
+ is up to your shell, and nothing is inferred from what your tsconfig compiles.
20
20
 
21
21
  ## Install
22
22
 
@@ -30,162 +30,208 @@ below use the shortcut.
30
30
 
31
31
  `typescript` (>= 5) is a peer dependency.
32
32
 
33
- ## Zero config
34
-
35
- With a `tsconfig.json`, there is nothing to configure:
33
+ ## Build
36
34
 
37
35
  ```bash
38
- tic build
36
+ tic build [<config>]
39
37
  ```
40
38
 
41
- ```text
42
- wrote src/base.bundle.ts (12 registrations)
43
- ```
39
+ `tic build` prints the bundle to **stdout** and writes nothing. `<config>` is a
40
+ config file — JSON for `.json`, YAML for `.yaml` / `.yml`. Without it (or with
41
+ `-`) the config is read from **stdin**. Warnings and errors go to stderr, so
42
+ they never end up in the bundle.
44
43
 
45
- The bundle registers every exported class decorated with `@register` among the
46
- files your `tsconfig.json` compiles, test files excluded, and lands at the root
47
- of those sources — `rootDir`, else their common folder (`src/`), so `tsc`
48
- compiles it too. It exports `BaseBundle`:
44
+ ```bash
45
+ # a config file in, the bundle saved by the shell
46
+ tic build app.bundle.json > src/di/app.bundle.ts
47
+ tic build prod.bundle.yml > src/di/prod.bundle.ts
49
48
 
50
- ```ts
51
- import { BaseBundle } from './base.bundle';
49
+ # the config piped in — it has no file name, so it sets `name` ("name": "dev")
50
+ cat dev.bundle.json | tic build > src/di/dev.bundle.ts
51
+ tic build < dev.bundle.yml > src/di/dev.bundle.ts
52
+
53
+ # into any command: format it, show it and save it, ...
54
+ tic build app.bundle.json | npx prettier --stdin-filepath app.bundle.ts > src/di/app.bundle.ts
55
+ cat dev.bundle.json | tic build | tee src/di/dev.bundle.ts
52
56
 
53
- const container = new Container().useModule(new BaseBundle());
57
+ # a config built on the fly
58
+ printf 'name: app\nglob:\n paths: ["@app/services"]\n' | tic build > src/di/app.bundle.ts
59
+ jq '.name = "admin"' app.bundle.json | tic build > src/di/admin.bundle.ts
60
+
61
+ # CI: is the saved bundle current?
62
+ tic build app.bundle.json | diff - src/di/app.bundle.ts
54
63
  ```
55
64
 
56
- `tic build` works on the package it is invoked in: it walks up from the working
57
- directory to the nearest `package.json` — the project root, or in a monorepo
58
- the package — and uses the `tsconfig.json` there. It never falls back to a
59
- workspace root's tsconfig.
65
+ A bundle can be saved anywhere, because every import in it is written through a
66
+ tsconfig `paths` alias (`import { Greeter } from '@app/services/Greeter'`) —
67
+ never a path relative to where the file ends up. A selected class no alias
68
+ covers is an error naming its file; add a `paths` entry for its folder.
69
+
70
+ Relative paths in a config file resolve against the file; in a piped config,
71
+ against the working directory. The bundle is named after the config file —
72
+ `prod.bundle.json` exports `ProdBundle` — unless the config sets `name`, which a
73
+ piped config, or a file not named `<name>.bundle.json`, must.
60
74
 
61
75
  ## Configure
62
76
 
63
- To change a default, add a config file next to your `tsconfig.json`. One config
64
- file describes one bundle and is named `<name>.bundle.json` — `app.bundle.json`:
77
+ A config describes one bundle: the folders it scans and the classes it
78
+ registers. Point your editor at the schema:
65
79
 
66
80
  ```json
67
81
  {
68
82
  "$schema": "./node_modules/@ts-ioc-container/bundler/tic.schema.json",
69
- "output": "src/di/app.bundle.ts"
83
+ "glob": {
84
+ "paths": ["@app/services", { "path": "./src/infra", "recursive": false }]
85
+ }
70
86
  }
71
87
  ```
72
88
 
73
- or, in YAML, `<name>.bundle.yaml` / `<name>.bundle.yml` — same fields, same schema:
74
-
75
89
  ```yaml
76
90
  # yaml-language-server: $schema=./node_modules/@ts-ioc-container/bundler/tic.schema.json
77
- extends: ./tsconfig.json
78
- output: src/di/app.bundle.ts
79
- include: [src/**/*.service.ts]
80
- compilerOptions:
81
- classes:
82
- decorators: [register, service]
91
+ glob:
92
+ paths: ['@app/services', ./src/infra]
93
+ include: ['**/*.service.ts']
83
94
  ```
84
95
 
85
- An empty YAML file is a bundle with every setting at its default. Describing
86
- one bundle in two formats (`app.bundle.json` and `app.bundle.yaml`) is an error.
96
+ | Field | Default | Meaning |
97
+ | ----------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
98
+ | `glob` | — (required, with `paths`) | Which files are parsed — see [Selecting files](#selecting-files) |
99
+ | `className` | exported classes with `@register` | Which classes of a parsed file are registered — see [Selecting classes](#selecting-classes) |
100
+ | `name` | the config file's (`app.bundle.json` → `app`) | The bundle's name: names the class, `AppBundle`. Required for a piped config or a file named otherwise |
101
+ | `tsconfig` | `./tsconfig.json` | Source of the `paths` aliases every import is written through, and of the import extension — never of files |
102
+ | `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
87
103
 
88
- Every field is optional; `{}` is the zero-config bundle. The tsconfig fields:
104
+ An unknown field is an error, so a misspelled or removed option never goes
105
+ unnoticed.
89
106
 
90
- | Field | Default | Meaning |
91
- | ----------------- | --------------------------------- | --------------------------------------------------------------------------------------------- |
92
- | `extends` | `./tsconfig.json` (may be absent) | The tsconfig this config extends; one named here must exist |
93
- | `include` | the extended tsconfig's | Which files are scanned — see [Selecting files](#selecting-files) |
94
- | `exclude` | the extended tsconfig's | Which of them are left out — see [Selecting files](#selecting-files) |
95
- | `compilerOptions` | the extended tsconfig's | tsconfig compiler options (`paths`, `rootDir`, `moduleResolution`, …) plus the bundler's below |
107
+ ## Several bundles
96
108
 
97
- The bundler's own fields, at the top level:
109
+ Need more than one bundle — per environment, per app? Keep a config per bundle
110
+ and build each one:
98
111
 
99
- | Field | Default | Meaning |
100
- | -------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
101
- | `output` | `<root>/<name>.bundle.ts` | The bundle file; `<root>` as in [Zero config](#zero-config) |
102
- | `name` | the config's stem, else `base` | The bundle's name (`production.bundle.json` → `production`): names the default output and the class, `ProductionBundle` |
112
+ ```bash
113
+ for env in prod dev test; do
114
+ tic build "$env.bundle.yml" > "src/di/$env.bundle.ts"
115
+ done
116
+ ```
103
117
 
104
- and under `compilerOptions`:
118
+ ```json
119
+ {
120
+ "scripts": {
121
+ "bundle": "tic build app.bundle.json > src/di/app.bundle.ts",
122
+ "bundle:check": "tic build app.bundle.json | diff - src/di/app.bundle.ts"
123
+ }
124
+ }
125
+ ```
105
126
 
106
- | Option | Default | Meaning |
107
- | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------- |
108
- | `classes` | exported classes with `@register` | Which classes of a scanned file are registered — see [Selecting classes](#selecting-classes) |
109
- | `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
127
+ Each config selects its own files and classes:
110
128
 
111
- An unknown top-level field is an error, so a misspelled or removed option never
112
- goes unnoticed. Every other compiler option is handed to TypeScript, which
113
- reports an unknown one (`Unknown compiler option 'pathz'. Did you mean 'paths'?`).
129
+ ```yaml
130
+ # prod.bundle.yml: services + adapters, minus dev-only *.dev.ts files
131
+ glob:
132
+ paths: ['@app/services', ./src/infra]
133
+ exclude:
134
+ - '**/*.dev.ts'
135
+ - '**/*.spec.ts'
136
+ - '**/*.test.ts'
137
+ - '**/*.spec.tsx'
138
+ - '**/*.test.tsx'
139
+ - '**/__tests__/**'
140
+ - '**/node_modules/**'
141
+ ```
114
142
 
115
- ## Several bundles
143
+ ```yaml
144
+ # dev.bundle.yml: the *.dev.ts logger replaces the production one, dropped by class name
145
+ glob:
146
+ paths: ['@app/services', ./src/infra]
147
+ className:
148
+ exclude: JsonLogger
149
+ ```
116
150
 
117
- Need more than one bundle — per environment, per app? Add a config per bundle.
118
- `tic build` builds every `*.bundle.json`, `*.bundle.yaml` and `*.bundle.yml` at
119
- the package root:
151
+ A saved bundle is never registered by another build, even when it sits in a
152
+ folder that build scans.
120
153
 
121
- ```text
122
- production.bundle.json → src/di/production.bundle.ts
123
- development.bundle.json → src/di/development.bundle.ts
124
- test.bundle.json → src/di/test.bundle.ts
125
- ```
154
+ ## Selection in two stages
155
+
156
+ A bundle picks its registrations in two stages:
157
+
158
+ 1. **`glob`** — decided by path alone, before anything is read. Only files that
159
+ pass are parsed.
160
+ 2. **`className`** — decided per class, on the files stage 1 let through.
126
161
 
127
- Each one can extend its own tsconfig and select its own files:
162
+ Parsing is where the time goes, so when your project names files by convention
163
+ (`user.service.ts`, `user.repository.ts`), say so in `glob.include` and the
164
+ bundler never reads anything else:
128
165
 
129
- ```jsonc
130
- // production.bundle.json
166
+ ```json
131
167
  {
132
- "extends": "./tsconfig.production.json",
133
- "exclude": ["src/**/*.development.ts"]
168
+ "glob": {
169
+ "paths": ["@app/services"],
170
+ "include": ["**/*.service.ts", "**/*.repository.ts"]
171
+ },
172
+ "className": { "decorators": ["register"] }
134
173
  }
135
174
  ```
136
175
 
137
- It writes `src/production.bundle.ts`, exporting `ProductionBundle`.
138
-
139
- A bundle generated by one config is never registered by another, even when it
140
- sits in a folder the other scans.
141
-
142
176
  ## Selecting files
143
177
 
144
- The files a bundle scans are the files its config compiles, as `tsc` would see
145
- them: the extended tsconfig's `files` / `include` / `exclude` (following its own
146
- `extends`), with this config's `include` and `exclude` replacing the parent's —
147
- exactly as in a child tsconfig. Globs are relative to the config file.
178
+ `paths` are the folders a bundle's files come from, and the only ones: what
179
+ your tsconfig compiles plays no part. Each entry is a folder relative to the
180
+ config file (`./src/services`) or a tsconfig `paths` alias (`@app/services`,
181
+ for a `"@app/*": ["./src/*"]` alias), scanned recursively unless it says
182
+ `{ "path": "...", "recursive": false }`.
148
183
 
149
- Test files (`*.spec.ts`, `*.test.ts`, their `.tsx` twins and `__tests__/`) and
150
- `node_modules/` are never scanned, whatever the tsconfig compiles, since test
151
- classes usually carry the same `@register` decorators as production ones. Files
152
- `tic build` generated are never input either.
184
+ Within them, a file is parsed when it matches **one of** `include` and **none of**
185
+ `exclude`. Globs are relative to the config file and `/`-separated; `**`
186
+ spans folders.
153
187
 
154
- Parsing is where the time goes, so when your project names files by convention
155
- (`user.service.ts`, `user.repository.ts`), say so in `include` and the bundler
156
- never reads anything else:
188
+ | Rule | Default | Meaning |
189
+ | --------- | ----------------------------------------- | ---------------------------------------------------------- |
190
+ | `paths` | — (required) | Folders to scan |
191
+ | `include` | every source file | Globs a file must match one of, e.g. `"**/*.service.ts"` |
192
+ | `exclude` | test files, `__tests__/`, `node_modules/` | Globs of files never read; replaces the default when given |
193
+
194
+ A non-empty `exclude` replaces the defaults, so restate the ones you still want:
157
195
 
158
196
  ```json
159
- {
160
- "output": "src/di/app.bundle.ts",
161
- "include": ["src/**/*.service.ts", "src/**/*.repository.ts"],
162
- "exclude": ["src/legacy/**"]
197
+ "glob": {
198
+ "exclude": [
199
+ "**/*.spec.ts",
200
+ "**/*.test.ts",
201
+ "**/*.spec.tsx",
202
+ "**/*.test.tsx",
203
+ "**/__tests__/**",
204
+ "**/node_modules/**",
205
+ "frontend/api/generated/**"
206
+ ]
163
207
  }
164
208
  ```
165
209
 
210
+ If it omits a default glob the build warns, since test classes usually carry
211
+ the same `@register` decorators as production ones. `"exclude": []` deliberately
212
+ parses everything, including tests. Giving only `include` keeps the default
213
+ `exclude`, so `**/*.service.ts` still skips `user.service.spec.ts`.
214
+
166
215
  ## Selecting classes
167
216
 
168
- By default every exported, non-abstract class of a scanned file that carries
169
- `@register` is registered. `compilerOptions.classes` changes that; a class must
170
- meet every criterion that is set:
217
+ By default every exported, non-abstract class of a parsed file that carries
218
+ `@register` is registered. `className` changes that; a class must meet every
219
+ criterion that is set:
171
220
 
172
221
  ```json
173
- "compilerOptions": {
174
- "classes": {
175
- "export": "named",
176
- "decorators": ["register", "repository"],
177
- "name": "*Service"
178
- }
222
+ "className": {
223
+ "export": "named",
224
+ "decorators": ["register", "repository"],
225
+ "glob": "*Service"
179
226
  }
180
227
  ```
181
228
 
182
- | Criterion | Default | Meaning |
183
- | ------------------ | ------- | ----------------------------------------------------------------------------------------------------------- |
184
- | `export` | `any` | `any`, `named` or `default` — which exports count |
185
- | `decorators` | `["register"]` | The class must carry one of these, by name — also renamed imports and `@ioc.register()`; list composed ones. `[]` requires none |
186
- | `name` | — | Glob the class name must match, e.g. `"*Service"`; an anonymous default export is named after its file (`user-service.ts` → `UserService`) |
187
- | `excludeClasses` | — | Class names to drop, e.g. `["MockDashboardRepository"]` |
188
- | `excludeName` | — | Glob the class name must **not** match, e.g. `"*Mock"` |
229
+ | Criterion | Default | Meaning |
230
+ | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
231
+ | `export` | `any` | `any`, `named` or `default` — which exports count |
232
+ | `decorators` | `["register"]` | The class must carry one of these, by name — also renamed imports and `@ioc.register()`; list composed ones. `[]` requires none |
233
+ | `glob` | — | A glob, or a list of them, the class name must match one of: `"*Service"`, `["*Service", "*Repository"]`; an anonymous default export is named after its file (`user-service.ts` → `UserService`) |
234
+ | `exclude` | — | A glob, or a list of them, the class name must match **none** of: `"*Mock"`, `["MockDashboardRepository", "*Fake"]` |
189
235
 
190
236
  Abstract and non-exported classes are never registered.
191
237
 
@@ -193,20 +239,18 @@ Abstract and non-exported classes are never registered.
193
239
 
194
240
  When a real implementation and a test-only stand-in are both decorated and both
195
241
  bind the same token, registration is last-wins and the stand-in silently wins.
196
- `excludeClasses` and `excludeName` drop a class right in the config:
242
+ `exclude` drops it right in the config. A plain class name is a glob that
243
+ matches only itself, so one list drops a class by name and a family by pattern:
197
244
 
198
245
  ```json
199
- "compilerOptions": {
200
- "classes": {
201
- "decorators": ["repository", "service"],
202
- "excludeClasses": ["MockDashboardRepository"],
203
- "excludeName": "*Fake"
204
- }
246
+ "className": {
247
+ "decorators": ["repository", "service"],
248
+ "exclude": ["MockDashboardRepository", "*Fake"]
205
249
  }
206
250
  ```
207
251
 
208
- Both apply after every other criterion, so they can be combined with
209
- `decorators` and `name` freely.
252
+ `exclude` applies after every other criterion, so it combines with
253
+ `decorators` and `glob` freely.
210
254
 
211
255
  As a safety net, the build warns when two selected classes pass the same
212
256
  plain-identifier first argument to a decorator — usually the binding token, as in
@@ -216,7 +260,7 @@ are not resolved), so it can only be a heuristic:
216
260
  ```text
217
261
  tic: warning: app.bundle.json: decorator token "IDashboardRepositoryToken" is passed by
218
262
  HttpDashboardRepository, MockDashboardRepository; registration is last-wins,
219
- exclude one with compilerOptions.classes.excludeClasses
263
+ exclude one with className.exclude
220
264
  ```
221
265
 
222
266
  Classes that are scope-gated are not last-wins, so they do not warn: when the
@@ -224,27 +268,19 @@ colliding classes share a decorator called with different arguments, e.g.
224
268
  `@perPage('stations')` vs `@perPage('sessions')`, each scope registers its own
225
269
  class and nothing is lost.
226
270
 
227
- ## Build
228
-
229
- ```bash
230
- tic build # every *.bundle.{json,yaml,yml} of this package, else its tsconfig.json
231
- tic build -c app.bundle.json -c admin/admin.bundle.json # only these
232
- tic build --check # CI: write nothing, exit 1 if a bundle is out of date
233
- ```
271
+ ## Output
234
272
 
235
273
  The output is plain TypeScript over the public `ts-ioc-container` API:
236
274
 
237
275
  ```ts
238
276
  // src/di/app.bundle.ts
239
- // Generated by `tic build` from ../../app.bundle.json. Do not edit by hand.
277
+ // Generated by `tic build`. Do not edit by hand.
278
+ // Paths: @app/services, @app/infra
240
279
  import { type IContainer, type IContainerModule, type IRegistration, Registration } from 'ts-ioc-container';
241
280
  import { MemoryLogger } from '@app/infra/logging/MemoryLogger';
242
281
  import { Greeter } from '@app/services/Greeter';
243
282
 
244
- export const registrations: IRegistration[] = [
245
- Registration.fromClass(MemoryLogger),
246
- Registration.fromClass(Greeter),
247
- ];
283
+ export const registrations: IRegistration[] = [Registration.fromClass(MemoryLogger), Registration.fromClass(Greeter)];
248
284
 
249
285
  export class AppBundle implements IContainerModule {
250
286
  applyTo(container: IContainer): void {
@@ -258,8 +294,24 @@ const container = new Container().useModule(new AppBundle());
258
294
  ```
259
295
 
260
296
  Treat `*.bundle.ts` as build output: add it to `.prettierignore` (and any
261
- other formatter's ignore list), since `--check` compares files byte for byte.
297
+ other formatter's ignore list) unless you format it in the pipe, so a
298
+ `tic build … | diff - …` check compares like with like.
262
299
 
263
300
  Each class keeps its own `@register(...)` config (key, scope, singleton, …);
264
301
  an undecorated class is bound by its class name. Discovery is syntactic — no
265
302
  type checker runs.
303
+
304
+ ## Programmatic API
305
+
306
+ `build()` returns the bundle and writes nothing, like the CLI:
307
+
308
+ ```ts
309
+ import { writeFileSync } from 'node:fs';
310
+ import { build } from '@ts-ioc-container/bundler';
311
+
312
+ const { content, warnings } = build({ config: 'app.bundle.json' });
313
+ writeFileSync('src/di/app.bundle.ts', content);
314
+
315
+ // or config text, as `tic build` reads from stdin
316
+ build({ text: 'name: app\nglob:\n paths: ["@app/services"]\n' });
317
+ ```
package/cjm/app.js CHANGED
@@ -9,8 +9,8 @@ const ts_ioc_container_1 = require("ts-ioc-container");
9
9
  const Application_1 = require("./cli/Application");
10
10
  const CliIo_1 = require("./domain/CliIo");
11
11
  const BuildModule_1 = require("./features/build/BuildModule");
12
- const BundleBuilder_1 = require("./features/build/services/BundleBuilder");
13
12
  const TicConfigService_1 = require("./features/build/services/TicConfigService");
13
+ const BundleBuilder_1 = require("./features/build/services/BundleBuilder");
14
14
  const HelpModule_1 = require("./features/help/HelpModule");
15
15
  const CommonModule_1 = require("./modules/CommonModule");
16
16
  function createContainer(io = (0, CliIo_1.processIo)()) {
@@ -31,7 +31,7 @@ function withContainer(io, fn) {
31
31
  function run(argv, io = (0, CliIo_1.processIo)()) {
32
32
  return Application_1.Application.bootstrap(createContainer(io)).run(...argv);
33
33
  }
34
- function build({ cwd = process.cwd(), ...request } = {}) {
34
+ function build({ cwd = process.cwd(), ...request }) {
35
35
  return withContainer({ ...(0, CliIo_1.processIo)(), cwd }, (container) => BundleBuilder_1.IBundleBuilderKey.resolve(container).build(request));
36
36
  }
37
37
  function loadConfig(file) {
@@ -36,11 +36,10 @@ class Application {
36
36
  if (!this.scope.hasRegistration(command))
37
37
  throw new DomainException_1.UnknownCommandError(command);
38
38
  const controller = this.scope.resolve(command);
39
- const collector = new ts_ioc_container_1.HookCollector({
40
- key: action,
41
- createExecutionContext: (0, ts_ioc_container_1.createHookContextFactory)({ args: argv }),
42
- });
43
- const actions = collector.getActions(controller, { scope: this.scope });
39
+ const actionsOf = (key) => new ts_ioc_container_1.HookCollector({ key, createExecutionContext: (0, ts_ioc_container_1.createHookContextFactory)({ args: argv }) }).getActions(controller, { scope: this.scope });
40
+ let actions = actionsOf(action);
41
+ if (actions.length === 0 && action !== decorators_1.DEFAULT_ACTION)
42
+ actions = actionsOf(decorators_1.DEFAULT_ACTION);
44
43
  if (actions.length === 0)
45
44
  throw new DomainException_1.UnknownActionError(command, action);
46
45
  for (const item of actions)
@@ -6,10 +6,12 @@ const zod_1 = require("zod");
6
6
  const DomainException_1 = require("../exceptions/DomainException");
7
7
  const commandArgs = ({ args = [] }) => args.map(String);
8
8
  exports.commandArgs = commandArgs;
9
- const parseOptions = (options) => (argv) => {
9
+ const parseOptions = (options, { positionals = false } = {}) => (argv) => {
10
10
  const firstFlag = argv.findIndex((arg) => arg.startsWith('-'));
11
+ const args = positionals ? argv.slice(1) : firstFlag >= 0 ? argv.slice(firstFlag) : [];
11
12
  try {
12
- return { ...(0, node_util_1.parseArgs)({ args: firstFlag >= 0 ? argv.slice(firstFlag) : [], options, strict: true }).values };
13
+ const parsed = (0, node_util_1.parseArgs)({ args, options, strict: true, allowPositionals: positionals });
14
+ return positionals ? { ...parsed.values, positionals: parsed.positionals } : { ...parsed.values };
13
15
  }
14
16
  catch (e) {
15
17
  throw new DomainException_1.UsageError(e.message);