@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.
- package/AGENTS.md +45 -40
- package/README.md +181 -129
- package/cjm/app.js +2 -2
- package/cjm/cli/Application.js +4 -5
- package/cjm/cli/options.js +4 -2
- package/cjm/cli/usage.js +11 -8
- package/cjm/domain/CliIo.js +7 -1
- package/cjm/exceptions/DomainException.js +7 -7
- package/cjm/features/build/BuildConfig.js +108 -59
- package/cjm/features/build/BuildController.js +36 -46
- package/cjm/features/build/domain/ImportPaths.js +30 -8
- package/cjm/features/build/domain/emit.js +2 -2
- package/cjm/features/build/domain/scan.js +20 -4
- package/cjm/features/build/domain/tokenCollisions.js +1 -1
- package/cjm/features/build/domain/tsconfig.js +10 -35
- package/cjm/features/build/services/BundleBuilder.js +36 -29
- package/cjm/features/build/services/TicConfigService.js +10 -31
- package/cjm/index.js +9 -10
- package/cjm/schema/ticConfigSchema.js +5 -1
- package/package.json +2 -2
- package/tic.schema.json +117 -73
- package/tpl/index.cjs +15 -15
- package/typings/app.d.ts +7 -6
- package/typings/cli/Application.d.ts.map +1 -1
- package/typings/cli/options.d.ts +7 -1
- package/typings/cli/options.d.ts.map +1 -1
- package/typings/cli/usage.d.ts.map +1 -1
- package/typings/domain/CliIo.d.ts +2 -0
- package/typings/domain/CliIo.d.ts.map +1 -1
- package/typings/domain/GlobalConfig.d.ts +1 -1
- package/typings/domain/GlobalConfig.d.ts.map +1 -1
- package/typings/exceptions/DomainException.d.ts +4 -4
- package/typings/exceptions/DomainException.d.ts.map +1 -1
- package/typings/features/build/BuildConfig.d.ts +107 -94
- package/typings/features/build/BuildConfig.d.ts.map +1 -1
- package/typings/features/build/BuildController.d.ts +23 -17
- package/typings/features/build/BuildController.d.ts.map +1 -1
- package/typings/features/build/domain/ImportPaths.d.ts +15 -4
- package/typings/features/build/domain/ImportPaths.d.ts.map +1 -1
- package/typings/features/build/domain/emit.d.ts +4 -4
- package/typings/features/build/domain/emit.d.ts.map +1 -1
- package/typings/features/build/domain/scan.d.ts +2 -0
- package/typings/features/build/domain/scan.d.ts.map +1 -1
- package/typings/features/build/domain/tsconfig.d.ts +9 -16
- package/typings/features/build/domain/tsconfig.d.ts.map +1 -1
- package/typings/features/build/services/BundleBuilder.d.ts +25 -29
- package/typings/features/build/services/BundleBuilder.d.ts.map +1 -1
- package/typings/features/build/services/TicConfigService.d.ts +11 -15
- package/typings/features/build/services/TicConfigService.d.ts.map +1 -1
- package/typings/index.d.ts +6 -7
- package/typings/schema/ticConfigSchema.d.ts.map +1 -1
- package/cjm/features/build/domain/configFiles.js +0 -35
- package/typings/features/build/domain/configFiles.d.ts +0 -13
- 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
|
|
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)
|
|
11
|
-
YAML
|
|
12
|
-
(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`
|
|
27
|
-
`
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
scanned
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
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
|
|
42
|
-
|
|
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,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
5
|
-
class in
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
include: [
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
104
|
+
An unknown field is an error, so a misspelled or removed option never goes
|
|
105
|
+
unnoticed.
|
|
89
106
|
|
|
90
|
-
|
|
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
|
-
|
|
109
|
+
Need more than one bundle — per environment, per app? Keep a config per bundle
|
|
110
|
+
and build each one:
|
|
98
111
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
130
|
-
// production.bundle.json
|
|
166
|
+
```json
|
|
131
167
|
{
|
|
132
|
-
"
|
|
133
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
`
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
`
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
"
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
169
|
-
`@register` is registered. `
|
|
170
|
-
|
|
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
|
-
"
|
|
174
|
-
"
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"name": "*Service"
|
|
178
|
-
}
|
|
222
|
+
"className": {
|
|
223
|
+
"export": "named",
|
|
224
|
+
"decorators": ["register", "repository"],
|
|
225
|
+
"glob": "*Service"
|
|
179
226
|
}
|
|
180
227
|
```
|
|
181
228
|
|
|
182
|
-
| Criterion
|
|
183
|
-
|
|
|
184
|
-
| `export`
|
|
185
|
-
| `decorators`
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
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
|
-
`
|
|
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
|
-
"
|
|
200
|
-
"
|
|
201
|
-
|
|
202
|
-
"excludeClasses": ["MockDashboardRepository"],
|
|
203
|
-
"excludeName": "*Fake"
|
|
204
|
-
}
|
|
246
|
+
"className": {
|
|
247
|
+
"decorators": ["repository", "service"],
|
|
248
|
+
"exclude": ["MockDashboardRepository", "*Fake"]
|
|
205
249
|
}
|
|
206
250
|
```
|
|
207
251
|
|
|
208
|
-
|
|
209
|
-
`decorators` and `
|
|
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
|
|
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
|
-
##
|
|
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
|
|
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)
|
|
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) {
|
package/cjm/cli/Application.js
CHANGED
|
@@ -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
|
|
40
|
-
|
|
41
|
-
|
|
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)
|
package/cjm/cli/options.js
CHANGED
|
@@ -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
|
-
|
|
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);
|