@ts-ioc-container/bundler 2.1.0 → 3.1.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 +32 -22
- package/README.md +84 -84
- package/cjm/exceptions/DomainException.js +1 -7
- package/cjm/features/build/BuildConfig.js +48 -73
- package/cjm/features/build/domain/ImportPaths.js +13 -29
- package/cjm/features/build/domain/emit.js +2 -2
- package/cjm/features/build/domain/scan.js +0 -18
- package/cjm/features/build/domain/tokenCollisions.js +1 -1
- package/cjm/features/build/domain/tsconfig.js +16 -10
- package/cjm/features/build/services/BundleBuilder.js +8 -31
- package/cjm/index.js +2 -4
- package/cjm/schema/ticConfigSchema.js +1 -5
- package/package.json +2 -2
- package/tic.schema.json +85 -103
- package/tpl/index.cjs +19 -41
- package/typings/app.d.ts +1 -2
- package/typings/exceptions/DomainException.d.ts +0 -4
- package/typings/exceptions/DomainException.d.ts.map +1 -1
- package/typings/features/build/BuildConfig.d.ts +71 -82
- package/typings/features/build/BuildConfig.d.ts.map +1 -1
- package/typings/features/build/BuildController.d.ts +0 -1
- package/typings/features/build/BuildController.d.ts.map +1 -1
- package/typings/features/build/domain/ImportPaths.d.ts +13 -11
- package/typings/features/build/domain/ImportPaths.d.ts.map +1 -1
- package/typings/features/build/domain/emit.d.ts +1 -7
- package/typings/features/build/domain/emit.d.ts.map +1 -1
- package/typings/features/build/domain/scan.d.ts +0 -2
- package/typings/features/build/domain/scan.d.ts.map +1 -1
- package/typings/features/build/domain/tsconfig.d.ts +11 -15
- package/typings/features/build/domain/tsconfig.d.ts.map +1 -1
- package/typings/features/build/services/BundleBuilder.d.ts +5 -8
- package/typings/features/build/services/BundleBuilder.d.ts.map +1 -1
- package/typings/index.d.ts +4 -4
- package/typings/schema/ticConfigSchema.d.ts.map +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 what a tsconfig compiles 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.
|
|
@@ -9,8 +9,10 @@ it.
|
|
|
9
9
|
- The CLI is `ts-ioc-container`, with `tic` as a shortcut (both `bin` entries,
|
|
10
10
|
same program). One config file describes one bundle: `<name>.bundle.json`, or the same in
|
|
11
11
|
YAML as `<name>.bundle.yaml` / `.yml` (an empty YAML file = all defaults)
|
|
12
|
-
(schema: `tic.schema.json`),
|
|
13
|
-
`
|
|
12
|
+
(schema: `tic.schema.json`), shaped like a tsconfig — `extends`, `include`,
|
|
13
|
+
`exclude`, `compilerOptions` — plus the bundler's `baseUrl`, `output` and `name`
|
|
14
|
+
at the top level and `compilerOptions.importExtension` / `compilerOptions.classes`;
|
|
15
|
+
all optional. `tic build` works on
|
|
14
16
|
the package it is invoked in (nearest `package.json` up from the working
|
|
15
17
|
directory; never a workspace root): it builds every `*.bundle.{json,yaml,yml}` there
|
|
16
18
|
(or the ones named with repeatable `-c`), else — zero config — one bundle
|
|
@@ -18,24 +20,31 @@ it.
|
|
|
18
20
|
applied with `container.useModule(new AppBundle())`. Several bundles (e.g.
|
|
19
21
|
`production.bundle.json`, `development.bundle.json`) mean several configs.
|
|
20
22
|
`--check` writes nothing and exits 1 when an output is stale — run it in CI.
|
|
21
|
-
- A config `extends`
|
|
22
|
-
named explicitly must exist)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
- A config is a tsconfig that `extends` another (default `./tsconfig.json`, may
|
|
24
|
+
be absent; one named explicitly must exist): TypeScript parses it with the
|
|
25
|
+
bundler's own fields removed, so the files it compiles (its `include` /
|
|
26
|
+
`exclude` replacing the parent's) are the bundle's file set, and the merged
|
|
27
|
+
`paths` aliases, `baseUrl` and module resolution shape the generated imports
|
|
28
|
+
(most specific alias, else a path relative to `baseUrl` when the file is under
|
|
29
|
+
it, else a relative path). Every compiler option but `classes`
|
|
30
|
+
and `importExtension` goes to TypeScript, which reports unknown ones.
|
|
31
|
+
The top-level `baseUrl` (default the config's directory) is the base the
|
|
32
|
+
config's own `include`, `exclude` and `output` resolve against — so they need
|
|
33
|
+
not repeat a shared prefix — and is passed to TypeScript as
|
|
34
|
+
`compilerOptions.baseUrl` unless that is set too; `extends` stays relative to
|
|
35
|
+
the config file. With `baseUrl` set, an omitted `include` defaults to the whole
|
|
36
|
+
`baseUrl` folder (`.`); without it, an omitted `include` still inherits the
|
|
37
|
+
extended tsconfig's.
|
|
38
|
+
`output` defaults to `<root>/<name>.bundle.ts`: root = compiled `rootDir`,
|
|
39
|
+
else the common folder of the files compiled. `name` defaults to the config's
|
|
40
|
+
stem (`production.bundle.json` → `production`), else `base`; the class is
|
|
28
41
|
`toClassName(name)` — `ProductionBundle`, `BaseBundle`.
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
are relative to the config. With file naming conventions, `include` is the
|
|
36
|
-
speed lever — nothing else is read. Files `tic build` generated are never input.
|
|
37
|
-
- `classes: { export, decorators, name, excludeClasses, excludeName }`
|
|
38
|
-
then picks classes of the parsed files; default is every exported,
|
|
42
|
+
- Test files, `__tests__/` and `node_modules/` (`DEFAULT_EXCLUDE`) are never
|
|
43
|
+
scanned, whatever is compiled; neither are files `tic build` generated. With
|
|
44
|
+
file naming conventions, a narrow `include` is the speed lever — nothing else
|
|
45
|
+
is read.
|
|
46
|
+
- `compilerOptions.classes: { export, decorators, name, excludeClasses, excludeName }`
|
|
47
|
+
picks classes of the scanned files; default is every exported,
|
|
39
48
|
non-abstract class decorated with `@register` (`decorators` defaults to
|
|
40
49
|
`["register"]`; `[]` requires none); positive criteria must all hold and then `excludeClasses` (exact
|
|
41
50
|
names) and `excludeName` (glob) drop classes.
|
|
@@ -45,11 +54,12 @@ it.
|
|
|
45
54
|
(scope-gated, e.g. `@perPage('a')` vs `@perPage('b')`).
|
|
46
55
|
- Never edit a `*.bundle.ts` by hand — change the classes or the config and
|
|
47
56
|
rerun `tic build`.
|
|
48
|
-
- Unknown config fields are errors,
|
|
57
|
+
- Unknown config fields are errors, except inside `compilerOptions`, where
|
|
58
|
+
TypeScript reports unknown compiler options.
|
|
49
59
|
- Programmatic API: `build({ config, cwd, check })` (one config, one output),
|
|
50
60
|
`findConfigFiles(dir)`, `run(argv, io)`, `loadConfig(file)`; every error the
|
|
51
61
|
bundler raises on purpose is a `TicError` with a stable `code`
|
|
52
|
-
(`TicConfigError` = `INVALID_CONFIG`, `
|
|
62
|
+
(`TicConfigError` = `INVALID_CONFIG`, `UsageError`,
|
|
53
63
|
`StaleBundlesError`, …).
|
|
54
64
|
- Internals follow `release-monorepo-semantically` (ADR 0023): `createContainer(io)`
|
|
55
65
|
in `lib/app.ts` is the composition root; `Application` resolves the controller
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @ts-ioc-container/bundler
|
|
2
2
|
|
|
3
3
|
**Bundles your dependencies into a single container module.** `tic build` scans
|
|
4
|
-
what your tsconfig compiles
|
|
5
|
-
|
|
4
|
+
what your tsconfig compiles and generates one typed bundle that registers every
|
|
5
|
+
class in it, ready for
|
|
6
6
|
[`ts-ioc-container`](https://www.npmjs.com/package/ts-ioc-container):
|
|
7
7
|
|
|
8
8
|
```ts
|
|
@@ -14,8 +14,9 @@ Adding a service means writing the class — no hand-maintained list of
|
|
|
14
14
|
TypeScript, generated at build time, so it type-checks, bundles and tree-shakes
|
|
15
15
|
like code you wrote yourself.
|
|
16
16
|
|
|
17
|
-
A bundle is
|
|
18
|
-
|
|
17
|
+
A bundle config **is** a tsconfig that `extends` yours — `include`, `exclude`
|
|
18
|
+
and `compilerOptions` mean what they mean to `tsc` — plus a few fields of the
|
|
19
|
+
bundler's own.
|
|
19
20
|
|
|
20
21
|
## Install
|
|
21
22
|
|
|
@@ -73,28 +74,62 @@ or, in YAML, `<name>.bundle.yaml` / `<name>.bundle.yml` — same fields, same sc
|
|
|
73
74
|
|
|
74
75
|
```yaml
|
|
75
76
|
# yaml-language-server: $schema=./node_modules/@ts-ioc-container/bundler/tic.schema.json
|
|
77
|
+
extends: ./tsconfig.json
|
|
76
78
|
output: src/di/app.bundle.ts
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
include: [src/**/*.service.ts]
|
|
80
|
+
compilerOptions:
|
|
81
|
+
classes:
|
|
82
|
+
decorators: [register, service]
|
|
79
83
|
```
|
|
80
84
|
|
|
81
85
|
An empty YAML file is a bundle with every setting at its default. Describing
|
|
82
86
|
one bundle in two formats (`app.bundle.json` and `app.bundle.yaml`) is an error.
|
|
83
87
|
|
|
84
|
-
Every field is optional; `{}` is the zero-config bundle.
|
|
88
|
+
Every field is optional; `{}` is the zero-config bundle. The tsconfig fields:
|
|
85
89
|
|
|
86
|
-
| Field | Default
|
|
87
|
-
| ----------------- |
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
|
|
93
|
-
| `files` | what the tsconfig compiles, no tests | Which files are parsed — see [Selecting files](#selecting-files) |
|
|
94
|
-
| `classes` | exported classes with `@register` | Which classes of a parsed file are registered — see [Selecting classes](#selecting-classes) |
|
|
90
|
+
| Field | Default | Meaning |
|
|
91
|
+
| ----------------- | --------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
92
|
+
| `extends` | `./tsconfig.json` (may be absent) | The tsconfig this config extends; one named here must exist |
|
|
93
|
+
| `include` | the extended tsconfig's | Which files are scanned — see [Selecting files](#selecting-files) |
|
|
94
|
+
| `exclude` | the extended tsconfig's | Which of them are left out — see [Selecting files](#selecting-files) |
|
|
95
|
+
| `compilerOptions` | the extended tsconfig's | tsconfig compiler options (`paths`, `rootDir`, `moduleResolution`, …) plus the bundler's below |
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
The bundler's own fields, at the top level:
|
|
98
|
+
|
|
99
|
+
| Field | Default | Meaning |
|
|
100
|
+
| --------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| `baseUrl` | the config file's directory | The base `include`, `exclude` and `output` resolve against, so they need not repeat a shared prefix such as `src`; also passed to TypeScript as `compilerOptions.baseUrl` |
|
|
102
|
+
| `output` | `<root>/<name>.bundle.ts` | The bundle file; `<root>` as in [Zero config](#zero-config) |
|
|
103
|
+
| `name` | the config's stem, else `base` | The bundle's name (`production.bundle.json` → `production`): names the default output and the class, `ProductionBundle` |
|
|
104
|
+
|
|
105
|
+
and under `compilerOptions`:
|
|
106
|
+
|
|
107
|
+
| Option | Default | Meaning |
|
|
108
|
+
| ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
109
|
+
| `classes` | exported classes with `@register` | Which classes of a scanned file are registered — see [Selecting classes](#selecting-classes) |
|
|
110
|
+
| `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
|
|
111
|
+
|
|
112
|
+
An unknown top-level field is an error, so a misspelled or removed option never
|
|
113
|
+
goes unnoticed. Every other compiler option is handed to TypeScript, which
|
|
114
|
+
reports an unknown one (`Unknown compiler option 'pathz'. Did you mean 'paths'?`).
|
|
115
|
+
|
|
116
|
+
`baseUrl` lets a config read in source terms instead of from the package root:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"baseUrl": "src",
|
|
121
|
+
"output": ".generated/container.bundle.ts",
|
|
122
|
+
"exclude": [".generated/**", "db/testing/**"]
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
writes `src/.generated/container.bundle.ts` from everything under `src/` except
|
|
127
|
+
those two folders — with `baseUrl` set, an omitted `include` scans the whole
|
|
128
|
+
`baseUrl` folder. `extends` still resolves against the config file.
|
|
129
|
+
|
|
130
|
+
Generated imports read like the tsconfig you compile with: a file covered by a
|
|
131
|
+
`paths` alias is imported in that alias form, a file under `baseUrl` in
|
|
132
|
+
`baseUrl` form (`services/Logger`), and anything else relative to the bundle.
|
|
98
133
|
|
|
99
134
|
## Several bundles
|
|
100
135
|
|
|
@@ -114,7 +149,7 @@ Each one can extend its own tsconfig and select its own files:
|
|
|
114
149
|
// production.bundle.json
|
|
115
150
|
{
|
|
116
151
|
"extends": "./tsconfig.production.json",
|
|
117
|
-
"
|
|
152
|
+
"exclude": ["src/**/*.development.ts"]
|
|
118
153
|
}
|
|
119
154
|
```
|
|
120
155
|
|
|
@@ -123,80 +158,44 @@ It writes `src/production.bundle.ts`, exporting `ProductionBundle`.
|
|
|
123
158
|
A bundle generated by one config is never registered by another, even when it
|
|
124
159
|
sits in a folder the other scans.
|
|
125
160
|
|
|
126
|
-
##
|
|
161
|
+
## Selecting files
|
|
127
162
|
|
|
128
|
-
|
|
163
|
+
The files a bundle scans are the files its config compiles, as `tsc` would see
|
|
164
|
+
them: the extended tsconfig's `files` / `include` / `exclude` (following its own
|
|
165
|
+
`extends`), with this config's `include` and `exclude` replacing the parent's —
|
|
166
|
+
exactly as in a child tsconfig. Globs are relative to `baseUrl`, defaulting to
|
|
167
|
+
the config file's directory.
|
|
129
168
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
169
|
+
Test files (`*.spec.ts`, `*.test.ts`, their `.tsx` twins and `__tests__/`) and
|
|
170
|
+
`node_modules/` are never scanned, whatever the tsconfig compiles, since test
|
|
171
|
+
classes usually carry the same `@register` decorators as production ones. Files
|
|
172
|
+
`tic build` generated are never input either.
|
|
133
173
|
|
|
134
174
|
Parsing is where the time goes, so when your project names files by convention
|
|
135
|
-
(`user.service.ts`, `user.repository.ts`), say so in `
|
|
136
|
-
|
|
175
|
+
(`user.service.ts`, `user.repository.ts`), say so in `include` and the bundler
|
|
176
|
+
never reads anything else:
|
|
137
177
|
|
|
138
178
|
```json
|
|
139
179
|
{
|
|
140
180
|
"output": "src/di/app.bundle.ts",
|
|
141
|
-
"
|
|
142
|
-
|
|
143
|
-
"include": ["**/*.service.ts", "**/*.repository.ts"]
|
|
144
|
-
},
|
|
145
|
-
"classes": { "decorators": ["register"] }
|
|
181
|
+
"include": ["src/**/*.service.ts", "src/**/*.repository.ts"],
|
|
182
|
+
"exclude": ["src/legacy/**"]
|
|
146
183
|
}
|
|
147
184
|
```
|
|
148
185
|
|
|
149
|
-
## Selecting files
|
|
150
|
-
|
|
151
|
-
The candidates are the files the extended tsconfig compiles (its `files` /
|
|
152
|
-
`include` / `exclude`, following its own `extends`). `paths` overrides that set
|
|
153
|
-
the way a child tsconfig's `include` overrides its parent's: the bundle then scans
|
|
154
|
-
those folders instead — relative to the config file (`./src/services`) or tsconfig
|
|
155
|
-
`paths` aliases (`@app/services`), recursively unless an entry says
|
|
156
|
-
`{ "path": "...", "recursive": false }`. Without a tsconfig, `paths` is required.
|
|
157
|
-
|
|
158
|
-
A candidate is parsed when it matches **one of** `include` and **none of**
|
|
159
|
-
`exclude`. Globs are relative to the config file and `/`-separated; `**` spans
|
|
160
|
-
folders.
|
|
161
|
-
|
|
162
|
-
| Rule | Default | Meaning |
|
|
163
|
-
| --------- | ----------------------------------------- | ---------------------------------------------------------- |
|
|
164
|
-
| `paths` | what the tsconfig compiles | Folders to scan instead |
|
|
165
|
-
| `include` | every source file | Globs a file must match one of, e.g. `"**/*.service.ts"` |
|
|
166
|
-
| `exclude` | test files, `__tests__/`, `node_modules/` | Globs of files never read; replaces the default when given |
|
|
167
|
-
|
|
168
|
-
A non-empty `exclude` replaces the defaults, so restate the ones you still want:
|
|
169
|
-
|
|
170
|
-
```json
|
|
171
|
-
"files": {
|
|
172
|
-
"exclude": [
|
|
173
|
-
"**/*.spec.ts",
|
|
174
|
-
"**/*.test.ts",
|
|
175
|
-
"**/*.spec.tsx",
|
|
176
|
-
"**/*.test.tsx",
|
|
177
|
-
"**/__tests__/**",
|
|
178
|
-
"**/node_modules/**",
|
|
179
|
-
"frontend/api/generated/**"
|
|
180
|
-
]
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
If it omits a default glob the build warns, since test classes usually carry
|
|
185
|
-
the same `@register` decorators as production ones. `"exclude": []` deliberately
|
|
186
|
-
parses everything, including tests. Giving only `include` keeps the default
|
|
187
|
-
`exclude`, so `**/*.service.ts` still skips `user.service.spec.ts`.
|
|
188
|
-
|
|
189
186
|
## Selecting classes
|
|
190
187
|
|
|
191
|
-
By default every exported, non-abstract class of a
|
|
192
|
-
`@register` is registered. `classes` changes that; a class must
|
|
193
|
-
criterion that is set:
|
|
188
|
+
By default every exported, non-abstract class of a scanned file that carries
|
|
189
|
+
`@register` is registered. `compilerOptions.classes` changes that; a class must
|
|
190
|
+
meet every criterion that is set:
|
|
194
191
|
|
|
195
192
|
```json
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
|
|
199
|
-
|
|
193
|
+
"compilerOptions": {
|
|
194
|
+
"classes": {
|
|
195
|
+
"export": "named",
|
|
196
|
+
"decorators": ["register", "repository"],
|
|
197
|
+
"name": "*Service"
|
|
198
|
+
}
|
|
200
199
|
}
|
|
201
200
|
```
|
|
202
201
|
|
|
@@ -217,10 +216,12 @@ bind the same token, registration is last-wins and the stand-in silently wins.
|
|
|
217
216
|
`excludeClasses` and `excludeName` drop a class right in the config:
|
|
218
217
|
|
|
219
218
|
```json
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
"compilerOptions": {
|
|
220
|
+
"classes": {
|
|
221
|
+
"decorators": ["repository", "service"],
|
|
222
|
+
"excludeClasses": ["MockDashboardRepository"],
|
|
223
|
+
"excludeName": "*Fake"
|
|
224
|
+
}
|
|
224
225
|
}
|
|
225
226
|
```
|
|
226
227
|
|
|
@@ -235,7 +236,7 @@ are not resolved), so it can only be a heuristic:
|
|
|
235
236
|
```text
|
|
236
237
|
tic: warning: app.bundle.json: decorator token "IDashboardRepositoryToken" is passed by
|
|
237
238
|
HttpDashboardRepository, MockDashboardRepository; registration is last-wins,
|
|
238
|
-
exclude one with classes.excludeClasses
|
|
239
|
+
exclude one with compilerOptions.classes.excludeClasses
|
|
239
240
|
```
|
|
240
241
|
|
|
241
242
|
Classes that are scope-gated are not last-wins, so they do not warn: when the
|
|
@@ -256,7 +257,6 @@ The output is plain TypeScript over the public `ts-ioc-container` API:
|
|
|
256
257
|
```ts
|
|
257
258
|
// src/di/app.bundle.ts
|
|
258
259
|
// Generated by `tic build` from ../../app.bundle.json. Do not edit by hand.
|
|
259
|
-
// Paths: @app/services, @app/infra
|
|
260
260
|
import { type IContainer, type IContainerModule, type IRegistration, Registration } from 'ts-ioc-container';
|
|
261
261
|
import { MemoryLogger } from '@app/infra/logging/MemoryLogger';
|
|
262
262
|
import { Greeter } from '@app/services/Greeter';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.StaleBundlesError = exports.UsageError = exports.UnknownActionError = exports.UnknownCommandError = exports.MissingCommandError = exports.
|
|
3
|
+
exports.StaleBundlesError = exports.UsageError = exports.UnknownActionError = exports.UnknownCommandError = exports.MissingCommandError = exports.TicConfigError = exports.TicError = void 0;
|
|
4
4
|
class TicError extends Error {
|
|
5
5
|
code;
|
|
6
6
|
constructor(code, message) {
|
|
@@ -16,12 +16,6 @@ class TicConfigError extends TicError {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
exports.TicConfigError = TicConfigError;
|
|
19
|
-
class NamespaceNotFoundError extends TicError {
|
|
20
|
-
constructor(message) {
|
|
21
|
-
super('NAMESPACE_NOT_FOUND', message);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
exports.NamespaceNotFoundError = NamespaceNotFoundError;
|
|
25
19
|
class MissingCommandError extends TicError {
|
|
26
20
|
constructor(usage) {
|
|
27
21
|
super('MISSING_COMMAND', `missing command\n${usage}`);
|
|
@@ -30,28 +30,6 @@ const strings = (what, minItems) => zod_1.z
|
|
|
30
30
|
.meta({ type: 'array', items: { type: 'string', minLength: 1 }, ...(minItems > 0 && { minItems }) });
|
|
31
31
|
const stringList = () => strings('an array of strings', 0);
|
|
32
32
|
const nonEmptyStringList = () => strings('a non-empty array of strings', 1);
|
|
33
|
-
const PATH_SCHEMA = zod_1.z.union([
|
|
34
|
-
nonEmptyString(),
|
|
35
|
-
zod_1.z.object({
|
|
36
|
-
path: nonEmptyString(),
|
|
37
|
-
recursive: zod_1.z.boolean(expected('a boolean')).default(true),
|
|
38
|
-
}, expected('a string or an object with "path"')),
|
|
39
|
-
], expected('a string or an object with "path"'));
|
|
40
|
-
const FILES_SCHEMA = zod_1.z
|
|
41
|
-
.strictObject({
|
|
42
|
-
paths: zod_1.z
|
|
43
|
-
.array(PATH_SCHEMA, expected('a non-empty array'))
|
|
44
|
-
.min(1, expected('a non-empty array'))
|
|
45
|
-
.optional()
|
|
46
|
-
.describe("Folders to scan instead of the extended tsconfig's file set: paths relative to this file (./src/services) or tsconfig paths aliases (@app/services). Required when there is no tsconfig to extend."),
|
|
47
|
-
include: nonEmptyStringList()
|
|
48
|
-
.optional()
|
|
49
|
-
.describe('Globs a file must match one of, e.g. ["**/*.service.ts", "**/*.repository.ts"]. Default: every source file.'),
|
|
50
|
-
exclude: stringList()
|
|
51
|
-
.default(exports.DEFAULT_EXCLUDE)
|
|
52
|
-
.describe('Globs of files never read. Replaces the default when given. A non-empty list that omits a default glob makes `tic build` warn.'),
|
|
53
|
-
}, expected('an object'))
|
|
54
|
-
.describe('Which files this bundle reads and parses — decided by path alone, before parsing. The candidates are what the extended tsconfig compiles, or the folders in `paths` when given. A candidate is parsed when it matches one of `include` and none of `exclude`. Globs are relative to this file. With a file naming convention, `include` keeps every other file unread.');
|
|
55
33
|
const CLASSES_SCHEMA = zod_1.z
|
|
56
34
|
.strictObject({
|
|
57
35
|
export: zod_1.z
|
|
@@ -71,28 +49,47 @@ const CLASSES_SCHEMA = zod_1.z
|
|
|
71
49
|
.optional()
|
|
72
50
|
.describe('Glob the class name must NOT match, e.g. "*Mock". An anonymous default export is named after its file.'),
|
|
73
51
|
}, expected('an object'))
|
|
74
|
-
.describe('Which classes of a
|
|
52
|
+
.describe('Which classes of a scanned file are registered. A class is selected when it is exported, not abstract, and meets every criterion set here; by default that is every exported class decorated with @register.');
|
|
53
|
+
const COMPILER_OPTIONS_SCHEMA = zod_1.z
|
|
54
|
+
.looseObject({
|
|
55
|
+
importExtension: zod_1.z
|
|
56
|
+
.string(expected('a string'))
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('Extension of generated imports, e.g. ".js". Inferred from the tsconfig: ".js" under node16/nodenext resolution, none otherwise.'),
|
|
59
|
+
classes: CLASSES_SCHEMA.optional(),
|
|
60
|
+
paths: zod_1.z
|
|
61
|
+
.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string()))
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('tsconfig `paths`: aliases the generated imports are written in.'),
|
|
64
|
+
rootDir: zod_1.z
|
|
65
|
+
.string()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe('tsconfig `rootDir`: where the default output (<rootDir>/<name>.bundle.ts) goes.'),
|
|
68
|
+
}, expected('an object'))
|
|
69
|
+
.describe("tsconfig `compilerOptions`, plus the bundler's own `importExtension` and `classes`. Every other option is handed to TypeScript on top of the extended tsconfig's, which validates it.");
|
|
70
|
+
const tsconfigGlobs = (field, note = '') => stringList()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe(`tsconfig \`${field}\`: replaces the extended tsconfig's. Relative to \`baseUrl\`.${note}`);
|
|
75
73
|
exports.BUNDLE_CONFIG_SCHEMA = zod_1.z.strictObject({
|
|
76
74
|
$schema: zod_1.z.string().optional(),
|
|
75
|
+
extends: nonEmptyString()
|
|
76
|
+
.optional()
|
|
77
|
+
.meta({ default: exports.DEFAULT_EXTENDS })
|
|
78
|
+
.describe("The tsconfig this bundle extends, as a tsconfig's own `extends`: its file set, `paths` aliases and module resolution apply unless this file overrides them. Relative to this file. The default may be absent; a tsconfig named here must exist."),
|
|
79
|
+
baseUrl: nonEmptyString()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("The base folder this config's relative `include`, `exclude` and `output` resolve against, so they need not repeat a shared prefix such as `src`. Relative to this file; default this file's directory. Also handed to TypeScript as `compilerOptions.baseUrl` (unless that is set too), so generated imports use it."),
|
|
77
82
|
output: nonEmptyString()
|
|
78
83
|
.optional()
|
|
79
|
-
.describe(
|
|
84
|
+
.describe('The bundle file (convention: *.bundle.ts). Default `<root>/<name>.bundle.ts`: <root> is the compiled rootDir, else the common folder of the files compiled.'),
|
|
80
85
|
name: zod_1.z
|
|
81
86
|
.string(expected('letters, digits, "-" or "_", starting with a letter'))
|
|
82
87
|
.regex(BUNDLE_NAME, expected('letters, digits, "-" or "_", starting with a letter'))
|
|
83
88
|
.optional()
|
|
84
89
|
.describe('The bundle\'s name: letters, digits, "-" and "_", starting with a letter. Default: this file\'s stem (production.bundle.json → production), else "base". It names the default output (production.bundle.ts) and the generated IContainerModule class (ProductionBundle).'),
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.meta({ default: exports.DEFAULT_EXTENDS })
|
|
89
|
-
.describe("The tsconfig this bundle builds on, like a tsconfig's own `extends`: what it compiles (`files` / `include` / `exclude`) is the bundle's file set unless `files.paths` overrides it, and its `paths` aliases and module resolution shape the generated imports. Relative to this file. The default may be absent; a tsconfig named here must exist."),
|
|
90
|
-
importExtension: zod_1.z
|
|
91
|
-
.string(expected('a string'))
|
|
92
|
-
.optional()
|
|
93
|
-
.describe('Extension of generated imports, e.g. ".js". Inferred from the tsconfig: ".js" under node16/nodenext resolution, none otherwise.'),
|
|
94
|
-
files: FILES_SCHEMA.optional(),
|
|
95
|
-
classes: CLASSES_SCHEMA.optional(),
|
|
90
|
+
include: tsconfigGlobs('include', ' When `baseUrl` is set and this is omitted, the whole `baseUrl` folder (`.`) is scanned.'),
|
|
91
|
+
exclude: tsconfigGlobs('exclude'),
|
|
92
|
+
compilerOptions: COMPILER_OPTIONS_SCHEMA.optional(),
|
|
96
93
|
}, expected('an object'));
|
|
97
94
|
function formatPath(segments) {
|
|
98
95
|
const formatted = segments
|
|
@@ -101,29 +98,13 @@ function formatPath(segments) {
|
|
|
101
98
|
.replace(/^\./, '');
|
|
102
99
|
return formatted || 'config';
|
|
103
100
|
}
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const at = [...prefix, ...issue.path];
|
|
110
|
-
if (issue.code === 'unrecognized_keys')
|
|
111
|
-
return issue.keys.map((key) => `${formatPath([...at, key])}: unknown field`);
|
|
112
|
-
const branch = issue.code === 'invalid_union' ? matchedBranch(issue) : undefined;
|
|
113
|
-
if (branch)
|
|
114
|
-
return branch.flatMap((inner) => formatIssue(inner, at));
|
|
115
|
-
return [`${formatPath(at)}: ${issue.message}`];
|
|
101
|
+
function formatIssue(issue) {
|
|
102
|
+
if (issue.code === 'unrecognized_keys') {
|
|
103
|
+
return issue.keys.map((key) => `${formatPath([...issue.path, key])}: unknown field`);
|
|
104
|
+
}
|
|
105
|
+
return [`${formatPath(issue.path)}: ${issue.message}`];
|
|
116
106
|
}
|
|
117
107
|
const formatIssues = (error) => error.issues.flatMap((issue) => formatIssue(issue)).join('; ');
|
|
118
|
-
function toFileSelector(files) {
|
|
119
|
-
if (files === undefined)
|
|
120
|
-
return { exclude: exports.DEFAULT_EXCLUDE.map(glob_1.globToRegExp) };
|
|
121
|
-
return {
|
|
122
|
-
paths: files.paths?.map((entry) => (typeof entry === 'string' ? { path: entry, recursive: true } : entry)),
|
|
123
|
-
include: files.include?.map(glob_1.globToRegExp),
|
|
124
|
-
exclude: files.exclude.map(glob_1.globToRegExp),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
108
|
function toClassSelector(classes) {
|
|
128
109
|
if (classes === undefined)
|
|
129
110
|
return { export: 'any', decorators: exports.DEFAULT_DECORATORS };
|
|
@@ -135,18 +116,6 @@ function toClassSelector(classes) {
|
|
|
135
116
|
excludeName: excludeName === undefined ? undefined : (0, glob_1.globToRegExp)(excludeName),
|
|
136
117
|
};
|
|
137
118
|
}
|
|
138
|
-
function excludeWarnings(files) {
|
|
139
|
-
const exclude = files?.exclude ?? [];
|
|
140
|
-
if (exclude.length === 0)
|
|
141
|
-
return [];
|
|
142
|
-
const missing = exports.DEFAULT_EXCLUDE.filter((glob) => !exclude.includes(glob));
|
|
143
|
-
if (missing.length === 0)
|
|
144
|
-
return [];
|
|
145
|
-
return [
|
|
146
|
-
`files.exclude replaces the default excludes and omits ${missing.map((glob) => `"${glob}"`).join(', ')}; ` +
|
|
147
|
-
`add them back`,
|
|
148
|
-
];
|
|
149
|
-
}
|
|
150
119
|
exports.CONFIG_FILE_SUFFIXES = ['.bundle.json', '.bundle.yaml', '.bundle.yml'];
|
|
151
120
|
const configSuffix = (file) => exports.CONFIG_FILE_SUFFIXES.find((suffix) => file.endsWith(suffix));
|
|
152
121
|
exports.configSuffix = configSuffix;
|
|
@@ -164,20 +133,26 @@ function resolveConfig(content, file) {
|
|
|
164
133
|
const result = exports.BUNDLE_CONFIG_SCHEMA.safeParse(content);
|
|
165
134
|
if (!result.success)
|
|
166
135
|
throw new DomainException_1.TicConfigError(formatIssues(result.error));
|
|
167
|
-
const { output, name,
|
|
136
|
+
const { output, name, baseUrl, extends: tsconfig, include, exclude, compilerOptions = {} } = result.data;
|
|
137
|
+
const { importExtension, classes, ...tsCompilerOptions } = compilerOptions;
|
|
168
138
|
const dir = node_path_1.default.dirname(file);
|
|
139
|
+
const base = baseUrl === undefined ? dir : node_path_1.default.resolve(dir, baseUrl);
|
|
140
|
+
const includeGlobs = include ?? (baseUrl === undefined ? undefined : ['.']);
|
|
141
|
+
const rebase = (globs) => globs?.map((glob) => (node_path_1.default.isAbsolute(glob) ? glob : (0, glob_1.toPosix)(node_path_1.default.relative(dir, node_path_1.default.resolve(base, glob))) || '.'));
|
|
169
142
|
const bundleName = name ?? configStem(file) ?? exports.DEFAULT_BUNDLE_NAME;
|
|
170
143
|
return {
|
|
171
144
|
file,
|
|
172
145
|
dir,
|
|
173
|
-
output: output === undefined ? undefined : node_path_1.default.resolve(
|
|
146
|
+
output: output === undefined ? undefined : node_path_1.default.resolve(base, output),
|
|
174
147
|
name: bundleName,
|
|
175
148
|
className: toClassName(bundleName),
|
|
176
|
-
tags,
|
|
177
149
|
tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.DEFAULT_EXTENDS), required: tsconfig !== undefined },
|
|
150
|
+
overrides: {
|
|
151
|
+
include: rebase(includeGlobs),
|
|
152
|
+
exclude: rebase(exclude),
|
|
153
|
+
compilerOptions: baseUrl === undefined ? tsCompilerOptions : { baseUrl, ...tsCompilerOptions },
|
|
154
|
+
},
|
|
178
155
|
importExtension,
|
|
179
|
-
files: toFileSelector(files),
|
|
180
156
|
classes: toClassSelector(classes),
|
|
181
|
-
warnings: excludeWarnings(files),
|
|
182
157
|
};
|
|
183
158
|
}
|
|
@@ -37,14 +37,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.ImportPaths = void 0;
|
|
40
|
-
const node_fs_1 = require("node:fs");
|
|
41
40
|
const node_path_1 = __importDefault(require("node:path"));
|
|
42
41
|
const ts = __importStar(require("typescript"));
|
|
43
|
-
const DomainException_1 = require("../../../exceptions/DomainException");
|
|
44
42
|
const glob_1 = require("./glob");
|
|
45
43
|
const SOURCE_EXTENSION = /\.(tsx?|mts|cts)$/;
|
|
46
|
-
const isDirectory = (dir) => (0, node_fs_1.existsSync)(dir) && (0, node_fs_1.statSync)(dir).isDirectory();
|
|
47
|
-
const isRelative = (spec) => spec.startsWith('.') || node_path_1.default.isAbsolute(spec);
|
|
48
44
|
function splitWildcard(pattern) {
|
|
49
45
|
const star = pattern.indexOf('*');
|
|
50
46
|
return star === -1 ? undefined : [pattern.slice(0, star), pattern.slice(star + 1)];
|
|
@@ -52,6 +48,7 @@ function splitWildcard(pattern) {
|
|
|
52
48
|
class ImportPaths {
|
|
53
49
|
aliases;
|
|
54
50
|
extension;
|
|
51
|
+
baseUrl;
|
|
55
52
|
static fromOptions(options, tsconfigFile, importExtension) {
|
|
56
53
|
const { paths = {}, baseUrl, pathsBasePath, moduleResolution } = options;
|
|
57
54
|
const base = baseUrl ?? (typeof pathsBasePath === 'string' ? pathsBasePath : node_path_1.default.dirname(tsconfigFile));
|
|
@@ -60,22 +57,12 @@ class ImportPaths {
|
|
|
60
57
|
targets: targets.map((target) => (0, glob_1.toPosix)(node_path_1.default.resolve(base, target))),
|
|
61
58
|
}));
|
|
62
59
|
const nodeEsm = moduleResolution === ts.ModuleResolutionKind.Node16 || moduleResolution === ts.ModuleResolutionKind.NodeNext;
|
|
63
|
-
return new ImportPaths(aliases, importExtension ?? (nodeEsm ? '.js' : ''));
|
|
60
|
+
return new ImportPaths(aliases, importExtension ?? (nodeEsm ? '.js' : ''), baseUrl === undefined ? undefined : (0, glob_1.toPosix)(baseUrl));
|
|
64
61
|
}
|
|
65
|
-
constructor(aliases, extension) {
|
|
62
|
+
constructor(aliases, extension, baseUrl) {
|
|
66
63
|
this.aliases = aliases;
|
|
67
64
|
this.extension = extension;
|
|
68
|
-
|
|
69
|
-
resolveNamespace(namespace, baseDir) {
|
|
70
|
-
const folder = node_path_1.default.resolve(baseDir, namespace);
|
|
71
|
-
if (isDirectory(folder))
|
|
72
|
-
return folder;
|
|
73
|
-
if (!isRelative(namespace)) {
|
|
74
|
-
const found = [namespace, `${namespace}/`].flatMap((spec) => this.aliasTargets(spec)).find(isDirectory);
|
|
75
|
-
if (found)
|
|
76
|
-
return node_path_1.default.normalize(found);
|
|
77
|
-
}
|
|
78
|
-
throw new DomainException_1.NamespaceNotFoundError(`namespace "${namespace}" is neither a folder relative to ${baseDir} nor a tsconfig paths alias of one`);
|
|
65
|
+
this.baseUrl = baseUrl;
|
|
79
66
|
}
|
|
80
67
|
specifier(fromFile, toFile) {
|
|
81
68
|
const target = (0, glob_1.toPosix)(toFile);
|
|
@@ -83,9 +70,18 @@ class ImportPaths {
|
|
|
83
70
|
const best = this.bestAlias(target, bare);
|
|
84
71
|
if (best)
|
|
85
72
|
return best.spec;
|
|
73
|
+
const fromBaseUrl = this.baseUrlSpecifier(bare);
|
|
74
|
+
if (fromBaseUrl)
|
|
75
|
+
return `${fromBaseUrl}${this.extension}`;
|
|
86
76
|
const relative = (0, glob_1.toPosix)(node_path_1.default.relative(node_path_1.default.dirname(fromFile), bare));
|
|
87
77
|
return `${relative.startsWith('.') ? relative : `./${relative}`}${this.extension}`;
|
|
88
78
|
}
|
|
79
|
+
baseUrlSpecifier(bare) {
|
|
80
|
+
if (!this.baseUrl)
|
|
81
|
+
return undefined;
|
|
82
|
+
const prefix = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/`;
|
|
83
|
+
return bare.startsWith(prefix) ? bare.slice(prefix.length) : undefined;
|
|
84
|
+
}
|
|
89
85
|
bestAlias(target, bare) {
|
|
90
86
|
let best;
|
|
91
87
|
for (const { pattern, targets } of this.aliases) {
|
|
@@ -97,18 +93,6 @@ class ImportPaths {
|
|
|
97
93
|
}
|
|
98
94
|
return best;
|
|
99
95
|
}
|
|
100
|
-
aliasTargets(spec) {
|
|
101
|
-
return this.aliases.flatMap(({ pattern, targets }) => {
|
|
102
|
-
const wildcard = splitWildcard(pattern);
|
|
103
|
-
if (!wildcard)
|
|
104
|
-
return pattern === spec ? targets : [];
|
|
105
|
-
const [prefix, suffix] = wildcard;
|
|
106
|
-
if (!spec.startsWith(prefix) || !spec.endsWith(suffix) || spec.length < prefix.length + suffix.length)
|
|
107
|
-
return [];
|
|
108
|
-
const captured = spec.slice(prefix.length, spec.length - suffix.length);
|
|
109
|
-
return targets.map((target) => target.replace('*', captured));
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
96
|
matchAlias(pattern, aliasTarget, file, bare) {
|
|
113
97
|
const wildcard = splitWildcard(aliasTarget);
|
|
114
98
|
if (!wildcard) {
|
|
@@ -7,7 +7,7 @@ const HandlebarsRenderService_1 = require("../../../services/HandlebarsRenderSer
|
|
|
7
7
|
exports.GENERATED_HEADER = '// Generated by `tic build`';
|
|
8
8
|
exports.BUNDLE_PROTOCOL = 'Bundle.ts.hbs';
|
|
9
9
|
const RUNTIME_IMPORTS = ['IContainer', 'IContainerModule', 'IRegistration', 'Registration'];
|
|
10
|
-
function bundleView({ name, configPath,
|
|
10
|
+
function bundleView({ name, configPath, classes }) {
|
|
11
11
|
const taken = new Set([...RUNTIME_IMPORTS, 'registrations', name]);
|
|
12
12
|
const uniqueName = (wanted) => {
|
|
13
13
|
let candidate = wanted;
|
|
@@ -27,7 +27,7 @@ function bundleView({ name, configPath, paths, tsconfigPath, classes }) {
|
|
|
27
27
|
entry.named.push(binding === cls.exportName ? binding : `${cls.exportName} as ${binding}`);
|
|
28
28
|
return binding;
|
|
29
29
|
});
|
|
30
|
-
return { configPath,
|
|
30
|
+
return { configPath, name, imports: [...byFile.values()], registrations: bindings };
|
|
31
31
|
}
|
|
32
32
|
function emitBundle(input) {
|
|
33
33
|
return (0, HandlebarsRenderService_1.renderProtocol)(exports.BUNDLE_PROTOCOL, bundleView(input));
|