@ts-ioc-container/bundler 3.1.0 → 4.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 (54) hide show
  1. package/AGENTS.md +45 -48
  2. package/README.md +177 -145
  3. package/cjm/app.js +2 -2
  4. package/cjm/cli/Application.js +4 -5
  5. package/cjm/cli/options.js +4 -2
  6. package/cjm/cli/usage.js +11 -8
  7. package/cjm/domain/CliIo.js +7 -1
  8. package/cjm/exceptions/DomainException.js +7 -7
  9. package/cjm/features/build/BuildConfig.js +108 -71
  10. package/cjm/features/build/BuildController.js +36 -46
  11. package/cjm/features/build/domain/ImportPaths.js +31 -20
  12. package/cjm/features/build/domain/emit.js +2 -2
  13. package/cjm/features/build/domain/scan.js +20 -4
  14. package/cjm/features/build/domain/tokenCollisions.js +1 -1
  15. package/cjm/features/build/domain/tsconfig.js +10 -35
  16. package/cjm/features/build/services/BundleBuilder.js +36 -29
  17. package/cjm/features/build/services/TicConfigService.js +10 -31
  18. package/cjm/index.js +9 -10
  19. package/cjm/schema/ticConfigSchema.js +5 -1
  20. package/package.json +2 -2
  21. package/tic.schema.json +117 -84
  22. package/tpl/index.cjs +15 -15
  23. package/typings/app.d.ts +7 -6
  24. package/typings/cli/Application.d.ts.map +1 -1
  25. package/typings/cli/options.d.ts +7 -1
  26. package/typings/cli/options.d.ts.map +1 -1
  27. package/typings/cli/usage.d.ts.map +1 -1
  28. package/typings/domain/CliIo.d.ts +2 -0
  29. package/typings/domain/CliIo.d.ts.map +1 -1
  30. package/typings/domain/GlobalConfig.d.ts +1 -1
  31. package/typings/domain/GlobalConfig.d.ts.map +1 -1
  32. package/typings/exceptions/DomainException.d.ts +4 -4
  33. package/typings/exceptions/DomainException.d.ts.map +1 -1
  34. package/typings/features/build/BuildConfig.d.ts +107 -103
  35. package/typings/features/build/BuildConfig.d.ts.map +1 -1
  36. package/typings/features/build/BuildController.d.ts +23 -17
  37. package/typings/features/build/BuildController.d.ts.map +1 -1
  38. package/typings/features/build/domain/ImportPaths.d.ts +16 -14
  39. package/typings/features/build/domain/ImportPaths.d.ts.map +1 -1
  40. package/typings/features/build/domain/emit.d.ts +4 -4
  41. package/typings/features/build/domain/emit.d.ts.map +1 -1
  42. package/typings/features/build/domain/scan.d.ts +2 -0
  43. package/typings/features/build/domain/scan.d.ts.map +1 -1
  44. package/typings/features/build/domain/tsconfig.d.ts +9 -16
  45. package/typings/features/build/domain/tsconfig.d.ts.map +1 -1
  46. package/typings/features/build/services/BundleBuilder.d.ts +25 -29
  47. package/typings/features/build/services/BundleBuilder.d.ts.map +1 -1
  48. package/typings/features/build/services/TicConfigService.d.ts +11 -15
  49. package/typings/features/build/services/TicConfigService.d.ts.map +1 -1
  50. package/typings/index.d.ts +6 -7
  51. package/typings/schema/ticConfigSchema.d.ts.map +1 -1
  52. package/cjm/features/build/domain/configFiles.js +0 -35
  53. package/typings/features/build/domain/configFiles.d.ts +0 -13
  54. package/typings/features/build/domain/configFiles.d.ts.map +0 -1
@@ -6,10 +6,12 @@ const zod_1 = require("zod");
6
6
  const DomainException_1 = require("../exceptions/DomainException");
7
7
  const commandArgs = ({ args = [] }) => args.map(String);
8
8
  exports.commandArgs = commandArgs;
9
- const parseOptions = (options) => (argv) => {
9
+ const parseOptions = (options, { positionals = false } = {}) => (argv) => {
10
10
  const firstFlag = argv.findIndex((arg) => arg.startsWith('-'));
11
+ const args = positionals ? argv.slice(1) : firstFlag >= 0 ? argv.slice(firstFlag) : [];
11
12
  try {
12
- return { ...(0, node_util_1.parseArgs)({ args: firstFlag >= 0 ? argv.slice(firstFlag) : [], options, strict: true }).values };
13
+ const parsed = (0, node_util_1.parseArgs)({ args, options, strict: true, allowPositionals: positionals });
14
+ return positionals ? { ...parsed.values, positionals: parsed.positionals } : { ...parsed.values };
13
15
  }
14
16
  catch (e) {
15
17
  throw new DomainException_1.UsageError(e.message);
package/cjm/cli/usage.js CHANGED
@@ -6,14 +6,17 @@ exports.USAGE = [
6
6
  ' tic is a shortcut for ts-ioc-container',
7
7
  '',
8
8
  'Commands:',
9
- ' ts-ioc-container build [--config <path>]... [--check] generate one bundle per *.bundle.{json,yaml,yml} config',
10
- ' (none: one bundle from tsconfig.json, all defaults)',
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
+ ' 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',
13
13
  '',
14
14
  'Options:',
15
- ' -c, --config <path> build only this config; repeatable (default: every *.bundle.{json,yaml,yml} of the package, else tsconfig.json)',
16
- ' --check write nothing; exit 1 when a generated bundle is out of date',
17
- ' -h, --help show this help',
18
- ' -v, --version show the version',
15
+ ' -h, --help show this help',
16
+ ' -v, --version show the version',
17
+ '',
18
+ 'The bundle goes to stdout and warnings to stderr; saving it is up to you:',
19
+ ' tic build app.bundle.json > src/di/app.bundle.ts',
20
+ ' cat app.bundle.yml | tic build > src/di/app.bundle.ts # config from stdin (must set name)',
21
+ ' tic build app.bundle.json | diff - src/di/app.bundle.ts # CI: is it current?',
19
22
  ].join('\n');
@@ -1,7 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.processIo = exports.CliIoKey = void 0;
4
+ const node_fs_1 = require("node:fs");
4
5
  const ts_ioc_container_1 = require("ts-ioc-container");
5
6
  exports.CliIoKey = new ts_ioc_container_1.SingleToken('CliIo');
6
- const processIo = () => ({ cwd: process.cwd(), stdout: console.log, stderr: console.error });
7
+ const processIo = () => ({
8
+ cwd: process.cwd(),
9
+ stdout: console.log,
10
+ stderr: console.error,
11
+ stdin: () => (process.stdin.isTTY ? undefined : (0, node_fs_1.readFileSync)(0, 'utf8')),
12
+ });
7
13
  exports.processIo = processIo;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StaleBundlesError = exports.UsageError = exports.UnknownActionError = exports.UnknownCommandError = exports.MissingCommandError = exports.TicConfigError = exports.TicError = void 0;
3
+ exports.UsageError = exports.UnknownActionError = exports.UnknownCommandError = exports.MissingCommandError = exports.NamespaceNotFoundError = exports.TicConfigError = exports.TicError = void 0;
4
4
  class TicError extends Error {
5
5
  code;
6
6
  constructor(code, message) {
@@ -16,6 +16,12 @@ class TicConfigError extends TicError {
16
16
  }
17
17
  }
18
18
  exports.TicConfigError = TicConfigError;
19
+ class NamespaceNotFoundError extends TicError {
20
+ constructor(message) {
21
+ super('NAMESPACE_NOT_FOUND', message);
22
+ }
23
+ }
24
+ exports.NamespaceNotFoundError = NamespaceNotFoundError;
19
25
  class MissingCommandError extends TicError {
20
26
  constructor(usage) {
21
27
  super('MISSING_COMMAND', `missing command\n${usage}`);
@@ -40,9 +46,3 @@ class UsageError extends TicError {
40
46
  }
41
47
  }
42
48
  exports.UsageError = UsageError;
43
- class StaleBundlesError extends TicError {
44
- constructor(count) {
45
- super('STALE_BUNDLES', `${count} generated bundle${count === 1 ? ' is' : 's are'} out of date — run \`tic build\``);
46
- }
47
- }
48
- exports.StaleBundlesError = StaleBundlesError;
@@ -3,15 +3,16 @@ 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.configSuffix = exports.CONFIG_FILE_SUFFIXES = exports.BUNDLE_CONFIG_SCHEMA = exports.DEFAULT_EXCLUDE = exports.DEFAULT_DECORATORS = exports.DEFAULT_BUNDLE_NAME = exports.DEFAULT_EXTENDS = void 0;
6
+ exports.configFormat = exports.STDIN = exports.CONFIG_FILE_SUFFIXES = exports.BUNDLE_CONFIG_SCHEMA = exports.DEFAULT_EXCLUDE = exports.DEFAULT_DECORATORS = exports.DEFAULT_TSCONFIG = void 0;
7
+ exports.parseConfig = parseConfig;
7
8
  exports.toClassName = toClassName;
8
9
  exports.resolveConfig = resolveConfig;
9
10
  const node_path_1 = __importDefault(require("node:path"));
11
+ const yaml_1 = require("yaml");
10
12
  const zod_1 = require("zod");
11
13
  const DomainException_1 = require("../../exceptions/DomainException");
12
14
  const glob_1 = require("./domain/glob");
13
- exports.DEFAULT_EXTENDS = './tsconfig.json';
14
- exports.DEFAULT_BUNDLE_NAME = 'base';
15
+ exports.DEFAULT_TSCONFIG = './tsconfig.json';
15
16
  exports.DEFAULT_DECORATORS = ['register'];
16
17
  exports.DEFAULT_EXCLUDE = [
17
18
  '**/*.spec.ts',
@@ -30,6 +31,36 @@ const strings = (what, minItems) => zod_1.z
30
31
  .meta({ type: 'array', items: { type: 'string', minLength: 1 }, ...(minItems > 0 && { minItems }) });
31
32
  const stringList = () => strings('an array of strings', 0);
32
33
  const nonEmptyStringList = () => strings('a non-empty array of strings', 1);
34
+ const globs = () => zod_1.z
35
+ .custom((value) => isNonEmptyString(value) || (Array.isArray(value) && value.length > 0 && value.every(isNonEmptyString)), expected('a glob or a non-empty array of globs'))
36
+ .meta({
37
+ anyOf: [
38
+ { type: 'string', minLength: 1 },
39
+ { type: 'array', items: { type: 'string', minLength: 1 }, minItems: 1 },
40
+ ],
41
+ });
42
+ const toGlobs = (value) => value === undefined ? undefined : (Array.isArray(value) ? value : [value]).map(glob_1.globToRegExp);
43
+ const PATH_SCHEMA = zod_1.z.union([
44
+ nonEmptyString(),
45
+ zod_1.z.object({
46
+ path: nonEmptyString(),
47
+ recursive: zod_1.z.boolean(expected('a boolean')).default(true),
48
+ }, expected('a string or an object with "path"')),
49
+ ], expected('a string or an object with "path"'));
50
+ const GLOB_SCHEMA = zod_1.z
51
+ .strictObject({
52
+ paths: zod_1.z
53
+ .array(PATH_SCHEMA, expected('a non-empty array'))
54
+ .min(1, expected('a non-empty array'))
55
+ .describe('Folders to scan: paths relative to the config file (./src/services) or tsconfig paths aliases (@app/services). Sub-folders are scanned too unless an entry says { "path": "...", "recursive": false }.'),
56
+ include: nonEmptyStringList()
57
+ .optional()
58
+ .describe('Globs a file must match one of, e.g. ["**/*.service.ts", "**/*.repository.ts"]. Default: every source file.'),
59
+ exclude: stringList()
60
+ .default(exports.DEFAULT_EXCLUDE)
61
+ .describe('Globs of files never read. Replaces the default when given. A non-empty list that omits a default glob makes `tic build` warn.'),
62
+ }, expected('an object'))
63
+ .describe('Which files this bundle reads and parses — decided by path alone, before parsing. `paths` are the folders the files come from; within them a file is parsed when it matches one of `include` and none of `exclude`. Globs are relative to the config file. With a file naming convention, `include` keeps every other file unread.');
33
64
  const CLASSES_SCHEMA = zod_1.z
34
65
  .strictObject({
35
66
  export: zod_1.z
@@ -39,57 +70,31 @@ const CLASSES_SCHEMA = zod_1.z
39
70
  decorators: stringList()
40
71
  .default(exports.DEFAULT_DECORATORS)
41
72
  .describe('The class must carry one of these decorators, recognised by name (e.g. "register", or a composed decorator). Default ["register"]; [] requires none.'),
42
- name: nonEmptyString()
43
- .optional()
44
- .describe('Glob the class name must match, e.g. "*Service". An anonymous default export is named after its file.'),
45
- excludeClasses: nonEmptyStringList()
46
- .optional()
47
- .describe('Class names to drop after selection, e.g. one test double sitting next to the real registration in the same file.'),
48
- excludeName: nonEmptyString()
49
- .optional()
50
- .describe('Glob the class name must NOT match, e.g. "*Mock". An anonymous default export is named after its file.'),
51
- }, expected('an object'))
52
- .describe('Which classes of a scanned file are registered. A class is selected when it is exported, not abstract, and meets every criterion set here; by default that is every exported class decorated with @register.');
53
- const COMPILER_OPTIONS_SCHEMA = zod_1.z
54
- .looseObject({
55
- importExtension: zod_1.z
56
- .string(expected('a string'))
57
- .optional()
58
- .describe('Extension of generated imports, e.g. ".js". Inferred from the tsconfig: ".js" under node16/nodenext resolution, none otherwise.'),
59
- classes: CLASSES_SCHEMA.optional(),
60
- paths: zod_1.z
61
- .record(zod_1.z.string(), zod_1.z.array(zod_1.z.string()))
73
+ glob: globs()
62
74
  .optional()
63
- .describe('tsconfig `paths`: aliases the generated imports are written in.'),
64
- rootDir: zod_1.z
65
- .string()
75
+ .describe('Glob, or globs, the class name must match one of, e.g. "*Service" or ["*Service", "*Repository"]. An anonymous default export is named after its file.'),
76
+ exclude: globs()
66
77
  .optional()
67
- .describe('tsconfig `rootDir`: where the default output (<rootDir>/<name>.bundle.ts) goes.'),
78
+ .describe('Glob, or globs, the class name must match none of, applied after every other criterion. A plain name is a glob too: ["MockUserRepository", "*Fake"].'),
68
79
  }, expected('an object'))
69
- .describe("tsconfig `compilerOptions`, plus the bundler's own `importExtension` and `classes`. Every other option is handed to TypeScript on top of the extended tsconfig's, which validates it.");
70
- const tsconfigGlobs = (field, note = '') => stringList()
71
- .optional()
72
- .describe(`tsconfig \`${field}\`: replaces the extended tsconfig's. Relative to \`baseUrl\`.${note}`);
80
+ .describe('Which classes of a parsed file are registered. A class is selected when it is exported, not abstract, and meets every criterion set here; by default that is every exported class decorated with @register.');
73
81
  exports.BUNDLE_CONFIG_SCHEMA = zod_1.z.strictObject({
74
82
  $schema: zod_1.z.string().optional(),
75
- extends: nonEmptyString()
76
- .optional()
77
- .meta({ default: exports.DEFAULT_EXTENDS })
78
- .describe("The tsconfig this bundle extends, as a tsconfig's own `extends`: its file set, `paths` aliases and module resolution apply unless this file overrides them. Relative to this file. The default may be absent; a tsconfig named here must exist."),
79
- baseUrl: nonEmptyString()
80
- .optional()
81
- .describe("The base folder this config's relative `include`, `exclude` and `output` resolve against, so they need not repeat a shared prefix such as `src`. Relative to this file; default this file's directory. Also handed to TypeScript as `compilerOptions.baseUrl` (unless that is set too), so generated imports use it."),
82
- output: nonEmptyString()
83
- .optional()
84
- .describe('The bundle file (convention: *.bundle.ts). Default `<root>/<name>.bundle.ts`: <root> is the compiled rootDir, else the common folder of the files compiled.'),
85
83
  name: zod_1.z
86
84
  .string(expected('letters, digits, "-" or "_", starting with a letter'))
87
85
  .regex(BUNDLE_NAME, expected('letters, digits, "-" or "_", starting with a letter'))
88
86
  .optional()
89
- .describe('The bundle\'s name: letters, digits, "-" and "_", starting with a letter. Default: this file\'s stem (production.bundle.json → production), else "base". It names the default output (production.bundle.ts) and the generated IContainerModule class (ProductionBundle).'),
90
- include: tsconfigGlobs('include', ' When `baseUrl` is set and this is omitted, the whole `baseUrl` folder (`.`) is scanned.'),
91
- exclude: tsconfigGlobs('exclude'),
92
- compilerOptions: COMPILER_OPTIONS_SCHEMA.optional(),
87
+ .describe('The bundle\'s name: letters, digits, "-" and "_", starting with a letter. Default: the config file\'s stem (production.bundle.json → production); required for a config file named otherwise. It names the generated IContainerModule class (ProductionBundle).'),
88
+ tsconfig: nonEmptyString()
89
+ .optional()
90
+ .meta({ default: exports.DEFAULT_TSCONFIG })
91
+ .describe('The tsconfig whose `paths` aliases glob.paths may name and generated imports are written in, and whose module resolution sets the import extension. It adds no files: the bundle scans glob.paths alone. Relative to the config file. The default may be absent; a tsconfig named here must exist.'),
92
+ importExtension: zod_1.z
93
+ .string(expected('a string'))
94
+ .optional()
95
+ .describe('Extension of generated imports, e.g. ".js". Inferred from the tsconfig: ".js" under node16/nodenext resolution, none otherwise.'),
96
+ glob: GLOB_SCHEMA,
97
+ className: CLASSES_SCHEMA.optional(),
93
98
  }, expected('an object'));
94
99
  function formatPath(segments) {
95
100
  const formatted = segments
@@ -98,33 +103,68 @@ function formatPath(segments) {
98
103
  .replace(/^\./, '');
99
104
  return formatted || 'config';
100
105
  }
101
- function formatIssue(issue) {
102
- if (issue.code === 'unrecognized_keys') {
103
- return issue.keys.map((key) => `${formatPath([...issue.path, key])}: unknown field`);
104
- }
105
- return [`${formatPath(issue.path)}: ${issue.message}`];
106
+ function matchedBranch(issue) {
107
+ const typeMatched = issue.errors.filter((branch) => branch.every((inner) => inner.code !== 'invalid_type' || inner.path.length > 0));
108
+ return typeMatched.length === 1 ? typeMatched[0] : undefined;
109
+ }
110
+ function formatIssue(issue, prefix = []) {
111
+ const at = [...prefix, ...issue.path];
112
+ if (issue.code === 'unrecognized_keys')
113
+ return issue.keys.map((key) => `${formatPath([...at, key])}: unknown field`);
114
+ const branch = issue.code === 'invalid_union' ? matchedBranch(issue) : undefined;
115
+ if (branch)
116
+ return branch.flatMap((inner) => formatIssue(inner, at));
117
+ return [`${formatPath(at)}: ${issue.message}`];
106
118
  }
107
119
  const formatIssues = (error) => error.issues.flatMap((issue) => formatIssue(issue)).join('; ');
120
+ function toGlobSelector(glob) {
121
+ return {
122
+ paths: glob.paths.map((entry) => (typeof entry === 'string' ? { path: entry, recursive: true } : entry)),
123
+ include: glob.include?.map(glob_1.globToRegExp),
124
+ exclude: glob.exclude.map(glob_1.globToRegExp),
125
+ };
126
+ }
108
127
  function toClassSelector(classes) {
109
128
  if (classes === undefined)
110
129
  return { export: 'any', decorators: exports.DEFAULT_DECORATORS };
111
- const { decorators, name, excludeName, ...rest } = classes;
130
+ const { decorators, glob, exclude, ...rest } = classes;
112
131
  return {
113
132
  ...rest,
114
133
  decorators: decorators.length > 0 ? decorators : undefined,
115
- name: name === undefined ? undefined : (0, glob_1.globToRegExp)(name),
116
- excludeName: excludeName === undefined ? undefined : (0, glob_1.globToRegExp)(excludeName),
134
+ glob: toGlobs(glob),
135
+ exclude: toGlobs(exclude),
117
136
  };
118
137
  }
138
+ function excludeWarnings(glob) {
139
+ const { exclude } = glob;
140
+ if (exclude.length === 0)
141
+ return [];
142
+ const missing = exports.DEFAULT_EXCLUDE.filter((glob) => !exclude.includes(glob));
143
+ if (missing.length === 0)
144
+ return [];
145
+ return [
146
+ `glob.exclude replaces the default excludes and omits ${missing.map((glob) => `"${glob}"`).join(', ')}; ` +
147
+ `add them back`,
148
+ ];
149
+ }
119
150
  exports.CONFIG_FILE_SUFFIXES = ['.bundle.json', '.bundle.yaml', '.bundle.yml'];
120
- const configSuffix = (file) => exports.CONFIG_FILE_SUFFIXES.find((suffix) => file.endsWith(suffix));
121
- exports.configSuffix = configSuffix;
122
151
  function configStem(file) {
123
152
  const base = node_path_1.default.basename(file);
124
- const suffix = (0, exports.configSuffix)(base);
153
+ const suffix = exports.CONFIG_FILE_SUFFIXES.find((candidate) => base.endsWith(candidate));
125
154
  const stem = suffix ? base.slice(0, -suffix.length) : '';
126
155
  return BUNDLE_NAME.test(stem) ? stem : undefined;
127
156
  }
157
+ exports.STDIN = '-';
158
+ const configFormat = (file) => file.endsWith('.json') ? 'json' : file.endsWith('.yaml') || file.endsWith('.yml') ? 'yaml' : undefined;
159
+ exports.configFormat = configFormat;
160
+ function parseConfig(text, format) {
161
+ try {
162
+ return format === 'yaml' ? ((0, yaml_1.parse)(text) ?? {}) : JSON.parse(text);
163
+ }
164
+ catch (e) {
165
+ throw new DomainException_1.TicConfigError(`not valid ${format.toUpperCase()}: ${e.message}`);
166
+ }
167
+ }
128
168
  function toClassName(name) {
129
169
  const words = name.split(/[-_]+/).filter(Boolean);
130
170
  return `${words.map((word) => word[0].toUpperCase() + word.slice(1)).join('')}Bundle`;
@@ -133,26 +173,23 @@ function resolveConfig(content, file) {
133
173
  const result = exports.BUNDLE_CONFIG_SCHEMA.safeParse(content);
134
174
  if (!result.success)
135
175
  throw new DomainException_1.TicConfigError(formatIssues(result.error));
136
- const { output, name, baseUrl, extends: tsconfig, include, exclude, compilerOptions = {} } = result.data;
137
- const { importExtension, classes, ...tsCompilerOptions } = compilerOptions;
176
+ const { name, tsconfig, importExtension, glob, className } = result.data;
177
+ const bundleName = name ?? configStem(file);
178
+ if (bundleName === undefined) {
179
+ throw new DomainException_1.TicConfigError(node_path_1.default.basename(file) === exports.STDIN
180
+ ? 'name: required for a config read from stdin'
181
+ : `name: required when the config file is not named <name>${exports.CONFIG_FILE_SUFFIXES.join(' / ')}`);
182
+ }
138
183
  const dir = node_path_1.default.dirname(file);
139
- const base = baseUrl === undefined ? dir : node_path_1.default.resolve(dir, baseUrl);
140
- const includeGlobs = include ?? (baseUrl === undefined ? undefined : ['.']);
141
- const rebase = (globs) => globs?.map((glob) => (node_path_1.default.isAbsolute(glob) ? glob : (0, glob_1.toPosix)(node_path_1.default.relative(dir, node_path_1.default.resolve(base, glob))) || '.'));
142
- const bundleName = name ?? configStem(file) ?? exports.DEFAULT_BUNDLE_NAME;
143
184
  return {
144
185
  file,
145
186
  dir,
146
- output: output === undefined ? undefined : node_path_1.default.resolve(base, output),
147
187
  name: bundleName,
148
- className: toClassName(bundleName),
149
- tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.DEFAULT_EXTENDS), required: tsconfig !== undefined },
150
- overrides: {
151
- include: rebase(includeGlobs),
152
- exclude: rebase(exclude),
153
- compilerOptions: baseUrl === undefined ? tsCompilerOptions : { baseUrl, ...tsCompilerOptions },
154
- },
188
+ bundleClassName: toClassName(bundleName),
189
+ tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.DEFAULT_TSCONFIG), required: tsconfig !== undefined },
155
190
  importExtension,
156
- classes: toClassSelector(classes),
191
+ glob: toGlobSelector(glob),
192
+ className: toClassSelector(className),
193
+ warnings: excludeWarnings(glob),
157
194
  };
158
195
  }
@@ -11,65 +11,56 @@ 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
- };
17
14
  Object.defineProperty(exports, "__esModule", { value: true });
18
15
  exports.BuildController = exports.BUILD_OPTIONS = void 0;
19
- const node_path_1 = __importDefault(require("node:path"));
20
16
  const ts_ioc_container_1 = require("ts-ioc-container");
21
17
  const zod_1 = require("zod");
22
18
  const cli_1 = require("../../cli");
23
- const GlobalConfig_1 = require("../../domain/GlobalConfig");
19
+ const CliIo_1 = require("../../domain/CliIo");
24
20
  const DomainException_1 = require("../../exceptions/DomainException");
25
21
  const ConsoleLogger_1 = require("../../services/ConsoleLogger");
26
22
  const OutputService_1 = require("../../services/OutputService");
23
+ const BuildConfig_1 = require("./BuildConfig");
27
24
  const BundleBuilder_1 = require("./services/BundleBuilder");
28
- const TicConfigService_1 = require("./services/TicConfigService");
29
- exports.BUILD_OPTIONS = zod_1.z.object({
30
- config: zod_1.z.array(zod_1.z.string().min(1)).default([]),
31
- check: zod_1.z.boolean().default(false),
32
- });
33
- const buildArgs = (0, cli_1.parseOptions)({
34
- config: { type: 'string', short: 'c', multiple: true },
35
- check: { type: 'boolean' },
25
+ 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(' ')}` });
28
+ return zod_1.z.NEVER;
29
+ }
30
+ return { config: positionals[0] ?? BuildConfig_1.STDIN };
36
31
  });
37
- const VERB = { written: 'wrote', unchanged: 'unchanged', stale: 'stale' };
38
- const plural = (n) => `${n} registration${n === 1 ? '' : 's'}`;
32
+ const buildArgs = (0, cli_1.parseOptions)({}, { positionals: true });
39
33
  let BuildController = class BuildController {
40
- cwd;
41
- configs;
34
+ io;
42
35
  builder;
43
36
  output;
44
37
  logger;
45
- constructor(cwd, configs, builder, output, logger) {
46
- this.cwd = cwd;
47
- this.configs = configs;
38
+ constructor(io, builder, output, logger) {
39
+ this.io = io;
48
40
  this.builder = builder;
49
41
  this.output = output;
50
42
  this.logger = logger;
51
43
  }
52
- build(options) {
53
- let stale = 0;
54
- for (const config of this.configs.discover(options.config)) {
55
- const name = config === undefined ? TicConfigService_1.TSCONFIG_FILE : node_path_1.default.relative(this.cwd, config);
56
- try {
57
- const { output, warnings } = this.builder.build({ config, check: options.check });
58
- for (const warning of warnings)
59
- this.logger.warn(`${name}: ${warning}`);
60
- const { status, file, registrations } = output;
61
- this.output.write(`${VERB[status].padEnd(10)}${node_path_1.default.relative(this.cwd, file)} (${plural(registrations)})`);
62
- if (status === 'stale')
63
- stale++;
64
- }
65
- catch (e) {
66
- if (e instanceof DomainException_1.TicError)
67
- e.message = `${name}: ${e.message}`;
68
- throw e;
69
- }
44
+ build({ config }) {
45
+ const label = config === BuildConfig_1.STDIN ? '<stdin>' : config;
46
+ try {
47
+ const { content, warnings } = this.builder.build(config === BuildConfig_1.STDIN ? { text: this.stdin() } : { config });
48
+ for (const warning of warnings)
49
+ this.logger.warn(`${label}: ${warning}`);
50
+ this.output.write(content.replace(/\n$/, ''));
51
+ }
52
+ catch (e) {
53
+ if (e instanceof DomainException_1.TicError && !(e instanceof DomainException_1.UsageError))
54
+ e.message = `${label}: ${e.message}`;
55
+ throw e;
56
+ }
57
+ }
58
+ stdin() {
59
+ const text = this.io.stdin?.();
60
+ if (text === undefined) {
61
+ throw new DomainException_1.UsageError('missing <config>: name a config file, or pipe one in (cat app.bundle.json | tic build)');
70
62
  }
71
- if (stale > 0)
72
- throw new DomainException_1.StaleBundlesError(stale);
63
+ return text;
73
64
  }
74
65
  };
75
66
  exports.BuildController = BuildController;
@@ -82,10 +73,9 @@ __decorate([
82
73
  ], BuildController.prototype, "build", null);
83
74
  exports.BuildController = BuildController = __decorate([
84
75
  (0, ts_ioc_container_1.register)('build'),
85
- __param(0, (0, ts_ioc_container_1.inject)((0, GlobalConfig_1.globalConfig)('cwd'))),
86
- __param(1, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(TicConfigService_1.ITicConfigServiceKey))),
87
- __param(2, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(BundleBuilder_1.IBundleBuilderKey))),
88
- __param(3, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(OutputService_1.IOutputServiceKey))),
89
- __param(4, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(ConsoleLogger_1.ILoggerKey))),
90
- __metadata("design:paramtypes", [String, Object, Object, Object, Object])
76
+ __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])
91
81
  ], BuildController);
@@ -37,10 +37,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.ImportPaths = void 0;
40
+ const node_fs_1 = require("node:fs");
40
41
  const node_path_1 = __importDefault(require("node:path"));
41
42
  const ts = __importStar(require("typescript"));
43
+ const DomainException_1 = require("../../../exceptions/DomainException");
42
44
  const glob_1 = require("./glob");
43
45
  const SOURCE_EXTENSION = /\.(tsx?|mts|cts)$/;
46
+ const isDirectory = (dir) => (0, node_fs_1.existsSync)(dir) && (0, node_fs_1.statSync)(dir).isDirectory();
47
+ const isRelative = (spec) => spec.startsWith('.') || node_path_1.default.isAbsolute(spec);
44
48
  function splitWildcard(pattern) {
45
49
  const star = pattern.indexOf('*');
46
50
  return star === -1 ? undefined : [pattern.slice(0, star), pattern.slice(star + 1)];
@@ -48,7 +52,6 @@ function splitWildcard(pattern) {
48
52
  class ImportPaths {
49
53
  aliases;
50
54
  extension;
51
- baseUrl;
52
55
  static fromOptions(options, tsconfigFile, importExtension) {
53
56
  const { paths = {}, baseUrl, pathsBasePath, moduleResolution } = options;
54
57
  const base = baseUrl ?? (typeof pathsBasePath === 'string' ? pathsBasePath : node_path_1.default.dirname(tsconfigFile));
@@ -57,30 +60,26 @@ class ImportPaths {
57
60
  targets: targets.map((target) => (0, glob_1.toPosix)(node_path_1.default.resolve(base, target))),
58
61
  }));
59
62
  const nodeEsm = moduleResolution === ts.ModuleResolutionKind.Node16 || moduleResolution === ts.ModuleResolutionKind.NodeNext;
60
- return new ImportPaths(aliases, importExtension ?? (nodeEsm ? '.js' : ''), baseUrl === undefined ? undefined : (0, glob_1.toPosix)(baseUrl));
63
+ return new ImportPaths(aliases, importExtension ?? (nodeEsm ? '.js' : ''));
61
64
  }
62
- constructor(aliases, extension, baseUrl) {
65
+ constructor(aliases, extension) {
63
66
  this.aliases = aliases;
64
67
  this.extension = extension;
65
- this.baseUrl = baseUrl;
66
68
  }
67
- specifier(fromFile, toFile) {
68
- const target = (0, glob_1.toPosix)(toFile);
69
- const bare = target.replace(SOURCE_EXTENSION, '');
70
- const best = this.bestAlias(target, bare);
71
- if (best)
72
- return best.spec;
73
- const fromBaseUrl = this.baseUrlSpecifier(bare);
74
- if (fromBaseUrl)
75
- return `${fromBaseUrl}${this.extension}`;
76
- const relative = (0, glob_1.toPosix)(node_path_1.default.relative(node_path_1.default.dirname(fromFile), bare));
77
- return `${relative.startsWith('.') ? relative : `./${relative}`}${this.extension}`;
69
+ resolveNamespace(namespace, baseDir) {
70
+ const folder = node_path_1.default.resolve(baseDir, namespace);
71
+ if (isDirectory(folder))
72
+ return folder;
73
+ if (!isRelative(namespace)) {
74
+ const found = [namespace, `${namespace}/`].flatMap((spec) => this.aliasTargets(spec)).find(isDirectory);
75
+ if (found)
76
+ return node_path_1.default.normalize(found);
77
+ }
78
+ throw new DomainException_1.NamespaceNotFoundError(`namespace "${namespace}" is neither a folder relative to ${baseDir} nor a tsconfig paths alias of one`);
78
79
  }
79
- baseUrlSpecifier(bare) {
80
- if (!this.baseUrl)
81
- return undefined;
82
- const prefix = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/`;
83
- return bare.startsWith(prefix) ? bare.slice(prefix.length) : undefined;
80
+ specifier(file) {
81
+ const target = (0, glob_1.toPosix)(file);
82
+ return this.bestAlias(target, target.replace(SOURCE_EXTENSION, ''))?.spec;
84
83
  }
85
84
  bestAlias(target, bare) {
86
85
  let best;
@@ -93,6 +92,18 @@ class ImportPaths {
93
92
  }
94
93
  return best;
95
94
  }
95
+ aliasTargets(spec) {
96
+ return this.aliases.flatMap(({ pattern, targets }) => {
97
+ const wildcard = splitWildcard(pattern);
98
+ if (!wildcard)
99
+ return pattern === spec ? targets : [];
100
+ const [prefix, suffix] = wildcard;
101
+ if (!spec.startsWith(prefix) || !spec.endsWith(suffix) || spec.length < prefix.length + suffix.length)
102
+ return [];
103
+ const captured = spec.slice(prefix.length, spec.length - suffix.length);
104
+ return targets.map((target) => target.replace('*', captured));
105
+ });
106
+ }
96
107
  matchAlias(pattern, aliasTarget, file, bare) {
97
108
  const wildcard = splitWildcard(aliasTarget);
98
109
  if (!wildcard) {
@@ -7,7 +7,7 @@ const HandlebarsRenderService_1 = require("../../../services/HandlebarsRenderSer
7
7
  exports.GENERATED_HEADER = '// Generated by `tic build`';
8
8
  exports.BUNDLE_PROTOCOL = 'Bundle.ts.hbs';
9
9
  const RUNTIME_IMPORTS = ['IContainer', 'IContainerModule', 'IRegistration', 'Registration'];
10
- function bundleView({ name, configPath, classes }) {
10
+ function bundleView({ name, paths, classes }) {
11
11
  const taken = new Set([...RUNTIME_IMPORTS, 'registrations', name]);
12
12
  const uniqueName = (wanted) => {
13
13
  let candidate = wanted;
@@ -27,7 +27,7 @@ function bundleView({ name, configPath, classes }) {
27
27
  entry.named.push(binding === cls.exportName ? binding : `${cls.exportName} as ${binding}`);
28
28
  return binding;
29
29
  });
30
- return { configPath, name, imports: [...byFile.values()], registrations: bindings };
30
+ return { paths, name, imports: [...byFile.values()], registrations: bindings };
31
31
  }
32
32
  function emitBundle(input) {
33
33
  return (0, HandlebarsRenderService_1.renderProtocol)(exports.BUNDLE_PROTOCOL, bundleView(input));
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.isSourceFile = void 0;
40
+ exports.listSourceFiles = listSourceFiles;
40
41
  exports.findClasses = findClasses;
41
42
  const node_fs_1 = require("node:fs");
42
43
  const node_path_1 = __importDefault(require("node:path"));
@@ -45,6 +46,23 @@ const SOURCE_FILE = /\.(tsx?|mts|cts)$/;
45
46
  const DECLARATION_FILE = /\.d\.[mc]?tsx?$/;
46
47
  const isSourceFile = (file) => SOURCE_FILE.test(file) && !DECLARATION_FILE.test(file);
47
48
  exports.isSourceFile = isSourceFile;
49
+ function listSourceFiles(dir, recursive, isExcluded) {
50
+ const files = [];
51
+ const walk = (current) => {
52
+ for (const entry of (0, node_fs_1.readdirSync)(current, { withFileTypes: true })) {
53
+ const full = node_path_1.default.join(current, entry.name);
54
+ if (entry.isDirectory()) {
55
+ if (recursive && entry.name !== 'node_modules')
56
+ walk(full);
57
+ }
58
+ else if ((0, exports.isSourceFile)(entry.name) && !isExcluded(full)) {
59
+ files.push(full);
60
+ }
61
+ }
62
+ };
63
+ walk(dir);
64
+ return files.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
65
+ }
48
66
  const hasModifier = (node, kind) => ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((m) => m.kind === kind);
49
67
  function decoratorName(decorator, imports) {
50
68
  const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
@@ -128,11 +146,9 @@ function findClasses(file, selector, text = (0, node_fs_1.readFileSync)(file, 'u
128
146
  return false;
129
147
  if (selector.decorators && !selector.decorators.some((name) => decorators.includes(name)))
130
148
  return false;
131
- if (selector.name && !selector.name.test(className))
132
- return false;
133
- if (selector.excludeClasses?.includes(className))
149
+ if (selector.glob && !selector.glob.some((glob) => glob.test(className)))
134
150
  return false;
135
- if (selector.excludeName?.test(className))
151
+ if (selector.exclude?.some((glob) => glob.test(className)))
136
152
  return false;
137
153
  return true;
138
154
  })
@@ -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 compilerOptions.classes.excludeClasses`);
20
+ `registration is last-wins, exclude one with className.exclude`);
21
21
  });
22
22
  }
23
23
  function scopeDisjoint(group) {