@ts-ioc-container/bundler 1.5.0 → 2.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.
@@ -37,14 +37,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.ImportPaths = void 0;
40
- exports.normalizeAliasName = normalizeAliasName;
41
40
  const node_fs_1 = require("node:fs");
42
41
  const node_path_1 = __importDefault(require("node:path"));
43
42
  const ts = __importStar(require("typescript"));
44
43
  const errors_1 = require("./errors");
45
44
  const glob_1 = require("./glob");
46
45
  const SOURCE_EXTENSION = /\.(tsx?|mts|cts)$/;
47
- const NO_INPUTS = 18003;
48
46
  const isDirectory = (dir) => (0, node_fs_1.existsSync)(dir) && (0, node_fs_1.statSync)(dir).isDirectory();
49
47
  const isRelative = (spec) => spec.startsWith('.') || node_path_1.default.isAbsolute(spec);
50
48
  function splitWildcard(pattern) {
@@ -54,20 +52,9 @@ function splitWildcard(pattern) {
54
52
  class ImportPaths {
55
53
  aliases;
56
54
  extension;
57
- static load(tsconfig, importExtension) {
58
- if (!(0, node_fs_1.existsSync)(tsconfig.file)) {
59
- if (tsconfig.required)
60
- throw new errors_1.TicConfigError(`tsconfig not found: ${tsconfig.file}`);
61
- return new ImportPaths([], importExtension ?? '');
62
- }
63
- const { config, error } = ts.readConfigFile(tsconfig.file, ts.sys.readFile);
64
- const parsed = ts.parseJsonConfigFileContent(config, ts.sys, node_path_1.default.dirname(tsconfig.file), undefined, tsconfig.file);
65
- const [fatal] = [error, ...parsed.errors].filter((d) => !!d && d.code !== NO_INPUTS);
66
- if (fatal) {
67
- throw new errors_1.TicConfigError(`${tsconfig.file}: ${ts.flattenDiagnosticMessageText(fatal.messageText, '\n')}`);
68
- }
69
- const { paths = {}, baseUrl, pathsBasePath, moduleResolution } = parsed.options;
70
- const base = baseUrl ?? (typeof pathsBasePath === 'string' ? pathsBasePath : node_path_1.default.dirname(tsconfig.file));
55
+ static fromOptions(options, tsconfigFile, importExtension) {
56
+ const { paths = {}, baseUrl, pathsBasePath, moduleResolution } = options;
57
+ const base = baseUrl ?? (typeof pathsBasePath === 'string' ? pathsBasePath : node_path_1.default.dirname(tsconfigFile));
71
58
  const aliases = Object.entries(paths).map(([pattern, targets]) => ({
72
59
  pattern,
73
60
  targets: targets.map((target) => (0, glob_1.toPosix)(node_path_1.default.resolve(base, target))),
@@ -99,12 +86,6 @@ class ImportPaths {
99
86
  const relative = (0, glob_1.toPosix)(node_path_1.default.relative(node_path_1.default.dirname(fromFile), bare));
100
87
  return `${relative.startsWith('.') ? relative : `./${relative}`}${this.extension}`;
101
88
  }
102
- aliasName(file) {
103
- const target = (0, glob_1.toPosix)(file);
104
- const bare = target.replace(SOURCE_EXTENSION, '');
105
- const best = this.bestAlias(target, bare);
106
- return best && normalizeAliasName(best.pattern);
107
- }
108
89
  bestAlias(target, bare) {
109
90
  let best;
110
91
  for (const { pattern, targets } of this.aliases) {
@@ -132,17 +113,14 @@ class ImportPaths {
132
113
  const wildcard = splitWildcard(aliasTarget);
133
114
  if (!wildcard) {
134
115
  const exact = aliasTarget === file || aliasTarget.replace(SOURCE_EXTENSION, '') === bare;
135
- return exact && !splitWildcard(pattern) ? { spec: pattern, score: Infinity, pattern } : undefined;
116
+ return exact && !splitWildcard(pattern) ? { spec: pattern, score: Infinity } : undefined;
136
117
  }
137
118
  const [prefix, suffix] = wildcard;
138
119
  if (!bare.startsWith(prefix) || !bare.endsWith(suffix) || bare.length < prefix.length + suffix.length) {
139
120
  return undefined;
140
121
  }
141
122
  const captured = bare.slice(prefix.length, bare.length - suffix.length);
142
- return { spec: `${pattern.replace('*', captured)}${this.extension}`, score: prefix.length, pattern };
123
+ return { spec: `${pattern.replace('*', captured)}${this.extension}`, score: prefix.length };
143
124
  }
144
125
  }
145
126
  exports.ImportPaths = ImportPaths;
146
- function normalizeAliasName(pattern) {
147
- return pattern.replace(/^@/, '').replace(/\/?\*$/, '');
148
- }
package/cjm/build.js CHANGED
@@ -3,108 +3,82 @@ 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.TSCONFIG_FILE = void 0;
6
7
  exports.build = build;
7
8
  const node_fs_1 = require("node:fs");
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  const config_1 = require("./config");
10
11
  const emit_1 = require("./emit");
11
12
  const glob_1 = require("./glob");
12
- const ImportPaths_1 = require("./ImportPaths");
13
- const exportPredicate_1 = require("./exportPredicate");
14
- const inclusion_1 = require("./inclusion");
13
+ const errors_1 = require("./errors");
15
14
  const scan_1 = require("./scan");
16
- function build({ config = config_1.DEFAULT_CONFIG_FILE, cwd = process.cwd(), check = false, include, filterExports, } = {}) {
17
- const resolved = (0, config_1.loadConfig)(node_path_1.default.resolve(cwd, config));
18
- const paths = ImportPaths_1.ImportPaths.load(resolved.tsconfig, resolved.importExtension);
19
- const includeFor = predicateResolver({
20
- override: include,
21
- conventional: (0, inclusion_1.findConventionalPredicate)(resolved.dir),
22
- fileOf: (bundle) => bundle.include,
23
- load: inclusion_1.loadInclusionPredicate,
24
- });
25
- const filterExportsFor = predicateResolver({
26
- override: filterExports,
27
- conventional: (0, exportPredicate_1.findConventionalExportPredicate)(resolved.dir),
28
- fileOf: (bundle) => bundle.filterExports,
29
- load: exportPredicate_1.loadExportPredicate,
30
- });
31
- const generated = resolved.bundles.map((bundle, i) => generate(resolved, bundle, `bundles[${i}]`, paths, {
32
- include: includeFor(bundle),
33
- filterExports: filterExportsFor(bundle),
34
- }));
35
- const outputs = generated.map(({ bundle, content, registrations }) => {
36
- const current = (0, node_fs_1.existsSync)(bundle.output) ? (0, node_fs_1.readFileSync)(bundle.output, 'utf8') : undefined;
37
- const status = current === content ? 'unchanged' : check ? 'stale' : 'written';
38
- if (status === 'written') {
39
- (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(bundle.output), { recursive: true });
40
- (0, node_fs_1.writeFileSync)(bundle.output, content);
41
- }
42
- return { file: bundle.output, bundle: bundle.name, status, registrations, content };
43
- });
15
+ const tsconfig_1 = require("./tsconfig");
16
+ exports.TSCONFIG_FILE = 'tsconfig.json';
17
+ function build({ config, cwd = process.cwd(), check = false } = {}) {
18
+ const resolved = config === undefined
19
+ ? (0, config_1.resolveConfig)({ extends: config_1.DEFAULT_EXTENDS }, node_path_1.default.join((0, config_1.findPackageRoot)(cwd), exports.TSCONFIG_FILE))
20
+ : (0, config_1.loadConfig)(node_path_1.default.resolve(cwd, config));
21
+ const { output, content, registrations, warnings } = generate(resolved);
22
+ const current = (0, node_fs_1.existsSync)(output) ? (0, node_fs_1.readFileSync)(output, 'utf8') : undefined;
23
+ const status = current === content ? 'unchanged' : check ? 'stale' : 'written';
24
+ if (status === 'written') {
25
+ (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(output), { recursive: true });
26
+ (0, node_fs_1.writeFileSync)(output, content);
27
+ }
44
28
  return {
45
29
  config: resolved.file,
46
- outputs,
47
- warnings: [...resolved.warnings, ...generated.flatMap((g) => g.warnings)],
48
- };
49
- }
50
- function predicateResolver({ override, conventional, fileOf, load, }) {
51
- const loaded = new Map();
52
- return (bundle) => {
53
- if (override)
54
- return override;
55
- const file = fileOf(bundle) ?? conventional;
56
- if (file === undefined)
57
- return undefined;
58
- if (!loaded.has(file))
59
- loaded.set(file, load(file));
60
- return loaded.get(file);
30
+ output: { file: output, bundle: resolved.className, status, registrations, content },
31
+ warnings: [...resolved.warnings, ...warnings],
61
32
  };
62
33
  }
63
- function generate(config, bundle, field, paths, { include, filterExports }) {
34
+ function generate(config) {
35
+ const { importPaths: paths, fileNames, rootDir } = (0, tsconfig_1.loadTsconfig)(config.tsconfig, config.importExtension);
36
+ const output = config.output ?? (rootDir && node_path_1.default.join(rootDir, `${config.name}.bundle.ts`));
37
+ if (output === undefined)
38
+ throw new errors_1.TicConfigError('output: required when there is no tsconfig to extend');
64
39
  const relative = (file) => (0, glob_1.toPosix)(node_path_1.default.relative(config.dir, file));
65
- const outputs = new Set(config.bundles.map((m) => m.output));
66
- const excludes = bundle.exclude.map(glob_1.globToRegExp);
40
+ const { include, exclude } = config.files;
67
41
  const isExcluded = (file) => {
68
- if (outputs.has(file))
42
+ if (file === output)
69
43
  return true;
70
44
  const filename = relative(file);
71
- return excludes.some((glob) => glob.test(filename)) || (include !== undefined && !include({ filename }));
45
+ if (include && !include.some((glob) => glob.test(filename)))
46
+ return true;
47
+ return exclude.some((glob) => glob.test(filename));
72
48
  };
73
49
  const files = new Set();
74
- for (const namespace of bundle.namespaces) {
75
- const dir = paths.resolveNamespace(namespace.path, config.dir);
76
- for (const file of (0, scan_1.listSourceFiles)(dir, namespace.recursive, isExcluded))
77
- files.add(file);
50
+ if (config.files.paths) {
51
+ for (const entry of config.files.paths) {
52
+ const dir = paths.resolveNamespace(entry.path, config.dir);
53
+ for (const file of (0, scan_1.listSourceFiles)(dir, entry.recursive, isExcluded))
54
+ files.add(file);
55
+ }
56
+ }
57
+ else if (fileNames) {
58
+ for (const file of fileNames)
59
+ if ((0, scan_1.isSourceFile)(file) && !isExcluded(file))
60
+ files.add(file);
61
+ }
62
+ else {
63
+ throw new errors_1.TicConfigError(`files.paths: required when there is no tsconfig to extend (${config.tsconfig.file} not found)`);
78
64
  }
79
- const excludedAliases = new Set((bundle.select.excludeAliases ?? []).map(ImportPaths_1.normalizeAliasName));
80
- const isAliasExcluded = (file) => {
81
- if (excludedAliases.size === 0)
82
- return false;
83
- const alias = paths.aliasName(file);
84
- return alias !== undefined && excludedAliases.has(alias);
85
- };
86
65
  const classes = [...files]
87
66
  .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
88
- .flatMap((file) => (0, scan_1.findClasses)(file, bundle.select))
89
- .filter((cls) => {
90
- if (isAliasExcluded(cls.file))
91
- return false;
92
- if (!filterExports)
93
- return true;
94
- const filename = relative(cls.file);
95
- const { exportName, className, isDefault, decorators } = cls;
96
- return filterExports({ filename, exportName, className, isDefault, decorators, tags: (0, inclusion_1.fileTags)(filename) });
67
+ .flatMap((file) => {
68
+ const text = (0, node_fs_1.readFileSync)(file, 'utf8');
69
+ return text.startsWith(emit_1.GENERATED_HEADER) ? [] : (0, scan_1.findClasses)(file, config.classes, text);
97
70
  })
98
- .map((cls) => ({ ...cls, specifier: paths.specifier(bundle.output, cls.file) }));
71
+ .map((cls) => ({ ...cls, specifier: paths.specifier(output, cls.file) }));
99
72
  const content = (0, emit_1.emitBundle)({
100
- name: bundle.name,
101
- configPath: (0, glob_1.toPosix)(node_path_1.default.relative(node_path_1.default.dirname(bundle.output), config.file)),
102
- namespaces: bundle.namespaces.map((ns) => ns.path),
73
+ name: config.className,
74
+ configPath: (0, glob_1.toPosix)(node_path_1.default.relative(node_path_1.default.dirname(output), config.file)),
75
+ paths: config.files.paths?.map((entry) => entry.path),
76
+ tsconfigPath: (0, glob_1.toPosix)(node_path_1.default.relative(node_path_1.default.dirname(output), config.tsconfig.file)),
103
77
  classes,
104
78
  });
105
- return { bundle, content, registrations: classes.length, warnings: tokenCollisions(classes, field) };
79
+ return { output, content, registrations: classes.length, warnings: tokenCollisions(classes) };
106
80
  }
107
- function tokenCollisions(classes, field) {
81
+ function tokenCollisions(classes) {
108
82
  const owners = new Map();
109
83
  for (const cls of classes) {
110
84
  for (const token of cls.tokens) {
@@ -119,8 +93,8 @@ function tokenCollisions(classes, field) {
119
93
  .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
120
94
  .map(([token, group]) => {
121
95
  const names = group.map((cls) => cls.className);
122
- return (`${field}: decorator token "${token}" is passed by ${names.join(', ')}; ` +
123
- `registration is last-wins, exclude one with select.excludeClasses`);
96
+ return (`decorator token "${token}" is passed by ${names.join(', ')}; ` +
97
+ `registration is last-wins, exclude one with classes.excludeClasses`);
124
98
  });
125
99
  }
126
100
  function scopeDisjoint(group) {
package/cjm/cli.js CHANGED
@@ -7,16 +7,18 @@ exports.run = run;
7
7
  const node_fs_1 = require("node:fs");
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const build_1 = require("./build");
10
+ const config_1 = require("./config");
10
11
  const errors_1 = require("./errors");
11
12
  const USAGE = [
12
13
  'Usage: ts-ioc-container <command> [options]',
13
14
  ' tic is a shortcut for ts-ioc-container',
14
15
  '',
15
16
  'Commands:',
16
- ' ts-ioc-container build [--config <path>] [--check] generate the bundles described by tic.config.json',
17
+ ' ts-ioc-container build [--config <path>]... [--check] generate one bundle per *.bundle.{json,yaml,yml} config',
18
+ ' (none: one bundle from tsconfig.json, all defaults)',
17
19
  '',
18
20
  'Options:',
19
- ' -c, --config <path> config file (default: tic.config.json)',
21
+ ' -c, --config <path> build only this config; repeatable (default: every *.bundle.{json,yaml,yml} of the package, else tsconfig.json)',
20
22
  ' --check write nothing; exit 1 when a generated bundle is out of date',
21
23
  ' -h, --help show this help',
22
24
  ' -v, --version show the version',
@@ -29,23 +31,35 @@ function version() {
29
31
  class UsageError extends errors_1.TicError {
30
32
  }
31
33
  function parseBuildArgs(args) {
32
- const options = { check: false };
34
+ const options = { configs: [], check: false };
33
35
  for (let i = 0; i < args.length; i++) {
34
36
  const arg = args[i];
35
37
  if (arg === '--check')
36
38
  options.check = true;
37
39
  else if (arg === '--config' || arg === '-c') {
38
- options.config = args[++i];
39
- if (!options.config)
40
+ const config = args[++i];
41
+ if (!config)
40
42
  throw new UsageError(`${arg} needs a path`);
43
+ options.configs.push(config);
41
44
  }
42
45
  else if (arg.startsWith('--config='))
43
- options.config = arg.slice('--config='.length);
46
+ options.configs.push(arg.slice('--config='.length));
44
47
  else
45
48
  throw new UsageError(`unknown option "${arg}"`);
46
49
  }
47
50
  return options;
48
51
  }
52
+ function configsToBuild(named, cwd) {
53
+ if (named.length > 0)
54
+ return named.map((config) => node_path_1.default.resolve(cwd, config));
55
+ const root = (0, config_1.findPackageRoot)(cwd);
56
+ const found = (0, config_1.findConfigFiles)(root);
57
+ if (found.length > 0)
58
+ return found;
59
+ if ((0, node_fs_1.existsSync)(node_path_1.default.join(root, build_1.TSCONFIG_FILE)))
60
+ return [undefined];
61
+ throw new errors_1.TicConfigError(`no *.bundle.{json,yaml,yml} or ${build_1.TSCONFIG_FILE} in ${root}; name a config with --config`);
62
+ }
49
63
  function run(argv, io = { cwd: process.cwd(), stdout: console.log, stderr: console.error }) {
50
64
  const [command, ...rest] = argv;
51
65
  if (command === '--help' || command === '-h' || command === 'help') {
@@ -61,14 +75,25 @@ function run(argv, io = { cwd: process.cwd(), stdout: console.log, stderr: conso
61
75
  io.stderr(command ? `tic: unknown command "${command}"` : USAGE);
62
76
  return 1;
63
77
  }
64
- const { config, check } = parseBuildArgs(rest);
65
- const { outputs, warnings } = (0, build_1.build)({ config, check, cwd: io.cwd });
66
- for (const warning of warnings)
67
- io.stderr(`tic: warning: ${warning}`);
68
- for (const { status, file, registrations } of outputs) {
69
- io.stdout(`${VERB[status].padEnd(10)}${node_path_1.default.relative(io.cwd, file)} (${plural(registrations)})`);
78
+ const { configs, check } = parseBuildArgs(rest);
79
+ let stale = 0;
80
+ for (const config of configsToBuild(configs, io.cwd)) {
81
+ const name = config === undefined ? build_1.TSCONFIG_FILE : node_path_1.default.relative(io.cwd, config);
82
+ try {
83
+ const { output, warnings } = (0, build_1.build)({ config, check, cwd: io.cwd });
84
+ for (const warning of warnings)
85
+ io.stderr(`tic: warning: ${name}: ${warning}`);
86
+ const { status, file, registrations } = output;
87
+ io.stdout(`${VERB[status].padEnd(10)}${node_path_1.default.relative(io.cwd, file)} (${plural(registrations)})`);
88
+ if (status === 'stale')
89
+ stale++;
90
+ }
91
+ catch (e) {
92
+ if (e instanceof errors_1.TicError)
93
+ e.message = `${name}: ${e.message}`;
94
+ throw e;
95
+ }
70
96
  }
71
- const stale = outputs.filter((o) => o.status === 'stale').length;
72
97
  if (stale > 0) {
73
98
  io.stderr(`tic: ${stale} generated bundle${stale === 1 ? ' is' : 's are'} out of date — run \`tic build\``);
74
99
  return 1;
package/cjm/config.js CHANGED
@@ -3,15 +3,22 @@ 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.DEFAULT_EXCLUDE = exports.DEFAULT_BUNDLE_NAME = exports.DEFAULT_CONFIG_FILE = void 0;
6
+ exports.DEFAULT_EXCLUDE = exports.DEFAULT_DECORATORS = exports.DEFAULT_BUNDLE_NAME = exports.DEFAULT_EXTENDS = exports.CONFIG_FILE_SUFFIXES = void 0;
7
7
  exports.resolveConfig = resolveConfig;
8
+ exports.toClassName = toClassName;
9
+ exports.findPackageRoot = findPackageRoot;
10
+ exports.findConfigFiles = findConfigFiles;
8
11
  exports.loadConfig = loadConfig;
9
12
  const node_fs_1 = require("node:fs");
10
13
  const node_path_1 = __importDefault(require("node:path"));
14
+ const yaml_1 = require("yaml");
11
15
  const errors_1 = require("./errors");
12
16
  const glob_1 = require("./glob");
13
- exports.DEFAULT_CONFIG_FILE = 'tic.config.json';
14
- exports.DEFAULT_BUNDLE_NAME = 'Bundle';
17
+ exports.CONFIG_FILE_SUFFIXES = ['.bundle.json', '.bundle.yaml', '.bundle.yml'];
18
+ const configSuffix = (file) => exports.CONFIG_FILE_SUFFIXES.find((suffix) => file.endsWith(suffix));
19
+ exports.DEFAULT_EXTENDS = './tsconfig.json';
20
+ exports.DEFAULT_BUNDLE_NAME = 'base';
21
+ exports.DEFAULT_DECORATORS = ['register'];
15
22
  exports.DEFAULT_EXCLUDE = [
16
23
  '**/*.spec.ts',
17
24
  '**/*.test.ts',
@@ -23,11 +30,11 @@ exports.DEFAULT_EXCLUDE = [
23
30
  const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
24
31
  const isNonEmptyString = (value) => typeof value === 'string' && value.length > 0;
25
32
  const isStringArray = (value) => Array.isArray(value) && value.every(isNonEmptyString);
26
- const isIdentifier = (value) => typeof value === 'string' && /^[A-Za-z_$][\w$]*$/.test(value);
33
+ const isBundleName = (value) => typeof value === 'string' && /^[A-Za-z][\w-]*$/.test(value);
27
34
  const fail = (field, expected) => {
28
35
  throw new errors_1.TicConfigError(`${field}: expected ${expected}`);
29
36
  };
30
- function toNamespace(value, field) {
37
+ function toPath(value, field) {
31
38
  if (isNonEmptyString(value))
32
39
  return { path: value, recursive: true };
33
40
  if (!isObject(value))
@@ -38,125 +45,155 @@ function toNamespace(value, field) {
38
45
  return fail(`${field}.recursive`, 'a boolean');
39
46
  return { path: value.path, recursive: value.recursive ?? true };
40
47
  }
41
- const SELECTOR_FIELDS = new Set([
42
- 'export',
43
- 'decorators',
44
- 'nameGlob',
45
- 'excludeClasses',
46
- 'excludeNameGlob',
47
- 'excludeAliases',
48
- ]);
49
- function toSelector(value, field) {
48
+ const FILE_SELECTOR_FIELDS = new Set(['paths', 'include', 'exclude']);
49
+ function toFileSelector(value, field) {
50
50
  if (value === undefined)
51
- return { export: 'any' };
51
+ return { exclude: exports.DEFAULT_EXCLUDE.map(glob_1.globToRegExp) };
52
52
  if (!isObject(value))
53
53
  return fail(field, 'an object');
54
- const unknown = Object.keys(value).find((key) => !SELECTOR_FIELDS.has(key));
54
+ const unknown = Object.keys(value).find((key) => !FILE_SELECTOR_FIELDS.has(key));
55
55
  if (unknown)
56
56
  throw new errors_1.TicConfigError(`${field}.${unknown}: unknown field`);
57
- const { export: kind, decorators, nameGlob, excludeClasses, excludeNameGlob, excludeAliases } = value;
58
- if (kind !== undefined && kind !== 'any' && kind !== 'named' && kind !== 'default') {
59
- return fail(`${field}.export`, '"any", "named" or "default"');
60
- }
61
- if (decorators !== undefined && !(isStringArray(decorators) && decorators.length > 0)) {
62
- return fail(`${field}.decorators`, 'a non-empty array of strings');
63
- }
64
- if (nameGlob !== undefined && !isNonEmptyString(nameGlob))
65
- return fail(`${field}.nameGlob`, 'a non-empty string');
66
- if (excludeClasses !== undefined && !(isStringArray(excludeClasses) && excludeClasses.length > 0)) {
67
- return fail(`${field}.excludeClasses`, 'a non-empty array of strings');
57
+ const { paths, include, exclude } = value;
58
+ if (paths !== undefined && !(Array.isArray(paths) && paths.length > 0)) {
59
+ return fail(`${field}.paths`, 'a non-empty array');
68
60
  }
69
- if (excludeNameGlob !== undefined && !isNonEmptyString(excludeNameGlob)) {
70
- return fail(`${field}.excludeNameGlob`, 'a non-empty string');
71
- }
72
- if (excludeAliases !== undefined && !(isStringArray(excludeAliases) && excludeAliases.length > 0)) {
73
- return fail(`${field}.excludeAliases`, 'a non-empty array of strings');
61
+ if (include !== undefined && !(isStringArray(include) && include.length > 0)) {
62
+ return fail(`${field}.include`, 'a non-empty array of strings');
74
63
  }
64
+ if (exclude !== undefined && !isStringArray(exclude))
65
+ return fail(`${field}.exclude`, 'an array of strings');
75
66
  return {
76
- export: kind ?? 'any',
77
- decorators,
78
- nameGlob: nameGlob === undefined ? undefined : (0, glob_1.globToRegExp)(nameGlob),
79
- excludeClasses,
80
- excludeNameGlob: excludeNameGlob === undefined ? undefined : (0, glob_1.globToRegExp)(excludeNameGlob),
81
- excludeAliases,
67
+ paths: paths?.map((entry, i) => toPath(entry, `${field}.paths[${i}]`)),
68
+ include: include?.map(glob_1.globToRegExp),
69
+ exclude: (exclude ?? exports.DEFAULT_EXCLUDE).map(glob_1.globToRegExp),
82
70
  };
83
71
  }
84
- function toBundle(value, field, dir) {
72
+ const CLASS_SELECTOR_FIELDS = new Set(['export', 'decorators', 'name', 'excludeClasses', 'excludeName']);
73
+ function toClassSelector(value, field) {
74
+ if (value === undefined)
75
+ return { export: 'any', decorators: exports.DEFAULT_DECORATORS };
85
76
  if (!isObject(value))
86
77
  return fail(field, 'an object');
87
- const { output, name, tags, namespaces, select, exclude, additionalExclude, include, filterExports } = value;
88
- if (!isNonEmptyString(output))
89
- return fail(`${field}.output`, 'a non-empty string');
90
- if (name !== undefined && !isIdentifier(name))
91
- return fail(`${field}.name`, 'a valid identifier');
92
- if (tags !== undefined && !isStringArray(tags))
93
- return fail(`${field}.tags`, 'an array of strings');
94
- if (!Array.isArray(namespaces) || namespaces.length === 0)
95
- return fail(`${field}.namespaces`, 'a non-empty array');
96
- if (exclude !== undefined && !isStringArray(exclude))
97
- return fail(`${field}.exclude`, 'an array of strings');
98
- if (additionalExclude !== undefined && !isStringArray(additionalExclude)) {
99
- return fail(`${field}.additionalExclude`, 'an array of strings');
78
+ const unknown = Object.keys(value).find((key) => !CLASS_SELECTOR_FIELDS.has(key));
79
+ if (unknown)
80
+ throw new errors_1.TicConfigError(`${field}.${unknown}: unknown field`);
81
+ const { export: kind, decorators, name, excludeClasses, excludeName } = value;
82
+ if (kind !== undefined && kind !== 'any' && kind !== 'named' && kind !== 'default') {
83
+ return fail(`${field}.export`, '"any", "named" or "default"');
84
+ }
85
+ if (decorators !== undefined && !isStringArray(decorators))
86
+ return fail(`${field}.decorators`, 'an array of strings');
87
+ if (name !== undefined && !isNonEmptyString(name))
88
+ return fail(`${field}.name`, 'a non-empty string');
89
+ if (excludeClasses !== undefined && !(isStringArray(excludeClasses) && excludeClasses.length > 0)) {
90
+ return fail(`${field}.excludeClasses`, 'a non-empty array of strings');
100
91
  }
101
- if (include !== undefined && !isNonEmptyString(include))
102
- return fail(`${field}.include`, 'a non-empty string');
103
- if (filterExports !== undefined && !isNonEmptyString(filterExports)) {
104
- return fail(`${field}.filterExports`, 'a non-empty string');
92
+ if (excludeName !== undefined && !isNonEmptyString(excludeName)) {
93
+ return fail(`${field}.excludeName`, 'a non-empty string');
105
94
  }
106
95
  return {
107
- output: node_path_1.default.resolve(dir, output),
108
- name: name ?? exports.DEFAULT_BUNDLE_NAME,
109
- tags: tags ?? [],
110
- namespaces: namespaces.map((ns, i) => toNamespace(ns, `${field}.namespaces[${i}]`)),
111
- select: toSelector(select, `${field}.select`),
112
- exclude: [...(exclude ?? exports.DEFAULT_EXCLUDE), ...(additionalExclude ?? [])],
113
- include: include === undefined ? undefined : node_path_1.default.resolve(dir, include),
114
- filterExports: filterExports === undefined ? undefined : node_path_1.default.resolve(dir, filterExports),
96
+ export: kind ?? 'any',
97
+ decorators: decorators === undefined ? exports.DEFAULT_DECORATORS : decorators.length > 0 ? decorators : undefined,
98
+ name: name === undefined ? undefined : (0, glob_1.globToRegExp)(name),
99
+ excludeClasses,
100
+ excludeName: excludeName === undefined ? undefined : (0, glob_1.globToRegExp)(excludeName),
115
101
  };
116
102
  }
117
- function excludeWarnings(value, field) {
118
- if (!isObject(value))
103
+ function excludeWarnings(files) {
104
+ if (!isObject(files))
119
105
  return [];
120
- const exclude = value.exclude;
106
+ const exclude = files.exclude;
121
107
  if (!isStringArray(exclude) || exclude.length === 0)
122
108
  return [];
123
109
  const missing = exports.DEFAULT_EXCLUDE.filter((glob) => !exclude.includes(glob));
124
110
  if (missing.length === 0)
125
111
  return [];
126
112
  return [
127
- `${field}.exclude replaces the default excludes and omits ${missing.map((glob) => `"${glob}"`).join(', ')}; ` +
128
- `add them back or use ${field}.additionalExclude`,
113
+ `files.exclude replaces the default excludes and omits ${missing.map((glob) => `"${glob}"`).join(', ')}; ` +
114
+ `add them back`,
129
115
  ];
130
116
  }
117
+ const CONFIG_FIELDS = new Set(['$schema', 'output', 'name', 'tags', 'extends', 'importExtension', 'files', 'classes']);
131
118
  function resolveConfig(content, file) {
132
119
  const dir = node_path_1.default.dirname(file);
133
120
  if (!isObject(content))
134
- return fail('config', 'a JSON object');
135
- const { tsconfig, importExtension, bundles } = content;
121
+ return fail('config', 'an object');
122
+ const unknown = Object.keys(content).find((key) => !CONFIG_FIELDS.has(key));
123
+ if (unknown)
124
+ throw new errors_1.TicConfigError(`${unknown}: unknown field`);
125
+ const { output, name, tags, extends: tsconfig, importExtension, files, classes } = content;
126
+ if (output !== undefined && !isNonEmptyString(output))
127
+ return fail('output', 'a non-empty string');
128
+ if (name !== undefined && !isBundleName(name)) {
129
+ return fail('name', 'letters, digits, "-" or "_", starting with a letter');
130
+ }
131
+ const bundleName = name ?? configStem(file) ?? exports.DEFAULT_BUNDLE_NAME;
132
+ if (tags !== undefined && !isStringArray(tags))
133
+ return fail('tags', 'an array of strings');
136
134
  if (tsconfig !== undefined && !isNonEmptyString(tsconfig))
137
- return fail('tsconfig', 'a non-empty string');
135
+ return fail('extends', 'a non-empty string');
138
136
  if (importExtension !== undefined && typeof importExtension !== 'string')
139
137
  return fail('importExtension', 'a string');
140
- if (!Array.isArray(bundles) || bundles.length === 0)
141
- return fail('bundles', 'a non-empty array');
142
138
  return {
143
139
  file,
144
140
  dir,
145
- tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? 'tsconfig.json'), required: tsconfig !== undefined },
141
+ output: output === undefined ? undefined : node_path_1.default.resolve(dir, output),
142
+ name: bundleName,
143
+ className: toClassName(bundleName),
144
+ tags: tags ?? [],
145
+ tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.DEFAULT_EXTENDS), required: tsconfig !== undefined },
146
146
  importExtension,
147
- bundles: bundles.map((m, i) => toBundle(m, `bundles[${i}]`, dir)),
148
- warnings: bundles.flatMap((m, i) => excludeWarnings(m, `bundles[${i}]`)),
147
+ files: toFileSelector(files, 'files'),
148
+ classes: toClassSelector(classes, 'classes'),
149
+ warnings: excludeWarnings(files),
149
150
  };
150
151
  }
151
- function loadConfig(file) {
152
- if (!(0, node_fs_1.existsSync)(file))
153
- throw new errors_1.TicConfigError(`config file not found: ${file}`);
154
- let content;
152
+ function configStem(file) {
153
+ const base = node_path_1.default.basename(file);
154
+ const suffix = configSuffix(base);
155
+ const stem = suffix ? base.slice(0, -suffix.length) : '';
156
+ return isBundleName(stem) ? stem : undefined;
157
+ }
158
+ function toClassName(name) {
159
+ const words = name.split(/[-_]+/).filter(Boolean);
160
+ return `${words.map((word) => word[0].toUpperCase() + word.slice(1)).join('')}Bundle`;
161
+ }
162
+ function findPackageRoot(dir) {
163
+ const start = node_path_1.default.resolve(dir);
164
+ for (let current = start;; current = node_path_1.default.dirname(current)) {
165
+ if ((0, node_fs_1.existsSync)(node_path_1.default.join(current, 'package.json')))
166
+ return current;
167
+ if (node_path_1.default.dirname(current) === current)
168
+ return start;
169
+ }
170
+ }
171
+ function findConfigFiles(dir) {
172
+ const files = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })
173
+ .filter((entry) => entry.isFile() && configSuffix(entry.name) !== undefined)
174
+ .map((entry) => entry.name)
175
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
176
+ const byStem = new Map();
177
+ for (const file of files) {
178
+ const stem = file.slice(0, -configSuffix(file).length);
179
+ const other = byStem.get(stem);
180
+ if (other)
181
+ throw new errors_1.TicConfigError(`${other} and ${file} describe the same bundle; keep one`);
182
+ byStem.set(stem, file);
183
+ }
184
+ return files.map((file) => node_path_1.default.join(dir, file));
185
+ }
186
+ function parseConfig(file, text) {
187
+ const yaml = file.endsWith('.yaml') || file.endsWith('.yml');
155
188
  try {
156
- content = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
189
+ return yaml ? ((0, yaml_1.parse)(text) ?? {}) : JSON.parse(text);
157
190
  }
158
191
  catch (e) {
159
- throw new errors_1.TicConfigError(`${file} is not valid JSON: ${e.message}`);
192
+ throw new errors_1.TicConfigError(`${file} is not valid ${yaml ? 'YAML' : 'JSON'}: ${e.message}`);
160
193
  }
161
- return resolveConfig(content, file);
194
+ }
195
+ function loadConfig(file) {
196
+ if (!(0, node_fs_1.existsSync)(file))
197
+ throw new errors_1.TicConfigError(`config file not found: ${file}`);
198
+ return resolveConfig(parseConfig(file, (0, node_fs_1.readFileSync)(file, 'utf8')), file);
162
199
  }
package/cjm/emit.js CHANGED
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GENERATED_HEADER = void 0;
3
4
  exports.emitBundle = emitBundle;
4
5
  const render_1 = require("./render");
6
+ exports.GENERATED_HEADER = '// Generated by `tic build`';
5
7
  const RUNTIME_IMPORTS = ['IContainer', 'IContainerModule', 'IRegistration', 'Registration'];
6
- function emitBundle({ name, configPath, namespaces, classes }) {
8
+ function emitBundle({ name, configPath, paths, tsconfigPath, classes }) {
7
9
  const taken = new Set([...RUNTIME_IMPORTS, 'registrations', name]);
8
10
  const uniqueName = (wanted) => {
9
11
  let candidate = wanted;
@@ -25,7 +27,8 @@ function emitBundle({ name, configPath, namespaces, classes }) {
25
27
  });
26
28
  return (0, render_1.renderProtocol)('Bundle.ts.hbs', {
27
29
  configPath,
28
- namespaces,
30
+ paths,
31
+ tsconfigPath,
29
32
  name,
30
33
  imports: [...byFile.values()],
31
34
  registrations: bindings,