@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.
- 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 -84
- 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 -144
- 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 -65
- 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,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
|
|
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');
|
|
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 (
|
|
70
|
-
return fail(`${field}.
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
if (
|
|
95
|
-
return fail(`${field}.
|
|
96
|
-
if (
|
|
97
|
-
return fail(`${field}.
|
|
98
|
-
if (
|
|
99
|
-
return fail(`${field}.
|
|
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 (
|
|
102
|
-
return fail(`${field}.
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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(
|
|
118
|
-
if (!isObject(
|
|
103
|
+
function excludeWarnings(files) {
|
|
104
|
+
if (!isObject(files))
|
|
119
105
|
return [];
|
|
120
|
-
const 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
|
-
|
|
128
|
-
`add them back
|
|
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', '
|
|
135
|
-
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');
|
|
136
134
|
if (tsconfig !== undefined && !isNonEmptyString(tsconfig))
|
|
137
|
-
return fail('
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
147
|
+
files: toFileSelector(files, 'files'),
|
|
148
|
+
classes: toClassSelector(classes, 'classes'),
|
|
149
|
+
warnings: excludeWarnings(files),
|
|
149
150
|
};
|
|
150
151
|
}
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|