@ts-ioc-container/bundler 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +40 -0
- package/README.md +335 -0
- package/cjm/ImportPaths.js +134 -0
- package/cjm/bin.js +5 -0
- package/cjm/build.js +90 -0
- package/cjm/cli.js +82 -0
- package/cjm/config.js +118 -0
- package/cjm/emit.js +33 -0
- package/cjm/errors.js +15 -0
- package/cjm/exportPredicate.js +13 -0
- package/cjm/glob.js +27 -0
- package/cjm/inclusion.js +24 -0
- package/cjm/index.js +36 -0
- package/cjm/predicateFile.js +30 -0
- package/cjm/render.js +15 -0
- package/cjm/scan.js +144 -0
- package/cjm/utils.js +2 -0
- package/lib/ImportPaths.ts +127 -0
- package/lib/bin.ts +4 -0
- package/lib/build.ts +157 -0
- package/lib/cli.ts +88 -0
- package/lib/config.ts +206 -0
- package/lib/emit.ts +47 -0
- package/lib/errors.ts +14 -0
- package/lib/exportPredicate.ts +45 -0
- package/lib/glob.ts +24 -0
- package/lib/inclusion.ts +61 -0
- package/lib/index.ts +40 -0
- package/lib/predicateFile.ts +36 -0
- package/lib/render.ts +19 -0
- package/lib/scan.ts +133 -0
- package/lib/utils.ts +2 -0
- package/package.json +84 -0
- package/tic.schema.json +134 -0
- package/tpl/index.cjs +81 -0
- package/typings/ImportPaths.d.ts +37 -0
- package/typings/ImportPaths.d.ts.map +1 -0
- package/typings/bin.d.ts +3 -0
- package/typings/bin.d.ts.map +1 -0
- package/typings/build.d.ts +39 -0
- package/typings/build.d.ts.map +1 -0
- package/typings/cli.d.ts +8 -0
- package/typings/cli.d.ts.map +1 -0
- package/typings/config.d.ts +95 -0
- package/typings/config.d.ts.map +1 -0
- package/typings/emit.d.ts +18 -0
- package/typings/emit.d.ts.map +1 -0
- package/typings/errors.d.ts +13 -0
- package/typings/errors.d.ts.map +1 -0
- package/typings/exportPredicate.d.ts +36 -0
- package/typings/exportPredicate.d.ts.map +1 -0
- package/typings/glob.d.ts +7 -0
- package/typings/glob.d.ts.map +1 -0
- package/typings/inclusion.d.ts +42 -0
- package/typings/inclusion.d.ts.map +1 -0
- package/typings/index.d.ts +12 -0
- package/typings/index.d.ts.map +1 -0
- package/typings/predicateFile.d.ts +16 -0
- package/typings/predicateFile.d.ts.map +1 -0
- package/typings/render.d.ts +8 -0
- package/typings/render.d.ts.map +1 -0
- package/typings/scan.d.ts +22 -0
- package/typings/scan.d.ts.map +1 -0
- package/typings/utils.d.ts +3 -0
- package/typings/utils.d.ts.map +1 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# AGENTS.md — @ts-ioc-container/bundler
|
|
2
|
+
|
|
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
|
|
5
|
+
(`useModule(new AppBundle())`). Discovery happens at build time (ADR 0022);
|
|
6
|
+
nothing here runs inside the container, and the core package is not changed by
|
|
7
|
+
it.
|
|
8
|
+
|
|
9
|
+
- The CLI is `ts-ioc-container`, with `tic` as a shortcut (both `bin` entries,
|
|
10
|
+
same program). `tic build` reads `tic.config.json` (schema: `tic.schema.json`) and writes one
|
|
11
|
+
bundle per `bundles[]` entry — a generated class, `export class AppBundle implements IContainerModule`, applied with `container.useModule(new AppBundle())`. `--check` writes nothing and exits 1
|
|
12
|
+
when an output is stale — run it in CI.
|
|
13
|
+
- A namespace is a folder: relative to the config file, or a tsconfig `paths`
|
|
14
|
+
alias. Generated imports use the most specific alias, else a relative path.
|
|
15
|
+
- Default selection is every exported, non-abstract class. A module's
|
|
16
|
+
`select: { export, decorators, nameGlob }` narrows it; a class must meet every
|
|
17
|
+
criterion set. Decorators match by name, so composed ones must be listed.
|
|
18
|
+
- Never edit a `*.bundle.ts` by hand — change the classes or the config and
|
|
19
|
+
rerun `tic build`.
|
|
20
|
+
- Files take part when they match no `exclude` glob **and** an
|
|
21
|
+
`InclusionPredicate` (`({ filename }) => boolean`, filename relative to the
|
|
22
|
+
config) returns true. The predicate comes from `build({ include })`, else the
|
|
23
|
+
bundle's `include` file, else a conventional `tic.include.*` next to the
|
|
24
|
+
config. Predicate files are `require`d once per process — read env vars
|
|
25
|
+
inside the predicate. README "Recipe: generate per environment" shows the
|
|
26
|
+
per-environment setup.
|
|
27
|
+
- Right after parsing, an `ExportPredicate` (`FilterPredicate<ExportContext>`:
|
|
28
|
+
filename, exportName, className, isDefault, decorators, tags) filters class by
|
|
29
|
+
class, after `select`; found via `build({ filterExports })`, a bundle's
|
|
30
|
+
`filterExports` file, or `tic.exports.*` next to the config.
|
|
31
|
+
- Prefer `byTags((tags, context) => ...)` (a `TagInclusionPredicate`) over parsing
|
|
32
|
+
file names: tags are the dot-parts between base name and extension
|
|
33
|
+
(`Report.production.eu.ts` → `['production', 'eu']`). Usage examples:
|
|
34
|
+
`__tests__/examples/inclusion-predicates.spec.ts`.
|
|
35
|
+
- For a lean production build, the production entry must import only the
|
|
36
|
+
production bundle (README "One bundle per environment"); statically importing
|
|
37
|
+
both bundles ships both.
|
|
38
|
+
- Programmatic API: `build({ config, cwd, check })`, `run(argv, io)`,
|
|
39
|
+
`loadConfig(file)`; errors are `TicConfigError` / `NamespaceNotFoundError`
|
|
40
|
+
(both `TicError`).
|
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# @ts-ioc-container/bundler
|
|
2
|
+
|
|
3
|
+
**Bundles your dependencies into a single container module.** `tic build` scans
|
|
4
|
+
your folders — by path or tsconfig alias — and generates one typed bundle that
|
|
5
|
+
registers every class in them, ready for
|
|
6
|
+
[`ts-ioc-container`](../ts-ioc-container):
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
const container = new Container().useModule(new AppBundle());
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Adding a service means writing the class — no hand-maintained list of
|
|
13
|
+
`addRegistration(...)` calls to keep in sync. The bundle is ordinary
|
|
14
|
+
TypeScript, generated at build time, so it type-checks, bundles and tree-shakes
|
|
15
|
+
like code you wrote yourself.
|
|
16
|
+
|
|
17
|
+
In TypeScript a namespace is an import path, so folders are named the way
|
|
18
|
+
imports are: relative to the config file (`./src/services`) or through a
|
|
19
|
+
`tsconfig.json` `paths` alias (`@app/services`).
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add -D @ts-ioc-container/bundler
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It installs the CLI as `ts-ioc-container`, with **`tic`** as a shortcut —
|
|
28
|
+
`ts-ioc-container build` and `tic build` are the same command; the examples
|
|
29
|
+
below use the shortcut.
|
|
30
|
+
|
|
31
|
+
`typescript` (>= 5) is a peer dependency.
|
|
32
|
+
|
|
33
|
+
## Configure
|
|
34
|
+
|
|
35
|
+
`tic.config.json`, next to your `tsconfig.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"$schema": "./node_modules/@ts-ioc-container/bundler/tic.schema.json",
|
|
40
|
+
"bundles": [
|
|
41
|
+
{
|
|
42
|
+
"output": "src/di/app.bundle.ts",
|
|
43
|
+
"name": "AppBundle",
|
|
44
|
+
"namespaces": ["@app/services", { "path": "./src/infra", "recursive": false }]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Field | Default | Meaning |
|
|
51
|
+
| ---------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `tsconfig` | `tsconfig.json` (may be absent) | Source of `paths` aliases and of the import extension |
|
|
53
|
+
| `importExtension` | `.js` under node16/nodenext, else none | Extension of generated imports |
|
|
54
|
+
| `bundles[].output` | — | The bundle file (convention: `*.bundle.ts`) |
|
|
55
|
+
| `bundles[].name` | `Bundle` | Name of the generated class, an `IContainerModule` |
|
|
56
|
+
| `bundles[].namespaces` | — | Folders to scan; `{ path, recursive }` to stop at the folder itself |
|
|
57
|
+
| `bundles[].select` | every exported class | Which classes of a file are registered — see [Selecting classes](#selecting-classes) |
|
|
58
|
+
| `bundles[].exclude` | test files, `__tests__/`, `node_modules/` | Globs (relative to the config) never scanned; replaces the default |
|
|
59
|
+
| `bundles[].filterExports` | `tic.exports.*` next to the config | File exporting an `ExportPredicate` — see [Filtering classes](#filtering-classes-after-parsing) |
|
|
60
|
+
| `bundles[].include` | `tic.include.*` next to the config | File exporting an `InclusionPredicate` — see [Including files](#including-files-with-a-predicate) |
|
|
61
|
+
|
|
62
|
+
## Selecting classes
|
|
63
|
+
|
|
64
|
+
By default every exported, non-abstract class of a scanned file is registered.
|
|
65
|
+
`select` narrows that; a class must meet every criterion that is set:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
"select": {
|
|
69
|
+
"export": "named",
|
|
70
|
+
"decorators": ["register", "repository"],
|
|
71
|
+
"nameGlob": "*Service"
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Criterion | Default | Meaning |
|
|
76
|
+
| ------------ | ------- | ----------------------------------------------------------------------------------------------------------- |
|
|
77
|
+
| `export` | `any` | `any`, `named` or `default` — which exports count |
|
|
78
|
+
| `decorators` | — | The class must carry one of these, by name — also renamed imports and `@ioc.register()`; list composed ones |
|
|
79
|
+
| `nameGlob` | — | Glob on the class name; an anonymous default export is named after its file (`user-service.ts` → `UserService`) |
|
|
80
|
+
|
|
81
|
+
Abstract and non-exported classes are never registered.
|
|
82
|
+
|
|
83
|
+
## Including files with a predicate
|
|
84
|
+
|
|
85
|
+
When globs aren't enough, decide in code which files take part:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
type InclusionPredicate = (context: { filename: string }) => boolean;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`filename` is the path relative to the config file, `/`-separated
|
|
92
|
+
(`src/services/Logger.ts`). A file takes part only when it matches no `exclude`
|
|
93
|
+
glob **and** the predicate returns `true`.
|
|
94
|
+
|
|
95
|
+
By convention there is nothing to configure: put a `tic.include.ts` (or `.cjs`,
|
|
96
|
+
`.js`, `.mjs`, `.cts`, `.mts`) next to `tic.config.json` and its default export
|
|
97
|
+
applies to every bundle.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// tic.include.ts
|
|
101
|
+
import type { InclusionPredicate } from '@ts-ioc-container/bundler';
|
|
102
|
+
|
|
103
|
+
const include: InclusionPredicate = ({ filename }) => !filename.includes('/legacy/');
|
|
104
|
+
export default include;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| Where the predicate comes from | Applies to | Wins over |
|
|
108
|
+
| ------------------------------------ | ------------- | ------------------------------ |
|
|
109
|
+
| `build({ include })` (programmatic) | every bundle | everything |
|
|
110
|
+
| `bundles[].include: "./path/file"` | that bundle | the conventional file |
|
|
111
|
+
| `tic.include.*` next to the config | every bundle | — (used when nothing is named) |
|
|
112
|
+
|
|
113
|
+
Predicate files are loaded synchronously with `require`: `.cjs` / `.js`
|
|
114
|
+
everywhere, `.mjs` on Node 22.12+, `.ts` on Node versions that strip types
|
|
115
|
+
(22.18+, 23.6+). Like any `require`d module a file is loaded **once per
|
|
116
|
+
process**, so read environment variables inside the predicate, not at the top
|
|
117
|
+
of the file.
|
|
118
|
+
|
|
119
|
+
### Tags: `TagInclusionPredicate`
|
|
120
|
+
|
|
121
|
+
A file's **tags** are the dot-separated parts of its name between the base name
|
|
122
|
+
and the extension — `StripeGateway.production.ts` → `['production']`,
|
|
123
|
+
`Report.production.eu.ts` → `['production', 'eu']`, `Shared.ts` → `[]`. Write
|
|
124
|
+
a predicate over them and wrap it with `byTags`:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
type TagInclusionPredicate = (tags: string[], context: { filename: string }) => boolean;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// tic.include.ts
|
|
132
|
+
import { byTags } from '@ts-ioc-container/bundler';
|
|
133
|
+
|
|
134
|
+
export default byTags((tags) => !tags.includes('manual')); // Scheduler.manual.ts is never generated
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`byTags` returns an ordinary `InclusionPredicate`, so it goes anywhere one
|
|
138
|
+
does. `fileTags(filename)` is exported too. More examples — folders, name
|
|
139
|
+
conventions, environments, environment × region — run as tests in
|
|
140
|
+
[`__tests__/examples/inclusion-predicates.spec.ts`](__tests__/examples/inclusion-predicates.spec.ts).
|
|
141
|
+
|
|
142
|
+
## Filtering classes after parsing
|
|
143
|
+
|
|
144
|
+
`include` decides on whole files before they are read. To decide class by
|
|
145
|
+
class, right after a file is parsed, use an `ExportPredicate`. It runs on every
|
|
146
|
+
exported, non-abstract class that passes `select`:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
type FilterPredicate<Target> = (value: Target) => boolean;
|
|
150
|
+
type ExportPredicate = FilterPredicate<ExportContext>;
|
|
151
|
+
|
|
152
|
+
interface ExportContext {
|
|
153
|
+
filename: string; // relative to the config: 'src/Mailer.ts'
|
|
154
|
+
exportName: string; // 'default' for a default export
|
|
155
|
+
className: string; // an anonymous default export is named after its file
|
|
156
|
+
isDefault: boolean;
|
|
157
|
+
decorators: string[]; // renamed imports resolved: `register as reg` -> 'register'
|
|
158
|
+
tags: string[]; // file-name tags, as byTags reads them
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// tic.exports.ts, next to tic.config.json: picked up by convention
|
|
164
|
+
import type { ExportPredicate } from '@ts-ioc-container/bundler';
|
|
165
|
+
|
|
166
|
+
const filterExports: ExportPredicate = ({ exportName }) => !/(Stub|Mock|Fake)$/.test(exportName);
|
|
167
|
+
export default filterExports;
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
It is found and loaded like an inclusion predicate: `build({ filterExports })`,
|
|
171
|
+
else the bundle's `filterExports` file, else a `tic.exports.*` next to the
|
|
172
|
+
config. Examples (test doubles sharing a file, decorator combinations, tags
|
|
173
|
+
with class names) run as tests in
|
|
174
|
+
[`__tests__/examples/inclusion-predicates.spec.ts`](__tests__/examples/inclusion-predicates.spec.ts).
|
|
175
|
+
|
|
176
|
+
## Recipe: generate per environment
|
|
177
|
+
|
|
178
|
+
Keep environment-specific implementations side by side, named by convention:
|
|
179
|
+
`*.production.ts` joins only production, `*.development.ts` only development,
|
|
180
|
+
and every other file is shared.
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
src/services/
|
|
184
|
+
├── Shared.ts → every environment
|
|
185
|
+
├── StripeGateway.production.ts → production only
|
|
186
|
+
└── FakeGateway.development.ts → development only
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
One helper turns the convention into a predicate — the environment tags a file
|
|
190
|
+
carries must all be the wanted one, and a file without any is shared:
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
// tic/for-env.cjs
|
|
194
|
+
const { byTags } = require('@ts-ioc-container/bundler');
|
|
195
|
+
const ENVS = ['development', 'production', 'test'];
|
|
196
|
+
|
|
197
|
+
module.exports = (env) => byTags((tags) => tags.filter((tag) => ENVS.includes(tag)).every((tag) => tag === env));
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### A. One bundle per environment (recommended)
|
|
201
|
+
|
|
202
|
+
Every environment gets its own bundle, and both are committed. The files don't
|
|
203
|
+
depend on who ran `tic build`, so `tic build --check` stays stable in CI.
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"bundles": [
|
|
208
|
+
{
|
|
209
|
+
"output": "src/di/app.production.bundle.ts",
|
|
210
|
+
"name": "AppBundle",
|
|
211
|
+
"namespaces": ["@app/services"],
|
|
212
|
+
"include": "./tic/production.cjs"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"output": "src/di/app.development.bundle.ts",
|
|
216
|
+
"name": "AppBundle",
|
|
217
|
+
"namespaces": ["@app/services"],
|
|
218
|
+
"include": "./tic/development.cjs"
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
// tic/production.cjs
|
|
226
|
+
module.exports = require('./for-env.cjs')('production');
|
|
227
|
+
// tic/development.cjs
|
|
228
|
+
module.exports = require('./for-env.cjs')('development');
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
To keep development code out of the production build, the production entry
|
|
232
|
+
point must import **only** the production bundle — a bundler includes what is
|
|
233
|
+
imported, and a registration keeps its class alive, so nothing can be dropped
|
|
234
|
+
after the fact. Give each environment its own entry:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// src/main.production.ts — production build entry
|
|
238
|
+
import { Container } from 'ts-ioc-container';
|
|
239
|
+
import { AppBundle } from './di/app.production.bundle';
|
|
240
|
+
|
|
241
|
+
export const container = new Container().useModule(new AppBundle());
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
// src/main.development.ts — development entry
|
|
246
|
+
import { Container } from 'ts-ioc-container';
|
|
247
|
+
import { AppBundle } from './di/app.development.bundle';
|
|
248
|
+
|
|
249
|
+
export const container = new Container().useModule(new AppBundle());
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
With a single entry, pick the bundle with a dynamic `import()`: the bundler
|
|
253
|
+
splits each into its own chunk and only the chosen one is loaded — the other
|
|
254
|
+
chunk is still emitted, just never fetched.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
const { AppBundle } =
|
|
258
|
+
process.env.NODE_ENV === 'production'
|
|
259
|
+
? await import('./di/app.production.bundle')
|
|
260
|
+
: await import('./di/app.development.bundle');
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
| How the app picks the bundle | Development classes in the production build |
|
|
264
|
+
| ---------------------------------------------------------------- | ------------------------------------------- |
|
|
265
|
+
| One entry per environment, importing its own bundle | ✅ none |
|
|
266
|
+
| Recipe B below — only the production bundle exists at build time | ✅ none |
|
|
267
|
+
| Dynamic `import()` of the bundle | ⚠️ emitted as a separate chunk, never loaded |
|
|
268
|
+
| Static imports of both bundles plus a runtime condition | ❌ both bundles, in full |
|
|
269
|
+
|
|
270
|
+
Don't rely on the bundler dropping a statically imported bundle behind
|
|
271
|
+
`process.env.NODE_ENV === 'production'`: the classes it imports run decorators
|
|
272
|
+
at load time, which bundlers treat as side effects and keep.
|
|
273
|
+
|
|
274
|
+
### B. One output, environment chosen when `tic build` runs
|
|
275
|
+
|
|
276
|
+
The smallest production build: only the production bundle ever exists while it
|
|
277
|
+
is built. For pipelines that generate at deploy time instead of committing the
|
|
278
|
+
output:
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
// tic.include.cjs — picked up by convention
|
|
282
|
+
const forEnv = require('./tic/for-env.cjs');
|
|
283
|
+
|
|
284
|
+
// Read TIC_ENV per call: the file is loaded once per process.
|
|
285
|
+
module.exports = (context) => forEnv(process.env.TIC_ENV ?? 'development')(context);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
TIC_ENV=production tic build
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The output now depends on `TIC_ENV`. Either don't commit it (generate it in
|
|
293
|
+
every pipeline), or run `tic build --check` with the same `TIC_ENV` it was
|
|
294
|
+
generated with.
|
|
295
|
+
|
|
296
|
+
## Build
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
tic build # tic.config.json in the working directory
|
|
300
|
+
tic build -c path/to/tic.config.json
|
|
301
|
+
tic build --check # CI: write nothing, exit 1 if a bundle is out of date
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The output is plain TypeScript over the public `ts-ioc-container` API:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
// src/di/app.bundle.ts
|
|
308
|
+
// Generated by `tic build` from ../../tic.config.json. Do not edit by hand.
|
|
309
|
+
// Namespaces: @app/services, @app/infra
|
|
310
|
+
import { type IContainer, type IContainerModule, type IRegistration, Registration } from 'ts-ioc-container';
|
|
311
|
+
import { MemoryLogger } from '@app/infra/logging/MemoryLogger';
|
|
312
|
+
import { Greeter } from '@app/services/Greeter';
|
|
313
|
+
|
|
314
|
+
export const registrations: IRegistration[] = [
|
|
315
|
+
Registration.fromClass(MemoryLogger),
|
|
316
|
+
Registration.fromClass(Greeter),
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
export class AppBundle implements IContainerModule {
|
|
320
|
+
applyTo(container: IContainer): void {
|
|
321
|
+
for (const registration of registrations) container.addRegistration(registration);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
const container = new Container().useModule(new AppBundle());
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Treat `*.bundle.ts` as build output: add it to `.prettierignore` (and any
|
|
331
|
+
other formatter's ignore list), since `--check` compares files byte for byte.
|
|
332
|
+
|
|
333
|
+
Each class keeps its own `@register(...)` config (key, scope, singleton, …);
|
|
334
|
+
an undecorated class is bound by its class name. Discovery is syntactic — no
|
|
335
|
+
type checker runs.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.ImportPaths = void 0;
|
|
40
|
+
const node_fs_1 = require("node:fs");
|
|
41
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
+
const ts = __importStar(require("typescript"));
|
|
43
|
+
const errors_1 = require("./errors");
|
|
44
|
+
const glob_1 = require("./glob");
|
|
45
|
+
const SOURCE_EXTENSION = /\.(tsx?|mts|cts)$/;
|
|
46
|
+
const NO_INPUTS = 18003;
|
|
47
|
+
const isDirectory = (dir) => (0, node_fs_1.existsSync)(dir) && (0, node_fs_1.statSync)(dir).isDirectory();
|
|
48
|
+
const isRelative = (spec) => spec.startsWith('.') || node_path_1.default.isAbsolute(spec);
|
|
49
|
+
function splitWildcard(pattern) {
|
|
50
|
+
const star = pattern.indexOf('*');
|
|
51
|
+
return star === -1 ? undefined : [pattern.slice(0, star), pattern.slice(star + 1)];
|
|
52
|
+
}
|
|
53
|
+
class ImportPaths {
|
|
54
|
+
aliases;
|
|
55
|
+
extension;
|
|
56
|
+
static load(tsconfig, importExtension) {
|
|
57
|
+
if (!(0, node_fs_1.existsSync)(tsconfig.file)) {
|
|
58
|
+
if (tsconfig.required)
|
|
59
|
+
throw new errors_1.TicConfigError(`tsconfig not found: ${tsconfig.file}`);
|
|
60
|
+
return new ImportPaths([], importExtension ?? '');
|
|
61
|
+
}
|
|
62
|
+
const { config, error } = ts.readConfigFile(tsconfig.file, ts.sys.readFile);
|
|
63
|
+
const parsed = ts.parseJsonConfigFileContent(config, ts.sys, node_path_1.default.dirname(tsconfig.file), undefined, tsconfig.file);
|
|
64
|
+
const [fatal] = [error, ...parsed.errors].filter((d) => !!d && d.code !== NO_INPUTS);
|
|
65
|
+
if (fatal) {
|
|
66
|
+
throw new errors_1.TicConfigError(`${tsconfig.file}: ${ts.flattenDiagnosticMessageText(fatal.messageText, '\n')}`);
|
|
67
|
+
}
|
|
68
|
+
const { paths = {}, baseUrl, pathsBasePath, moduleResolution } = parsed.options;
|
|
69
|
+
const base = baseUrl ?? (typeof pathsBasePath === 'string' ? pathsBasePath : node_path_1.default.dirname(tsconfig.file));
|
|
70
|
+
const aliases = Object.entries(paths).map(([pattern, targets]) => ({
|
|
71
|
+
pattern,
|
|
72
|
+
targets: targets.map((target) => (0, glob_1.toPosix)(node_path_1.default.resolve(base, target))),
|
|
73
|
+
}));
|
|
74
|
+
const nodeEsm = moduleResolution === ts.ModuleResolutionKind.Node16 || moduleResolution === ts.ModuleResolutionKind.NodeNext;
|
|
75
|
+
return new ImportPaths(aliases, importExtension ?? (nodeEsm ? '.js' : ''));
|
|
76
|
+
}
|
|
77
|
+
constructor(aliases, extension) {
|
|
78
|
+
this.aliases = aliases;
|
|
79
|
+
this.extension = extension;
|
|
80
|
+
}
|
|
81
|
+
resolveNamespace(namespace, baseDir) {
|
|
82
|
+
const folder = node_path_1.default.resolve(baseDir, namespace);
|
|
83
|
+
if (isDirectory(folder))
|
|
84
|
+
return folder;
|
|
85
|
+
if (!isRelative(namespace)) {
|
|
86
|
+
const found = [namespace, `${namespace}/`].flatMap((spec) => this.aliasTargets(spec)).find(isDirectory);
|
|
87
|
+
if (found)
|
|
88
|
+
return node_path_1.default.normalize(found);
|
|
89
|
+
}
|
|
90
|
+
throw new errors_1.NamespaceNotFoundError(`namespace "${namespace}" is neither a folder relative to ${baseDir} nor a tsconfig paths alias of one`);
|
|
91
|
+
}
|
|
92
|
+
specifier(fromFile, toFile) {
|
|
93
|
+
const target = (0, glob_1.toPosix)(toFile);
|
|
94
|
+
const bare = target.replace(SOURCE_EXTENSION, '');
|
|
95
|
+
let best;
|
|
96
|
+
for (const { pattern, targets } of this.aliases) {
|
|
97
|
+
for (const aliasTarget of targets) {
|
|
98
|
+
const match = this.matchAlias(pattern, aliasTarget, target, bare);
|
|
99
|
+
if (match && (!best || match.score > best.score))
|
|
100
|
+
best = match;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (best)
|
|
104
|
+
return best.spec;
|
|
105
|
+
const relative = (0, glob_1.toPosix)(node_path_1.default.relative(node_path_1.default.dirname(fromFile), bare));
|
|
106
|
+
return `${relative.startsWith('.') ? relative : `./${relative}`}${this.extension}`;
|
|
107
|
+
}
|
|
108
|
+
aliasTargets(spec) {
|
|
109
|
+
return this.aliases.flatMap(({ pattern, targets }) => {
|
|
110
|
+
const wildcard = splitWildcard(pattern);
|
|
111
|
+
if (!wildcard)
|
|
112
|
+
return pattern === spec ? targets : [];
|
|
113
|
+
const [prefix, suffix] = wildcard;
|
|
114
|
+
if (!spec.startsWith(prefix) || !spec.endsWith(suffix) || spec.length < prefix.length + suffix.length)
|
|
115
|
+
return [];
|
|
116
|
+
const captured = spec.slice(prefix.length, spec.length - suffix.length);
|
|
117
|
+
return targets.map((target) => target.replace('*', captured));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
matchAlias(pattern, aliasTarget, file, bare) {
|
|
121
|
+
const wildcard = splitWildcard(aliasTarget);
|
|
122
|
+
if (!wildcard) {
|
|
123
|
+
const exact = aliasTarget === file || aliasTarget.replace(SOURCE_EXTENSION, '') === bare;
|
|
124
|
+
return exact && !splitWildcard(pattern) ? { spec: pattern, score: Infinity } : undefined;
|
|
125
|
+
}
|
|
126
|
+
const [prefix, suffix] = wildcard;
|
|
127
|
+
if (!bare.startsWith(prefix) || !bare.endsWith(suffix) || bare.length < prefix.length + suffix.length) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const captured = bare.slice(prefix.length, bare.length - suffix.length);
|
|
131
|
+
return { spec: `${pattern.replace('*', captured)}${this.extension}`, score: prefix.length };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
exports.ImportPaths = ImportPaths;
|
package/cjm/bin.js
ADDED
package/cjm/build.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.build = build;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
const emit_1 = require("./emit");
|
|
11
|
+
const glob_1 = require("./glob");
|
|
12
|
+
const ImportPaths_1 = require("./ImportPaths");
|
|
13
|
+
const exportPredicate_1 = require("./exportPredicate");
|
|
14
|
+
const inclusion_1 = require("./inclusion");
|
|
15
|
+
const scan_1 = require("./scan");
|
|
16
|
+
function build({ config = config_1.DEFAULT_CONFIG_FILE, cwd = process.cwd(), check = false, include, filterExports, } = {}) {
|
|
17
|
+
const resolved = (0, config_1.loadConfig)(node_path_1.default.resolve(cwd, config));
|
|
18
|
+
const paths = ImportPaths_1.ImportPaths.load(resolved.tsconfig, resolved.importExtension);
|
|
19
|
+
const includeFor = predicateResolver({
|
|
20
|
+
override: include,
|
|
21
|
+
conventional: (0, inclusion_1.findConventionalPredicate)(resolved.dir),
|
|
22
|
+
fileOf: (bundle) => bundle.include,
|
|
23
|
+
load: inclusion_1.loadInclusionPredicate,
|
|
24
|
+
});
|
|
25
|
+
const filterExportsFor = predicateResolver({
|
|
26
|
+
override: filterExports,
|
|
27
|
+
conventional: (0, exportPredicate_1.findConventionalExportPredicate)(resolved.dir),
|
|
28
|
+
fileOf: (bundle) => bundle.filterExports,
|
|
29
|
+
load: exportPredicate_1.loadExportPredicate,
|
|
30
|
+
});
|
|
31
|
+
const generated = resolved.bundles.map((bundle) => generate(resolved, bundle, paths, { include: includeFor(bundle), filterExports: filterExportsFor(bundle) }));
|
|
32
|
+
const outputs = generated.map(({ bundle, content, registrations }) => {
|
|
33
|
+
const current = (0, node_fs_1.existsSync)(bundle.output) ? (0, node_fs_1.readFileSync)(bundle.output, 'utf8') : undefined;
|
|
34
|
+
const status = current === content ? 'unchanged' : check ? 'stale' : 'written';
|
|
35
|
+
if (status === 'written') {
|
|
36
|
+
(0, node_fs_1.mkdirSync)(node_path_1.default.dirname(bundle.output), { recursive: true });
|
|
37
|
+
(0, node_fs_1.writeFileSync)(bundle.output, content);
|
|
38
|
+
}
|
|
39
|
+
return { file: bundle.output, bundle: bundle.name, status, registrations, content };
|
|
40
|
+
});
|
|
41
|
+
return { config: resolved.file, outputs };
|
|
42
|
+
}
|
|
43
|
+
function predicateResolver({ override, conventional, fileOf, load, }) {
|
|
44
|
+
const loaded = new Map();
|
|
45
|
+
return (bundle) => {
|
|
46
|
+
if (override)
|
|
47
|
+
return override;
|
|
48
|
+
const file = fileOf(bundle) ?? conventional;
|
|
49
|
+
if (file === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (!loaded.has(file))
|
|
52
|
+
loaded.set(file, load(file));
|
|
53
|
+
return loaded.get(file);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function generate(config, bundle, paths, { include, filterExports }) {
|
|
57
|
+
const relative = (file) => (0, glob_1.toPosix)(node_path_1.default.relative(config.dir, file));
|
|
58
|
+
const outputs = new Set(config.bundles.map((m) => m.output));
|
|
59
|
+
const excludes = bundle.exclude.map(glob_1.globToRegExp);
|
|
60
|
+
const isExcluded = (file) => {
|
|
61
|
+
if (outputs.has(file))
|
|
62
|
+
return true;
|
|
63
|
+
const filename = relative(file);
|
|
64
|
+
return excludes.some((glob) => glob.test(filename)) || (include !== undefined && !include({ filename }));
|
|
65
|
+
};
|
|
66
|
+
const files = new Set();
|
|
67
|
+
for (const namespace of bundle.namespaces) {
|
|
68
|
+
const dir = paths.resolveNamespace(namespace.path, config.dir);
|
|
69
|
+
for (const file of (0, scan_1.listSourceFiles)(dir, namespace.recursive, isExcluded))
|
|
70
|
+
files.add(file);
|
|
71
|
+
}
|
|
72
|
+
const classes = [...files]
|
|
73
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
74
|
+
.flatMap((file) => (0, scan_1.findClasses)(file, bundle.select))
|
|
75
|
+
.filter((cls) => {
|
|
76
|
+
if (!filterExports)
|
|
77
|
+
return true;
|
|
78
|
+
const filename = relative(cls.file);
|
|
79
|
+
const { exportName, className, isDefault, decorators } = cls;
|
|
80
|
+
return filterExports({ filename, exportName, className, isDefault, decorators, tags: (0, inclusion_1.fileTags)(filename) });
|
|
81
|
+
})
|
|
82
|
+
.map((cls) => ({ ...cls, specifier: paths.specifier(bundle.output, cls.file) }));
|
|
83
|
+
const content = (0, emit_1.emitBundle)({
|
|
84
|
+
name: bundle.name,
|
|
85
|
+
configPath: (0, glob_1.toPosix)(node_path_1.default.relative(node_path_1.default.dirname(bundle.output), config.file)),
|
|
86
|
+
namespaces: bundle.namespaces.map((ns) => ns.path),
|
|
87
|
+
classes,
|
|
88
|
+
});
|
|
89
|
+
return { bundle, content, registrations: classes.length };
|
|
90
|
+
}
|