@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
@@ -0,0 +1,45 @@
1
+ import { conventionalNames, findConventionalFile, loadPredicateFile } from './predicateFile';
2
+ import type { FilterPredicate } from './utils';
3
+
4
+ /** What an {@link ExportPredicate} decides on: one exported class of a parsed file. */
5
+ export interface ExportContext {
6
+ /** The file's path relative to the config file, `/`-separated: `src/services/Logger.ts`. */
7
+ filename: string;
8
+ /** The name the class is exported under; `'default'` for a default export. */
9
+ exportName: string;
10
+ /** The declared class name; an anonymous default export is named after its file. */
11
+ className: string;
12
+ isDefault: boolean;
13
+ /** Names of the class's decorators, renamed imports resolved (`register as reg` -> `'register'`). */
14
+ decorators: string[];
15
+ /** The file-name tags `byTags` reads: `Report.production.eu.ts` -> `['production', 'eu']`. */
16
+ tags: string[];
17
+ }
18
+
19
+ /**
20
+ * Decides, right after a file is parsed, whether one of its exported classes becomes a
21
+ * registration. It runs on every exported, non-abstract class that passes `select`.
22
+ *
23
+ * @example
24
+ * // tic.exports.ts, next to tic.config.json - picked up by convention
25
+ * const filterExports: ExportPredicate = ({ exportName }) => !/(Stub|Mock|Fake)$/.test(exportName);
26
+ * export default filterExports;
27
+ */
28
+ export type ExportPredicate = FilterPredicate<ExportContext>;
29
+
30
+ /** Export predicate files looked up next to the config, in this order, when no bundle names one. */
31
+ export const EXPORTS_CONVENTION = conventionalNames('tic.exports');
32
+
33
+ /** The conventional export predicate file in `dir`, if there is one. */
34
+ export function findConventionalExportPredicate(dir: string): string | undefined {
35
+ return findConventionalFile(dir, 'tic.exports');
36
+ }
37
+
38
+ /**
39
+ * Loads an {@link ExportPredicate} file (see `loadPredicateFile` for formats and caching).
40
+ *
41
+ * @throws {TicConfigError} when the file is missing or does not export a function.
42
+ */
43
+ export function loadExportPredicate(file: string): ExportPredicate {
44
+ return loadPredicateFile<ExportPredicate>(file, 'ExportPredicate');
45
+ }
package/lib/glob.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Converts a glob to an anchored RegExp over `/`-separated paths: `**` spans
3
+ * folders (`**\/` also matches none), `*` and `?` stay within one segment.
4
+ */
5
+ export function globToRegExp(glob: string): RegExp {
6
+ let source = '';
7
+ for (let i = 0; i < glob.length; i++) {
8
+ const char = glob[i];
9
+ if (char === '*' && glob[i + 1] === '*') {
10
+ const slash = glob[i + 2] === '/';
11
+ source += slash ? '(?:.*/)?' : '.*';
12
+ i += slash ? 2 : 1;
13
+ } else if (char === '*') {
14
+ source += '[^/]*';
15
+ } else if (char === '?') {
16
+ source += '[^/]';
17
+ } else {
18
+ source += char.replace(/[.+^${}()|[\]\\]/g, '\\$&');
19
+ }
20
+ }
21
+ return new RegExp(`^${source}$`);
22
+ }
23
+
24
+ export const toPosix = (file: string): string => file.split('\\').join('/');
@@ -0,0 +1,61 @@
1
+ import { conventionalNames, findConventionalFile, loadPredicateFile } from './predicateFile';
2
+ import type { FilterPredicate } from './utils';
3
+
4
+ /** What an {@link InclusionPredicate} decides on. */
5
+ export interface InclusionContext {
6
+ /** The file's path relative to the config file, `/`-separated: `src/services/Logger.ts`. */
7
+ filename: string;
8
+ }
9
+
10
+ /**
11
+ * Decides whether a scanned file takes part in code generation. A file takes
12
+ * part only when it matches no `exclude` glob and the predicate returns `true`.
13
+ *
14
+ * @example
15
+ * // tic.include.ts, next to tic.config.json - picked up by convention
16
+ * const include: InclusionPredicate = ({ filename }) => !filename.includes('/legacy/');
17
+ * export default include;
18
+ */
19
+ export type InclusionPredicate = FilterPredicate<InclusionContext>;
20
+
21
+ /**
22
+ * An {@link InclusionPredicate} over a file's tags, the dot-separated parts of its name between
23
+ * the base name and the extension. Wrap it with {@link byTags} to use it anywhere an
24
+ * `InclusionPredicate` goes.
25
+ *
26
+ * @example
27
+ * // StripeGateway.production.ts joins production only; Shared.ts joins every environment
28
+ * const ENVS = ['development', 'production', 'test'];
29
+ * export default byTags((tags) => tags.filter((t) => ENVS.includes(t)).every((t) => t === process.env.TIC_ENV));
30
+ */
31
+ export type TagInclusionPredicate = (tags: string[], context: InclusionContext) => boolean;
32
+
33
+ /** `src/Report.production.eu.ts` -> `['production', 'eu']`; `src/Shared.ts` -> `[]`. */
34
+ export function fileTags(filename: string): string[] {
35
+ return filename
36
+ .slice(filename.lastIndexOf('/') + 1)
37
+ .split('.')
38
+ .slice(1, -1);
39
+ }
40
+
41
+ /** Wraps a {@link TagInclusionPredicate} into the {@link InclusionPredicate} the bundler calls. */
42
+ export function byTags(predicate: TagInclusionPredicate): InclusionPredicate {
43
+ return (context) => predicate(fileTags(context.filename), context);
44
+ }
45
+
46
+ /** Predicate files looked up next to the config, in this order, when no bundle names one. */
47
+ export const INCLUSION_CONVENTION = conventionalNames('tic.include');
48
+
49
+ /** The conventional inclusion predicate file in `dir`, if there is one. */
50
+ export function findConventionalPredicate(dir: string): string | undefined {
51
+ return findConventionalFile(dir, 'tic.include');
52
+ }
53
+
54
+ /**
55
+ * Loads an {@link InclusionPredicate} file (see `loadPredicateFile` for formats and caching).
56
+ *
57
+ * @throws {TicConfigError} when the file is missing or does not export a function.
58
+ */
59
+ export function loadInclusionPredicate(file: string): InclusionPredicate {
60
+ return loadPredicateFile<InclusionPredicate>(file, 'InclusionPredicate');
61
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ export { build, type BuildOptions, type BuildResult, type OutputResult, type OutputStatus } from './build';
2
+ export { run, type CliIo } from './cli';
3
+ export {
4
+ loadConfig,
5
+ resolveConfig,
6
+ DEFAULT_CONFIG_FILE,
7
+ DEFAULT_EXCLUDE,
8
+ DEFAULT_BUNDLE_NAME,
9
+ type TicConfig,
10
+ type BundleConfig,
11
+ type NamespaceConfig,
12
+ type ClassSelector,
13
+ type ExportKind,
14
+ type ResolvedSelector,
15
+ type ResolvedConfig,
16
+ type ResolvedBundle,
17
+ } from './config';
18
+ export { ImportPaths } from './ImportPaths';
19
+ export { type FilterPredicate } from './utils';
20
+ export {
21
+ type ExportPredicate,
22
+ type ExportContext,
23
+ EXPORTS_CONVENTION,
24
+ findConventionalExportPredicate,
25
+ loadExportPredicate,
26
+ } from './exportPredicate';
27
+ export {
28
+ type InclusionPredicate,
29
+ type InclusionContext,
30
+ type TagInclusionPredicate,
31
+ byTags,
32
+ fileTags,
33
+ INCLUSION_CONVENTION,
34
+ findConventionalPredicate,
35
+ loadInclusionPredicate,
36
+ } from './inclusion';
37
+ export { findClasses, listSourceFiles, type DiscoveredClass } from './scan';
38
+ export { emitBundle, type EmitInput } from './emit';
39
+ export { globToRegExp } from './glob';
40
+ export { TicError, TicConfigError, NamespaceNotFoundError } from './errors';
@@ -0,0 +1,36 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { TicConfigError } from './errors';
5
+ import type { FilterPredicate } from './utils';
6
+
7
+ const EXTENSIONS = ['cjs', 'js', 'mjs', 'ts', 'cts', 'mts'];
8
+
9
+ /** `tic.include` -> `tic.include.cjs`, `tic.include.js`, ... in lookup order. */
10
+ export const conventionalNames = (base: string): string[] => EXTENSIONS.map((ext) => `${base}.${ext}`);
11
+
12
+ /** The first conventional file for `base` that exists in `dir`, if any. */
13
+ export function findConventionalFile(dir: string, base: string): string | undefined {
14
+ return conventionalNames(base)
15
+ .map((name) => path.join(dir, name))
16
+ .find((file) => existsSync(file));
17
+ }
18
+
19
+ /**
20
+ * Loads a predicate file synchronously with `require` - `.cjs` / `.js`, ES modules
21
+ * (Node 22.12+) and `.ts` (Node with type stripping). Its default export, or
22
+ * `module.exports` itself, is the predicate. Like any `require`d module it is loaded
23
+ * once per process, so a predicate that depends on the environment should read it
24
+ * when called, not when loaded.
25
+ *
26
+ * @throws {TicConfigError} when the file is missing or does not export a function.
27
+ */
28
+ export function loadPredicateFile<T extends FilterPredicate<never>>(file: string, typeName: string): T {
29
+ if (!existsSync(file)) throw new TicConfigError(`predicate file not found: ${file}`);
30
+ const loaded: unknown = createRequire(file)(file);
31
+ const predicate = typeof loaded === 'function' ? loaded : (loaded as { default?: unknown } | undefined)?.default;
32
+ if (typeof predicate !== 'function') {
33
+ throw new TicConfigError(`${file} must export an ${typeName} function (default export or module.exports)`);
34
+ }
35
+ return predicate as T;
36
+ }
package/lib/render.ts ADDED
@@ -0,0 +1,19 @@
1
+ import '../tpl/index.cjs';
2
+ import Handlebars from 'handlebars';
3
+
4
+ // A protocol is the declarative definition (schema) of a generated file, written as a Handlebars
5
+ // template in lib/protocols/. `hbs:compile` precompiles them into tpl/index.cjs, which registers each
6
+ // one in the process-global `Handlebars.templates` under its file basename. Output is TypeScript,
7
+ // not HTML, so protocols interpolate with `{{{ }}}` and helpers return plain strings.
8
+ Handlebars.registerHelper('join', (items: string[], separator: string) => items.join(separator));
9
+
10
+ /**
11
+ * Renders a precompiled protocol, looked up by its file basename (`Bundle.ts.hbs`).
12
+ *
13
+ * @throws {Error} when no protocol is registered under `name`.
14
+ */
15
+ export function renderProtocol(name: string, data: unknown): string {
16
+ const protocol = Handlebars.templates?.[name];
17
+ if (!protocol) throw new Error(`Protocol not found: ${name}`);
18
+ return protocol(data);
19
+ }
package/lib/scan.ts ADDED
@@ -0,0 +1,133 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import * as ts from 'typescript';
4
+ import type { ResolvedSelector } from './config';
5
+
6
+ /** An exported class the generated bundle registers. */
7
+ export interface DiscoveredClass {
8
+ file: string;
9
+ /** The name it is exported under; `default` for a default export. */
10
+ exportName: string;
11
+ /** The local name the generated bundle would like to import it under. */
12
+ localName: string;
13
+ /** The declared class name; an anonymous default export is named after its file. */
14
+ className: string;
15
+ isDefault: boolean;
16
+ /** Names of the class's decorators, renamed imports resolved. */
17
+ decorators: string[];
18
+ }
19
+
20
+ const SOURCE_FILE = /\.(tsx?|mts|cts)$/;
21
+ const DECLARATION_FILE = /\.d\.[mc]?tsx?$/;
22
+
23
+ /** Source files of `dir`, sorted by path so the generated output is stable across machines. */
24
+ export function listSourceFiles(dir: string, recursive: boolean, isExcluded: (file: string) => boolean): string[] {
25
+ const files: string[] = [];
26
+ const walk = (current: string) => {
27
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
28
+ const full = path.join(current, entry.name);
29
+ if (entry.isDirectory()) {
30
+ if (recursive && entry.name !== 'node_modules') walk(full);
31
+ } else if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name) && !isExcluded(full)) {
32
+ files.push(full);
33
+ }
34
+ }
35
+ };
36
+ walk(dir);
37
+ return files.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
38
+ }
39
+
40
+ const hasModifier = (node: ts.Node, kind: ts.SyntaxKind) =>
41
+ ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((m) => m.kind === kind);
42
+
43
+ /** `@register`, `@register(...)`, `@ioc.register(...)` -> `register`; renamed imports map back to the imported name. */
44
+ function decoratorName(decorator: ts.Decorator, imports: Map<string, string>): string | undefined {
45
+ const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
46
+ if (ts.isIdentifier(callee)) return imports.get(callee.text) ?? callee.text;
47
+ if (ts.isPropertyAccessExpression(callee)) return callee.name.text;
48
+ return undefined;
49
+ }
50
+
51
+ /**
52
+ * `user-service.ts` -> `UserService`: the import name of an anonymous default-exported class.
53
+ * File-name tags are not part of it: `user-service.production.ts` -> `UserService`.
54
+ */
55
+ function nameFromFile(file: string): string {
56
+ const words = path
57
+ .basename(file)
58
+ .split('.')[0]
59
+ .split(/[^A-Za-z0-9_$]+/)
60
+ .filter(Boolean);
61
+ const name = words.map((w: string) => w[0].toUpperCase() + w.slice(1)).join('');
62
+ return /^[A-Za-z_$]/.test(name) ? name : `_${name}`;
63
+ }
64
+
65
+ /**
66
+ * The exported, non-abstract classes of one file that `selector` selects, in
67
+ * declaration order. Syntax only: decorators are recognised by name, not by type.
68
+ */
69
+ export function findClasses(
70
+ file: string,
71
+ selector: ResolvedSelector,
72
+ text = readFileSync(file, 'utf8'),
73
+ ): DiscoveredClass[] {
74
+ const kind = file.endsWith('x') ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
75
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, false, kind);
76
+
77
+ const imports = new Map<string, string>(); // local name -> imported name
78
+ const classes = new Map<string, ts.ClassDeclaration>();
79
+ const exported: { node: ts.ClassDeclaration | undefined; local?: string; exportName: string }[] = [];
80
+
81
+ for (const statement of source.statements) {
82
+ if (ts.isImportDeclaration(statement)) {
83
+ const bindings = statement.importClause?.namedBindings;
84
+ if (bindings && ts.isNamedImports(bindings)) {
85
+ for (const el of bindings.elements) imports.set(el.name.text, (el.propertyName ?? el.name).text);
86
+ }
87
+ } else if (ts.isClassDeclaration(statement)) {
88
+ if (statement.name) classes.set(statement.name.text, statement);
89
+ if (hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
90
+ const isDefault = hasModifier(statement, ts.SyntaxKind.DefaultKeyword);
91
+ exported.push({ node: statement, exportName: isDefault ? 'default' : statement.name!.text });
92
+ }
93
+ } else if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && !statement.isTypeOnly) {
94
+ const clause = statement.exportClause;
95
+ if (clause && ts.isNamedExports(clause)) {
96
+ for (const el of clause.elements) {
97
+ if (!el.isTypeOnly)
98
+ exported.push({ node: undefined, local: (el.propertyName ?? el.name).text, exportName: el.name.text });
99
+ }
100
+ }
101
+ } else if (ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) {
102
+ exported.push({ node: undefined, local: statement.expression.text, exportName: 'default' });
103
+ }
104
+ }
105
+
106
+ const decoratorsOf = (node: ts.ClassDeclaration) =>
107
+ (ts.getDecorators(node) ?? []).map((d) => decoratorName(d, imports)).filter((n): n is string => !!n);
108
+
109
+ return exported
110
+ .map(({ node, local, exportName }) => ({ node: node ?? (local ? classes.get(local) : undefined), exportName }))
111
+ .filter((e): e is { node: ts.ClassDeclaration; exportName: string } => !!e.node)
112
+ .map(({ node, exportName }) => ({
113
+ node,
114
+ exportName,
115
+ className: node.name?.text ?? nameFromFile(file),
116
+ decorators: decoratorsOf(node),
117
+ }))
118
+ .filter(({ node, exportName, className, decorators }) => {
119
+ if (hasModifier(node, ts.SyntaxKind.AbstractKeyword)) return false;
120
+ if (selector.export !== 'any' && (exportName === 'default') !== (selector.export === 'default')) return false;
121
+ if (selector.decorators && !selector.decorators.some((name) => decorators.includes(name))) return false;
122
+ return !selector.nameGlob || selector.nameGlob.test(className);
123
+ })
124
+ .sort((a, b) => a.node.pos - b.node.pos)
125
+ .map(({ exportName, className, decorators }) => ({
126
+ file,
127
+ exportName,
128
+ localName: exportName === 'default' ? className : exportName,
129
+ className,
130
+ isDefault: exportName === 'default',
131
+ decorators,
132
+ }));
133
+ }
package/lib/utils.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Decides whether a value is kept. The base of every predicate the bundler takes. */
2
+ export type FilterPredicate<Target> = (value: Target) => boolean;
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@ts-ioc-container/bundler",
3
+ "version": "0.0.0",
4
+ "description": "Bundles your dependencies into a single container module for ts-ioc-container: the `tic` CLI scans folders (by path or tsconfig alias) and generates one typed bundle that registers every class \u2014 `container.useModule(new AppBundle())`.",
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "registry": "https://registry.npmjs.org/"
8
+ },
9
+ "author": "ibabkin <igba14@gmail.com>",
10
+ "homepage": "https://github.com/IgorBabkin/ts-ioc-container/tree/main/packages/bundler#readme",
11
+ "license": "ISC",
12
+ "sideEffects": false,
13
+ "main": "cjm/index.js",
14
+ "types": "typings/index.d.ts",
15
+ "bin": {
16
+ "ts-ioc-container": "cjm/bin.js",
17
+ "tic": "cjm/bin.js"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./typings/index.d.ts",
22
+ "default": "./cjm/index.js"
23
+ },
24
+ "./tic.schema.json": "./tic.schema.json",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "keywords": [
28
+ "dependency-injection",
29
+ "ioc",
30
+ "di",
31
+ "codegen",
32
+ "bundler",
33
+ "cli",
34
+ "component-scan",
35
+ "tsconfig-paths",
36
+ "ts-ioc-container"
37
+ ],
38
+ "files": [
39
+ "cjm/**/*",
40
+ "typings/**/*",
41
+ "tpl/**/*",
42
+ "lib/**/*",
43
+ "!lib/**/*.hbs",
44
+ "tic.schema.json",
45
+ "AGENTS.md"
46
+ ],
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/IgorBabkin/ts-ioc-container",
50
+ "directory": "packages/bundler"
51
+ },
52
+ "scripts": {
53
+ "hbs:compile": "mkdir -p tpl && handlebars lib/protocols/*.hbs -f tpl/index.cjs -c handlebars",
54
+ "build:cjm": "node ../scripts/build.mjs cjm",
55
+ "build:types": "node ../scripts/build.mjs types",
56
+ "build": "pnpm run hbs:compile && pnpm run build:cjm && pnpm run build:types",
57
+ "prepack": "pnpm run hbs:compile",
58
+ "test": "pnpm run hbs:compile && pnpm run build:cjm && vitest run",
59
+ "test:coverage": "pnpm run hbs:compile && pnpm run build:cjm && vitest run --coverage --coverage.reporter=lcov",
60
+ "type-check": "pnpm run hbs:compile && tsc --noEmit",
61
+ "lint": "eslint lib/**/*.ts __tests__/**/*.ts",
62
+ "lint:fix": "pnpm run lint --fix",
63
+ "format": "prettier --write \"lib/**/*.ts\" \"__tests__/**/*.ts\"",
64
+ "format:check": "prettier --check \"lib/**/*.ts\" \"__tests__/**/*.ts\""
65
+ },
66
+ "dependencies": {
67
+ "handlebars": "^4.7.9"
68
+ },
69
+ "peerDependencies": {
70
+ "typescript": ">=5"
71
+ },
72
+ "devDependencies": {
73
+ "@types/node": "^25.5.2",
74
+ "@vitest/coverage-v8": "^4.1.2",
75
+ "reflect-metadata": "^0.2.2",
76
+ "ts-ioc-container": "workspace:*",
77
+ "typescript": "5.8.3",
78
+ "unplugin-swc": "^1.6.0",
79
+ "vitest": "^4.1.11"
80
+ },
81
+ "engines": {
82
+ "node": ">= 18"
83
+ }
84
+ }
@@ -0,0 +1,134 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/IgorBabkin/ts-ioc-container/tree/main/packages/bundler/tic.schema.json",
4
+ "title": "tic.config.json",
5
+ "description": "Bundles generated by `tic build` (@ts-ioc-container/bundler). Relative paths resolve against this file's directory.",
6
+ "type": "object",
7
+ "required": [
8
+ "bundles"
9
+ ],
10
+ "additionalProperties": false,
11
+ "properties": {
12
+ "$schema": {
13
+ "type": "string"
14
+ },
15
+ "tsconfig": {
16
+ "type": "string",
17
+ "description": "tsconfig whose `paths` aliases namespaces and imports may use. Default `tsconfig.json`, which may be absent."
18
+ },
19
+ "importExtension": {
20
+ "type": "string",
21
+ "description": "Extension of generated imports, e.g. \".js\". Inferred from the tsconfig: \".js\" under node16/nodenext resolution, none otherwise."
22
+ },
23
+ "bundles": {
24
+ "type": "array",
25
+ "minItems": 1,
26
+ "items": {
27
+ "type": "object",
28
+ "required": [
29
+ "output",
30
+ "namespaces"
31
+ ],
32
+ "additionalProperties": false,
33
+ "properties": {
34
+ "output": {
35
+ "type": "string",
36
+ "minLength": 1,
37
+ "description": "The bundle file (convention: *.bundle.ts)."
38
+ },
39
+ "name": {
40
+ "type": "string",
41
+ "pattern": "^[A-Za-z_$][\\w$]*$",
42
+ "default": "Bundle",
43
+ "description": "Name of the generated class, an IContainerModule."
44
+ },
45
+ "namespaces": {
46
+ "type": "array",
47
+ "minItems": 1,
48
+ "description": "Folders to scan: paths relative to this file (./src/services) or tsconfig paths aliases (@app/services).",
49
+ "items": {
50
+ "oneOf": [
51
+ {
52
+ "type": "string",
53
+ "minLength": 1
54
+ },
55
+ {
56
+ "type": "object",
57
+ "required": [
58
+ "path"
59
+ ],
60
+ "additionalProperties": false,
61
+ "properties": {
62
+ "path": {
63
+ "type": "string",
64
+ "minLength": 1
65
+ },
66
+ "recursive": {
67
+ "type": "boolean",
68
+ "default": true
69
+ }
70
+ }
71
+ }
72
+ ]
73
+ }
74
+ },
75
+ "select": {
76
+ "type": "object",
77
+ "additionalProperties": false,
78
+ "description": "How target classes are picked out of a file. A class is selected when it is exported, not abstract, and meets every criterion set here; omit it to register every exported class.",
79
+ "properties": {
80
+ "export": {
81
+ "enum": [
82
+ "any",
83
+ "named",
84
+ "default"
85
+ ],
86
+ "default": "any",
87
+ "description": "Which exports count."
88
+ },
89
+ "decorators": {
90
+ "type": "array",
91
+ "minItems": 1,
92
+ "items": {
93
+ "type": "string",
94
+ "minLength": 1
95
+ },
96
+ "description": "The class must carry one of these decorators, recognised by name (e.g. \"register\", or a composed decorator)."
97
+ },
98
+ "nameGlob": {
99
+ "type": "string",
100
+ "minLength": 1,
101
+ "description": "Glob the class name must match, e.g. \"*Service\". An anonymous default export is named after its file."
102
+ }
103
+ }
104
+ },
105
+ "exclude": {
106
+ "type": "array",
107
+ "items": {
108
+ "type": "string"
109
+ },
110
+ "default": [
111
+ "**/*.spec.ts",
112
+ "**/*.test.ts",
113
+ "**/*.spec.tsx",
114
+ "**/*.test.tsx",
115
+ "**/__tests__/**",
116
+ "**/node_modules/**"
117
+ ],
118
+ "description": "Globs, relative to this file, of files never scanned. Replaces the default when given."
119
+ },
120
+ "include": {
121
+ "type": "string",
122
+ "minLength": 1,
123
+ "description": "A file exporting an InclusionPredicate ((context: { filename }) => boolean), relative to this file. Overrides the conventional tic.include.* next to this file. A file takes part only when it matches no `exclude` glob and the predicate returns true."
124
+ },
125
+ "filterExports": {
126
+ "type": "string",
127
+ "minLength": 1,
128
+ "description": "A file exporting an ExportPredicate ((context: { filename, exportName, className, isDefault, decorators, tags }) => boolean), relative to this file. Runs on every exported, non-abstract class that passes `select`, right after parsing. Overrides the conventional tic.exports.* next to this file."
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }