@ts-ioc-container/bundler 3.1.0 → 4.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 +45 -48
- package/README.md +177 -145
- package/cjm/app.js +2 -2
- package/cjm/cli/Application.js +4 -5
- package/cjm/cli/options.js +4 -2
- package/cjm/cli/usage.js +11 -8
- package/cjm/domain/CliIo.js +7 -1
- package/cjm/exceptions/DomainException.js +7 -7
- package/cjm/features/build/BuildConfig.js +108 -71
- package/cjm/features/build/BuildController.js +36 -46
- package/cjm/features/build/domain/ImportPaths.js +31 -20
- package/cjm/features/build/domain/emit.js +2 -2
- package/cjm/features/build/domain/scan.js +20 -4
- package/cjm/features/build/domain/tokenCollisions.js +1 -1
- package/cjm/features/build/domain/tsconfig.js +10 -35
- package/cjm/features/build/services/BundleBuilder.js +36 -29
- package/cjm/features/build/services/TicConfigService.js +10 -31
- package/cjm/index.js +9 -10
- package/cjm/schema/ticConfigSchema.js +5 -1
- package/package.json +2 -2
- package/tic.schema.json +117 -84
- package/tpl/index.cjs +15 -15
- package/typings/app.d.ts +7 -6
- package/typings/cli/Application.d.ts.map +1 -1
- package/typings/cli/options.d.ts +7 -1
- package/typings/cli/options.d.ts.map +1 -1
- package/typings/cli/usage.d.ts.map +1 -1
- package/typings/domain/CliIo.d.ts +2 -0
- package/typings/domain/CliIo.d.ts.map +1 -1
- package/typings/domain/GlobalConfig.d.ts +1 -1
- package/typings/domain/GlobalConfig.d.ts.map +1 -1
- package/typings/exceptions/DomainException.d.ts +4 -4
- package/typings/exceptions/DomainException.d.ts.map +1 -1
- package/typings/features/build/BuildConfig.d.ts +107 -103
- package/typings/features/build/BuildConfig.d.ts.map +1 -1
- package/typings/features/build/BuildController.d.ts +23 -17
- package/typings/features/build/BuildController.d.ts.map +1 -1
- package/typings/features/build/domain/ImportPaths.d.ts +16 -14
- package/typings/features/build/domain/ImportPaths.d.ts.map +1 -1
- package/typings/features/build/domain/emit.d.ts +4 -4
- package/typings/features/build/domain/emit.d.ts.map +1 -1
- package/typings/features/build/domain/scan.d.ts +2 -0
- package/typings/features/build/domain/scan.d.ts.map +1 -1
- package/typings/features/build/domain/tsconfig.d.ts +9 -16
- package/typings/features/build/domain/tsconfig.d.ts.map +1 -1
- package/typings/features/build/services/BundleBuilder.d.ts +25 -29
- package/typings/features/build/services/BundleBuilder.d.ts.map +1 -1
- package/typings/features/build/services/TicConfigService.d.ts +11 -15
- package/typings/features/build/services/TicConfigService.d.ts.map +1 -1
- package/typings/index.d.ts +6 -7
- package/typings/schema/ticConfigSchema.d.ts.map +1 -1
- package/cjm/features/build/domain/configFiles.js +0 -35
- package/typings/features/build/domain/configFiles.d.ts +0 -13
- package/typings/features/build/domain/configFiles.d.ts.map +0 -1
package/cjm/cli/options.js
CHANGED
|
@@ -6,10 +6,12 @@ const zod_1 = require("zod");
|
|
|
6
6
|
const DomainException_1 = require("../exceptions/DomainException");
|
|
7
7
|
const commandArgs = ({ args = [] }) => args.map(String);
|
|
8
8
|
exports.commandArgs = commandArgs;
|
|
9
|
-
const parseOptions = (options) => (argv) => {
|
|
9
|
+
const parseOptions = (options, { positionals = false } = {}) => (argv) => {
|
|
10
10
|
const firstFlag = argv.findIndex((arg) => arg.startsWith('-'));
|
|
11
|
+
const args = positionals ? argv.slice(1) : firstFlag >= 0 ? argv.slice(firstFlag) : [];
|
|
11
12
|
try {
|
|
12
|
-
|
|
13
|
+
const parsed = (0, node_util_1.parseArgs)({ args, options, strict: true, allowPositionals: positionals });
|
|
14
|
+
return positionals ? { ...parsed.values, positionals: parsed.positionals } : { ...parsed.values };
|
|
13
15
|
}
|
|
14
16
|
catch (e) {
|
|
15
17
|
throw new DomainException_1.UsageError(e.message);
|
package/cjm/cli/usage.js
CHANGED
|
@@ -6,14 +6,17 @@ exports.USAGE = [
|
|
|
6
6
|
' tic is a shortcut for ts-ioc-container',
|
|
7
7
|
'',
|
|
8
8
|
'Commands:',
|
|
9
|
-
' ts-ioc-container build [
|
|
10
|
-
'
|
|
11
|
-
' ts-ioc-container help
|
|
12
|
-
' ts-ioc-container version
|
|
9
|
+
' ts-ioc-container build [<config>] print the bundle a config file (.json, .yaml, .yml) describes;',
|
|
10
|
+
' without one (or with -), read the config from stdin',
|
|
11
|
+
' ts-ioc-container help show this help',
|
|
12
|
+
' ts-ioc-container version show the version',
|
|
13
13
|
'',
|
|
14
14
|
'Options:',
|
|
15
|
-
' -
|
|
16
|
-
' --
|
|
17
|
-
'
|
|
18
|
-
'
|
|
15
|
+
' -h, --help show this help',
|
|
16
|
+
' -v, --version show the version',
|
|
17
|
+
'',
|
|
18
|
+
'The bundle goes to stdout and warnings to stderr; saving it is up to you:',
|
|
19
|
+
' tic build app.bundle.json > src/di/app.bundle.ts',
|
|
20
|
+
' cat app.bundle.yml | tic build > src/di/app.bundle.ts # config from stdin (must set name)',
|
|
21
|
+
' tic build app.bundle.json | diff - src/di/app.bundle.ts # CI: is it current?',
|
|
19
22
|
].join('\n');
|
package/cjm/domain/CliIo.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.processIo = exports.CliIoKey = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
4
5
|
const ts_ioc_container_1 = require("ts-ioc-container");
|
|
5
6
|
exports.CliIoKey = new ts_ioc_container_1.SingleToken('CliIo');
|
|
6
|
-
const processIo = () => ({
|
|
7
|
+
const processIo = () => ({
|
|
8
|
+
cwd: process.cwd(),
|
|
9
|
+
stdout: console.log,
|
|
10
|
+
stderr: console.error,
|
|
11
|
+
stdin: () => (process.stdin.isTTY ? undefined : (0, node_fs_1.readFileSync)(0, 'utf8')),
|
|
12
|
+
});
|
|
7
13
|
exports.processIo = processIo;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.UsageError = exports.UnknownActionError = exports.UnknownCommandError = exports.MissingCommandError = exports.NamespaceNotFoundError = exports.TicConfigError = exports.TicError = void 0;
|
|
4
4
|
class TicError extends Error {
|
|
5
5
|
code;
|
|
6
6
|
constructor(code, message) {
|
|
@@ -16,6 +16,12 @@ class TicConfigError extends TicError {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
exports.TicConfigError = TicConfigError;
|
|
19
|
+
class NamespaceNotFoundError extends TicError {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super('NAMESPACE_NOT_FOUND', message);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.NamespaceNotFoundError = NamespaceNotFoundError;
|
|
19
25
|
class MissingCommandError extends TicError {
|
|
20
26
|
constructor(usage) {
|
|
21
27
|
super('MISSING_COMMAND', `missing command\n${usage}`);
|
|
@@ -40,9 +46,3 @@ class UsageError extends TicError {
|
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
exports.UsageError = UsageError;
|
|
43
|
-
class StaleBundlesError extends TicError {
|
|
44
|
-
constructor(count) {
|
|
45
|
-
super('STALE_BUNDLES', `${count} generated bundle${count === 1 ? ' is' : 's are'} out of date — run \`tic build\``);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
exports.StaleBundlesError = StaleBundlesError;
|
|
@@ -3,15 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.configFormat = exports.STDIN = exports.CONFIG_FILE_SUFFIXES = exports.BUNDLE_CONFIG_SCHEMA = exports.DEFAULT_EXCLUDE = exports.DEFAULT_DECORATORS = exports.DEFAULT_TSCONFIG = void 0;
|
|
7
|
+
exports.parseConfig = parseConfig;
|
|
7
8
|
exports.toClassName = toClassName;
|
|
8
9
|
exports.resolveConfig = resolveConfig;
|
|
9
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const yaml_1 = require("yaml");
|
|
10
12
|
const zod_1 = require("zod");
|
|
11
13
|
const DomainException_1 = require("../../exceptions/DomainException");
|
|
12
14
|
const glob_1 = require("./domain/glob");
|
|
13
|
-
exports.
|
|
14
|
-
exports.DEFAULT_BUNDLE_NAME = 'base';
|
|
15
|
+
exports.DEFAULT_TSCONFIG = './tsconfig.json';
|
|
15
16
|
exports.DEFAULT_DECORATORS = ['register'];
|
|
16
17
|
exports.DEFAULT_EXCLUDE = [
|
|
17
18
|
'**/*.spec.ts',
|
|
@@ -30,6 +31,36 @@ const strings = (what, minItems) => zod_1.z
|
|
|
30
31
|
.meta({ type: 'array', items: { type: 'string', minLength: 1 }, ...(minItems > 0 && { minItems }) });
|
|
31
32
|
const stringList = () => strings('an array of strings', 0);
|
|
32
33
|
const nonEmptyStringList = () => strings('a non-empty array of strings', 1);
|
|
34
|
+
const globs = () => zod_1.z
|
|
35
|
+
.custom((value) => isNonEmptyString(value) || (Array.isArray(value) && value.length > 0 && value.every(isNonEmptyString)), expected('a glob or a non-empty array of globs'))
|
|
36
|
+
.meta({
|
|
37
|
+
anyOf: [
|
|
38
|
+
{ type: 'string', minLength: 1 },
|
|
39
|
+
{ type: 'array', items: { type: 'string', minLength: 1 }, minItems: 1 },
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
const toGlobs = (value) => value === undefined ? undefined : (Array.isArray(value) ? value : [value]).map(glob_1.globToRegExp);
|
|
43
|
+
const PATH_SCHEMA = zod_1.z.union([
|
|
44
|
+
nonEmptyString(),
|
|
45
|
+
zod_1.z.object({
|
|
46
|
+
path: nonEmptyString(),
|
|
47
|
+
recursive: zod_1.z.boolean(expected('a boolean')).default(true),
|
|
48
|
+
}, expected('a string or an object with "path"')),
|
|
49
|
+
], expected('a string or an object with "path"'));
|
|
50
|
+
const GLOB_SCHEMA = zod_1.z
|
|
51
|
+
.strictObject({
|
|
52
|
+
paths: zod_1.z
|
|
53
|
+
.array(PATH_SCHEMA, expected('a non-empty array'))
|
|
54
|
+
.min(1, expected('a non-empty array'))
|
|
55
|
+
.describe('Folders to scan: paths relative to the config file (./src/services) or tsconfig paths aliases (@app/services). Sub-folders are scanned too unless an entry says { "path": "...", "recursive": false }.'),
|
|
56
|
+
include: nonEmptyStringList()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('Globs a file must match one of, e.g. ["**/*.service.ts", "**/*.repository.ts"]. Default: every source file.'),
|
|
59
|
+
exclude: stringList()
|
|
60
|
+
.default(exports.DEFAULT_EXCLUDE)
|
|
61
|
+
.describe('Globs of files never read. Replaces the default when given. A non-empty list that omits a default glob makes `tic build` warn.'),
|
|
62
|
+
}, expected('an object'))
|
|
63
|
+
.describe('Which files this bundle reads and parses — decided by path alone, before parsing. `paths` are the folders the files come from; within them a file is parsed when it matches one of `include` and none of `exclude`. Globs are relative to the config file. With a file naming convention, `include` keeps every other file unread.');
|
|
33
64
|
const CLASSES_SCHEMA = zod_1.z
|
|
34
65
|
.strictObject({
|
|
35
66
|
export: zod_1.z
|
|
@@ -39,57 +70,31 @@ const CLASSES_SCHEMA = zod_1.z
|
|
|
39
70
|
decorators: stringList()
|
|
40
71
|
.default(exports.DEFAULT_DECORATORS)
|
|
41
72
|
.describe('The class must carry one of these decorators, recognised by name (e.g. "register", or a composed decorator). Default ["register"]; [] requires none.'),
|
|
42
|
-
|
|
43
|
-
.optional()
|
|
44
|
-
.describe('Glob the class name must match, e.g. "*Service". An anonymous default export is named after its file.'),
|
|
45
|
-
excludeClasses: nonEmptyStringList()
|
|
46
|
-
.optional()
|
|
47
|
-
.describe('Class names to drop after selection, e.g. one test double sitting next to the real registration in the same file.'),
|
|
48
|
-
excludeName: nonEmptyString()
|
|
49
|
-
.optional()
|
|
50
|
-
.describe('Glob the class name must NOT match, e.g. "*Mock". An anonymous default export is named after its file.'),
|
|
51
|
-
}, expected('an object'))
|
|
52
|
-
.describe('Which classes of a scanned file are registered. A class is selected when it is exported, not abstract, and meets every criterion set here; by default that is every exported class decorated with @register.');
|
|
53
|
-
const COMPILER_OPTIONS_SCHEMA = zod_1.z
|
|
54
|
-
.looseObject({
|
|
55
|
-
importExtension: zod_1.z
|
|
56
|
-
.string(expected('a string'))
|
|
57
|
-
.optional()
|
|
58
|
-
.describe('Extension of generated imports, e.g. ".js". Inferred from the tsconfig: ".js" under node16/nodenext resolution, none otherwise.'),
|
|
59
|
-
classes: CLASSES_SCHEMA.optional(),
|
|
60
|
-
paths: zod_1.z
|
|
61
|
-
.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string()))
|
|
73
|
+
glob: globs()
|
|
62
74
|
.optional()
|
|
63
|
-
.describe('
|
|
64
|
-
|
|
65
|
-
.string()
|
|
75
|
+
.describe('Glob, or globs, the class name must match one of, e.g. "*Service" or ["*Service", "*Repository"]. An anonymous default export is named after its file.'),
|
|
76
|
+
exclude: globs()
|
|
66
77
|
.optional()
|
|
67
|
-
.describe('
|
|
78
|
+
.describe('Glob, or globs, the class name must match none of, applied after every other criterion. A plain name is a glob too: ["MockUserRepository", "*Fake"].'),
|
|
68
79
|
}, expected('an object'))
|
|
69
|
-
.describe(
|
|
70
|
-
const tsconfigGlobs = (field, note = '') => stringList()
|
|
71
|
-
.optional()
|
|
72
|
-
.describe(`tsconfig \`${field}\`: replaces the extended tsconfig's. Relative to \`baseUrl\`.${note}`);
|
|
80
|
+
.describe('Which classes of a parsed file are registered. A class is selected when it is exported, not abstract, and meets every criterion set here; by default that is every exported class decorated with @register.');
|
|
73
81
|
exports.BUNDLE_CONFIG_SCHEMA = zod_1.z.strictObject({
|
|
74
82
|
$schema: zod_1.z.string().optional(),
|
|
75
|
-
extends: nonEmptyString()
|
|
76
|
-
.optional()
|
|
77
|
-
.meta({ default: exports.DEFAULT_EXTENDS })
|
|
78
|
-
.describe("The tsconfig this bundle extends, as a tsconfig's own `extends`: its file set, `paths` aliases and module resolution apply unless this file overrides them. Relative to this file. The default may be absent; a tsconfig named here must exist."),
|
|
79
|
-
baseUrl: nonEmptyString()
|
|
80
|
-
.optional()
|
|
81
|
-
.describe("The base folder this config's relative `include`, `exclude` and `output` resolve against, so they need not repeat a shared prefix such as `src`. Relative to this file; default this file's directory. Also handed to TypeScript as `compilerOptions.baseUrl` (unless that is set too), so generated imports use it."),
|
|
82
|
-
output: nonEmptyString()
|
|
83
|
-
.optional()
|
|
84
|
-
.describe('The bundle file (convention: *.bundle.ts). Default `<root>/<name>.bundle.ts`: <root> is the compiled rootDir, else the common folder of the files compiled.'),
|
|
85
83
|
name: zod_1.z
|
|
86
84
|
.string(expected('letters, digits, "-" or "_", starting with a letter'))
|
|
87
85
|
.regex(BUNDLE_NAME, expected('letters, digits, "-" or "_", starting with a letter'))
|
|
88
86
|
.optional()
|
|
89
|
-
.describe('The bundle\'s name: letters, digits, "-" and "_", starting with a letter. Default:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
.describe('The bundle\'s name: letters, digits, "-" and "_", starting with a letter. Default: the config file\'s stem (production.bundle.json → production); required for a config file named otherwise. It names the generated IContainerModule class (ProductionBundle).'),
|
|
88
|
+
tsconfig: nonEmptyString()
|
|
89
|
+
.optional()
|
|
90
|
+
.meta({ default: exports.DEFAULT_TSCONFIG })
|
|
91
|
+
.describe('The tsconfig whose `paths` aliases glob.paths may name and generated imports are written in, and whose module resolution sets the import extension. It adds no files: the bundle scans glob.paths alone. Relative to the config file. The default may be absent; a tsconfig named here must exist.'),
|
|
92
|
+
importExtension: zod_1.z
|
|
93
|
+
.string(expected('a string'))
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('Extension of generated imports, e.g. ".js". Inferred from the tsconfig: ".js" under node16/nodenext resolution, none otherwise.'),
|
|
96
|
+
glob: GLOB_SCHEMA,
|
|
97
|
+
className: CLASSES_SCHEMA.optional(),
|
|
93
98
|
}, expected('an object'));
|
|
94
99
|
function formatPath(segments) {
|
|
95
100
|
const formatted = segments
|
|
@@ -98,33 +103,68 @@ function formatPath(segments) {
|
|
|
98
103
|
.replace(/^\./, '');
|
|
99
104
|
return formatted || 'config';
|
|
100
105
|
}
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
function matchedBranch(issue) {
|
|
107
|
+
const typeMatched = issue.errors.filter((branch) => branch.every((inner) => inner.code !== 'invalid_type' || inner.path.length > 0));
|
|
108
|
+
return typeMatched.length === 1 ? typeMatched[0] : undefined;
|
|
109
|
+
}
|
|
110
|
+
function formatIssue(issue, prefix = []) {
|
|
111
|
+
const at = [...prefix, ...issue.path];
|
|
112
|
+
if (issue.code === 'unrecognized_keys')
|
|
113
|
+
return issue.keys.map((key) => `${formatPath([...at, key])}: unknown field`);
|
|
114
|
+
const branch = issue.code === 'invalid_union' ? matchedBranch(issue) : undefined;
|
|
115
|
+
if (branch)
|
|
116
|
+
return branch.flatMap((inner) => formatIssue(inner, at));
|
|
117
|
+
return [`${formatPath(at)}: ${issue.message}`];
|
|
106
118
|
}
|
|
107
119
|
const formatIssues = (error) => error.issues.flatMap((issue) => formatIssue(issue)).join('; ');
|
|
120
|
+
function toGlobSelector(glob) {
|
|
121
|
+
return {
|
|
122
|
+
paths: glob.paths.map((entry) => (typeof entry === 'string' ? { path: entry, recursive: true } : entry)),
|
|
123
|
+
include: glob.include?.map(glob_1.globToRegExp),
|
|
124
|
+
exclude: glob.exclude.map(glob_1.globToRegExp),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
108
127
|
function toClassSelector(classes) {
|
|
109
128
|
if (classes === undefined)
|
|
110
129
|
return { export: 'any', decorators: exports.DEFAULT_DECORATORS };
|
|
111
|
-
const { decorators,
|
|
130
|
+
const { decorators, glob, exclude, ...rest } = classes;
|
|
112
131
|
return {
|
|
113
132
|
...rest,
|
|
114
133
|
decorators: decorators.length > 0 ? decorators : undefined,
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
glob: toGlobs(glob),
|
|
135
|
+
exclude: toGlobs(exclude),
|
|
117
136
|
};
|
|
118
137
|
}
|
|
138
|
+
function excludeWarnings(glob) {
|
|
139
|
+
const { exclude } = glob;
|
|
140
|
+
if (exclude.length === 0)
|
|
141
|
+
return [];
|
|
142
|
+
const missing = exports.DEFAULT_EXCLUDE.filter((glob) => !exclude.includes(glob));
|
|
143
|
+
if (missing.length === 0)
|
|
144
|
+
return [];
|
|
145
|
+
return [
|
|
146
|
+
`glob.exclude replaces the default excludes and omits ${missing.map((glob) => `"${glob}"`).join(', ')}; ` +
|
|
147
|
+
`add them back`,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
119
150
|
exports.CONFIG_FILE_SUFFIXES = ['.bundle.json', '.bundle.yaml', '.bundle.yml'];
|
|
120
|
-
const configSuffix = (file) => exports.CONFIG_FILE_SUFFIXES.find((suffix) => file.endsWith(suffix));
|
|
121
|
-
exports.configSuffix = configSuffix;
|
|
122
151
|
function configStem(file) {
|
|
123
152
|
const base = node_path_1.default.basename(file);
|
|
124
|
-
const suffix = (
|
|
153
|
+
const suffix = exports.CONFIG_FILE_SUFFIXES.find((candidate) => base.endsWith(candidate));
|
|
125
154
|
const stem = suffix ? base.slice(0, -suffix.length) : '';
|
|
126
155
|
return BUNDLE_NAME.test(stem) ? stem : undefined;
|
|
127
156
|
}
|
|
157
|
+
exports.STDIN = '-';
|
|
158
|
+
const configFormat = (file) => file.endsWith('.json') ? 'json' : file.endsWith('.yaml') || file.endsWith('.yml') ? 'yaml' : undefined;
|
|
159
|
+
exports.configFormat = configFormat;
|
|
160
|
+
function parseConfig(text, format) {
|
|
161
|
+
try {
|
|
162
|
+
return format === 'yaml' ? ((0, yaml_1.parse)(text) ?? {}) : JSON.parse(text);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
throw new DomainException_1.TicConfigError(`not valid ${format.toUpperCase()}: ${e.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
128
168
|
function toClassName(name) {
|
|
129
169
|
const words = name.split(/[-_]+/).filter(Boolean);
|
|
130
170
|
return `${words.map((word) => word[0].toUpperCase() + word.slice(1)).join('')}Bundle`;
|
|
@@ -133,26 +173,23 @@ function resolveConfig(content, file) {
|
|
|
133
173
|
const result = exports.BUNDLE_CONFIG_SCHEMA.safeParse(content);
|
|
134
174
|
if (!result.success)
|
|
135
175
|
throw new DomainException_1.TicConfigError(formatIssues(result.error));
|
|
136
|
-
const {
|
|
137
|
-
const
|
|
176
|
+
const { name, tsconfig, importExtension, glob, className } = result.data;
|
|
177
|
+
const bundleName = name ?? configStem(file);
|
|
178
|
+
if (bundleName === undefined) {
|
|
179
|
+
throw new DomainException_1.TicConfigError(node_path_1.default.basename(file) === exports.STDIN
|
|
180
|
+
? 'name: required for a config read from stdin'
|
|
181
|
+
: `name: required when the config file is not named <name>${exports.CONFIG_FILE_SUFFIXES.join(' / ')}`);
|
|
182
|
+
}
|
|
138
183
|
const dir = node_path_1.default.dirname(file);
|
|
139
|
-
const base = baseUrl === undefined ? dir : node_path_1.default.resolve(dir, baseUrl);
|
|
140
|
-
const includeGlobs = include ?? (baseUrl === undefined ? undefined : ['.']);
|
|
141
|
-
const rebase = (globs) => globs?.map((glob) => (node_path_1.default.isAbsolute(glob) ? glob : (0, glob_1.toPosix)(node_path_1.default.relative(dir, node_path_1.default.resolve(base, glob))) || '.'));
|
|
142
|
-
const bundleName = name ?? configStem(file) ?? exports.DEFAULT_BUNDLE_NAME;
|
|
143
184
|
return {
|
|
144
185
|
file,
|
|
145
186
|
dir,
|
|
146
|
-
output: output === undefined ? undefined : node_path_1.default.resolve(base, output),
|
|
147
187
|
name: bundleName,
|
|
148
|
-
|
|
149
|
-
tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.
|
|
150
|
-
overrides: {
|
|
151
|
-
include: rebase(includeGlobs),
|
|
152
|
-
exclude: rebase(exclude),
|
|
153
|
-
compilerOptions: baseUrl === undefined ? tsCompilerOptions : { baseUrl, ...tsCompilerOptions },
|
|
154
|
-
},
|
|
188
|
+
bundleClassName: toClassName(bundleName),
|
|
189
|
+
tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.DEFAULT_TSCONFIG), required: tsconfig !== undefined },
|
|
155
190
|
importExtension,
|
|
156
|
-
|
|
191
|
+
glob: toGlobSelector(glob),
|
|
192
|
+
className: toClassSelector(className),
|
|
193
|
+
warnings: excludeWarnings(glob),
|
|
157
194
|
};
|
|
158
195
|
}
|
|
@@ -11,65 +11,56 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
11
11
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
12
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
13
|
};
|
|
14
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
-
};
|
|
17
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
15
|
exports.BuildController = exports.BUILD_OPTIONS = void 0;
|
|
19
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
20
16
|
const ts_ioc_container_1 = require("ts-ioc-container");
|
|
21
17
|
const zod_1 = require("zod");
|
|
22
18
|
const cli_1 = require("../../cli");
|
|
23
|
-
const
|
|
19
|
+
const CliIo_1 = require("../../domain/CliIo");
|
|
24
20
|
const DomainException_1 = require("../../exceptions/DomainException");
|
|
25
21
|
const ConsoleLogger_1 = require("../../services/ConsoleLogger");
|
|
26
22
|
const OutputService_1 = require("../../services/OutputService");
|
|
23
|
+
const BuildConfig_1 = require("./BuildConfig");
|
|
27
24
|
const BundleBuilder_1 = require("./services/BundleBuilder");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
config: { type: 'string', short: 'c', multiple: true },
|
|
35
|
-
check: { type: 'boolean' },
|
|
25
|
+
exports.BUILD_OPTIONS = zod_1.z.object({ positionals: zod_1.z.array(zod_1.z.string().min(1)) }).transform(({ positionals }, ctx) => {
|
|
26
|
+
if (positionals.length > 1) {
|
|
27
|
+
ctx.addIssue({ code: 'custom', path: ['config'], message: `expected one <config>, got ${positionals.join(' ')}` });
|
|
28
|
+
return zod_1.z.NEVER;
|
|
29
|
+
}
|
|
30
|
+
return { config: positionals[0] ?? BuildConfig_1.STDIN };
|
|
36
31
|
});
|
|
37
|
-
const
|
|
38
|
-
const plural = (n) => `${n} registration${n === 1 ? '' : 's'}`;
|
|
32
|
+
const buildArgs = (0, cli_1.parseOptions)({}, { positionals: true });
|
|
39
33
|
let BuildController = class BuildController {
|
|
40
|
-
|
|
41
|
-
configs;
|
|
34
|
+
io;
|
|
42
35
|
builder;
|
|
43
36
|
output;
|
|
44
37
|
logger;
|
|
45
|
-
constructor(
|
|
46
|
-
this.
|
|
47
|
-
this.configs = configs;
|
|
38
|
+
constructor(io, builder, output, logger) {
|
|
39
|
+
this.io = io;
|
|
48
40
|
this.builder = builder;
|
|
49
41
|
this.output = output;
|
|
50
42
|
this.logger = logger;
|
|
51
43
|
}
|
|
52
|
-
build(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
44
|
+
build({ config }) {
|
|
45
|
+
const label = config === BuildConfig_1.STDIN ? '<stdin>' : config;
|
|
46
|
+
try {
|
|
47
|
+
const { content, warnings } = this.builder.build(config === BuildConfig_1.STDIN ? { text: this.stdin() } : { config });
|
|
48
|
+
for (const warning of warnings)
|
|
49
|
+
this.logger.warn(`${label}: ${warning}`);
|
|
50
|
+
this.output.write(content.replace(/\n$/, ''));
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
if (e instanceof DomainException_1.TicError && !(e instanceof DomainException_1.UsageError))
|
|
54
|
+
e.message = `${label}: ${e.message}`;
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
stdin() {
|
|
59
|
+
const text = this.io.stdin?.();
|
|
60
|
+
if (text === undefined) {
|
|
61
|
+
throw new DomainException_1.UsageError('missing <config>: name a config file, or pipe one in (cat app.bundle.json | tic build)');
|
|
70
62
|
}
|
|
71
|
-
|
|
72
|
-
throw new DomainException_1.StaleBundlesError(stale);
|
|
63
|
+
return text;
|
|
73
64
|
}
|
|
74
65
|
};
|
|
75
66
|
exports.BuildController = BuildController;
|
|
@@ -82,10 +73,9 @@ __decorate([
|
|
|
82
73
|
], BuildController.prototype, "build", null);
|
|
83
74
|
exports.BuildController = BuildController = __decorate([
|
|
84
75
|
(0, ts_ioc_container_1.register)('build'),
|
|
85
|
-
__param(0, (0, ts_ioc_container_1.inject)((0,
|
|
86
|
-
__param(1, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(
|
|
87
|
-
__param(2, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(
|
|
88
|
-
__param(3, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(
|
|
89
|
-
|
|
90
|
-
__metadata("design:paramtypes", [String, Object, Object, Object, Object])
|
|
76
|
+
__param(0, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(CliIo_1.CliIoKey))),
|
|
77
|
+
__param(1, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(BundleBuilder_1.IBundleBuilderKey))),
|
|
78
|
+
__param(2, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(OutputService_1.IOutputServiceKey))),
|
|
79
|
+
__param(3, (0, ts_ioc_container_1.inject)((0, ts_ioc_container_1.by)(ConsoleLogger_1.ILoggerKey))),
|
|
80
|
+
__metadata("design:paramtypes", [Object, Object, Object, Object])
|
|
91
81
|
], BuildController);
|
|
@@ -37,10 +37,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.ImportPaths = void 0;
|
|
40
|
+
const node_fs_1 = require("node:fs");
|
|
40
41
|
const node_path_1 = __importDefault(require("node:path"));
|
|
41
42
|
const ts = __importStar(require("typescript"));
|
|
43
|
+
const DomainException_1 = require("../../../exceptions/DomainException");
|
|
42
44
|
const glob_1 = require("./glob");
|
|
43
45
|
const SOURCE_EXTENSION = /\.(tsx?|mts|cts)$/;
|
|
46
|
+
const isDirectory = (dir) => (0, node_fs_1.existsSync)(dir) && (0, node_fs_1.statSync)(dir).isDirectory();
|
|
47
|
+
const isRelative = (spec) => spec.startsWith('.') || node_path_1.default.isAbsolute(spec);
|
|
44
48
|
function splitWildcard(pattern) {
|
|
45
49
|
const star = pattern.indexOf('*');
|
|
46
50
|
return star === -1 ? undefined : [pattern.slice(0, star), pattern.slice(star + 1)];
|
|
@@ -48,7 +52,6 @@ function splitWildcard(pattern) {
|
|
|
48
52
|
class ImportPaths {
|
|
49
53
|
aliases;
|
|
50
54
|
extension;
|
|
51
|
-
baseUrl;
|
|
52
55
|
static fromOptions(options, tsconfigFile, importExtension) {
|
|
53
56
|
const { paths = {}, baseUrl, pathsBasePath, moduleResolution } = options;
|
|
54
57
|
const base = baseUrl ?? (typeof pathsBasePath === 'string' ? pathsBasePath : node_path_1.default.dirname(tsconfigFile));
|
|
@@ -57,30 +60,26 @@ class ImportPaths {
|
|
|
57
60
|
targets: targets.map((target) => (0, glob_1.toPosix)(node_path_1.default.resolve(base, target))),
|
|
58
61
|
}));
|
|
59
62
|
const nodeEsm = moduleResolution === ts.ModuleResolutionKind.Node16 || moduleResolution === ts.ModuleResolutionKind.NodeNext;
|
|
60
|
-
return new ImportPaths(aliases, importExtension ?? (nodeEsm ? '.js' : '')
|
|
63
|
+
return new ImportPaths(aliases, importExtension ?? (nodeEsm ? '.js' : ''));
|
|
61
64
|
}
|
|
62
|
-
constructor(aliases, extension
|
|
65
|
+
constructor(aliases, extension) {
|
|
63
66
|
this.aliases = aliases;
|
|
64
67
|
this.extension = extension;
|
|
65
|
-
this.baseUrl = baseUrl;
|
|
66
68
|
}
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return `${relative.startsWith('.') ? relative : `./${relative}`}${this.extension}`;
|
|
69
|
+
resolveNamespace(namespace, baseDir) {
|
|
70
|
+
const folder = node_path_1.default.resolve(baseDir, namespace);
|
|
71
|
+
if (isDirectory(folder))
|
|
72
|
+
return folder;
|
|
73
|
+
if (!isRelative(namespace)) {
|
|
74
|
+
const found = [namespace, `${namespace}/`].flatMap((spec) => this.aliasTargets(spec)).find(isDirectory);
|
|
75
|
+
if (found)
|
|
76
|
+
return node_path_1.default.normalize(found);
|
|
77
|
+
}
|
|
78
|
+
throw new DomainException_1.NamespaceNotFoundError(`namespace "${namespace}" is neither a folder relative to ${baseDir} nor a tsconfig paths alias of one`);
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const prefix = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/`;
|
|
83
|
-
return bare.startsWith(prefix) ? bare.slice(prefix.length) : undefined;
|
|
80
|
+
specifier(file) {
|
|
81
|
+
const target = (0, glob_1.toPosix)(file);
|
|
82
|
+
return this.bestAlias(target, target.replace(SOURCE_EXTENSION, ''))?.spec;
|
|
84
83
|
}
|
|
85
84
|
bestAlias(target, bare) {
|
|
86
85
|
let best;
|
|
@@ -93,6 +92,18 @@ class ImportPaths {
|
|
|
93
92
|
}
|
|
94
93
|
return best;
|
|
95
94
|
}
|
|
95
|
+
aliasTargets(spec) {
|
|
96
|
+
return this.aliases.flatMap(({ pattern, targets }) => {
|
|
97
|
+
const wildcard = splitWildcard(pattern);
|
|
98
|
+
if (!wildcard)
|
|
99
|
+
return pattern === spec ? targets : [];
|
|
100
|
+
const [prefix, suffix] = wildcard;
|
|
101
|
+
if (!spec.startsWith(prefix) || !spec.endsWith(suffix) || spec.length < prefix.length + suffix.length)
|
|
102
|
+
return [];
|
|
103
|
+
const captured = spec.slice(prefix.length, spec.length - suffix.length);
|
|
104
|
+
return targets.map((target) => target.replace('*', captured));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
96
107
|
matchAlias(pattern, aliasTarget, file, bare) {
|
|
97
108
|
const wildcard = splitWildcard(aliasTarget);
|
|
98
109
|
if (!wildcard) {
|
|
@@ -7,7 +7,7 @@ const HandlebarsRenderService_1 = require("../../../services/HandlebarsRenderSer
|
|
|
7
7
|
exports.GENERATED_HEADER = '// Generated by `tic build`';
|
|
8
8
|
exports.BUNDLE_PROTOCOL = 'Bundle.ts.hbs';
|
|
9
9
|
const RUNTIME_IMPORTS = ['IContainer', 'IContainerModule', 'IRegistration', 'Registration'];
|
|
10
|
-
function bundleView({ name,
|
|
10
|
+
function bundleView({ name, paths, classes }) {
|
|
11
11
|
const taken = new Set([...RUNTIME_IMPORTS, 'registrations', name]);
|
|
12
12
|
const uniqueName = (wanted) => {
|
|
13
13
|
let candidate = wanted;
|
|
@@ -27,7 +27,7 @@ function bundleView({ name, configPath, classes }) {
|
|
|
27
27
|
entry.named.push(binding === cls.exportName ? binding : `${cls.exportName} as ${binding}`);
|
|
28
28
|
return binding;
|
|
29
29
|
});
|
|
30
|
-
return {
|
|
30
|
+
return { paths, name, imports: [...byFile.values()], registrations: bindings };
|
|
31
31
|
}
|
|
32
32
|
function emitBundle(input) {
|
|
33
33
|
return (0, HandlebarsRenderService_1.renderProtocol)(exports.BUNDLE_PROTOCOL, bundleView(input));
|
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.isSourceFile = void 0;
|
|
40
|
+
exports.listSourceFiles = listSourceFiles;
|
|
40
41
|
exports.findClasses = findClasses;
|
|
41
42
|
const node_fs_1 = require("node:fs");
|
|
42
43
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -45,6 +46,23 @@ const SOURCE_FILE = /\.(tsx?|mts|cts)$/;
|
|
|
45
46
|
const DECLARATION_FILE = /\.d\.[mc]?tsx?$/;
|
|
46
47
|
const isSourceFile = (file) => SOURCE_FILE.test(file) && !DECLARATION_FILE.test(file);
|
|
47
48
|
exports.isSourceFile = isSourceFile;
|
|
49
|
+
function listSourceFiles(dir, recursive, isExcluded) {
|
|
50
|
+
const files = [];
|
|
51
|
+
const walk = (current) => {
|
|
52
|
+
for (const entry of (0, node_fs_1.readdirSync)(current, { withFileTypes: true })) {
|
|
53
|
+
const full = node_path_1.default.join(current, entry.name);
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
if (recursive && entry.name !== 'node_modules')
|
|
56
|
+
walk(full);
|
|
57
|
+
}
|
|
58
|
+
else if ((0, exports.isSourceFile)(entry.name) && !isExcluded(full)) {
|
|
59
|
+
files.push(full);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
walk(dir);
|
|
64
|
+
return files.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
65
|
+
}
|
|
48
66
|
const hasModifier = (node, kind) => ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((m) => m.kind === kind);
|
|
49
67
|
function decoratorName(decorator, imports) {
|
|
50
68
|
const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
@@ -128,11 +146,9 @@ function findClasses(file, selector, text = (0, node_fs_1.readFileSync)(file, 'u
|
|
|
128
146
|
return false;
|
|
129
147
|
if (selector.decorators && !selector.decorators.some((name) => decorators.includes(name)))
|
|
130
148
|
return false;
|
|
131
|
-
if (selector.
|
|
132
|
-
return false;
|
|
133
|
-
if (selector.excludeClasses?.includes(className))
|
|
149
|
+
if (selector.glob && !selector.glob.some((glob) => glob.test(className)))
|
|
134
150
|
return false;
|
|
135
|
-
if (selector.
|
|
151
|
+
if (selector.exclude?.some((glob) => glob.test(className)))
|
|
136
152
|
return false;
|
|
137
153
|
return true;
|
|
138
154
|
})
|
|
@@ -17,7 +17,7 @@ function tokenCollisions(classes) {
|
|
|
17
17
|
.map(([token, group]) => {
|
|
18
18
|
const names = group.map((cls) => cls.className);
|
|
19
19
|
return (`decorator token "${token}" is passed by ${names.join(', ')}; ` +
|
|
20
|
-
`registration is last-wins, exclude one with
|
|
20
|
+
`registration is last-wins, exclude one with className.exclude`);
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
function scopeDisjoint(group) {
|