@ts-ioc-container/bundler 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/AGENTS.md +24 -22
  2. package/README.md +64 -84
  3. package/cjm/exceptions/DomainException.js +1 -7
  4. package/cjm/features/build/BuildConfig.js +35 -72
  5. package/cjm/features/build/domain/ImportPaths.js +0 -27
  6. package/cjm/features/build/domain/emit.js +2 -2
  7. package/cjm/features/build/domain/scan.js +0 -18
  8. package/cjm/features/build/domain/tokenCollisions.js +1 -1
  9. package/cjm/features/build/domain/tsconfig.js +16 -10
  10. package/cjm/features/build/services/BundleBuilder.js +8 -31
  11. package/cjm/index.js +2 -4
  12. package/cjm/schema/ticConfigSchema.js +0 -4
  13. package/package.json +2 -2
  14. package/tic.schema.json +73 -102
  15. package/tpl/index.cjs +19 -41
  16. package/typings/app.d.ts +1 -2
  17. package/typings/exceptions/DomainException.d.ts +0 -4
  18. package/typings/exceptions/DomainException.d.ts.map +1 -1
  19. package/typings/features/build/BuildConfig.d.ts +61 -81
  20. package/typings/features/build/BuildConfig.d.ts.map +1 -1
  21. package/typings/features/build/BuildController.d.ts +0 -1
  22. package/typings/features/build/BuildController.d.ts.map +1 -1
  23. package/typings/features/build/domain/ImportPaths.d.ts +2 -9
  24. package/typings/features/build/domain/ImportPaths.d.ts.map +1 -1
  25. package/typings/features/build/domain/emit.d.ts +1 -7
  26. package/typings/features/build/domain/emit.d.ts.map +1 -1
  27. package/typings/features/build/domain/scan.d.ts +0 -2
  28. package/typings/features/build/domain/scan.d.ts.map +1 -1
  29. package/typings/features/build/domain/tsconfig.d.ts +11 -15
  30. package/typings/features/build/domain/tsconfig.d.ts.map +1 -1
  31. package/typings/features/build/services/BundleBuilder.d.ts +5 -8
  32. package/typings/features/build/services/BundleBuilder.d.ts.map +1 -1
  33. package/typings/index.d.ts +4 -4
  34. 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 folders and generates a typed bundle
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`), flat — `output`, `name`, `tags`, `extends`,
13
- `importExtension`, `files`, `classes`, all optional. `tic build` works on
12
+ (schema: `tic.schema.json`), shaped like a tsconfig — `extends`, `include`,
13
+ `exclude`, `compilerOptions` — plus the bundler's `output` and `name` at
14
+ 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,23 @@ 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` a tsconfig (default `./tsconfig.json`, may be absent; one
22
- named explicitly must exist), like a child tsconfig: what it compiles is the
23
- bundle's file set, and its `paths` aliases and module resolution shape the
24
- generated imports (most specific alias, else a relative path). `output`
25
- defaults to `<root>/<name>.bundle.ts`: root = tsconfig `rootDir`, else the
26
- common folder of its files. `name` defaults to the config's stem
27
- (`production.bundle.json` → `production`), else `base`; the class is
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 and module resolution shape the generated imports (most
28
+ specific alias, else a relative path). Every compiler option but `classes`
29
+ and `importExtension` goes to TypeScript, which reports unknown ones.
30
+ `output` defaults to `<root>/<name>.bundle.ts`: root = compiled `rootDir`,
31
+ else the common folder of the files compiled. `name` defaults to the config's
32
+ stem (`production.bundle.json` → `production`), else `base`; the class is
28
33
  `toClassName(name)` — `ProductionBundle`, `BaseBundle`.
29
- - Selection has two stages. `files: { paths, include, exclude }` decides by path
30
- which files are read and parsed at all. `paths` override the tsconfig's file
31
- set with folders (relative to the config, or tsconfig aliases); required when
32
- there is no tsconfig. A candidate must match one of `include` (default: all)
33
- and none of `exclude` (default: test files, `__tests__/`, `node_modules/`; a
34
- non-empty list replaces it, and the build warns if it drops a default). Globs
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,
34
+ - Test files, `__tests__/` and `node_modules/` (`DEFAULT_EXCLUDE`) are never
35
+ scanned, whatever is compiled; neither are files `tic build` generated. With
36
+ file naming conventions, a narrow `include` is the speed lever — nothing else
37
+ is read.
38
+ - `compilerOptions.classes: { export, decorators, name, excludeClasses, excludeName }`
39
+ picks classes of the scanned files; default is every exported,
39
40
  non-abstract class decorated with `@register` (`decorators` defaults to
40
41
  `["register"]`; `[]` requires none); positive criteria must all hold and then `excludeClasses` (exact
41
42
  names) and `excludeName` (glob) drop classes.
@@ -45,11 +46,12 @@ it.
45
46
  (scope-gated, e.g. `@perPage('a')` vs `@perPage('b')`).
46
47
  - Never edit a `*.bundle.ts` by hand — change the classes or the config and
47
48
  rerun `tic build`.
48
- - Unknown config fields are errors, at every level.
49
+ - Unknown config fields are errors, except inside `compilerOptions`, where
50
+ TypeScript reports unknown compiler options.
49
51
  - Programmatic API: `build({ config, cwd, check })` (one config, one output),
50
52
  `findConfigFiles(dir)`, `run(argv, io)`, `loadConfig(file)`; every error the
51
53
  bundler raises on purpose is a `TicError` with a stable `code`
52
- (`TicConfigError` = `INVALID_CONFIG`, `NamespaceNotFoundError`, `UsageError`,
54
+ (`TicConfigError` = `INVALID_CONFIG`, `UsageError`,
53
55
  `StaleBundlesError`, …).
54
56
  - Internals follow `release-monorepo-semantically` (ADR 0023): `createContainer(io)`
55
57
  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 — or the folders you name, by path or tsconfig alias —
5
- and generates one typed bundle that registers every class in them, ready for
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 configured like a tsconfig that `extends` yours: by default it
18
- takes the files your tsconfig compiles, and narrows or overrides them.
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,43 @@ 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
- files:
78
- include: ['**/*.service.ts']
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 | Meaning |
87
- | ----------------- | -------------------------------------- | --------------------------------------------------------------------------------------------- |
88
- | `output` | `<root>/<name>.bundle.ts` | The bundle file; `<root>` as in [Zero config](#zero-config). Required without a tsconfig |
89
- | `name` | the config's stem, else `base` | The bundle's name (`production.bundle.json` → `production`): names the default output and the class, `ProductionBundle` |
90
- | `tags` | `[]` | Tags associated with the bundle |
91
- | `extends` | `./tsconfig.json` (may be absent) | The tsconfig to build on: its file set, `paths` aliases and import extension; one named here must exist |
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
- An unknown field is an error, so a misspelled or removed option never goes
97
- unnoticed.
97
+ The bundler's own fields, at the top level:
98
+
99
+ | Field | Default | Meaning |
100
+ | -------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
101
+ | `output` | `<root>/<name>.bundle.ts` | The bundle file; `<root>` as in [Zero config](#zero-config) |
102
+ | `name` | the config's stem, else `base` | The bundle's name (`production.bundle.json` → `production`): names the default output and the class, `ProductionBundle` |
103
+
104
+ and under `compilerOptions`:
105
+
106
+ | Option | Default | Meaning |
107
+ | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------- |
108
+ | `classes` | exported classes with `@register` | Which classes of a scanned file are registered — see [Selecting classes](#selecting-classes) |
109
+ | `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
110
+
111
+ An unknown top-level field is an error, so a misspelled or removed option never
112
+ goes unnoticed. Every other compiler option is handed to TypeScript, which
113
+ reports an unknown one (`Unknown compiler option 'pathz'. Did you mean 'paths'?`).
98
114
 
99
115
  ## Several bundles
100
116
 
@@ -114,7 +130,7 @@ Each one can extend its own tsconfig and select its own files:
114
130
  // production.bundle.json
115
131
  {
116
132
  "extends": "./tsconfig.production.json",
117
- "files": { "exclude": ["**/*.development.ts", "**/*.test.ts", "**/*.spec.ts", "**/__tests__/**", "**/node_modules/**"] }
133
+ "exclude": ["src/**/*.development.ts"]
118
134
  }
119
135
  ```
120
136
 
@@ -123,80 +139,43 @@ It writes `src/production.bundle.ts`, exporting `ProductionBundle`.
123
139
  A bundle generated by one config is never registered by another, even when it
124
140
  sits in a folder the other scans.
125
141
 
126
- ## Selection in two stages
142
+ ## Selecting files
127
143
 
128
- A bundle picks its registrations in two stages:
144
+ The files a bundle scans are the files its config compiles, as `tsc` would see
145
+ them: the extended tsconfig's `files` / `include` / `exclude` (following its own
146
+ `extends`), with this config's `include` and `exclude` replacing the parent's —
147
+ exactly as in a child tsconfig. Globs are relative to the config file.
129
148
 
130
- 1. **`files`** — decided by path alone, before anything is read. Only files that
131
- pass are parsed.
132
- 2. **`classes`** — decided per class, on the files stage 1 let through.
149
+ Test files (`*.spec.ts`, `*.test.ts`, their `.tsx` twins and `__tests__/`) and
150
+ `node_modules/` are never scanned, whatever the tsconfig compiles, since test
151
+ classes usually carry the same `@register` decorators as production ones. Files
152
+ `tic build` generated are never input either.
133
153
 
134
154
  Parsing is where the time goes, so when your project names files by convention
135
- (`user.service.ts`, `user.repository.ts`), say so in `files.include` and the
136
- bundler never reads anything else:
155
+ (`user.service.ts`, `user.repository.ts`), say so in `include` and the bundler
156
+ never reads anything else:
137
157
 
138
158
  ```json
139
159
  {
140
160
  "output": "src/di/app.bundle.ts",
141
- "files": {
142
- "paths": ["@app/services"],
143
- "include": ["**/*.service.ts", "**/*.repository.ts"]
144
- },
145
- "classes": { "decorators": ["register"] }
146
- }
147
- ```
148
-
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
- ]
161
+ "include": ["src/**/*.service.ts", "src/**/*.repository.ts"],
162
+ "exclude": ["src/legacy/**"]
181
163
  }
182
164
  ```
183
165
 
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
166
  ## Selecting classes
190
167
 
191
- By default every exported, non-abstract class of a parsed file that carries
192
- `@register` is registered. `classes` changes that; a class must meet every
193
- criterion that is set:
168
+ By default every exported, non-abstract class of a scanned file that carries
169
+ `@register` is registered. `compilerOptions.classes` changes that; a class must
170
+ meet every criterion that is set:
194
171
 
195
172
  ```json
196
- "classes": {
197
- "export": "named",
198
- "decorators": ["register", "repository"],
199
- "name": "*Service"
173
+ "compilerOptions": {
174
+ "classes": {
175
+ "export": "named",
176
+ "decorators": ["register", "repository"],
177
+ "name": "*Service"
178
+ }
200
179
  }
201
180
  ```
202
181
 
@@ -217,10 +196,12 @@ bind the same token, registration is last-wins and the stand-in silently wins.
217
196
  `excludeClasses` and `excludeName` drop a class right in the config:
218
197
 
219
198
  ```json
220
- "classes": {
221
- "decorators": ["repository", "service"],
222
- "excludeClasses": ["MockDashboardRepository"],
223
- "excludeName": "*Fake"
199
+ "compilerOptions": {
200
+ "classes": {
201
+ "decorators": ["repository", "service"],
202
+ "excludeClasses": ["MockDashboardRepository"],
203
+ "excludeName": "*Fake"
204
+ }
224
205
  }
225
206
  ```
226
207
 
@@ -235,7 +216,7 @@ are not resolved), so it can only be a heuristic:
235
216
  ```text
236
217
  tic: warning: app.bundle.json: decorator token "IDashboardRepositoryToken" is passed by
237
218
  HttpDashboardRepository, MockDashboardRepository; registration is last-wins,
238
- exclude one with classes.excludeClasses
219
+ exclude one with compilerOptions.classes.excludeClasses
239
220
  ```
240
221
 
241
222
  Classes that are scope-gated are not last-wins, so they do not warn: when the
@@ -256,7 +237,6 @@ The output is plain TypeScript over the public `ts-ioc-container` API:
256
237
  ```ts
257
238
  // src/di/app.bundle.ts
258
239
  // Generated by `tic build` from ../../app.bundle.json. Do not edit by hand.
259
- // Paths: @app/services, @app/infra
260
240
  import { type IContainer, type IContainerModule, type IRegistration, Registration } from 'ts-ioc-container';
261
241
  import { MemoryLogger } from '@app/infra/logging/MemoryLogger';
262
242
  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.NamespaceNotFoundError = exports.TicConfigError = exports.TicError = void 0;
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,42 @@ 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 parsed 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.');
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) => stringList().optional().describe(`tsconfig \`${field}\`: replaces the extended tsconfig's. Relative to this file.`);
75
71
  exports.BUNDLE_CONFIG_SCHEMA = zod_1.z.strictObject({
76
72
  $schema: zod_1.z.string().optional(),
73
+ extends: nonEmptyString()
74
+ .optional()
75
+ .meta({ default: exports.DEFAULT_EXTENDS })
76
+ .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."),
77
77
  output: nonEmptyString()
78
78
  .optional()
79
- .describe("The bundle file (convention: *.bundle.ts). Default `<root>/<name>.bundle.ts`: <root> is the tsconfig's rootDir, else the common folder of the files it compiles. Required when there is no tsconfig to extend."),
79
+ .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
80
  name: zod_1.z
81
81
  .string(expected('letters, digits, "-" or "_", starting with a letter'))
82
82
  .regex(BUNDLE_NAME, expected('letters, digits, "-" or "_", starting with a letter'))
83
83
  .optional()
84
84
  .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
- tags: stringList().default([]).describe('Tags associated with the bundle.'),
86
- extends: nonEmptyString()
87
- .optional()
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(),
85
+ include: tsconfigGlobs('include'),
86
+ exclude: tsconfigGlobs('exclude'),
87
+ compilerOptions: COMPILER_OPTIONS_SCHEMA.optional(),
96
88
  }, expected('an object'));
97
89
  function formatPath(segments) {
98
90
  const formatted = segments
@@ -101,29 +93,13 @@ function formatPath(segments) {
101
93
  .replace(/^\./, '');
102
94
  return formatted || 'config';
103
95
  }
104
- function matchedBranch(issue) {
105
- const typeMatched = issue.errors.filter((branch) => branch.every((inner) => inner.code !== 'invalid_type' || inner.path.length > 0));
106
- return typeMatched.length === 1 ? typeMatched[0] : undefined;
107
- }
108
- function formatIssue(issue, prefix = []) {
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}`];
96
+ function formatIssue(issue) {
97
+ if (issue.code === 'unrecognized_keys') {
98
+ return issue.keys.map((key) => `${formatPath([...issue.path, key])}: unknown field`);
99
+ }
100
+ return [`${formatPath(issue.path)}: ${issue.message}`];
116
101
  }
117
102
  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
103
  function toClassSelector(classes) {
128
104
  if (classes === undefined)
129
105
  return { export: 'any', decorators: exports.DEFAULT_DECORATORS };
@@ -135,18 +111,6 @@ function toClassSelector(classes) {
135
111
  excludeName: excludeName === undefined ? undefined : (0, glob_1.globToRegExp)(excludeName),
136
112
  };
137
113
  }
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
114
  exports.CONFIG_FILE_SUFFIXES = ['.bundle.json', '.bundle.yaml', '.bundle.yml'];
151
115
  const configSuffix = (file) => exports.CONFIG_FILE_SUFFIXES.find((suffix) => file.endsWith(suffix));
152
116
  exports.configSuffix = configSuffix;
@@ -164,7 +128,8 @@ function resolveConfig(content, file) {
164
128
  const result = exports.BUNDLE_CONFIG_SCHEMA.safeParse(content);
165
129
  if (!result.success)
166
130
  throw new DomainException_1.TicConfigError(formatIssues(result.error));
167
- const { output, name, tags, extends: tsconfig, importExtension, files, classes } = result.data;
131
+ const { output, name, extends: tsconfig, include, exclude, compilerOptions = {} } = result.data;
132
+ const { importExtension, classes, ...tsCompilerOptions } = compilerOptions;
168
133
  const dir = node_path_1.default.dirname(file);
169
134
  const bundleName = name ?? configStem(file) ?? exports.DEFAULT_BUNDLE_NAME;
170
135
  return {
@@ -173,11 +138,9 @@ function resolveConfig(content, file) {
173
138
  output: output === undefined ? undefined : node_path_1.default.resolve(dir, output),
174
139
  name: bundleName,
175
140
  className: toClassName(bundleName),
176
- tags,
177
141
  tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.DEFAULT_EXTENDS), required: tsconfig !== undefined },
142
+ overrides: { include, exclude, compilerOptions: tsCompilerOptions },
178
143
  importExtension,
179
- files: toFileSelector(files),
180
144
  classes: toClassSelector(classes),
181
- warnings: excludeWarnings(files),
182
145
  };
183
146
  }
@@ -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)];
@@ -66,17 +62,6 @@ class ImportPaths {
66
62
  this.aliases = aliases;
67
63
  this.extension = extension;
68
64
  }
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`);
79
- }
80
65
  specifier(fromFile, toFile) {
81
66
  const target = (0, glob_1.toPosix)(toFile);
82
67
  const bare = target.replace(SOURCE_EXTENSION, '');
@@ -97,18 +82,6 @@ class ImportPaths {
97
82
  }
98
83
  return best;
99
84
  }
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
85
  matchAlias(pattern, aliasTarget, file, bare) {
113
86
  const wildcard = splitWildcard(aliasTarget);
114
87
  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, paths, tsconfigPath, classes }) {
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, paths, tsconfigPath, name, imports: [...byFile.values()], registrations: bindings };
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));
@@ -37,7 +37,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.isSourceFile = void 0;
40
- exports.listSourceFiles = listSourceFiles;
41
40
  exports.findClasses = findClasses;
42
41
  const node_fs_1 = require("node:fs");
43
42
  const node_path_1 = __importDefault(require("node:path"));
@@ -46,23 +45,6 @@ const SOURCE_FILE = /\.(tsx?|mts|cts)$/;
46
45
  const DECLARATION_FILE = /\.d\.[mc]?tsx?$/;
47
46
  const isSourceFile = (file) => SOURCE_FILE.test(file) && !DECLARATION_FILE.test(file);
48
47
  exports.isSourceFile = isSourceFile;
49
- function listSourceFiles(dir, recursive, isExcluded) {
50
- const files = [];
51
- const walk = (current) => {
52
- for (const entry of (0, node_fs_1.readdirSync)(current, { withFileTypes: true })) {
53
- const full = node_path_1.default.join(current, entry.name);
54
- if (entry.isDirectory()) {
55
- if (recursive && entry.name !== 'node_modules')
56
- walk(full);
57
- }
58
- else if ((0, exports.isSourceFile)(entry.name) && !isExcluded(full)) {
59
- files.push(full);
60
- }
61
- }
62
- };
63
- walk(dir);
64
- return files.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
65
- }
66
48
  const hasModifier = (node, kind) => ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((m) => m.kind === kind);
67
49
  function decoratorName(decorator, imports) {
68
50
  const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
@@ -17,7 +17,7 @@ function tokenCollisions(classes) {
17
17
  .map(([token, group]) => {
18
18
  const names = group.map((cls) => cls.className);
19
19
  return (`decorator token "${token}" is passed by ${names.join(', ')}; ` +
20
- `registration is last-wins, exclude one with classes.excludeClasses`);
20
+ `registration is last-wins, exclude one with compilerOptions.classes.excludeClasses`);
21
21
  });
22
22
  }
23
23
  function scopeDisjoint(group) {