@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.
Files changed (65) hide show
  1. package/AGENTS.md +40 -0
  2. package/README.md +335 -0
  3. package/cjm/ImportPaths.js +134 -0
  4. package/cjm/bin.js +5 -0
  5. package/cjm/build.js +90 -0
  6. package/cjm/cli.js +82 -0
  7. package/cjm/config.js +118 -0
  8. package/cjm/emit.js +33 -0
  9. package/cjm/errors.js +15 -0
  10. package/cjm/exportPredicate.js +13 -0
  11. package/cjm/glob.js +27 -0
  12. package/cjm/inclusion.js +24 -0
  13. package/cjm/index.js +36 -0
  14. package/cjm/predicateFile.js +30 -0
  15. package/cjm/render.js +15 -0
  16. package/cjm/scan.js +144 -0
  17. package/cjm/utils.js +2 -0
  18. package/lib/ImportPaths.ts +127 -0
  19. package/lib/bin.ts +4 -0
  20. package/lib/build.ts +157 -0
  21. package/lib/cli.ts +88 -0
  22. package/lib/config.ts +206 -0
  23. package/lib/emit.ts +47 -0
  24. package/lib/errors.ts +14 -0
  25. package/lib/exportPredicate.ts +45 -0
  26. package/lib/glob.ts +24 -0
  27. package/lib/inclusion.ts +61 -0
  28. package/lib/index.ts +40 -0
  29. package/lib/predicateFile.ts +36 -0
  30. package/lib/render.ts +19 -0
  31. package/lib/scan.ts +133 -0
  32. package/lib/utils.ts +2 -0
  33. package/package.json +84 -0
  34. package/tic.schema.json +134 -0
  35. package/tpl/index.cjs +81 -0
  36. package/typings/ImportPaths.d.ts +37 -0
  37. package/typings/ImportPaths.d.ts.map +1 -0
  38. package/typings/bin.d.ts +3 -0
  39. package/typings/bin.d.ts.map +1 -0
  40. package/typings/build.d.ts +39 -0
  41. package/typings/build.d.ts.map +1 -0
  42. package/typings/cli.d.ts +8 -0
  43. package/typings/cli.d.ts.map +1 -0
  44. package/typings/config.d.ts +95 -0
  45. package/typings/config.d.ts.map +1 -0
  46. package/typings/emit.d.ts +18 -0
  47. package/typings/emit.d.ts.map +1 -0
  48. package/typings/errors.d.ts +13 -0
  49. package/typings/errors.d.ts.map +1 -0
  50. package/typings/exportPredicate.d.ts +36 -0
  51. package/typings/exportPredicate.d.ts.map +1 -0
  52. package/typings/glob.d.ts +7 -0
  53. package/typings/glob.d.ts.map +1 -0
  54. package/typings/inclusion.d.ts +42 -0
  55. package/typings/inclusion.d.ts.map +1 -0
  56. package/typings/index.d.ts +12 -0
  57. package/typings/index.d.ts.map +1 -0
  58. package/typings/predicateFile.d.ts +16 -0
  59. package/typings/predicateFile.d.ts.map +1 -0
  60. package/typings/render.d.ts +8 -0
  61. package/typings/render.d.ts.map +1 -0
  62. package/typings/scan.d.ts +22 -0
  63. package/typings/scan.d.ts.map +1 -0
  64. package/typings/utils.d.ts +3 -0
  65. 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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const cli_1 = require("./cli");
5
+ process.exitCode = (0, cli_1.run)(process.argv.slice(2));
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
+ }