@ts-ioc-container/bundler 5.1.0 → 7.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 +31 -17
- package/README.md +96 -58
- package/cjm/app.js +2 -2
- package/cjm/cli/usage.js +6 -4
- package/cjm/features/build/BuildConfig.js +56 -35
- package/cjm/features/build/BuildController.js +11 -6
- package/cjm/features/build/domain/emit.js +2 -2
- package/cjm/features/build/domain/tokenCollisions.js +1 -1
- package/cjm/features/build/services/BundleBuilder.js +5 -5
- package/cjm/features/build/services/TicConfigService.js +16 -5
- package/cjm/index.js +4 -2
- package/cjm/schema/ticConfigSchema.js +2 -2
- package/package.json +2 -2
- package/tic.schema.json +46 -70
- package/tpl/index.cjs +11 -20
- package/typings/app.d.ts +5 -4
- package/typings/cli/usage.d.ts.map +1 -1
- package/typings/features/build/BuildConfig.d.ts +65 -22
- package/typings/features/build/BuildConfig.d.ts.map +1 -1
- package/typings/features/build/BuildController.d.ts +14 -9
- package/typings/features/build/BuildController.d.ts.map +1 -1
- package/typings/features/build/domain/emit.d.ts +1 -1
- package/typings/features/build/domain/emit.d.ts.map +1 -1
- package/typings/features/build/services/BundleBuilder.d.ts +9 -6
- package/typings/features/build/services/BundleBuilder.d.ts.map +1 -1
- package/typings/features/build/services/TicConfigService.d.ts +11 -5
- package/typings/features/build/services/TicConfigService.d.ts.map +1 -1
- package/typings/index.d.ts +1 -1
- package/typings/schema/ticConfigSchema.d.ts.map +1 -1
package/AGENTS.md
CHANGED
|
@@ -7,10 +7,11 @@ 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): `tic build [<config
|
|
11
|
-
file `<config>` (JSON for `.json`, YAML for `.yaml` / `.yml`) —
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
same program): `tic build [<config>...]` is a Unix filter. It reads the config
|
|
11
|
+
file `<config>` (JSON for `.json`, YAML for `.yaml` / `.yml`) — several are
|
|
12
|
+
merged left to right into one bundle — or, without one or with `-`, config
|
|
13
|
+
text from stdin (parsed as YAML, which covers JSON; never merged) — and
|
|
14
|
+
prints the bundle to **stdout**; it writes nothing. Saving is the shell's
|
|
14
15
|
job: `tic build app.bundle.json > src/di/app.bundle.ts`,
|
|
15
16
|
`cat dev.bundle.json | tic build > src/di/dev.bundle.ts`; CI checks with
|
|
16
17
|
`tic build app.bundle.json | diff - src/di/app.bundle.ts`. Warnings and errors
|
|
@@ -19,21 +20,22 @@ IContainerModule`, applied with `container.useModule(new AppBundle())`.
|
|
|
19
20
|
- Every import in a bundle is written through a tsconfig `paths` alias, so the
|
|
20
21
|
bundle works wherever it is saved; a selected class no alias covers fails the
|
|
21
22
|
build naming its file. There are no relative imports.
|
|
22
|
-
- Config fields (schema: `tic.schema.json`), flat: `
|
|
23
|
-
required; `
|
|
23
|
+
- Config fields (schema: `tic.schema.json`), flat: `paths` with `paths.include`
|
|
24
|
+
required; `match`, `name`, `tsconfig`, `importExtension` optional.
|
|
24
25
|
Relative paths resolve against the config file; for piped text, against the
|
|
25
26
|
working directory. `name` defaults to the file's stem (`prod.bundle.json` →
|
|
26
27
|
`prod`) and is required for piped text or a file named otherwise; the class
|
|
27
28
|
is `toClassName(name)` — `ProdBundle`.
|
|
28
29
|
- A config does **not** extend its tsconfig and never takes files from it. The
|
|
29
30
|
`tsconfig` field (default `./tsconfig.json`, may be absent; one named
|
|
30
|
-
explicitly must exist) only supplies `paths` aliases — which `
|
|
31
|
+
explicitly must exist) only supplies `paths` aliases — which `paths.include` may
|
|
31
32
|
name and generated imports are written in (most specific alias, else a
|
|
32
33
|
relative path) — and the module resolution that sets the import extension.
|
|
33
|
-
- Selection has two stages,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
- Selection has two stages, `paths: { include, exclude }` and
|
|
35
|
+
`match: { ..., glob, exclude }`. `paths` is always an object (a string
|
|
36
|
+
or a list in its place is an error); `match` also takes a string or a
|
|
37
|
+
list as a shortcut for `match.glob`. `paths` decides by path which files
|
|
38
|
+
are read and parsed at all. `paths.include` (required: one folder or a
|
|
37
39
|
non-empty list) names the folders scanned, always with their sub-folders —
|
|
38
40
|
relative to the config file (`./src/services`) or tsconfig aliases
|
|
39
41
|
(`@app/services`); there is no `{ path, recursive }` form. A file in them is parsed unless it matches one
|
|
@@ -41,9 +43,9 @@ IContainerModule`, applied with `container.useModule(new AppBundle())`.
|
|
|
41
43
|
with a tsconfig alias — `@generated/**`, or `@generated` for the whole folder;
|
|
42
44
|
catch-all `"*"` aliases are skipped; default: test files, `__tests__/`,
|
|
43
45
|
`node_modules/`; a non-empty value replaces it, and the build warns if it drops
|
|
44
|
-
a default).
|
|
45
|
-
- `
|
|
46
|
-
/ `['*Service', '*Repository']` = `
|
|
46
|
+
a default). Files `tic build` generated are never input.
|
|
47
|
+
- `match: { export, decorators, glob, exclude }` (or `match: '*Service'`
|
|
48
|
+
/ `['*Service', '*Repository']` = `match.glob`, default `export` and
|
|
47
49
|
`decorators`) then picks classes of the parsed files; default is every exported,
|
|
48
50
|
non-abstract class decorated with `@register` (`decorators` defaults to
|
|
49
51
|
`["register"]`; `[]` requires none); positive criteria must all hold — `glob` is one glob
|
|
@@ -53,12 +55,23 @@ IContainerModule`, applied with `container.useModule(new AppBundle())`.
|
|
|
53
55
|
two selected classes pass the same plain-identifier first decorator argument (a
|
|
54
56
|
same-token heuristic), unless a decorator they share has differing arguments
|
|
55
57
|
(scope-gated, e.g. `@perPage('a')` vs `@perPage('b')`).
|
|
58
|
+
- Several configs merge like webpack-merge (`mergeConfigs`):
|
|
59
|
+
`tic build common.bundle.yml prod.bundle.yml > src/di/prod.bundle.ts`.
|
|
60
|
+
Objects merge field by field; the list fields `paths.include`,
|
|
61
|
+
`paths.exclude`, `match.glob`, `match.exclude` and `match.decorators`
|
|
62
|
+
concatenate without duplicates (a later config only adds); any other value
|
|
63
|
+
set later replaces an earlier one; the `match` shorthand merges as
|
|
64
|
+
`match.glob`. Each file is validated alone (`BUNDLE_CONFIG_FILE_SCHEMA`:
|
|
65
|
+
`paths` optional; errors name the file), the merged result as one config
|
|
66
|
+
(`BUNDLE_CONFIG_SCHEMA`). Defaults apply to the merged config only. The last
|
|
67
|
+
file names the bundle unless one sets `name`; all must sit in one folder.
|
|
56
68
|
- Never edit a `*.bundle.ts` by hand — change the classes or the config and
|
|
57
69
|
rerun `tic build`.
|
|
58
70
|
- Unknown config fields are errors, at every level.
|
|
59
|
-
- Programmatic API: `build({ config, cwd })`
|
|
71
|
+
- Programmatic API: `build({ config, cwd })` (`config`: a file or a list merged
|
|
72
|
+
left to right) or `build({ text, cwd })` returns
|
|
60
73
|
`{ content, bundle, registrations, warnings }` and writes nothing;
|
|
61
|
-
`loadConfig(file)`, `run(argv, io)` (`io.stdin` backs piped configs); every error the
|
|
74
|
+
`loadConfig(file | files)`, `run(argv, io)` (`io.stdin` backs piped configs); every error the
|
|
62
75
|
bundler raises on purpose is a `TicError` with a stable `code`
|
|
63
76
|
(`TicConfigError` = `INVALID_CONFIG`, `NamespaceNotFoundError`, `UsageError`,
|
|
64
77
|
…).
|
|
@@ -71,4 +84,5 @@ IContainerModule`, applied with `container.useModule(new AppBundle())`.
|
|
|
71
84
|
services behind tokens (`IBundleBuilderKey`, `ITicConfigServiceKey`, …); pure
|
|
72
85
|
logic in `lib/features/build/domain/`. A new command is a controller plus a module.
|
|
73
86
|
- The config schema is `BUNDLE_CONFIG_SCHEMA` (zod); `tic.schema.json` is generated
|
|
74
|
-
from
|
|
87
|
+
from its per-file variant `BUNDLE_CONFIG_FILE_SCHEMA` with `pnpm run generate:schema`
|
|
88
|
+
— never edit it by hand.
|
package/README.md
CHANGED
|
@@ -33,11 +33,12 @@ below use the shortcut.
|
|
|
33
33
|
## Build
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
tic build [<config
|
|
36
|
+
tic build [<config>...]
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
`tic build` prints the bundle to **stdout** and writes nothing. `<config>` is a
|
|
40
|
-
config file — JSON for `.json`, YAML for `.yaml` / `.yml`.
|
|
40
|
+
config file — JSON for `.json`, YAML for `.yaml` / `.yml`. Several are
|
|
41
|
+
[merged](#merging-configs) left to right into one bundle. Without one (or with
|
|
41
42
|
`-`) the config is read from **stdin**. Warnings and errors go to stderr, so
|
|
42
43
|
they never end up in the bundle.
|
|
43
44
|
|
|
@@ -46,6 +47,9 @@ they never end up in the bundle.
|
|
|
46
47
|
tic build app.bundle.json > src/di/app.bundle.ts
|
|
47
48
|
tic build prod.bundle.yml > src/di/prod.bundle.ts
|
|
48
49
|
|
|
50
|
+
# several configs merged into one bundle, like webpack-merge
|
|
51
|
+
tic build common.bundle.yml prod.bundle.yml > src/di/prod.bundle.ts
|
|
52
|
+
|
|
49
53
|
# the config piped in — it has no file name, so it sets `name` ("name": "dev")
|
|
50
54
|
cat dev.bundle.json | tic build > src/di/dev.bundle.ts
|
|
51
55
|
tic build < dev.bundle.yml > src/di/dev.bundle.ts
|
|
@@ -55,7 +59,7 @@ tic build app.bundle.json | npx prettier --stdin-filepath app.bundle.ts > src/di
|
|
|
55
59
|
cat dev.bundle.json | tic build | tee src/di/dev.bundle.ts
|
|
56
60
|
|
|
57
61
|
# a config built on the fly
|
|
58
|
-
printf 'name: app\
|
|
62
|
+
printf 'name: app\npaths:\n include: "@app/services"\n' | tic build > src/di/app.bundle.ts
|
|
59
63
|
jq '.name = "admin"' app.bundle.json | tic build > src/di/admin.bundle.ts
|
|
60
64
|
|
|
61
65
|
# CI: is the saved bundle current?
|
|
@@ -69,8 +73,9 @@ covers is an error naming its file; add a `paths` entry for its folder.
|
|
|
69
73
|
|
|
70
74
|
Relative paths in a config file resolve against the file; in a piped config,
|
|
71
75
|
against the working directory. The bundle is named after the config file —
|
|
72
|
-
`prod.bundle.json` exports `ProdBundle
|
|
73
|
-
piped config, or a file not named
|
|
76
|
+
`prod.bundle.json` exports `ProdBundle`; of several merged, after the last —
|
|
77
|
+
unless the config sets `name`, which a piped config, or a file not named
|
|
78
|
+
`<name>.bundle.json`, must.
|
|
74
79
|
|
|
75
80
|
## Configure
|
|
76
81
|
|
|
@@ -80,19 +85,22 @@ registers. Point your editor at the schema:
|
|
|
80
85
|
```json
|
|
81
86
|
{
|
|
82
87
|
"$schema": "./node_modules/@ts-ioc-container/bundler/tic.schema.json",
|
|
83
|
-
"
|
|
88
|
+
"paths": { "include": ["@app/services", "./src/infra"] }
|
|
84
89
|
}
|
|
85
90
|
```
|
|
86
91
|
|
|
87
92
|
```yaml
|
|
88
93
|
# yaml-language-server: $schema=./node_modules/@ts-ioc-container/bundler/tic.schema.json
|
|
89
|
-
|
|
94
|
+
paths:
|
|
95
|
+
include:
|
|
96
|
+
- '@app/services'
|
|
97
|
+
- ./src/infra
|
|
90
98
|
```
|
|
91
99
|
|
|
92
100
|
| Field | Default | Meaning |
|
|
93
101
|
| ----------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
102
|
+
| `paths` | — (required) | Which files are parsed — see [Selecting files](#selecting-files). Merged configs need it in one of them |
|
|
103
|
+
| `match` | exported classes with `@register` | Which classes of a parsed file are registered — see [Selecting classes](#selecting-classes) |
|
|
96
104
|
| `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
105
|
| `tsconfig` | `./tsconfig.json` | Source of the `paths` aliases every import is written through, and of the import extension — never of files |
|
|
98
106
|
| `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
|
|
@@ -102,30 +110,38 @@ unnoticed.
|
|
|
102
110
|
|
|
103
111
|
## Several bundles
|
|
104
112
|
|
|
105
|
-
Need more than one bundle — per environment, per app? Keep
|
|
106
|
-
and build each
|
|
113
|
+
Need more than one bundle — per environment, per app? Keep what they share in
|
|
114
|
+
one config, what each adds in another, and build each environment from both:
|
|
107
115
|
|
|
108
116
|
```bash
|
|
109
117
|
for env in prod dev test; do
|
|
110
|
-
tic build "$env.bundle.yml" > "src/di/$env.bundle.ts"
|
|
118
|
+
tic build common.bundle.yml "$env.bundle.yml" > "src/di/$env.bundle.ts"
|
|
111
119
|
done
|
|
112
120
|
```
|
|
113
121
|
|
|
114
122
|
```json
|
|
115
123
|
{
|
|
116
124
|
"scripts": {
|
|
117
|
-
"bundle": "tic build
|
|
118
|
-
"bundle:check": "tic build
|
|
125
|
+
"bundle": "tic build common.bundle.yml prod.bundle.yml > src/di/prod.bundle.ts",
|
|
126
|
+
"bundle:check": "tic build common.bundle.yml prod.bundle.yml | diff - src/di/prod.bundle.ts"
|
|
119
127
|
}
|
|
120
128
|
}
|
|
121
129
|
```
|
|
122
130
|
|
|
123
|
-
|
|
131
|
+
```yaml
|
|
132
|
+
# common.bundle.yml: what every environment registers
|
|
133
|
+
paths:
|
|
134
|
+
include:
|
|
135
|
+
- '@app/services'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Each environment's config adds its own files and classes:
|
|
124
139
|
|
|
125
140
|
```yaml
|
|
126
|
-
# prod.bundle.yml:
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
# prod.bundle.yml: + the adapters, minus dev-only *.dev.ts files
|
|
142
|
+
paths:
|
|
143
|
+
include:
|
|
144
|
+
- ./src/infra
|
|
129
145
|
exclude:
|
|
130
146
|
- '**/*.dev.ts'
|
|
131
147
|
- '**/*.spec.ts'
|
|
@@ -137,12 +153,39 @@ glob:
|
|
|
137
153
|
```
|
|
138
154
|
|
|
139
155
|
```yaml
|
|
140
|
-
# dev.bundle.yml: the *.dev.ts logger replaces the production one, dropped by class name
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
# dev.bundle.yml: + the adapters; the *.dev.ts logger replaces the production one, dropped by class name
|
|
157
|
+
paths:
|
|
158
|
+
include:
|
|
159
|
+
- ./src/infra
|
|
160
|
+
match:
|
|
143
161
|
exclude: JsonLogger
|
|
144
162
|
```
|
|
145
163
|
|
|
164
|
+
`tic build common.bundle.yml prod.bundle.yml` then scans `@app/services` and
|
|
165
|
+
`./src/infra`, and exports `ProdBundle`.
|
|
166
|
+
|
|
167
|
+
### Merging configs
|
|
168
|
+
|
|
169
|
+
`tic build a.bundle.yml b.bundle.yml c.bundle.yml` merges the configs left to
|
|
170
|
+
right, the way webpack-merge merges webpack configs:
|
|
171
|
+
|
|
172
|
+
- objects merge field by field;
|
|
173
|
+
- list fields — `paths.include`, `paths.exclude`, `match.glob`, `match.exclude`,
|
|
174
|
+
`match.decorators` — concatenate in order, without duplicates (one folder or
|
|
175
|
+
glob counts as a list of one), so a later config can only add to them;
|
|
176
|
+
- any other value (`name`, `tsconfig`, `importExtension`, `match.export`) set
|
|
177
|
+
by a later config replaces an earlier one;
|
|
178
|
+
- the `match` shorthand merges as `match.glob`.
|
|
179
|
+
|
|
180
|
+
Each config is validated on its own — an error names the file it is in — so a
|
|
181
|
+
shared config may leave `paths` out; the merged config must have
|
|
182
|
+
`paths.include`. Defaults apply to the merged config only: a `paths.exclude` in
|
|
183
|
+
any of them replaces the default excludes, as it does in a single config, so
|
|
184
|
+
list the defaults in the shared config to keep them. The bundle is named after
|
|
185
|
+
the last config, unless one sets `name` — so leave `name` out of a shared
|
|
186
|
+
config. Merged configs must sit in one folder, which their relative paths
|
|
187
|
+
resolve against. A piped config (`-`) is never merged.
|
|
188
|
+
|
|
146
189
|
A saved bundle is never registered by another build, even when it sits in a
|
|
147
190
|
folder that build scans.
|
|
148
191
|
|
|
@@ -150,51 +193,47 @@ folder that build scans.
|
|
|
150
193
|
|
|
151
194
|
A bundle picks its registrations in two stages:
|
|
152
195
|
|
|
153
|
-
1. **`
|
|
196
|
+
1. **`paths`** — decided by path alone, before anything is read. Only files that
|
|
154
197
|
pass are parsed.
|
|
155
|
-
2. **`
|
|
198
|
+
2. **`match`** — decided per class, on the files stage 1 let through.
|
|
156
199
|
|
|
157
|
-
Parsing is where the time goes, so point `
|
|
200
|
+
Parsing is where the time goes, so point `paths` at the folders that hold your
|
|
158
201
|
registrations, and `exclude` what they should not contribute:
|
|
159
202
|
|
|
160
203
|
```json
|
|
161
204
|
{
|
|
162
|
-
"
|
|
163
|
-
"
|
|
205
|
+
"paths": {
|
|
206
|
+
"include": "@app/services",
|
|
164
207
|
"exclude": ["**/*.spec.ts", "**/*.test.ts", "**/__tests__/**", "**/node_modules/**", "**/legacy/**"]
|
|
165
208
|
},
|
|
166
|
-
"
|
|
209
|
+
"match": { "decorators": ["register"] }
|
|
167
210
|
}
|
|
168
211
|
```
|
|
169
212
|
|
|
170
213
|
## Selecting files
|
|
171
214
|
|
|
172
|
-
`
|
|
173
|
-
`{ glob, exclude }`. `glob.glob` names the folders a bundle's files come from,
|
|
215
|
+
`paths` is `{ include, exclude }`. `paths.include` names the folders a bundle's files come from,
|
|
174
216
|
and the only ones: what your tsconfig compiles plays no part. It is one folder
|
|
175
217
|
or a list, each relative to the config file (`./src/services`) or a tsconfig
|
|
176
218
|
`paths` alias (`@app/services`, for a `"@app/*": ["./src/*"]` alias), and
|
|
177
|
-
always scanned with its sub-folders.
|
|
178
|
-
|
|
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`:
|
|
219
|
+
always scanned with its sub-folders. `paths` is always an object — a folder or a
|
|
220
|
+
list in place of it is an error — and `exclude` may be left out to keep the default:
|
|
181
221
|
|
|
182
222
|
```yaml
|
|
183
|
-
|
|
223
|
+
paths:
|
|
224
|
+
include: ./src/services # one folder
|
|
184
225
|
```
|
|
185
226
|
|
|
186
227
|
```yaml
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
glob: ['@app/services', ./src/infra]
|
|
193
|
-
exclude: '**/*.dev.ts'
|
|
228
|
+
paths:
|
|
229
|
+
include: # or a list
|
|
230
|
+
- '@app/services'
|
|
231
|
+
- ./src/infra
|
|
232
|
+
exclude: '**/*.dev.ts' # a custom exclude
|
|
194
233
|
```
|
|
195
234
|
|
|
196
235
|
Within them, a file is parsed unless it matches **one of** `exclude`. Globs are relative to the config file and `/`-separated; `**`
|
|
197
|
-
spans folders. Like a folder in `
|
|
236
|
+
spans folders. Like a folder in `paths.include`, an exclude glob may start with a
|
|
198
237
|
tsconfig `paths` alias: with `"@generated/*": ["./src/.generated/*"]`,
|
|
199
238
|
`@generated/**` reads `src/.generated/**`, and `@generated` (or `@generated/`)
|
|
200
239
|
drops the whole folder. A catch-all `"*"` alias is never applied, so plain globs
|
|
@@ -202,13 +241,13 @@ such as `**/*.spec.ts` keep their meaning.
|
|
|
202
241
|
|
|
203
242
|
| Rule | Default | Meaning |
|
|
204
243
|
| --------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
205
|
-
| `
|
|
244
|
+
| `include` | — (required) | Folder, or folders, to scan |
|
|
206
245
|
| `exclude` | test files, `__tests__/`, `node_modules/` | A glob, or a list of them, of files never read; may start with a tsconfig alias; replaces the default when given |
|
|
207
246
|
|
|
208
247
|
A non-empty `exclude` replaces the defaults, so restate the ones you still want:
|
|
209
248
|
|
|
210
249
|
```json
|
|
211
|
-
"
|
|
250
|
+
"paths": {
|
|
212
251
|
"exclude": [
|
|
213
252
|
"**/*.spec.ts",
|
|
214
253
|
"**/*.test.ts",
|
|
@@ -229,11 +268,11 @@ parses everything, including tests.
|
|
|
229
268
|
## Selecting classes
|
|
230
269
|
|
|
231
270
|
By default every exported, non-abstract class of a parsed file that carries
|
|
232
|
-
`@register` is registered. `
|
|
271
|
+
`@register` is registered. `match` changes that; a class must meet every
|
|
233
272
|
criterion that is set:
|
|
234
273
|
|
|
235
274
|
```json
|
|
236
|
-
"
|
|
275
|
+
"match": {
|
|
237
276
|
"export": "named",
|
|
238
277
|
"decorators": ["register", "repository"],
|
|
239
278
|
"glob": "*Service"
|
|
@@ -257,7 +296,7 @@ bind the same token, registration is last-wins and the stand-in silently wins.
|
|
|
257
296
|
matches only itself, so one list drops a class by name and a family by pattern:
|
|
258
297
|
|
|
259
298
|
```json
|
|
260
|
-
"
|
|
299
|
+
"match": {
|
|
261
300
|
"decorators": ["repository", "service"],
|
|
262
301
|
"exclude": ["MockDashboardRepository", "*Fake"]
|
|
263
302
|
}
|
|
@@ -266,18 +305,18 @@ matches only itself, so one list drops a class by name and a family by pattern:
|
|
|
266
305
|
`exclude` applies after every other criterion, so it combines with
|
|
267
306
|
`decorators` and `glob` freely.
|
|
268
307
|
|
|
269
|
-
### The `
|
|
308
|
+
### The `match` shortcut
|
|
270
309
|
|
|
271
|
-
When the default `export` and `decorators` are all you need, `
|
|
310
|
+
When the default `export` and `decorators` are all you need, `match`
|
|
272
311
|
itself can be the class-name globs — a string or a list is a shortcut for
|
|
273
|
-
`
|
|
312
|
+
`match.glob`:
|
|
274
313
|
|
|
275
314
|
```yaml
|
|
276
|
-
|
|
315
|
+
match: '*Service' # = match: { glob: '*Service' }
|
|
277
316
|
```
|
|
278
317
|
|
|
279
318
|
```yaml
|
|
280
|
-
|
|
319
|
+
match: ['*Service', '*Repository'] # = match: { glob: ['*Service', '*Repository'] }
|
|
281
320
|
```
|
|
282
321
|
|
|
283
322
|
As a safety net, the build warns when two selected classes pass the same
|
|
@@ -288,7 +327,7 @@ are not resolved), so it can only be a heuristic:
|
|
|
288
327
|
```text
|
|
289
328
|
tic: warning: app.bundle.json: decorator token "IDashboardRepositoryToken" is passed by
|
|
290
329
|
HttpDashboardRepository, MockDashboardRepository; registration is last-wins,
|
|
291
|
-
exclude one with
|
|
330
|
+
exclude one with match.exclude
|
|
292
331
|
```
|
|
293
332
|
|
|
294
333
|
Classes that are scope-gated are not last-wins, so they do not warn: when the
|
|
@@ -304,15 +343,14 @@ The output is plain TypeScript over the public `ts-ioc-container` API:
|
|
|
304
343
|
// src/di/app.bundle.ts
|
|
305
344
|
// Generated by `tic build`. Do not edit by hand.
|
|
306
345
|
// Paths: @app/services, @app/infra
|
|
307
|
-
import { type IContainer, type IContainerModule,
|
|
346
|
+
import { type IContainer, type IContainerModule, Registration } from 'ts-ioc-container';
|
|
308
347
|
import { MemoryLogger } from '@app/infra/logging/MemoryLogger';
|
|
309
348
|
import { Greeter } from '@app/services/Greeter';
|
|
310
349
|
|
|
311
|
-
export const registrations: IRegistration[] = [Registration.fromClass(MemoryLogger), Registration.fromClass(Greeter)];
|
|
312
|
-
|
|
313
350
|
export class AppBundle implements IContainerModule {
|
|
314
351
|
applyTo(container: IContainer): void {
|
|
315
|
-
|
|
352
|
+
container.addRegistration(Registration.fromClass(MemoryLogger));
|
|
353
|
+
container.addRegistration(Registration.fromClass(Greeter));
|
|
316
354
|
}
|
|
317
355
|
}
|
|
318
356
|
```
|
|
@@ -341,5 +379,5 @@ const { content, warnings } = build({ config: 'app.bundle.json' });
|
|
|
341
379
|
writeFileSync('src/di/app.bundle.ts', content);
|
|
342
380
|
|
|
343
381
|
// or config text, as `tic build` reads from stdin
|
|
344
|
-
build({ text: 'name: app\
|
|
382
|
+
build({ text: 'name: app\npaths:\n include: ["@app/services"]\n' });
|
|
345
383
|
```
|
package/cjm/app.js
CHANGED
|
@@ -34,6 +34,6 @@ function run(argv, io = (0, CliIo_1.processIo)()) {
|
|
|
34
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
|
-
function loadConfig(
|
|
38
|
-
return withContainer((0, CliIo_1.processIo)(), (container) => TicConfigService_1.ITicConfigServiceKey.resolve(container).load(
|
|
37
|
+
function loadConfig(files) {
|
|
38
|
+
return withContainer((0, CliIo_1.processIo)(), (container) => TicConfigService_1.ITicConfigServiceKey.resolve(container).load(files));
|
|
39
39
|
}
|
package/cjm/cli/usage.js
CHANGED
|
@@ -6,10 +6,11 @@ exports.USAGE = [
|
|
|
6
6
|
' tic is a shortcut for ts-ioc-container',
|
|
7
7
|
'',
|
|
8
8
|
'Commands:',
|
|
9
|
-
' ts-ioc-container build [<config
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
' ts-ioc-container
|
|
9
|
+
' ts-ioc-container build [<config>...] print the bundle a config file (.json, .yaml, .yml) describes;',
|
|
10
|
+
' several are merged left to right into one bundle;',
|
|
11
|
+
' without one (or with -), read the config from stdin',
|
|
12
|
+
' ts-ioc-container help show this help',
|
|
13
|
+
' ts-ioc-container version show the version',
|
|
13
14
|
'',
|
|
14
15
|
'Options:',
|
|
15
16
|
' -h, --help show this help',
|
|
@@ -17,6 +18,7 @@ exports.USAGE = [
|
|
|
17
18
|
'',
|
|
18
19
|
'The bundle goes to stdout and warnings to stderr; saving it is up to you:',
|
|
19
20
|
' tic build app.bundle.json > src/di/app.bundle.ts',
|
|
21
|
+
' tic build common.bundle.yml prod.bundle.yml > src/di/prod.bundle.ts # merged left to right',
|
|
20
22
|
' cat app.bundle.yml | tic build > src/di/app.bundle.ts # config from stdin (must set name)',
|
|
21
23
|
' tic build app.bundle.json | diff - src/di/app.bundle.ts # CI: is it current?',
|
|
22
24
|
].join('\n');
|
|
@@ -3,9 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.configFormat = exports.STDIN = exports.CONFIG_FILE_SUFFIXES = exports.BUNDLE_CONFIG_SCHEMA = exports.DEFAULT_EXCLUDE = exports.DEFAULT_DECORATORS = exports.DEFAULT_TSCONFIG = void 0;
|
|
6
|
+
exports.configFormat = exports.STDIN = exports.CONFIG_FILE_SUFFIXES = exports.BUNDLE_CONFIG_FILE_SCHEMA = exports.BUNDLE_CONFIG_SCHEMA = exports.DEFAULT_EXCLUDE = exports.DEFAULT_DECORATORS = exports.DEFAULT_TSCONFIG = void 0;
|
|
7
7
|
exports.parseConfig = parseConfig;
|
|
8
8
|
exports.toClassName = toClassName;
|
|
9
|
+
exports.mergeConfigs = mergeConfigs;
|
|
9
10
|
exports.resolveConfig = resolveConfig;
|
|
10
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
12
|
const yaml_1 = require("yaml");
|
|
@@ -42,21 +43,17 @@ const globs = () => globList('a glob or a non-empty array of globs', 1);
|
|
|
42
43
|
const toList = (value) => (Array.isArray(value) ? value : [value]);
|
|
43
44
|
const toGlobs = (value) => value === undefined ? undefined : toList(value).map(glob_1.globToRegExp);
|
|
44
45
|
const FOLDERS_SCHEMA = zod_1.z.array(nonEmptyString(), expected('a non-empty array')).min(1, expected('a non-empty array'));
|
|
45
|
-
const
|
|
46
|
-
|
|
46
|
+
const PATHS_DESCRIPTION = 'Which files this bundle reads and parses — decided by path alone, before parsing. `include` names the folders the files come from; within them a file is parsed unless it matches one of `exclude`. Globs are relative to the config file.';
|
|
47
|
+
const PATHS_SCHEMA = zod_1.z
|
|
48
|
+
.strictObject({
|
|
49
|
+
include: zod_1.z
|
|
47
50
|
.union([nonEmptyString(), FOLDERS_SCHEMA], expected('a folder or a non-empty array of folders'))
|
|
48
51
|
.describe('Folder, or folders, to scan, sub-folders included: paths relative to the config file (./src/services) or tsconfig paths aliases (@app/services).'),
|
|
49
52
|
exclude: globList('a glob or an array of globs', 0)
|
|
50
53
|
.default(exports.DEFAULT_EXCLUDE)
|
|
51
54
|
.describe('Glob, or globs, of files never read, relative to the config file or starting with a tsconfig paths alias (@generated/**, or @generated for the whole folder). Replaces the default when given. A non-empty value that omits a default glob makes `tic build` warn.'),
|
|
52
|
-
}, expected('an object'))
|
|
53
|
-
|
|
54
|
-
.union([
|
|
55
|
-
nonEmptyString().describe('One folder to scan, with the default exclude: shorthand for { "glob": [...] }.'),
|
|
56
|
-
FOLDERS_SCHEMA.describe('Folders to scan, with the default exclude: shorthand for { "glob": [...] }.'),
|
|
57
|
-
GLOB_OBJECT_SCHEMA,
|
|
58
|
-
], expected('a folder, a non-empty array of folders or an object with "glob"'))
|
|
59
|
-
.describe('Which files this bundle reads and parses — decided by path alone, before parsing. `glob` names the folders the files come from; within them a file is parsed unless it matches one of `exclude`. Globs are relative to the config file. A folder, or a list of folders, is shorthand for { "glob": [...] }.');
|
|
55
|
+
}, expected('an object with "include"'))
|
|
56
|
+
.describe(PATHS_DESCRIPTION);
|
|
60
57
|
const CLASS_OBJECT_SCHEMA = zod_1.z.strictObject({
|
|
61
58
|
export: zod_1.z
|
|
62
59
|
.enum(['any', 'named', 'default'], expected('"any", "named" or "default"'))
|
|
@@ -92,14 +89,17 @@ exports.BUNDLE_CONFIG_SCHEMA = zod_1.z.strictObject({
|
|
|
92
89
|
tsconfig: nonEmptyString()
|
|
93
90
|
.optional()
|
|
94
91
|
.meta({ default: exports.DEFAULT_TSCONFIG })
|
|
95
|
-
.describe('The tsconfig whose `paths` aliases
|
|
92
|
+
.describe('The tsconfig whose `paths` aliases paths.include may name and generated imports are written in, and whose module resolution sets the import extension. It adds no files: the bundle scans paths.include alone. Relative to the config file. The default may be absent; a tsconfig named here must exist.'),
|
|
96
93
|
importExtension: zod_1.z
|
|
97
94
|
.string(expected('a string'))
|
|
98
95
|
.optional()
|
|
99
96
|
.describe('Extension of generated imports, e.g. ".js". Inferred from the tsconfig: ".js" under node16/nodenext resolution, none otherwise.'),
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
paths: PATHS_SCHEMA,
|
|
98
|
+
match: CLASSES_SCHEMA.optional(),
|
|
102
99
|
}, expected('an object'));
|
|
100
|
+
exports.BUNDLE_CONFIG_FILE_SCHEMA = exports.BUNDLE_CONFIG_SCHEMA.extend({
|
|
101
|
+
paths: PATHS_SCHEMA.partial().optional().describe(`${PATHS_DESCRIPTION} Required in the merged config.`),
|
|
102
|
+
});
|
|
103
103
|
function formatPath(segments) {
|
|
104
104
|
const formatted = segments
|
|
105
105
|
.map((segment) => (typeof segment === 'number' ? `[${segment}]` : `.${String(segment)}`))
|
|
@@ -121,24 +121,19 @@ function formatIssue(issue, prefix = []) {
|
|
|
121
121
|
return [`${formatPath(at)}: ${issue.message}`];
|
|
122
122
|
}
|
|
123
123
|
const formatIssues = (error) => error.issues.flatMap((issue) => formatIssue(issue)).join('; ');
|
|
124
|
-
function
|
|
125
|
-
if (typeof glob === 'string' || Array.isArray(glob))
|
|
126
|
-
return { glob, exclude: exports.DEFAULT_EXCLUDE };
|
|
127
|
-
return glob;
|
|
128
|
-
}
|
|
129
|
-
function toGlobSelector(glob) {
|
|
124
|
+
function toPathSelector(paths) {
|
|
130
125
|
return {
|
|
131
|
-
|
|
132
|
-
exclude: toList(
|
|
126
|
+
include: toList(paths.include),
|
|
127
|
+
exclude: toList(paths.exclude),
|
|
133
128
|
};
|
|
134
129
|
}
|
|
135
|
-
function toClassSelector(
|
|
136
|
-
if (
|
|
130
|
+
function toClassSelector(match) {
|
|
131
|
+
if (match === undefined)
|
|
137
132
|
return { export: 'any', decorators: exports.DEFAULT_DECORATORS };
|
|
138
|
-
if (typeof
|
|
139
|
-
return { export: 'any', decorators: exports.DEFAULT_DECORATORS, glob: toGlobs(
|
|
133
|
+
if (typeof match === 'string' || Array.isArray(match)) {
|
|
134
|
+
return { export: 'any', decorators: exports.DEFAULT_DECORATORS, glob: toGlobs(match) };
|
|
140
135
|
}
|
|
141
|
-
const { decorators, glob, exclude, ...rest } =
|
|
136
|
+
const { decorators, glob, exclude, ...rest } = match;
|
|
142
137
|
return {
|
|
143
138
|
...rest,
|
|
144
139
|
decorators: decorators.length > 0 ? decorators : undefined,
|
|
@@ -146,15 +141,15 @@ function toClassSelector(className) {
|
|
|
146
141
|
exclude: toGlobs(exclude),
|
|
147
142
|
};
|
|
148
143
|
}
|
|
149
|
-
function excludeWarnings(
|
|
150
|
-
const exclude = toList(
|
|
144
|
+
function excludeWarnings(paths) {
|
|
145
|
+
const exclude = toList(paths.exclude);
|
|
151
146
|
if (exclude.length === 0)
|
|
152
147
|
return [];
|
|
153
148
|
const missing = exports.DEFAULT_EXCLUDE.filter((glob) => !exclude.includes(glob));
|
|
154
149
|
if (missing.length === 0)
|
|
155
150
|
return [];
|
|
156
151
|
return [
|
|
157
|
-
`
|
|
152
|
+
`paths.exclude replaces the default excludes and omits ${missing.map((glob) => `"${glob}"`).join(', ')}; ` +
|
|
158
153
|
`add them back`,
|
|
159
154
|
];
|
|
160
155
|
}
|
|
@@ -180,11 +175,38 @@ function toClassName(name) {
|
|
|
180
175
|
const words = name.split(/[-_]+/).filter(Boolean);
|
|
181
176
|
return `${words.map((word) => word[0].toUpperCase() + word.slice(1)).join('')}Bundle`;
|
|
182
177
|
}
|
|
178
|
+
const LIST_FIELDS = new Set(['paths.include', 'paths.exclude', 'match.decorators', 'match.glob', 'match.exclude']);
|
|
179
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
180
|
+
function mergeInto(base, next, at = '') {
|
|
181
|
+
const merged = { ...base };
|
|
182
|
+
for (const [key, value] of Object.entries(next)) {
|
|
183
|
+
const field = `${at}${key}`;
|
|
184
|
+
const previous = merged[key];
|
|
185
|
+
if (LIST_FIELDS.has(field)) {
|
|
186
|
+
merged[key] = [
|
|
187
|
+
...new Set([...toList((previous ?? [])), ...toList(value)]),
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
merged[key] = isObject(previous) && isObject(value) ? mergeInto(previous, value, `${field}.`) : value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return merged;
|
|
195
|
+
}
|
|
196
|
+
const expandMatch = ({ match, ...rest }) => match === undefined ? rest : { ...rest, match: isObject(match) ? match : { glob: match } };
|
|
197
|
+
function mergeConfigs(configs) {
|
|
198
|
+
return configs.reduce((merged, { file, content }) => {
|
|
199
|
+
const result = exports.BUNDLE_CONFIG_FILE_SCHEMA.safeParse(content);
|
|
200
|
+
if (!result.success)
|
|
201
|
+
throw new DomainException_1.TicConfigError(`${node_path_1.default.basename(file)}: ${formatIssues(result.error)}`);
|
|
202
|
+
return mergeInto(merged, expandMatch(content));
|
|
203
|
+
}, {});
|
|
204
|
+
}
|
|
183
205
|
function resolveConfig(content, file) {
|
|
184
206
|
const result = exports.BUNDLE_CONFIG_SCHEMA.safeParse(content);
|
|
185
207
|
if (!result.success)
|
|
186
208
|
throw new DomainException_1.TicConfigError(formatIssues(result.error));
|
|
187
|
-
const { name, tsconfig, importExtension,
|
|
209
|
+
const { name, tsconfig, importExtension, paths, match } = result.data;
|
|
188
210
|
const bundleName = name ?? configStem(file);
|
|
189
211
|
if (bundleName === undefined) {
|
|
190
212
|
throw new DomainException_1.TicConfigError(node_path_1.default.basename(file) === exports.STDIN
|
|
@@ -192,7 +214,6 @@ function resolveConfig(content, file) {
|
|
|
192
214
|
: `name: required when the config file is not named <name>${exports.CONFIG_FILE_SUFFIXES.join(' / ')}`);
|
|
193
215
|
}
|
|
194
216
|
const dir = node_path_1.default.dirname(file);
|
|
195
|
-
const globObject = toGlobObject(glob);
|
|
196
217
|
return {
|
|
197
218
|
file,
|
|
198
219
|
dir,
|
|
@@ -200,8 +221,8 @@ function resolveConfig(content, file) {
|
|
|
200
221
|
bundleClassName: toClassName(bundleName),
|
|
201
222
|
tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.DEFAULT_TSCONFIG), required: tsconfig !== undefined },
|
|
202
223
|
importExtension,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
warnings: excludeWarnings(
|
|
224
|
+
paths: toPathSelector(paths),
|
|
225
|
+
match: toClassSelector(match),
|
|
226
|
+
warnings: excludeWarnings(paths),
|
|
206
227
|
};
|
|
207
228
|
}
|