@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.
- package/AGENTS.md +35 -30
- package/README.md +157 -280
- package/cjm/ImportPaths.js +5 -27
- package/cjm/build.js +53 -79
- package/cjm/cli.js +38 -13
- package/cjm/config.js +121 -81
- package/cjm/emit.js +5 -2
- package/cjm/index.js +12 -12
- package/cjm/scan.js +6 -3
- package/cjm/tsconfig.js +78 -0
- package/package.json +4 -3
- package/tic.schema.json +120 -136
- package/tpl/index.cjs +36 -16
- package/typings/ImportPaths.d.ts +4 -16
- package/typings/build.d.ts +13 -16
- package/typings/config.d.ts +104 -62
- package/typings/emit.d.ts +7 -2
- package/typings/errors.d.ts +1 -1
- package/typings/index.d.ts +5 -7
- package/typings/scan.d.ts +4 -2
- package/typings/tsconfig.d.ts +25 -0
- package/cjm/exportPredicate.js +0 -13
- package/cjm/inclusion.js +0 -24
- package/cjm/predicateFile.js +0 -30
- package/cjm/utils.js +0 -2
- package/typings/exportPredicate.d.ts +0 -36
- package/typings/inclusion.d.ts +0 -42
- package/typings/predicateFile.d.ts +0 -16
- package/typings/utils.d.ts +0 -3
package/cjm/ImportPaths.js
CHANGED
|
@@ -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
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
47
|
-
warnings: [...resolved.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
|
|
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
|
|
66
|
-
const excludes = bundle.exclude.map(glob_1.globToRegExp);
|
|
40
|
+
const { include, exclude } = config.files;
|
|
67
41
|
const isExcluded = (file) => {
|
|
68
|
-
if (
|
|
42
|
+
if (file === output)
|
|
69
43
|
return true;
|
|
70
44
|
const filename = relative(file);
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
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) =>
|
|
89
|
-
.
|
|
90
|
-
|
|
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(
|
|
71
|
+
.map((cls) => ({ ...cls, specifier: paths.specifier(output, cls.file) }));
|
|
99
72
|
const content = (0, emit_1.emitBundle)({
|
|
100
|
-
name:
|
|
101
|
-
configPath: (0, glob_1.toPosix)(node_path_1.default.relative(node_path_1.default.dirname(
|
|
102
|
-
|
|
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 {
|
|
79
|
+
return { output, content, registrations: classes.length, warnings: tokenCollisions(classes) };
|
|
106
80
|
}
|
|
107
|
-
function tokenCollisions(classes
|
|
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 (
|
|
123
|
-
`registration is last-wins, exclude one with
|
|
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
|
|
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
|
|
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
|
-
|
|
39
|
-
if (!
|
|
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.
|
|
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 {
|
|
65
|
-
|
|
66
|
-
for (const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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.
|
|
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.
|
|
14
|
-
exports.
|
|
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
|
|
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
|
|
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
|
|
42
|
-
|
|
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 {
|
|
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) => !
|
|
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 {
|
|
58
|
-
if (
|
|
59
|
-
return fail(`${field}.
|
|
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 (
|
|
73
|
-
return fail(`${field}.
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 (
|
|
100
|
-
return fail(`${field}.
|
|
101
|
-
if (
|
|
102
|
-
return fail(`${field}.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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(
|
|
115
|
-
if (!isObject(
|
|
103
|
+
function excludeWarnings(files) {
|
|
104
|
+
if (!isObject(files))
|
|
116
105
|
return [];
|
|
117
|
-
const 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
|
-
|
|
125
|
-
`add them back
|
|
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', '
|
|
132
|
-
const
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
147
|
+
files: toFileSelector(files, 'files'),
|
|
148
|
+
classes: toClassSelector(classes, 'classes'),
|
|
149
|
+
warnings: excludeWarnings(files),
|
|
146
150
|
};
|
|
147
151
|
}
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
30
|
+
paths,
|
|
31
|
+
tsconfigPath,
|
|
29
32
|
name,
|
|
30
33
|
imports: [...byFile.values()],
|
|
31
34
|
registrations: bindings,
|