@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.
- package/AGENTS.md +48 -48
- package/README.md +199 -144
- 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 +126 -77
- package/cjm/features/build/BuildController.js +36 -46
- package/cjm/features/build/domain/ImportPaths.js +31 -20
- 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 +35 -30
- 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 +132 -77
- 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 +102 -102
- 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 +16 -14
- 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,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
|
|
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
|
-
|
|
36
|
-
`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
is
|
|
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
|
|
50
|
-
|
|
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,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,182 +30,217 @@ 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
|
|
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
|
-
|
|
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": ["@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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 |
|
|
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
|
-
|
|
100
|
+
An unknown field is an error, so a misspelled or removed option never goes
|
|
101
|
+
unnoticed.
|
|
98
102
|
|
|
99
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
"
|
|
152
|
-
|
|
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
|
-
|
|
170
|
+
## Selecting files
|
|
157
171
|
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
`
|
|
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
|
-
|
|
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
|
-
"
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
189
|
-
`@register` is registered. `
|
|
190
|
-
|
|
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
|
-
"
|
|
194
|
-
"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"name": "*Service"
|
|
198
|
-
}
|
|
231
|
+
"className": {
|
|
232
|
+
"export": "named",
|
|
233
|
+
"decorators": ["register", "repository"],
|
|
234
|
+
"glob": "*Service"
|
|
199
235
|
}
|
|
200
236
|
```
|
|
201
237
|
|
|
202
|
-
| Criterion
|
|
203
|
-
|
|
|
204
|
-
| `export`
|
|
205
|
-
| `decorators`
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
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
|
-
`
|
|
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
|
-
"
|
|
220
|
-
"
|
|
221
|
-
|
|
222
|
-
"excludeClasses": ["MockDashboardRepository"],
|
|
223
|
-
"excludeName": "*Fake"
|
|
224
|
-
}
|
|
255
|
+
"className": {
|
|
256
|
+
"decorators": ["repository", "service"],
|
|
257
|
+
"exclude": ["MockDashboardRepository", "*Fake"]
|
|
225
258
|
}
|
|
226
259
|
```
|
|
227
260
|
|
|
228
|
-
|
|
229
|
-
`decorators` and `
|
|
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
|
|
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
|
-
##
|
|
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
|
|
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)
|
|
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) {
|