@ts-ioc-container/bundler 1.4.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,122 +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');
68
- }
69
- if (excludeNameGlob !== undefined && !isNonEmptyString(excludeNameGlob)) {
70
- return fail(`${field}.excludeNameGlob`, 'a non-empty string');
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');
71
60
  }
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, 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 (!Array.isArray(namespaces) || namespaces.length === 0)
93
- return fail(`${field}.namespaces`, 'a non-empty array');
94
- if (exclude !== undefined && !isStringArray(exclude))
95
- return fail(`${field}.exclude`, 'an array of strings');
96
- if (additionalExclude !== undefined && !isStringArray(additionalExclude)) {
97
- 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"');
98
84
  }
99
- if (include !== undefined && !isNonEmptyString(include))
100
- return fail(`${field}.include`, 'a non-empty string');
101
- if (filterExports !== undefined && !isNonEmptyString(filterExports)) {
102
- return fail(`${field}.filterExports`, 'a non-empty string');
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');
91
+ }
92
+ if (excludeName !== undefined && !isNonEmptyString(excludeName)) {
93
+ return fail(`${field}.excludeName`, 'a non-empty string');
103
94
  }
104
95
  return {
105
- output: node_path_1.default.resolve(dir, output),
106
- name: name ?? exports.DEFAULT_BUNDLE_NAME,
107
- namespaces: namespaces.map((ns, i) => toNamespace(ns, `${field}.namespaces[${i}]`)),
108
- select: toSelector(select, `${field}.select`),
109
- exclude: [...(exclude ?? exports.DEFAULT_EXCLUDE), ...(additionalExclude ?? [])],
110
- include: include === undefined ? undefined : node_path_1.default.resolve(dir, include),
111
- 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),
112
101
  };
113
102
  }
114
- function excludeWarnings(value, field) {
115
- if (!isObject(value))
103
+ function excludeWarnings(files) {
104
+ if (!isObject(files))
116
105
  return [];
117
- const exclude = value.exclude;
106
+ const exclude = files.exclude;
118
107
  if (!isStringArray(exclude) || exclude.length === 0)
119
108
  return [];
120
109
  const missing = exports.DEFAULT_EXCLUDE.filter((glob) => !exclude.includes(glob));
121
110
  if (missing.length === 0)
122
111
  return [];
123
112
  return [
124
- `${field}.exclude replaces the default excludes and omits ${missing.map((glob) => `"${glob}"`).join(', ')}; ` +
125
- `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`,
126
115
  ];
127
116
  }
117
+ const CONFIG_FIELDS = new Set(['$schema', 'output', 'name', 'tags', 'extends', 'importExtension', 'files', 'classes']);
128
118
  function resolveConfig(content, file) {
129
119
  const dir = node_path_1.default.dirname(file);
130
120
  if (!isObject(content))
131
- return fail('config', 'a JSON object');
132
- 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');
133
134
  if (tsconfig !== undefined && !isNonEmptyString(tsconfig))
134
- return fail('tsconfig', 'a non-empty string');
135
+ return fail('extends', 'a non-empty string');
135
136
  if (importExtension !== undefined && typeof importExtension !== 'string')
136
137
  return fail('importExtension', 'a string');
137
- if (!Array.isArray(bundles) || bundles.length === 0)
138
- return fail('bundles', 'a non-empty array');
139
138
  return {
140
139
  file,
141
140
  dir,
142
- 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 },
143
146
  importExtension,
144
- bundles: bundles.map((m, i) => toBundle(m, `bundles[${i}]`, dir)),
145
- warnings: bundles.flatMap((m, i) => excludeWarnings(m, `bundles[${i}]`)),
147
+ files: toFileSelector(files, 'files'),
148
+ classes: toClassSelector(classes, 'classes'),
149
+ warnings: excludeWarnings(files),
146
150
  };
147
151
  }
148
- function loadConfig(file) {
149
- if (!(0, node_fs_1.existsSync)(file))
150
- throw new errors_1.TicConfigError(`config file not found: ${file}`);
151
- 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');
152
188
  try {
153
- content = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
189
+ return yaml ? ((0, yaml_1.parse)(text) ?? {}) : JSON.parse(text);
154
190
  }
155
191
  catch (e) {
156
- 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}`);
157
193
  }
158
- 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);
159
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,