@ts-ioc-container/bundler 6.0.0 → 8.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 (63) hide show
  1. package/AGENTS.md +28 -15
  2. package/README.md +63 -28
  3. package/cjm/app.js +16 -6
  4. package/cjm/cli/usage.js +8 -5
  5. package/cjm/features/{build → bundle}/BuildConfig.js +42 -10
  6. package/cjm/features/{build → bundle}/BuildModule.js +6 -4
  7. package/cjm/features/{build/BuildController.js → bundle/BundleController.js} +39 -20
  8. package/cjm/features/{build → bundle}/domain/tokenCollisions.js +1 -1
  9. package/cjm/features/{build/services/BundleBuilder.js → bundle/services/BundleService.js} +25 -41
  10. package/cjm/features/bundle/services/BundleView.js +58 -0
  11. package/cjm/features/{build → bundle}/services/TicConfigService.js +16 -5
  12. package/cjm/index.js +22 -21
  13. package/cjm/schema/ticConfigSchema.js +3 -3
  14. package/cjm/services/ConsoleLogger.js +3 -0
  15. package/package.json +2 -2
  16. package/tic.schema.json +4 -10
  17. package/typings/app.d.ts +26 -7
  18. package/typings/cli/usage.d.ts.map +1 -1
  19. package/typings/features/{build → bundle}/BuildConfig.d.ts +51 -8
  20. package/typings/features/bundle/BuildConfig.d.ts.map +1 -0
  21. package/typings/features/bundle/BuildModule.d.ts.map +1 -0
  22. package/typings/features/bundle/BundleController.d.ts +45 -0
  23. package/typings/features/bundle/BundleController.d.ts.map +1 -0
  24. package/typings/features/bundle/domain/ImportPaths.d.ts.map +1 -0
  25. package/typings/features/bundle/domain/glob.d.ts.map +1 -0
  26. package/typings/features/bundle/domain/scan.d.ts.map +1 -0
  27. package/typings/features/bundle/domain/tokenCollisions.d.ts.map +1 -0
  28. package/typings/features/bundle/domain/tsconfig.d.ts.map +1 -0
  29. package/typings/features/bundle/services/BundleService.d.ts +39 -0
  30. package/typings/features/bundle/services/BundleService.d.ts.map +1 -0
  31. package/typings/features/bundle/services/BundleView.d.ts +33 -0
  32. package/typings/features/bundle/services/BundleView.d.ts.map +1 -0
  33. package/typings/features/{build → bundle}/services/TicConfigService.d.ts +14 -8
  34. package/typings/features/bundle/services/TicConfigService.d.ts.map +1 -0
  35. package/typings/index.d.ts +12 -12
  36. package/typings/schema/ticConfigSchema.d.ts.map +1 -1
  37. package/typings/services/ConsoleLogger.d.ts +4 -1
  38. package/typings/services/ConsoleLogger.d.ts.map +1 -1
  39. package/cjm/features/build/domain/emit.js +0 -34
  40. package/typings/features/build/BuildConfig.d.ts.map +0 -1
  41. package/typings/features/build/BuildController.d.ts +0 -37
  42. package/typings/features/build/BuildController.d.ts.map +0 -1
  43. package/typings/features/build/BuildModule.d.ts.map +0 -1
  44. package/typings/features/build/domain/ImportPaths.d.ts.map +0 -1
  45. package/typings/features/build/domain/emit.d.ts +0 -36
  46. package/typings/features/build/domain/emit.d.ts.map +0 -1
  47. package/typings/features/build/domain/glob.d.ts.map +0 -1
  48. package/typings/features/build/domain/scan.d.ts.map +0 -1
  49. package/typings/features/build/domain/tokenCollisions.d.ts.map +0 -1
  50. package/typings/features/build/domain/tsconfig.d.ts.map +0 -1
  51. package/typings/features/build/services/BundleBuilder.d.ts +0 -61
  52. package/typings/features/build/services/BundleBuilder.d.ts.map +0 -1
  53. package/typings/features/build/services/TicConfigService.d.ts.map +0 -1
  54. /package/cjm/features/{build → bundle}/domain/ImportPaths.js +0 -0
  55. /package/cjm/features/{build → bundle}/domain/glob.js +0 -0
  56. /package/cjm/features/{build → bundle}/domain/scan.js +0 -0
  57. /package/cjm/features/{build → bundle}/domain/tsconfig.js +0 -0
  58. /package/typings/features/{build → bundle}/BuildModule.d.ts +0 -0
  59. /package/typings/features/{build → bundle}/domain/ImportPaths.d.ts +0 -0
  60. /package/typings/features/{build → bundle}/domain/glob.d.ts +0 -0
  61. /package/typings/features/{build → bundle}/domain/scan.d.ts +0 -0
  62. /package/typings/features/{build → bundle}/domain/tokenCollisions.d.ts +0 -0
  63. /package/typings/features/{build → bundle}/domain/tsconfig.d.ts +0 -0
package/AGENTS.md CHANGED
@@ -7,10 +7,11 @@ nothing here runs inside the container, and the core package is not changed by
7
7
  it.
8
8
 
9
9
  - The CLI is `ts-ioc-container`, with `tic` as a shortcut (both `bin` entries,
10
- same program): `tic build [<config>]` 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
10
+ same program): `tic build [<config>...]` is a Unix filter. It reads the config
11
+ file `<config>` (JSON for `.json`, YAML for `.yaml` / `.yml`) — several are
12
+ merged left to right into one bundle — or, without one or with `-`, config
13
+ text from stdin (parsed as YAML, which covers JSON; never merged) — and
14
+ prints the bundle to **stdout**; it writes nothing. Saving is the shell's
14
15
  job: `tic build app.bundle.json > src/di/app.bundle.ts`,
15
16
  `cat dev.bundle.json | tic build > src/di/dev.bundle.ts`; CI checks with
16
17
  `tic build app.bundle.json | diff - src/di/app.bundle.ts`. Warnings and errors
@@ -20,7 +21,7 @@ IContainerModule`, applied with `container.useModule(new AppBundle())`.
20
21
  bundle works wherever it is saved; a selected class no alias covers fails the
21
22
  build naming its file. There are no relative imports.
22
23
  - Config fields (schema: `tic.schema.json`), flat: `paths` with `paths.include`
23
- required; `className`, `name`, `tsconfig`, `importExtension` optional.
24
+ required; `match`, `name`, `tsconfig`, `importExtension` optional.
24
25
  Relative paths resolve against the config file; for piped text, against the
25
26
  working directory. `name` defaults to the file's stem (`prod.bundle.json` →
26
27
  `prod`) and is required for piped text or a file named otherwise; the class
@@ -31,9 +32,9 @@ IContainerModule`, applied with `container.useModule(new AppBundle())`.
31
32
  name and generated imports are written in (most specific alias, else a
32
33
  relative path) — and the module resolution that sets the import extension.
33
34
  - Selection has two stages, `paths: { include, exclude }` and
34
- `className: { ..., glob, exclude }`. `paths` is always an object (a string
35
- or a list in its place is an error); `className` also takes a string or a
36
- list as a shortcut for `className.glob`. `paths` decides by path which files
35
+ `match: { ..., glob, exclude }`. `paths` is always an object (a string
36
+ or a list in its place is an error); `match` also takes a string or a
37
+ list as a shortcut for `match.glob`. `paths` decides by path which files
37
38
  are read and parsed at all. `paths.include` (required: one folder or a
38
39
  non-empty list) names the folders scanned, always with their sub-folders —
39
40
  relative to the config file (`./src/services`) or tsconfig aliases
@@ -43,8 +44,8 @@ IContainerModule`, applied with `container.useModule(new AppBundle())`.
43
44
  catch-all `"*"` aliases are skipped; default: test files, `__tests__/`,
44
45
  `node_modules/`; a non-empty value replaces it, and the build warns if it drops
45
46
  a default). Files `tic build` generated are never input.
46
- - `className: { export, decorators, glob, exclude }` (or `className: '*Service'`
47
- / `['*Service', '*Repository']` = `className.glob`, default `export` and
47
+ - `match: { export, decorators, glob, exclude }` (or `match: '*Service'`
48
+ / `['*Service', '*Repository']` = `match.glob`, default `export` and
48
49
  `decorators`) then picks classes of the parsed files; default is every exported,
49
50
  non-abstract class decorated with `@register` (`decorators` defaults to
50
51
  `["register"]`; `[]` requires none); positive criteria must all hold — `glob` is one glob
@@ -54,12 +55,23 @@ IContainerModule`, applied with `container.useModule(new AppBundle())`.
54
55
  two selected classes pass the same plain-identifier first decorator argument (a
55
56
  same-token heuristic), unless a decorator they share has differing arguments
56
57
  (scope-gated, e.g. `@perPage('a')` vs `@perPage('b')`).
58
+ - Several configs merge like webpack-merge (`mergeConfigs`):
59
+ `tic build common.bundle.yml prod.bundle.yml > src/di/prod.bundle.ts`.
60
+ Objects merge field by field; the list fields `paths.include`,
61
+ `paths.exclude`, `match.glob`, `match.exclude` and `match.decorators`
62
+ concatenate without duplicates (a later config only adds); any other value
63
+ set later replaces an earlier one; the `match` shorthand merges as
64
+ `match.glob`. Each file is validated alone (`BUNDLE_CONFIG_FILE_SCHEMA`:
65
+ `paths` optional; errors name the file), the merged result as one config
66
+ (`BUNDLE_CONFIG_SCHEMA`). Defaults apply to the merged config only. The last
67
+ file names the bundle unless one sets `name`; all must sit in one folder.
57
68
  - Never edit a `*.bundle.ts` by hand — change the classes or the config and
58
69
  rerun `tic build`.
59
70
  - Unknown config fields are errors, at every level.
60
- - Programmatic API: `build({ config, cwd })` or `build({ text, cwd })` returns
71
+ - Programmatic API: `build({ config, cwd })` (`config`: a file or a list merged
72
+ left to right) or `build({ text, cwd })` returns
61
73
  `{ content, bundle, registrations, warnings }` and writes nothing;
62
- `loadConfig(file)`, `run(argv, io)` (`io.stdin` backs piped configs); every error the
74
+ `loadConfig(file | files)`, `run(argv, io)` (`io.stdin` backs piped configs); every error the
63
75
  bundler raises on purpose is a `TicError` with a stable `code`
64
76
  (`TicConfigError` = `INVALID_CONFIG`, `NamespaceNotFoundError`, `UsageError`,
65
77
  …).
@@ -69,7 +81,8 @@ IContainerModule`, applied with `container.useModule(new AppBundle())`.
69
81
  `@onDefault(invoke)` action (a word in the action position that names no
70
82
  declared action is the default action's argument: `tic build <config>`), which parses its own options with
71
83
  `@inject(pipe(commandArgs, parseOptions(spec), validate(SCHEMA)))`. Work lives in
72
- services behind tokens (`IBundleBuilderKey`, `ITicConfigServiceKey`, …); pure
73
- logic in `lib/features/build/domain/`. A new command is a controller plus a module.
84
+ services behind tokens (`IBundleServiceKey`, `ITicConfigServiceKey`, …); pure
85
+ logic in `lib/features/bundle/domain/`. A new command is a controller plus a module.
74
86
  - The config schema is `BUNDLE_CONFIG_SCHEMA` (zod); `tic.schema.json` is generated
75
- from it with `pnpm run generate:schema` — never edit it by hand.
87
+ from its per-file variant `BUNDLE_CONFIG_FILE_SCHEMA` with `pnpm run generate:schema`
88
+ — never edit it by hand.
package/README.md CHANGED
@@ -33,11 +33,12 @@ below use the shortcut.
33
33
  ## Build
34
34
 
35
35
  ```bash
36
- tic build [<config>]
36
+ tic build [<config>...]
37
37
  ```
38
38
 
39
39
  `tic build` prints the bundle to **stdout** and writes nothing. `<config>` is a
40
- config file — JSON for `.json`, YAML for `.yaml` / `.yml`. Without it (or with
40
+ config file — JSON for `.json`, YAML for `.yaml` / `.yml`. Several are
41
+ [merged](#merging-configs) left to right into one bundle. Without one (or with
41
42
  `-`) the config is read from **stdin**. Warnings and errors go to stderr, so
42
43
  they never end up in the bundle.
43
44
 
@@ -46,6 +47,9 @@ they never end up in the bundle.
46
47
  tic build app.bundle.json > src/di/app.bundle.ts
47
48
  tic build prod.bundle.yml > src/di/prod.bundle.ts
48
49
 
50
+ # several configs merged into one bundle, like webpack-merge
51
+ tic build common.bundle.yml prod.bundle.yml > src/di/prod.bundle.ts
52
+
49
53
  # the config piped in — it has no file name, so it sets `name` ("name": "dev")
50
54
  cat dev.bundle.json | tic build > src/di/dev.bundle.ts
51
55
  tic build < dev.bundle.yml > src/di/dev.bundle.ts
@@ -69,8 +73,9 @@ covers is an error naming its file; add a `paths` entry for its folder.
69
73
 
70
74
  Relative paths in a config file resolve against the file; in a piped config,
71
75
  against the working directory. The bundle is named after the config file —
72
- `prod.bundle.json` exports `ProdBundle` — unless the config sets `name`, which a
73
- piped config, or a file not named `<name>.bundle.json`, must.
76
+ `prod.bundle.json` exports `ProdBundle`; of several merged, after the last —
77
+ unless the config sets `name`, which a piped config, or a file not named
78
+ `<name>.bundle.json`, must.
74
79
 
75
80
  ## Configure
76
81
 
@@ -94,8 +99,8 @@ paths:
94
99
 
95
100
  | Field | Default | Meaning |
96
101
  | ----------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
97
- | `paths` | — (required) | Which files are parsed — see [Selecting files](#selecting-files) |
98
- | `className` | exported classes with `@register` | Which classes of a parsed file are registered — see [Selecting classes](#selecting-classes) |
102
+ | `paths` | — (required) | Which files are parsed — see [Selecting files](#selecting-files). Merged configs need it in one of them |
103
+ | `match` | exported classes with `@register` | Which classes of a parsed file are registered — see [Selecting classes](#selecting-classes) |
99
104
  | `name` | the config file's (`app.bundle.json` → `app`) | The bundle's name: names the class, `AppBundle`. Required for a piped config or a file named otherwise |
100
105
  | `tsconfig` | `./tsconfig.json` | Source of the `paths` aliases every import is written through, and of the import extension — never of files |
101
106
  | `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
@@ -105,31 +110,37 @@ unnoticed.
105
110
 
106
111
  ## Several bundles
107
112
 
108
- Need more than one bundle — per environment, per app? Keep a config per bundle
109
- and build each one:
113
+ Need more than one bundle — per environment, per app? Keep what they share in
114
+ one config, what each adds in another, and build each environment from both:
110
115
 
111
116
  ```bash
112
117
  for env in prod dev test; do
113
- tic build "$env.bundle.yml" > "src/di/$env.bundle.ts"
118
+ tic build common.bundle.yml "$env.bundle.yml" > "src/di/$env.bundle.ts"
114
119
  done
115
120
  ```
116
121
 
117
122
  ```json
118
123
  {
119
124
  "scripts": {
120
- "bundle": "tic build app.bundle.json > src/di/app.bundle.ts",
121
- "bundle:check": "tic build app.bundle.json | diff - src/di/app.bundle.ts"
125
+ "bundle": "tic build common.bundle.yml prod.bundle.yml > src/di/prod.bundle.ts",
126
+ "bundle:check": "tic build common.bundle.yml prod.bundle.yml | diff - src/di/prod.bundle.ts"
122
127
  }
123
128
  }
124
129
  ```
125
130
 
126
- Each config selects its own files and classes:
127
-
128
131
  ```yaml
129
- # prod.bundle.yml: services + adapters, minus dev-only *.dev.ts files
132
+ # common.bundle.yml: what every environment registers
130
133
  paths:
131
134
  include:
132
135
  - '@app/services'
136
+ ```
137
+
138
+ Each environment's config adds its own files and classes:
139
+
140
+ ```yaml
141
+ # prod.bundle.yml: + the adapters, minus dev-only *.dev.ts files
142
+ paths:
143
+ include:
133
144
  - ./src/infra
134
145
  exclude:
135
146
  - '**/*.dev.ts'
@@ -142,15 +153,39 @@ paths:
142
153
  ```
143
154
 
144
155
  ```yaml
145
- # dev.bundle.yml: the *.dev.ts logger replaces the production one, dropped by class name
156
+ # dev.bundle.yml: + the adapters; the *.dev.ts logger replaces the production one, dropped by class name
146
157
  paths:
147
158
  include:
148
- - '@app/services'
149
159
  - ./src/infra
150
- className:
160
+ match:
151
161
  exclude: JsonLogger
152
162
  ```
153
163
 
164
+ `tic build common.bundle.yml prod.bundle.yml` then scans `@app/services` and
165
+ `./src/infra`, and exports `ProdBundle`.
166
+
167
+ ### Merging configs
168
+
169
+ `tic build a.bundle.yml b.bundle.yml c.bundle.yml` merges the configs left to
170
+ right, the way webpack-merge merges webpack configs:
171
+
172
+ - objects merge field by field;
173
+ - list fields — `paths.include`, `paths.exclude`, `match.glob`, `match.exclude`,
174
+ `match.decorators` — concatenate in order, without duplicates (one folder or
175
+ glob counts as a list of one), so a later config can only add to them;
176
+ - any other value (`name`, `tsconfig`, `importExtension`, `match.export`) set
177
+ by a later config replaces an earlier one;
178
+ - the `match` shorthand merges as `match.glob`.
179
+
180
+ Each config is validated on its own — an error names the file it is in — so a
181
+ shared config may leave `paths` out; the merged config must have
182
+ `paths.include`. Defaults apply to the merged config only: a `paths.exclude` in
183
+ any of them replaces the default excludes, as it does in a single config, so
184
+ list the defaults in the shared config to keep them. The bundle is named after
185
+ the last config, unless one sets `name` — so leave `name` out of a shared
186
+ config. Merged configs must sit in one folder, which their relative paths
187
+ resolve against. A piped config (`-`) is never merged.
188
+
154
189
  A saved bundle is never registered by another build, even when it sits in a
155
190
  folder that build scans.
156
191
 
@@ -160,7 +195,7 @@ A bundle picks its registrations in two stages:
160
195
 
161
196
  1. **`paths`** — decided by path alone, before anything is read. Only files that
162
197
  pass are parsed.
163
- 2. **`className`** — decided per class, on the files stage 1 let through.
198
+ 2. **`match`** — decided per class, on the files stage 1 let through.
164
199
 
165
200
  Parsing is where the time goes, so point `paths` at the folders that hold your
166
201
  registrations, and `exclude` what they should not contribute:
@@ -171,7 +206,7 @@ registrations, and `exclude` what they should not contribute:
171
206
  "include": "@app/services",
172
207
  "exclude": ["**/*.spec.ts", "**/*.test.ts", "**/__tests__/**", "**/node_modules/**", "**/legacy/**"]
173
208
  },
174
- "className": { "decorators": ["register"] }
209
+ "match": { "decorators": ["register"] }
175
210
  }
176
211
  ```
177
212
 
@@ -233,11 +268,11 @@ parses everything, including tests.
233
268
  ## Selecting classes
234
269
 
235
270
  By default every exported, non-abstract class of a parsed file that carries
236
- `@register` is registered. `className` changes that; a class must meet every
271
+ `@register` is registered. `match` changes that; a class must meet every
237
272
  criterion that is set:
238
273
 
239
274
  ```json
240
- "className": {
275
+ "match": {
241
276
  "export": "named",
242
277
  "decorators": ["register", "repository"],
243
278
  "glob": "*Service"
@@ -261,7 +296,7 @@ bind the same token, registration is last-wins and the stand-in silently wins.
261
296
  matches only itself, so one list drops a class by name and a family by pattern:
262
297
 
263
298
  ```json
264
- "className": {
299
+ "match": {
265
300
  "decorators": ["repository", "service"],
266
301
  "exclude": ["MockDashboardRepository", "*Fake"]
267
302
  }
@@ -270,18 +305,18 @@ matches only itself, so one list drops a class by name and a family by pattern:
270
305
  `exclude` applies after every other criterion, so it combines with
271
306
  `decorators` and `glob` freely.
272
307
 
273
- ### The `className` shortcut
308
+ ### The `match` shortcut
274
309
 
275
- When the default `export` and `decorators` are all you need, `className`
310
+ When the default `export` and `decorators` are all you need, `match`
276
311
  itself can be the class-name globs — a string or a list is a shortcut for
277
- `className.glob`:
312
+ `match.glob`:
278
313
 
279
314
  ```yaml
280
- className: '*Service' # = className: { glob: '*Service' }
315
+ match: '*Service' # = match: { glob: '*Service' }
281
316
  ```
282
317
 
283
318
  ```yaml
284
- className: ['*Service', '*Repository'] # = className: { glob: ['*Service', '*Repository'] }
319
+ match: ['*Service', '*Repository'] # = match: { glob: ['*Service', '*Repository'] }
285
320
  ```
286
321
 
287
322
  As a safety net, the build warns when two selected classes pass the same
@@ -292,7 +327,7 @@ are not resolved), so it can only be a heuristic:
292
327
  ```text
293
328
  tic: warning: app.bundle.json: decorator token "IDashboardRepositoryToken" is passed by
294
329
  HttpDashboardRepository, MockDashboardRepository; registration is last-wins,
295
- exclude one with className.exclude
330
+ exclude one with match.exclude
296
331
  ```
297
332
 
298
333
  Classes that are scope-gated are not last-wins, so they do not warn: when the
package/cjm/app.js CHANGED
@@ -1,16 +1,20 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.createContainer = createContainer;
4
7
  exports.run = run;
5
8
  exports.build = build;
6
9
  exports.loadConfig = loadConfig;
7
10
  require("reflect-metadata");
11
+ const node_path_1 = __importDefault(require("node:path"));
8
12
  const ts_ioc_container_1 = require("ts-ioc-container");
9
13
  const Application_1 = require("./cli/Application");
10
14
  const CliIo_1 = require("./domain/CliIo");
11
- const BuildModule_1 = require("./features/build/BuildModule");
12
- const TicConfigService_1 = require("./features/build/services/TicConfigService");
13
- const BundleBuilder_1 = require("./features/build/services/BundleBuilder");
15
+ const BuildModule_1 = require("./features/bundle/BuildModule");
16
+ const TicConfigService_1 = require("./features/bundle/services/TicConfigService");
17
+ const BundleService_1 = require("./features/bundle/services/BundleService");
14
18
  const HelpModule_1 = require("./features/help/HelpModule");
15
19
  const CommonModule_1 = require("./modules/CommonModule");
16
20
  function createContainer(io = (0, CliIo_1.processIo)()) {
@@ -32,8 +36,14 @@ function run(argv, io = (0, CliIo_1.processIo)()) {
32
36
  return Application_1.Application.bootstrap(createContainer(io)).run(...argv);
33
37
  }
34
38
  function build({ cwd = process.cwd(), ...request }) {
35
- return withContainer({ ...(0, CliIo_1.processIo)(), cwd }, (container) => BundleBuilder_1.IBundleBuilderKey.resolve(container).build(request));
39
+ return withContainer({ ...(0, CliIo_1.processIo)(), cwd }, (container) => {
40
+ const configService = TicConfigService_1.ITicConfigServiceKey.resolve(container);
41
+ const config = 'text' in request
42
+ ? configService.parse(request.text, cwd)
43
+ : configService.load([request.config].flat().map((file) => node_path_1.default.resolve(cwd, file)));
44
+ return BundleService_1.IBundleServiceKey.resolve(container).bundle(config);
45
+ });
36
46
  }
37
- function loadConfig(file) {
38
- return withContainer((0, CliIo_1.processIo)(), (container) => TicConfigService_1.ITicConfigServiceKey.resolve(container).load(file));
47
+ function loadConfig(files) {
48
+ return withContainer((0, CliIo_1.processIo)(), (container) => TicConfigService_1.ITicConfigServiceKey.resolve(container).load(files));
39
49
  }
package/cjm/cli/usage.js CHANGED
@@ -6,17 +6,20 @@ exports.USAGE = [
6
6
  ' tic is a shortcut for ts-ioc-container',
7
7
  '',
8
8
  'Commands:',
9
- ' ts-ioc-container build [<config>] print the bundle a config file (.json, .yaml, .yml) describes;',
10
- ' without one (or with -), read the config from stdin',
11
- ' ts-ioc-container help show this help',
12
- ' ts-ioc-container version show the version',
9
+ ' ts-ioc-container build [<config>...] print the bundle a config file (.json, .yaml, .yml) describes;',
10
+ ' several are merged left to right into one bundle;',
11
+ ' without one (or with -), read the config from stdin',
12
+ ' ts-ioc-container help show this help',
13
+ ' ts-ioc-container version show the version',
13
14
  '',
14
15
  'Options:',
15
16
  ' -h, --help show this help',
16
17
  ' -v, --version show the version',
17
18
  '',
18
- 'The bundle goes to stdout and warnings to stderr; saving it is up to you:',
19
+ 'The bundle goes to stdout; the files in it and warnings to stderr.',
20
+ 'Saving it is up to you:',
19
21
  ' tic build app.bundle.json > src/di/app.bundle.ts',
22
+ ' tic build common.bundle.yml prod.bundle.yml > src/di/prod.bundle.ts # merged left to right',
20
23
  ' cat app.bundle.yml | tic build > src/di/app.bundle.ts # config from stdin (must set name)',
21
24
  ' tic build app.bundle.json | diff - src/di/app.bundle.ts # CI: is it current?',
22
25
  ].join('\n');
@@ -3,9 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.configFormat = exports.STDIN = exports.CONFIG_FILE_SUFFIXES = exports.BUNDLE_CONFIG_SCHEMA = exports.DEFAULT_EXCLUDE = exports.DEFAULT_DECORATORS = exports.DEFAULT_TSCONFIG = void 0;
6
+ exports.configFormat = exports.STDIN = exports.CONFIG_FILE_SUFFIXES = exports.BUNDLE_CONFIG_FILE_SCHEMA = exports.BUNDLE_CONFIG_SCHEMA = exports.DEFAULT_EXCLUDE = exports.DEFAULT_DECORATORS = exports.DEFAULT_TSCONFIG = void 0;
7
7
  exports.parseConfig = parseConfig;
8
8
  exports.toClassName = toClassName;
9
+ exports.mergeConfigs = mergeConfigs;
9
10
  exports.resolveConfig = resolveConfig;
10
11
  const node_path_1 = __importDefault(require("node:path"));
11
12
  const yaml_1 = require("yaml");
@@ -42,6 +43,7 @@ const globs = () => globList('a glob or a non-empty array of globs', 1);
42
43
  const toList = (value) => (Array.isArray(value) ? value : [value]);
43
44
  const toGlobs = (value) => value === undefined ? undefined : toList(value).map(glob_1.globToRegExp);
44
45
  const FOLDERS_SCHEMA = zod_1.z.array(nonEmptyString(), expected('a non-empty array')).min(1, expected('a non-empty array'));
46
+ const PATHS_DESCRIPTION = 'Which files this bundle reads and parses — decided by path alone, before parsing. `include` names the folders the files come from; within them a file is parsed unless it matches one of `exclude`. Globs are relative to the config file.';
45
47
  const PATHS_SCHEMA = zod_1.z
46
48
  .strictObject({
47
49
  include: zod_1.z
@@ -51,7 +53,7 @@ const PATHS_SCHEMA = zod_1.z
51
53
  .default(exports.DEFAULT_EXCLUDE)
52
54
  .describe('Glob, or globs, of files never read, relative to the config file or starting with a tsconfig paths alias (@generated/**, or @generated for the whole folder). Replaces the default when given. A non-empty value that omits a default glob makes `tic build` warn.'),
53
55
  }, expected('an object with "include"'))
54
- .describe('Which files this bundle reads and parses — decided by path alone, before parsing. `include` names the folders the files come from; within them a file is parsed unless it matches one of `exclude`. Globs are relative to the config file.');
56
+ .describe(PATHS_DESCRIPTION);
55
57
  const CLASS_OBJECT_SCHEMA = zod_1.z.strictObject({
56
58
  export: zod_1.z
57
59
  .enum(['any', 'named', 'default'], expected('"any", "named" or "default"'))
@@ -93,8 +95,11 @@ exports.BUNDLE_CONFIG_SCHEMA = zod_1.z.strictObject({
93
95
  .optional()
94
96
  .describe('Extension of generated imports, e.g. ".js". Inferred from the tsconfig: ".js" under node16/nodenext resolution, none otherwise.'),
95
97
  paths: PATHS_SCHEMA,
96
- className: CLASSES_SCHEMA.optional(),
98
+ match: CLASSES_SCHEMA.optional(),
97
99
  }, expected('an object'));
100
+ exports.BUNDLE_CONFIG_FILE_SCHEMA = exports.BUNDLE_CONFIG_SCHEMA.extend({
101
+ paths: PATHS_SCHEMA.partial().optional().describe(`${PATHS_DESCRIPTION} Required in the merged config.`),
102
+ });
98
103
  function formatPath(segments) {
99
104
  const formatted = segments
100
105
  .map((segment) => (typeof segment === 'number' ? `[${segment}]` : `.${String(segment)}`))
@@ -122,13 +127,13 @@ function toPathSelector(paths) {
122
127
  exclude: toList(paths.exclude),
123
128
  };
124
129
  }
125
- function toClassSelector(className) {
126
- if (className === undefined)
130
+ function toClassSelector(match) {
131
+ if (match === undefined)
127
132
  return { export: 'any', decorators: exports.DEFAULT_DECORATORS };
128
- if (typeof className === 'string' || Array.isArray(className)) {
129
- return { export: 'any', decorators: exports.DEFAULT_DECORATORS, glob: toGlobs(className) };
133
+ if (typeof match === 'string' || Array.isArray(match)) {
134
+ return { export: 'any', decorators: exports.DEFAULT_DECORATORS, glob: toGlobs(match) };
130
135
  }
131
- const { decorators, glob, exclude, ...rest } = className;
136
+ const { decorators, glob, exclude, ...rest } = match;
132
137
  return {
133
138
  ...rest,
134
139
  decorators: decorators.length > 0 ? decorators : undefined,
@@ -170,11 +175,38 @@ function toClassName(name) {
170
175
  const words = name.split(/[-_]+/).filter(Boolean);
171
176
  return `${words.map((word) => word[0].toUpperCase() + word.slice(1)).join('')}Bundle`;
172
177
  }
178
+ const LIST_FIELDS = new Set(['paths.include', 'paths.exclude', 'match.decorators', 'match.glob', 'match.exclude']);
179
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
180
+ function mergeInto(base, next, at = '') {
181
+ const merged = { ...base };
182
+ for (const [key, value] of Object.entries(next)) {
183
+ const field = `${at}${key}`;
184
+ const previous = merged[key];
185
+ if (LIST_FIELDS.has(field)) {
186
+ merged[key] = [
187
+ ...new Set([...toList((previous ?? [])), ...toList(value)]),
188
+ ];
189
+ }
190
+ else {
191
+ merged[key] = isObject(previous) && isObject(value) ? mergeInto(previous, value, `${field}.`) : value;
192
+ }
193
+ }
194
+ return merged;
195
+ }
196
+ const expandMatch = ({ match, ...rest }) => match === undefined ? rest : { ...rest, match: isObject(match) ? match : { glob: match } };
197
+ function mergeConfigs(configs) {
198
+ return configs.reduce((merged, { file, content }) => {
199
+ const result = exports.BUNDLE_CONFIG_FILE_SCHEMA.safeParse(content);
200
+ if (!result.success)
201
+ throw new DomainException_1.TicConfigError(`${node_path_1.default.basename(file)}: ${formatIssues(result.error)}`);
202
+ return mergeInto(merged, expandMatch(content));
203
+ }, {});
204
+ }
173
205
  function resolveConfig(content, file) {
174
206
  const result = exports.BUNDLE_CONFIG_SCHEMA.safeParse(content);
175
207
  if (!result.success)
176
208
  throw new DomainException_1.TicConfigError(formatIssues(result.error));
177
- const { name, tsconfig, importExtension, paths, className } = result.data;
209
+ const { name, tsconfig, importExtension, paths, match } = result.data;
178
210
  const bundleName = name ?? configStem(file);
179
211
  if (bundleName === undefined) {
180
212
  throw new DomainException_1.TicConfigError(node_path_1.default.basename(file) === exports.STDIN
@@ -190,7 +222,7 @@ function resolveConfig(content, file) {
190
222
  tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.DEFAULT_TSCONFIG), required: tsconfig !== undefined },
191
223
  importExtension,
192
224
  paths: toPathSelector(paths),
193
- className: toClassSelector(className),
225
+ match: toClassSelector(match),
194
226
  warnings: excludeWarnings(paths),
195
227
  };
196
228
  }
@@ -2,14 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BuildModule = void 0;
4
4
  const ts_ioc_container_1 = require("ts-ioc-container");
5
- const BuildController_1 = require("./BuildController");
6
- const BundleBuilder_1 = require("./services/BundleBuilder");
5
+ const BundleController_1 = require("./BundleController");
6
+ const BundleService_1 = require("./services/BundleService");
7
+ const BundleView_1 = require("./services/BundleView");
7
8
  const TicConfigService_1 = require("./services/TicConfigService");
8
9
  class BuildModule {
9
10
  applyTo(container) {
10
11
  container
11
- .addRegistration(ts_ioc_container_1.Registration.fromClass(BuildController_1.BuildController))
12
- .addRegistration(ts_ioc_container_1.Registration.fromClass(BundleBuilder_1.BundleBuilder))
12
+ .addRegistration(ts_ioc_container_1.Registration.fromClass(BundleController_1.BundleController))
13
+ .addRegistration(ts_ioc_container_1.Registration.fromClass(BundleService_1.BundleService))
14
+ .addRegistration(ts_ioc_container_1.Registration.fromClass(BundleView_1.BundleView))
13
15
  .addRegistration(ts_ioc_container_1.Registration.fromClass(TicConfigService_1.TicConfigService));
14
16
  }
15
17
  }
@@ -11,8 +11,12 @@ var __metadata = (this && this.__metadata) || function (k, v) {
11
11
  var __param = (this && this.__param) || function (paramIndex, decorator) {
12
12
  return function (target, key) { decorator(target, key, paramIndex); }
13
13
  };
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
14
17
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.BuildController = exports.BUILD_OPTIONS = void 0;
18
+ exports.BundleController = exports.BUILD_OPTIONS = void 0;
19
+ const node_path_1 = __importDefault(require("node:path"));
16
20
  const ts_ioc_container_1 = require("ts-ioc-container");
17
21
  const zod_1 = require("zod");
18
22
  const cli_1 = require("../../cli");
@@ -21,30 +25,44 @@ const DomainException_1 = require("../../exceptions/DomainException");
21
25
  const ConsoleLogger_1 = require("../../services/ConsoleLogger");
22
26
  const OutputService_1 = require("../../services/OutputService");
23
27
  const BuildConfig_1 = require("./BuildConfig");
24
- const BundleBuilder_1 = require("./services/BundleBuilder");
28
+ const glob_1 = require("./domain/glob");
29
+ const BundleService_1 = require("./services/BundleService");
30
+ const TicConfigService_1 = require("./services/TicConfigService");
25
31
  exports.BUILD_OPTIONS = zod_1.z.object({ positionals: zod_1.z.array(zod_1.z.string().min(1)) }).transform(({ positionals }, ctx) => {
26
- if (positionals.length > 1) {
27
- ctx.addIssue({ code: 'custom', path: ['config'], message: `expected one <config>, got ${positionals.join(' ')}` });
32
+ if (positionals.length > 1 && positionals.includes(BuildConfig_1.STDIN)) {
33
+ ctx.addIssue({
34
+ code: 'custom',
35
+ path: ['config'],
36
+ message: `${BuildConfig_1.STDIN} reads one config from stdin and cannot be merged with config files`,
37
+ });
28
38
  return zod_1.z.NEVER;
29
39
  }
30
- return { config: positionals[0] ?? BuildConfig_1.STDIN };
40
+ return { configs: positionals.length > 0 ? positionals : [BuildConfig_1.STDIN] };
31
41
  });
32
42
  const buildArgs = (0, cli_1.parseOptions)({}, { positionals: true });
33
- let BuildController = class BuildController {
43
+ let BundleController = class BundleController {
34
44
  io;
35
- builder;
45
+ configService;
46
+ bundleService;
36
47
  output;
37
48
  logger;
38
- constructor(io, builder, output, logger) {
49
+ constructor(io, configService, bundleService, output, logger) {
39
50
  this.io = io;
40
- this.builder = builder;
51
+ this.configService = configService;
52
+ this.bundleService = bundleService;
41
53
  this.output = output;
42
54
  this.logger = logger;
43
55
  }
44
- build({ config }) {
45
- const label = config === BuildConfig_1.STDIN ? '<stdin>' : config;
56
+ bundle({ configs }) {
57
+ const stdin = configs[0] === BuildConfig_1.STDIN;
58
+ const label = stdin ? '<stdin>' : configs.join(' + ');
46
59
  try {
47
- const { content, warnings } = this.builder.build(config === BuildConfig_1.STDIN ? { text: this.stdin() } : { config });
60
+ const config = stdin
61
+ ? this.configService.parse(this.stdin(), this.io.cwd)
62
+ : this.configService.load(configs.map((file) => node_path_1.default.resolve(this.io.cwd, file)));
63
+ const { content, files, warnings } = this.bundleService.bundle(config);
64
+ for (const file of files)
65
+ this.logger.info((0, glob_1.toPosix)(node_path_1.default.relative(this.io.cwd, file)));
48
66
  for (const warning of warnings)
49
67
  this.logger.warn(`${label}: ${warning}`);
50
68
  this.output.write(content.replace(/\n$/, ''));
@@ -63,19 +81,20 @@ let BuildController = class BuildController {
63
81
  return text;
64
82
  }
65
83
  };
66
- exports.BuildController = BuildController;
84
+ exports.BundleController = BundleController;
67
85
  __decorate([
68
86
  (0, cli_1.onDefault)(ts_ioc_container_1.invoke),
69
87
  __param(0, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.pipe)(cli_1.commandArgs, buildArgs, (0, cli_1.validate)(exports.BUILD_OPTIONS)))),
70
88
  __metadata("design:type", Function),
71
89
  __metadata("design:paramtypes", [Object]),
72
90
  __metadata("design:returntype", void 0)
73
- ], BuildController.prototype, "build", null);
74
- exports.BuildController = BuildController = __decorate([
91
+ ], BundleController.prototype, "bundle", null);
92
+ exports.BundleController = BundleController = __decorate([
75
93
  (0, ts_ioc_container_1.register)('build'),
76
94
  __param(0, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(CliIo_1.CliIoKey))),
77
- __param(1, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(BundleBuilder_1.IBundleBuilderKey))),
78
- __param(2, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(OutputService_1.IOutputServiceKey))),
79
- __param(3, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(ConsoleLogger_1.ILoggerKey))),
80
- __metadata("design:paramtypes", [Object, Object, Object, Object])
81
- ], BuildController);
95
+ __param(1, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(TicConfigService_1.ITicConfigServiceKey))),
96
+ __param(2, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(BundleService_1.IBundleServiceKey))),
97
+ __param(3, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(OutputService_1.IOutputServiceKey))),
98
+ __param(4, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(ConsoleLogger_1.ILoggerKey))),
99
+ __metadata("design:paramtypes", [Object, Object, Object, Object, Object])
100
+ ], BundleController);
@@ -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 className.exclude`);
20
+ `registration is last-wins, exclude one with match.exclude`);
21
21
  });
22
22
  }
23
23
  function scopeDisjoint(group) {