@ts-ioc-container/bundler 3.1.0 → 5.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 +48 -48
  2. package/README.md +199 -144
  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 +126 -77
  10. package/cjm/features/build/BuildController.js +36 -46
  11. package/cjm/features/build/domain/ImportPaths.js +31 -20
  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 +35 -30
  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 +132 -77
  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 +102 -102
  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 +16 -14
  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,70 +1,70 @@
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 `baseUrl`, `output` and `name`
14
- at 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, `baseUrl` and module resolution shape the generated imports
28
- (most specific alias, else a path relative to `baseUrl` when the file is under
29
- it, else a relative path). Every compiler option but `classes`
30
- and `importExtension` goes to TypeScript, which reports unknown ones.
31
- The top-level `baseUrl` (default the config's directory) is the base the
32
- config's own `include`, `exclude` and `output` resolve against — so they need
33
- not repeat a shared prefix — and is passed to TypeScript as
34
- `compilerOptions.baseUrl` unless that is set too; `extends` stays relative to
35
- the config file. With `baseUrl` set, an omitted `include` defaults to the whole
36
- `baseUrl` folder (`.`); without it, an omitted `include` still inherits the
37
- extended tsconfig's.
38
- `output` defaults to `<root>/<name>.bundle.ts`: root = compiled `rootDir`,
39
- else the common folder of the files compiled. `name` defaults to the config's
40
- stem (`production.bundle.json` → `production`), else `base`; the class is
41
- `toClassName(name)` — `ProductionBundle`, `BaseBundle`.
42
- - Test files, `__tests__/` and `node_modules/` (`DEFAULT_EXCLUDE`) are never
43
- scanned, whatever is compiled; neither are files `tic build` generated. With
44
- file naming conventions, a narrow `include` is the speed lever — nothing else
45
- is read.
46
- - `compilerOptions.classes: { export, decorators, name, excludeClasses, excludeName }`
47
- 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.glob`
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.glob` 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, both shaped `{ glob, exclude }`, and both taking
34
+ a string or a list as a shortcut for their `glob`. `glob: { glob, exclude }`
35
+ decides by path which files are read and parsed at all (`glob: ./src` /
36
+ `glob: [./a, ./b]` = `glob.glob`). `glob.glob` (required: one folder or a
37
+ non-empty list) names the folders scanned, always with their sub-folders —
38
+ relative to the config file (`./src/services`) or tsconfig aliases
39
+ (`@app/services`); there is no `{ path, recursive }` form. A file in them is parsed unless it matches one
40
+ of `exclude` (one glob or a list; default: test files, `__tests__/`,
41
+ `node_modules/`; a non-empty value replaces it, and the build warns if it drops
42
+ a default). There is no `include`. Files `tic build` generated are never input.
43
+ - `className: { export, decorators, glob, exclude }` (or `className: '*Service'`
44
+ / `['*Service', '*Repository']` = `className.glob`, default `export` and
45
+ `decorators`) then picks classes of the parsed files; default is every exported,
48
46
  non-abstract class decorated with `@register` (`decorators` defaults to
49
- `["register"]`; `[]` requires none); positive criteria must all hold and then `excludeClasses` (exact
50
- names) and `excludeName` (glob) drop classes.
47
+ `["register"]`; `[]` requires none); positive criteria must all hold — `glob` is one glob
48
+ or a list the class name must match one of — and then `exclude` (one glob or a
49
+ list; a plain class name matches only itself) drops classes.
51
50
  Decorators match by name, so composed ones must be listed. The build warns when
52
51
  two selected classes pass the same plain-identifier first decorator argument (a
53
52
  same-token heuristic), unless a decorator they share has differing arguments
54
53
  (scope-gated, e.g. `@perPage('a')` vs `@perPage('b')`).
55
54
  - Never edit a `*.bundle.ts` by hand — change the classes or the config and
56
55
  rerun `tic build`.
57
- - Unknown config fields are errors, except inside `compilerOptions`, where
58
- TypeScript reports unknown compiler options.
59
- - Programmatic API: `build({ config, cwd, check })` (one config, one output),
60
- `findConfigFiles(dir)`, `run(argv, io)`, `loadConfig(file)`; every error the
56
+ - Unknown config fields are errors, at every level.
57
+ - Programmatic API: `build({ config, cwd })` or `build({ text, cwd })` returns
58
+ `{ content, bundle, registrations, warnings }` and writes nothing;
59
+ `loadConfig(file)`, `run(argv, io)` (`io.stdin` backs piped configs); every error the
61
60
  bundler raises on purpose is a `TicError` with a stable `code`
62
- (`TicConfigError` = `INVALID_CONFIG`, `UsageError`,
63
- `StaleBundlesError`, …).
61
+ (`TicConfigError` = `INVALID_CONFIG`, `NamespaceNotFoundError`, `UsageError`,
62
+ …).
64
63
  - Internals follow `release-monorepo-semantically` (ADR 0023): `createContainer(io)`
65
64
  in `lib/app.ts` is the composition root; `Application` resolves the controller
66
65
  registered under the command name (`@register('build')`) and runs its
67
- `@onDefault(invoke)` action, which parses its own options with
66
+ `@onDefault(invoke)` action (a word in the action position that names no
67
+ declared action is the default action's argument: `tic build <config>`), which parses its own options with
68
68
  `@inject(pipe(commandArgs, parseOptions(spec), validate(SCHEMA)))`. Work lives in
69
69
  services behind tokens (`IBundleBuilderKey`, `ITicConfigServiceKey`, …); pure
70
70
  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,182 +30,217 @@ 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
56
+
57
+ # a config built on the fly
58
+ printf 'name: app\nglob: "@app/services"\n' | tic build > src/di/app.bundle.ts
59
+ jq '.name = "admin"' app.bundle.json | tic build > src/di/admin.bundle.ts
52
60
 
53
- const container = new Container().useModule(new BaseBundle());
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": ["@app/services", "./src/infra"]
70
84
  }
71
85
  ```
72
86
 
73
- or, in YAML, `<name>.bundle.yaml` / `<name>.bundle.yml` — same fields, same schema:
74
-
75
87
  ```yaml
76
88
  # 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]
89
+ glob: ['@app/services', ./src/infra]
83
90
  ```
84
91
 
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.
87
-
88
- Every field is optional; `{}` is the zero-config bundle. The tsconfig fields:
89
-
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 |
92
+ | Field | Default | Meaning |
93
+ | ----------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
94
+ | `glob` | — (required) | Which files are parsed — see [Selecting files](#selecting-files) |
95
+ | `className` | exported classes with `@register` | Which classes of a parsed file are registered — see [Selecting classes](#selecting-classes) |
96
+ | `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 |
97
+ | `tsconfig` | `./tsconfig.json` | Source of the `paths` aliases every import is written through, and of the import extension — never of files |
98
+ | `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
96
99
 
97
- The bundler's own fields, at the top level:
100
+ An unknown field is an error, so a misspelled or removed option never goes
101
+ unnoticed.
98
102
 
99
- | Field | Default | Meaning |
100
- | --------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
101
- | `baseUrl` | the config file's directory | The base `include`, `exclude` and `output` resolve against, so they need not repeat a shared prefix such as `src`; also passed to TypeScript as `compilerOptions.baseUrl` |
102
- | `output` | `<root>/<name>.bundle.ts` | The bundle file; `<root>` as in [Zero config](#zero-config) |
103
- | `name` | the config's stem, else `base` | The bundle's name (`production.bundle.json` → `production`): names the default output and the class, `ProductionBundle` |
104
-
105
- and under `compilerOptions`:
106
-
107
- | Option | Default | Meaning |
108
- | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------- |
109
- | `classes` | exported classes with `@register` | Which classes of a scanned file are registered — see [Selecting classes](#selecting-classes) |
110
- | `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
103
+ ## Several bundles
111
104
 
112
- An unknown top-level field is an error, so a misspelled or removed option never
113
- goes unnoticed. Every other compiler option is handed to TypeScript, which
114
- reports an unknown one (`Unknown compiler option 'pathz'. Did you mean 'paths'?`).
105
+ Need more than one bundle — per environment, per app? Keep a config per bundle
106
+ and build each one:
115
107
 
116
- `baseUrl` lets a config read in source terms instead of from the package root:
108
+ ```bash
109
+ for env in prod dev test; do
110
+ tic build "$env.bundle.yml" > "src/di/$env.bundle.ts"
111
+ done
112
+ ```
117
113
 
118
114
  ```json
119
115
  {
120
- "baseUrl": "src",
121
- "output": ".generated/container.bundle.ts",
122
- "exclude": [".generated/**", "db/testing/**"]
116
+ "scripts": {
117
+ "bundle": "tic build app.bundle.json > src/di/app.bundle.ts",
118
+ "bundle:check": "tic build app.bundle.json | diff - src/di/app.bundle.ts"
119
+ }
123
120
  }
124
121
  ```
125
122
 
126
- writes `src/.generated/container.bundle.ts` from everything under `src/` except
127
- those two folders — with `baseUrl` set, an omitted `include` scans the whole
128
- `baseUrl` folder. `extends` still resolves against the config file.
123
+ Each config selects its own files and classes:
129
124
 
130
- Generated imports read like the tsconfig you compile with: a file covered by a
131
- `paths` alias is imported in that alias form, a file under `baseUrl` in
132
- `baseUrl` form (`services/Logger`), and anything else relative to the bundle.
125
+ ```yaml
126
+ # prod.bundle.yml: services + adapters, minus dev-only *.dev.ts files
127
+ glob:
128
+ glob: ['@app/services', ./src/infra]
129
+ exclude:
130
+ - '**/*.dev.ts'
131
+ - '**/*.spec.ts'
132
+ - '**/*.test.ts'
133
+ - '**/*.spec.tsx'
134
+ - '**/*.test.tsx'
135
+ - '**/__tests__/**'
136
+ - '**/node_modules/**'
137
+ ```
133
138
 
134
- ## Several bundles
139
+ ```yaml
140
+ # dev.bundle.yml: the *.dev.ts logger replaces the production one, dropped by class name
141
+ glob: ['@app/services', ./src/infra]
142
+ className:
143
+ exclude: JsonLogger
144
+ ```
135
145
 
136
- Need more than one bundle — per environment, per app? Add a config per bundle.
137
- `tic build` builds every `*.bundle.json`, `*.bundle.yaml` and `*.bundle.yml` at
138
- the package root:
146
+ A saved bundle is never registered by another build, even when it sits in a
147
+ folder that build scans.
139
148
 
140
- ```text
141
- production.bundle.json → src/di/production.bundle.ts
142
- development.bundle.json → src/di/development.bundle.ts
143
- test.bundle.json → src/di/test.bundle.ts
144
- ```
149
+ ## Selection in two stages
150
+
151
+ A bundle picks its registrations in two stages:
145
152
 
146
- Each one can extend its own tsconfig and select its own files:
153
+ 1. **`glob`** — decided by path alone, before anything is read. Only files that
154
+ pass are parsed.
155
+ 2. **`className`** — decided per class, on the files stage 1 let through.
147
156
 
148
- ```jsonc
149
- // production.bundle.json
157
+ Parsing is where the time goes, so point `glob` at the folders that hold your
158
+ registrations, and `exclude` what they should not contribute:
159
+
160
+ ```json
150
161
  {
151
- "extends": "./tsconfig.production.json",
152
- "exclude": ["src/**/*.development.ts"]
162
+ "glob": {
163
+ "glob": "@app/services",
164
+ "exclude": ["**/*.spec.ts", "**/*.test.ts", "**/__tests__/**", "**/node_modules/**", "**/legacy/**"]
165
+ },
166
+ "className": { "decorators": ["register"] }
153
167
  }
154
168
  ```
155
169
 
156
- It writes `src/production.bundle.ts`, exporting `ProductionBundle`.
170
+ ## Selecting files
157
171
 
158
- A bundle generated by one config is never registered by another, even when it
159
- sits in a folder the other scans.
172
+ `glob` takes the same shape as [`className`](#selecting-classes):
173
+ `{ glob, exclude }`. `glob.glob` names the folders a bundle's files come from,
174
+ and the only ones: what your tsconfig compiles plays no part. It is one folder
175
+ or a list, each relative to the config file (`./src/services`) or a tsconfig
176
+ `paths` alias (`@app/services`, for a `"@app/*": ["./src/*"]` alias), and
177
+ always scanned with its sub-folders.
160
178
 
161
- ## Selecting files
179
+ When the default `exclude` is all you need, `glob` itself can be the folders —
180
+ a string or a list is a shortcut for `glob.glob`:
181
+
182
+ ```yaml
183
+ glob: ./src/services # = glob: { glob: ./src/services }
184
+ ```
185
+
186
+ ```yaml
187
+ glob: ['@app/services', ./src/infra] # = glob: { glob: ['@app/services', ./src/infra] }
188
+ ```
189
+
190
+ ```yaml
191
+ glob: # the object form, for a custom exclude
192
+ glob: ['@app/services', ./src/infra]
193
+ exclude: '**/*.dev.ts'
194
+ ```
162
195
 
163
- The files a bundle scans are the files its config compiles, as `tsc` would see
164
- them: the extended tsconfig's `files` / `include` / `exclude` (following its own
165
- `extends`), with this config's `include` and `exclude` replacing the parent's —
166
- exactly as in a child tsconfig. Globs are relative to `baseUrl`, defaulting to
167
- the config file's directory.
196
+ Within them, a file is parsed unless it matches **one of** `exclude`. Globs are relative to the config file and `/`-separated; `**`
197
+ spans folders.
168
198
 
169
- Test files (`*.spec.ts`, `*.test.ts`, their `.tsx` twins and `__tests__/`) and
170
- `node_modules/` are never scanned, whatever the tsconfig compiles, since test
171
- classes usually carry the same `@register` decorators as production ones. Files
172
- `tic build` generated are never input either.
199
+ | Rule | Default | Meaning |
200
+ | --------- | ----------------------------------------- | ------------------------------------------------------------------------------- |
201
+ | `glob` | — (required) | Folder, or folders, to scan |
202
+ | `exclude` | test files, `__tests__/`, `node_modules/` | A glob, or a list of them, of files never read; replaces the default when given |
173
203
 
174
- Parsing is where the time goes, so when your project names files by convention
175
- (`user.service.ts`, `user.repository.ts`), say so in `include` and the bundler
176
- never reads anything else:
204
+ A non-empty `exclude` replaces the defaults, so restate the ones you still want:
177
205
 
178
206
  ```json
179
- {
180
- "output": "src/di/app.bundle.ts",
181
- "include": ["src/**/*.service.ts", "src/**/*.repository.ts"],
182
- "exclude": ["src/legacy/**"]
207
+ "glob": {
208
+ "exclude": [
209
+ "**/*.spec.ts",
210
+ "**/*.test.ts",
211
+ "**/*.spec.tsx",
212
+ "**/*.test.tsx",
213
+ "**/__tests__/**",
214
+ "**/node_modules/**",
215
+ "frontend/api/generated/**"
216
+ ]
183
217
  }
184
218
  ```
185
219
 
220
+ If it omits a default glob the build warns, since test classes usually carry
221
+ the same `@register` decorators as production ones. `"exclude": []` deliberately
222
+ parses everything, including tests.
223
+
186
224
  ## Selecting classes
187
225
 
188
- By default every exported, non-abstract class of a scanned file that carries
189
- `@register` is registered. `compilerOptions.classes` changes that; a class must
190
- meet every criterion that is set:
226
+ By default every exported, non-abstract class of a parsed file that carries
227
+ `@register` is registered. `className` changes that; a class must meet every
228
+ criterion that is set:
191
229
 
192
230
  ```json
193
- "compilerOptions": {
194
- "classes": {
195
- "export": "named",
196
- "decorators": ["register", "repository"],
197
- "name": "*Service"
198
- }
231
+ "className": {
232
+ "export": "named",
233
+ "decorators": ["register", "repository"],
234
+ "glob": "*Service"
199
235
  }
200
236
  ```
201
237
 
202
- | Criterion | Default | Meaning |
203
- | ------------------ | ------- | ----------------------------------------------------------------------------------------------------------- |
204
- | `export` | `any` | `any`, `named` or `default` — which exports count |
205
- | `decorators` | `["register"]` | The class must carry one of these, by name — also renamed imports and `@ioc.register()`; list composed ones. `[]` requires none |
206
- | `name` | — | Glob the class name must match, e.g. `"*Service"`; an anonymous default export is named after its file (`user-service.ts` → `UserService`) |
207
- | `excludeClasses` | — | Class names to drop, e.g. `["MockDashboardRepository"]` |
208
- | `excludeName` | — | Glob the class name must **not** match, e.g. `"*Mock"` |
238
+ | Criterion | Default | Meaning |
239
+ | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
240
+ | `export` | `any` | `any`, `named` or `default` — which exports count |
241
+ | `decorators` | `["register"]` | The class must carry one of these, by name — also renamed imports and `@ioc.register()`; list composed ones. `[]` requires none |
242
+ | `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`) |
243
+ | `exclude` | — | A glob, or a list of them, the class name must match **none** of: `"*Mock"`, `["MockDashboardRepository", "*Fake"]` |
209
244
 
210
245
  Abstract and non-exported classes are never registered.
211
246
 
@@ -213,20 +248,32 @@ Abstract and non-exported classes are never registered.
213
248
 
214
249
  When a real implementation and a test-only stand-in are both decorated and both
215
250
  bind the same token, registration is last-wins and the stand-in silently wins.
216
- `excludeClasses` and `excludeName` drop a class right in the config:
251
+ `exclude` drops it right in the config. A plain class name is a glob that
252
+ matches only itself, so one list drops a class by name and a family by pattern:
217
253
 
218
254
  ```json
219
- "compilerOptions": {
220
- "classes": {
221
- "decorators": ["repository", "service"],
222
- "excludeClasses": ["MockDashboardRepository"],
223
- "excludeName": "*Fake"
224
- }
255
+ "className": {
256
+ "decorators": ["repository", "service"],
257
+ "exclude": ["MockDashboardRepository", "*Fake"]
225
258
  }
226
259
  ```
227
260
 
228
- Both apply after every other criterion, so they can be combined with
229
- `decorators` and `name` freely.
261
+ `exclude` applies after every other criterion, so it combines with
262
+ `decorators` and `glob` freely.
263
+
264
+ ### The `className` shortcut
265
+
266
+ When the default `export` and `decorators` are all you need, `className`
267
+ itself can be the class-name globs — a string or a list is a shortcut for
268
+ `className.glob`, just as on `glob`:
269
+
270
+ ```yaml
271
+ className: '*Service' # = className: { glob: '*Service' }
272
+ ```
273
+
274
+ ```yaml
275
+ className: ['*Service', '*Repository'] # = className: { glob: ['*Service', '*Repository'] }
276
+ ```
230
277
 
231
278
  As a safety net, the build warns when two selected classes pass the same
232
279
  plain-identifier first argument to a decorator — usually the binding token, as in
@@ -236,7 +283,7 @@ are not resolved), so it can only be a heuristic:
236
283
  ```text
237
284
  tic: warning: app.bundle.json: decorator token "IDashboardRepositoryToken" is passed by
238
285
  HttpDashboardRepository, MockDashboardRepository; registration is last-wins,
239
- exclude one with compilerOptions.classes.excludeClasses
286
+ exclude one with className.exclude
240
287
  ```
241
288
 
242
289
  Classes that are scope-gated are not last-wins, so they do not warn: when the
@@ -244,27 +291,19 @@ colliding classes share a decorator called with different arguments, e.g.
244
291
  `@perPage('stations')` vs `@perPage('sessions')`, each scope registers its own
245
292
  class and nothing is lost.
246
293
 
247
- ## Build
248
-
249
- ```bash
250
- tic build # every *.bundle.{json,yaml,yml} of this package, else its tsconfig.json
251
- tic build -c app.bundle.json -c admin/admin.bundle.json # only these
252
- tic build --check # CI: write nothing, exit 1 if a bundle is out of date
253
- ```
294
+ ## Output
254
295
 
255
296
  The output is plain TypeScript over the public `ts-ioc-container` API:
256
297
 
257
298
  ```ts
258
299
  // src/di/app.bundle.ts
259
- // Generated by `tic build` from ../../app.bundle.json. Do not edit by hand.
300
+ // Generated by `tic build`. Do not edit by hand.
301
+ // Paths: @app/services, @app/infra
260
302
  import { type IContainer, type IContainerModule, type IRegistration, Registration } from 'ts-ioc-container';
261
303
  import { MemoryLogger } from '@app/infra/logging/MemoryLogger';
262
304
  import { Greeter } from '@app/services/Greeter';
263
305
 
264
- export const registrations: IRegistration[] = [
265
- Registration.fromClass(MemoryLogger),
266
- Registration.fromClass(Greeter),
267
- ];
306
+ export const registrations: IRegistration[] = [Registration.fromClass(MemoryLogger), Registration.fromClass(Greeter)];
268
307
 
269
308
  export class AppBundle implements IContainerModule {
270
309
  applyTo(container: IContainer): void {
@@ -278,8 +317,24 @@ const container = new Container().useModule(new AppBundle());
278
317
  ```
279
318
 
280
319
  Treat `*.bundle.ts` as build output: add it to `.prettierignore` (and any
281
- other formatter's ignore list), since `--check` compares files byte for byte.
320
+ other formatter's ignore list) unless you format it in the pipe, so a
321
+ `tic build … | diff - …` check compares like with like.
282
322
 
283
323
  Each class keeps its own `@register(...)` config (key, scope, singleton, …);
284
324
  an undecorated class is bound by its class name. Discovery is syntactic — no
285
325
  type checker runs.
326
+
327
+ ## Programmatic API
328
+
329
+ `build()` returns the bundle and writes nothing, like the CLI:
330
+
331
+ ```ts
332
+ import { writeFileSync } from 'node:fs';
333
+ import { build } from '@ts-ioc-container/bundler';
334
+
335
+ const { content, warnings } = build({ config: 'app.bundle.json' });
336
+ writeFileSync('src/di/app.bundle.ts', content);
337
+
338
+ // or config text, as `tic build` reads from stdin
339
+ build({ text: 'name: app\nglob:\n glob: ["@app/services"]\n' });
340
+ ```
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) {