@ts-ioc-container/bundler 3.1.0 → 5.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 +48 -48
  2. package/README.md +199 -144
  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 +126 -77
  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 +35 -30
  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 +132 -77
  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 +102 -102
  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
@@ -36,11 +36,10 @@ class Application {
36
36
  if (!this.scope.hasRegistration(command))
37
37
  throw new DomainException_1.UnknownCommandError(command);
38
38
  const controller = this.scope.resolve(command);
39
- const collector = new ts_ioc_container_1.HookCollector({
40
- key: action,
41
- createExecutionContext: (0, ts_ioc_container_1.createHookContextFactory)({ args: argv }),
42
- });
43
- const actions = collector.getActions(controller, { scope: this.scope });
39
+ const actionsOf = (key) => new ts_ioc_container_1.HookCollector({ key, createExecutionContext: (0, ts_ioc_container_1.createHookContextFactory)({ args: argv }) }).getActions(controller, { scope: this.scope });
40
+ let actions = actionsOf(action);
41
+ if (actions.length === 0 && action !== decorators_1.DEFAULT_ACTION)
42
+ actions = actionsOf(decorators_1.DEFAULT_ACTION);
44
43
  if (actions.length === 0)
45
44
  throw new DomainException_1.UnknownActionError(command, action);
46
45
  for (const item of actions)
@@ -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',
@@ -29,9 +30,34 @@ const strings = (what, minItems) => zod_1.z
29
30
  .custom((value) => Array.isArray(value) && value.length >= minItems && value.every(isNonEmptyString), expected(what))
30
31
  .meta({ type: 'array', items: { type: 'string', minLength: 1 }, ...(minItems > 0 && { minItems }) });
31
32
  const stringList = () => strings('an array of strings', 0);
32
- const nonEmptyStringList = () => strings('a non-empty array of strings', 1);
33
- const CLASSES_SCHEMA = zod_1.z
34
- .strictObject({
33
+ const globList = (what, minItems) => zod_1.z
34
+ .custom((value) => isNonEmptyString(value) || (Array.isArray(value) && value.length >= minItems && value.every(isNonEmptyString)), expected(what))
35
+ .meta({
36
+ anyOf: [
37
+ { type: 'string', minLength: 1 },
38
+ { type: 'array', items: { type: 'string', minLength: 1 }, ...(minItems > 0 && { minItems }) },
39
+ ],
40
+ });
41
+ const globs = () => globList('a glob or a non-empty array of globs', 1);
42
+ const toList = (value) => (Array.isArray(value) ? value : [value]);
43
+ const toGlobs = (value) => value === undefined ? undefined : toList(value).map(glob_1.globToRegExp);
44
+ const FOLDERS_SCHEMA = zod_1.z.array(nonEmptyString(), expected('a non-empty array')).min(1, expected('a non-empty array'));
45
+ const GLOB_OBJECT_SCHEMA = zod_1.z.strictObject({
46
+ glob: zod_1.z
47
+ .union([nonEmptyString(), FOLDERS_SCHEMA], expected('a folder or a non-empty array of folders'))
48
+ .describe('Folder, or folders, to scan, sub-folders included: paths relative to the config file (./src/services) or tsconfig paths aliases (@app/services).'),
49
+ exclude: globList('a glob or an array of globs', 0)
50
+ .default(exports.DEFAULT_EXCLUDE)
51
+ .describe('Glob, or globs, of files never read. Replaces the default when given. A non-empty value that omits a default glob makes `tic build` warn.'),
52
+ }, expected('an object'));
53
+ const GLOB_SCHEMA = zod_1.z
54
+ .union([
55
+ nonEmptyString().describe('One folder to scan, with the default exclude: shorthand for { "glob": [...] }.'),
56
+ FOLDERS_SCHEMA.describe('Folders to scan, with the default exclude: shorthand for { "glob": [...] }.'),
57
+ GLOB_OBJECT_SCHEMA,
58
+ ], expected('a folder, a non-empty array of folders or an object with "glob"'))
59
+ .describe('Which files this bundle reads and parses — decided by path alone, before parsing. `glob` names the folders the files come from; within them a file is parsed unless it matches one of `exclude`. Globs are relative to the config file. A folder, or a list of folders, is shorthand for { "glob": [...] }.');
60
+ const CLASS_OBJECT_SCHEMA = zod_1.z.strictObject({
35
61
  export: zod_1.z
36
62
  .enum(['any', 'named', 'default'], expected('"any", "named" or "default"'))
37
63
  .default('any')
@@ -39,57 +65,40 @@ const CLASSES_SCHEMA = zod_1.z
39
65
  decorators: stringList()
40
66
  .default(exports.DEFAULT_DECORATORS)
41
67
  .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()))
68
+ glob: globs()
62
69
  .optional()
63
- .describe('tsconfig `paths`: aliases the generated imports are written in.'),
64
- rootDir: zod_1.z
65
- .string()
70
+ .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.'),
71
+ exclude: globs()
66
72
  .optional()
67
- .describe('tsconfig `rootDir`: where the default output (<rootDir>/<name>.bundle.ts) goes.'),
68
- }, 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}`);
73
+ .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"].'),
74
+ }, expected('an object'));
75
+ const CLASSES_SCHEMA = zod_1.z
76
+ .union([
77
+ nonEmptyString().describe('One class-name glob, with the default export and decorators: shorthand for { "glob": "..." }.'),
78
+ zod_1.z
79
+ .array(nonEmptyString(), expected('a non-empty array'))
80
+ .min(1, expected('a non-empty array'))
81
+ .describe('Class-name globs, with the default export and decorators: shorthand for { "glob": [...] }.'),
82
+ CLASS_OBJECT_SCHEMA,
83
+ ], expected('a glob, a non-empty array of globs or an object'))
84
+ .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. A glob, or a list of globs, is shorthand for { "glob": ... }.');
73
85
  exports.BUNDLE_CONFIG_SCHEMA = zod_1.z.strictObject({
74
86
  $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
87
  name: zod_1.z
86
88
  .string(expected('letters, digits, "-" or "_", starting with a letter'))
87
89
  .regex(BUNDLE_NAME, expected('letters, digits, "-" or "_", starting with a letter'))
88
90
  .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(),
91
+ .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).'),
92
+ tsconfig: nonEmptyString()
93
+ .optional()
94
+ .meta({ default: exports.DEFAULT_TSCONFIG })
95
+ .describe('The tsconfig whose `paths` aliases glob.glob may name and generated imports are written in, and whose module resolution sets the import extension. It adds no files: the bundle scans glob.glob alone. Relative to the config file. The default may be absent; a tsconfig named here must exist.'),
96
+ importExtension: zod_1.z
97
+ .string(expected('a string'))
98
+ .optional()
99
+ .describe('Extension of generated imports, e.g. ".js". Inferred from the tsconfig: ".js" under node16/nodenext resolution, none otherwise.'),
100
+ glob: GLOB_SCHEMA,
101
+ className: CLASSES_SCHEMA.optional(),
93
102
  }, expected('an object'));
94
103
  function formatPath(segments) {
95
104
  const formatted = segments
@@ -98,33 +107,75 @@ function formatPath(segments) {
98
107
  .replace(/^\./, '');
99
108
  return formatted || 'config';
100
109
  }
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}`];
110
+ function matchedBranch(issue) {
111
+ const typeMatched = issue.errors.filter((branch) => branch.every((inner) => inner.code !== 'invalid_type' || inner.path.length > 0));
112
+ return typeMatched.length === 1 ? typeMatched[0] : undefined;
113
+ }
114
+ function formatIssue(issue, prefix = []) {
115
+ const at = [...prefix, ...issue.path];
116
+ if (issue.code === 'unrecognized_keys')
117
+ return issue.keys.map((key) => `${formatPath([...at, key])}: unknown field`);
118
+ const branch = issue.code === 'invalid_union' ? matchedBranch(issue) : undefined;
119
+ if (branch)
120
+ return branch.flatMap((inner) => formatIssue(inner, at));
121
+ return [`${formatPath(at)}: ${issue.message}`];
106
122
  }
107
123
  const formatIssues = (error) => error.issues.flatMap((issue) => formatIssue(issue)).join('; ');
108
- function toClassSelector(classes) {
109
- if (classes === undefined)
124
+ function toGlobObject(glob) {
125
+ if (typeof glob === 'string' || Array.isArray(glob))
126
+ return { glob, exclude: exports.DEFAULT_EXCLUDE };
127
+ return glob;
128
+ }
129
+ function toGlobSelector(glob) {
130
+ return {
131
+ glob: toList(glob.glob),
132
+ exclude: toList(glob.exclude).map(glob_1.globToRegExp),
133
+ };
134
+ }
135
+ function toClassSelector(className) {
136
+ if (className === undefined)
110
137
  return { export: 'any', decorators: exports.DEFAULT_DECORATORS };
111
- const { decorators, name, excludeName, ...rest } = classes;
138
+ if (typeof className === 'string' || Array.isArray(className)) {
139
+ return { export: 'any', decorators: exports.DEFAULT_DECORATORS, glob: toGlobs(className) };
140
+ }
141
+ const { decorators, glob, exclude, ...rest } = className;
112
142
  return {
113
143
  ...rest,
114
144
  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),
145
+ glob: toGlobs(glob),
146
+ exclude: toGlobs(exclude),
117
147
  };
118
148
  }
149
+ function excludeWarnings(glob) {
150
+ const exclude = toList(glob.exclude);
151
+ if (exclude.length === 0)
152
+ return [];
153
+ const missing = exports.DEFAULT_EXCLUDE.filter((glob) => !exclude.includes(glob));
154
+ if (missing.length === 0)
155
+ return [];
156
+ return [
157
+ `glob.exclude replaces the default excludes and omits ${missing.map((glob) => `"${glob}"`).join(', ')}; ` +
158
+ `add them back`,
159
+ ];
160
+ }
119
161
  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
162
  function configStem(file) {
123
163
  const base = node_path_1.default.basename(file);
124
- const suffix = (0, exports.configSuffix)(base);
164
+ const suffix = exports.CONFIG_FILE_SUFFIXES.find((candidate) => base.endsWith(candidate));
125
165
  const stem = suffix ? base.slice(0, -suffix.length) : '';
126
166
  return BUNDLE_NAME.test(stem) ? stem : undefined;
127
167
  }
168
+ exports.STDIN = '-';
169
+ const configFormat = (file) => file.endsWith('.json') ? 'json' : file.endsWith('.yaml') || file.endsWith('.yml') ? 'yaml' : undefined;
170
+ exports.configFormat = configFormat;
171
+ function parseConfig(text, format) {
172
+ try {
173
+ return format === 'yaml' ? ((0, yaml_1.parse)(text) ?? {}) : JSON.parse(text);
174
+ }
175
+ catch (e) {
176
+ throw new DomainException_1.TicConfigError(`not valid ${format.toUpperCase()}: ${e.message}`);
177
+ }
178
+ }
128
179
  function toClassName(name) {
129
180
  const words = name.split(/[-_]+/).filter(Boolean);
130
181
  return `${words.map((word) => word[0].toUpperCase() + word.slice(1)).join('')}Bundle`;
@@ -133,26 +184,24 @@ function resolveConfig(content, file) {
133
184
  const result = exports.BUNDLE_CONFIG_SCHEMA.safeParse(content);
134
185
  if (!result.success)
135
186
  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;
187
+ const { name, tsconfig, importExtension, glob, className } = result.data;
188
+ const bundleName = name ?? configStem(file);
189
+ if (bundleName === undefined) {
190
+ throw new DomainException_1.TicConfigError(node_path_1.default.basename(file) === exports.STDIN
191
+ ? 'name: required for a config read from stdin'
192
+ : `name: required when the config file is not named <name>${exports.CONFIG_FILE_SUFFIXES.join(' / ')}`);
193
+ }
138
194
  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;
195
+ const globObject = toGlobObject(glob);
143
196
  return {
144
197
  file,
145
198
  dir,
146
- output: output === undefined ? undefined : node_path_1.default.resolve(base, output),
147
199
  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
- },
200
+ bundleClassName: toClassName(bundleName),
201
+ tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.DEFAULT_TSCONFIG), required: tsconfig !== undefined },
155
202
  importExtension,
156
- classes: toClassSelector(classes),
203
+ glob: toGlobSelector(globObject),
204
+ className: toClassSelector(className),
205
+ warnings: excludeWarnings(globObject),
157
206
  };
158
207
  }
@@ -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));