@ts-ioc-container/bundler 3.0.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 -40
- package/README.md +181 -129
- 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 -59
- package/cjm/features/build/BuildController.js +36 -46
- package/cjm/features/build/domain/ImportPaths.js +30 -8
- 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 -73
- 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 -94
- 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 +15 -4
- 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/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,52 +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
|
-
|
|
73
|
+
glob: globs()
|
|
43
74
|
.optional()
|
|
44
|
-
.describe('Glob the class name must match, e.g. "*Service". An anonymous default export is named after its file.'),
|
|
45
|
-
|
|
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()
|
|
46
77
|
.optional()
|
|
47
|
-
.describe('
|
|
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.'),
|
|
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"].'),
|
|
51
79
|
}, expected('an object'))
|
|
52
|
-
.describe('Which classes of a
|
|
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()))
|
|
62
|
-
.optional()
|
|
63
|
-
.describe('tsconfig `paths`: aliases the generated imports are written in.'),
|
|
64
|
-
rootDir: zod_1.z
|
|
65
|
-
.string()
|
|
66
|
-
.optional()
|
|
67
|
-
.describe('tsconfig `rootDir`: where the default output (<rootDir>/<name>.bundle.ts) goes.'),
|
|
68
|
-
}, expected('an object'))
|
|
69
|
-
.describe("tsconfig `compilerOptions`, plus the bundler's own `importExtension` and `classes`. Every other option is handed to TypeScript on top of the extended tsconfig's, which validates it.");
|
|
70
|
-
const tsconfigGlobs = (field) => stringList().optional().describe(`tsconfig \`${field}\`: replaces the extended tsconfig's. Relative to this file.`);
|
|
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.');
|
|
71
81
|
exports.BUNDLE_CONFIG_SCHEMA = zod_1.z.strictObject({
|
|
72
82
|
$schema: zod_1.z.string().optional(),
|
|
73
|
-
extends: nonEmptyString()
|
|
74
|
-
.optional()
|
|
75
|
-
.meta({ default: exports.DEFAULT_EXTENDS })
|
|
76
|
-
.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."),
|
|
77
|
-
output: nonEmptyString()
|
|
78
|
-
.optional()
|
|
79
|
-
.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.'),
|
|
80
83
|
name: zod_1.z
|
|
81
84
|
.string(expected('letters, digits, "-" or "_", starting with a letter'))
|
|
82
85
|
.regex(BUNDLE_NAME, expected('letters, digits, "-" or "_", starting with a letter'))
|
|
83
86
|
.optional()
|
|
84
|
-
.describe('The bundle\'s name: letters, digits, "-" and "_", starting with a letter. Default:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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(),
|
|
88
98
|
}, expected('an object'));
|
|
89
99
|
function formatPath(segments) {
|
|
90
100
|
const formatted = segments
|
|
@@ -93,33 +103,68 @@ function formatPath(segments) {
|
|
|
93
103
|
.replace(/^\./, '');
|
|
94
104
|
return formatted || 'config';
|
|
95
105
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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}`];
|
|
101
118
|
}
|
|
102
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
|
+
}
|
|
103
127
|
function toClassSelector(classes) {
|
|
104
128
|
if (classes === undefined)
|
|
105
129
|
return { export: 'any', decorators: exports.DEFAULT_DECORATORS };
|
|
106
|
-
const { decorators,
|
|
130
|
+
const { decorators, glob, exclude, ...rest } = classes;
|
|
107
131
|
return {
|
|
108
132
|
...rest,
|
|
109
133
|
decorators: decorators.length > 0 ? decorators : undefined,
|
|
110
|
-
|
|
111
|
-
|
|
134
|
+
glob: toGlobs(glob),
|
|
135
|
+
exclude: toGlobs(exclude),
|
|
112
136
|
};
|
|
113
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
|
+
}
|
|
114
150
|
exports.CONFIG_FILE_SUFFIXES = ['.bundle.json', '.bundle.yaml', '.bundle.yml'];
|
|
115
|
-
const configSuffix = (file) => exports.CONFIG_FILE_SUFFIXES.find((suffix) => file.endsWith(suffix));
|
|
116
|
-
exports.configSuffix = configSuffix;
|
|
117
151
|
function configStem(file) {
|
|
118
152
|
const base = node_path_1.default.basename(file);
|
|
119
|
-
const suffix = (
|
|
153
|
+
const suffix = exports.CONFIG_FILE_SUFFIXES.find((candidate) => base.endsWith(candidate));
|
|
120
154
|
const stem = suffix ? base.slice(0, -suffix.length) : '';
|
|
121
155
|
return BUNDLE_NAME.test(stem) ? stem : undefined;
|
|
122
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
|
+
}
|
|
123
168
|
function toClassName(name) {
|
|
124
169
|
const words = name.split(/[-_]+/).filter(Boolean);
|
|
125
170
|
return `${words.map((word) => word[0].toUpperCase() + word.slice(1)).join('')}Bundle`;
|
|
@@ -128,19 +173,23 @@ function resolveConfig(content, file) {
|
|
|
128
173
|
const result = exports.BUNDLE_CONFIG_SCHEMA.safeParse(content);
|
|
129
174
|
if (!result.success)
|
|
130
175
|
throw new DomainException_1.TicConfigError(formatIssues(result.error));
|
|
131
|
-
const {
|
|
132
|
-
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
|
+
}
|
|
133
183
|
const dir = node_path_1.default.dirname(file);
|
|
134
|
-
const bundleName = name ?? configStem(file) ?? exports.DEFAULT_BUNDLE_NAME;
|
|
135
184
|
return {
|
|
136
185
|
file,
|
|
137
186
|
dir,
|
|
138
|
-
output: output === undefined ? undefined : node_path_1.default.resolve(dir, output),
|
|
139
187
|
name: bundleName,
|
|
140
|
-
|
|
141
|
-
tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.
|
|
142
|
-
overrides: { include, exclude, compilerOptions: tsCompilerOptions },
|
|
188
|
+
bundleClassName: toClassName(bundleName),
|
|
189
|
+
tsconfig: { file: node_path_1.default.resolve(dir, tsconfig ?? exports.DEFAULT_TSCONFIG), required: tsconfig !== undefined },
|
|
143
190
|
importExtension,
|
|
144
|
-
|
|
191
|
+
glob: toGlobSelector(glob),
|
|
192
|
+
className: toClassSelector(className),
|
|
193
|
+
warnings: excludeWarnings(glob),
|
|
145
194
|
};
|
|
146
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)];
|
|
@@ -62,14 +66,20 @@ class ImportPaths {
|
|
|
62
66
|
this.aliases = aliases;
|
|
63
67
|
this.extension = extension;
|
|
64
68
|
}
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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`);
|
|
79
|
+
}
|
|
80
|
+
specifier(file) {
|
|
81
|
+
const target = (0, glob_1.toPosix)(file);
|
|
82
|
+
return this.bestAlias(target, target.replace(SOURCE_EXTENSION, ''))?.spec;
|
|
73
83
|
}
|
|
74
84
|
bestAlias(target, bare) {
|
|
75
85
|
let best;
|
|
@@ -82,6 +92,18 @@ class ImportPaths {
|
|
|
82
92
|
}
|
|
83
93
|
return best;
|
|
84
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
|
+
}
|
|
85
107
|
matchAlias(pattern, aliasTarget, file, bare) {
|
|
86
108
|
const wildcard = splitWildcard(aliasTarget);
|
|
87
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) {
|
|
@@ -36,49 +36,24 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.
|
|
39
|
+
exports.loadImportPaths = loadImportPaths;
|
|
40
40
|
const node_fs_1 = require("node:fs");
|
|
41
41
|
const node_path_1 = __importDefault(require("node:path"));
|
|
42
42
|
const ts = __importStar(require("typescript"));
|
|
43
43
|
const DomainException_1 = require("../../../exceptions/DomainException");
|
|
44
44
|
const ImportPaths_1 = require("./ImportPaths");
|
|
45
|
-
const scan_1 = require("./scan");
|
|
46
45
|
const NO_INPUTS = 18003;
|
|
47
|
-
function
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const parts of rest) {
|
|
53
|
-
let i = 0;
|
|
54
|
-
while (i < length && i < parts.length && parts[i] === first[i])
|
|
55
|
-
i++;
|
|
56
|
-
length = i;
|
|
46
|
+
function loadImportPaths(tsconfig, importExtension) {
|
|
47
|
+
if (!(0, node_fs_1.existsSync)(tsconfig.file)) {
|
|
48
|
+
if (tsconfig.required)
|
|
49
|
+
throw new DomainException_1.TicConfigError(`tsconfig not found: ${tsconfig.file}`);
|
|
50
|
+
return new ImportPaths_1.ImportPaths([], importExtension ?? '');
|
|
57
51
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
function asTsconfig(config) {
|
|
61
|
-
if (!(0, node_fs_1.existsSync)(config.tsconfig.file)) {
|
|
62
|
-
if (config.tsconfig.required)
|
|
63
|
-
throw new DomainException_1.TicConfigError(`tsconfig not found: ${config.tsconfig.file}`);
|
|
64
|
-
return { config: config.overrides };
|
|
65
|
-
}
|
|
66
|
-
if (config.file === config.tsconfig.file)
|
|
67
|
-
return ts.readConfigFile(config.file, ts.sys.readFile);
|
|
68
|
-
return { config: { extends: config.tsconfig.file, ...config.overrides } };
|
|
69
|
-
}
|
|
70
|
-
function loadTsconfig(config) {
|
|
71
|
-
const { config: json, error } = asTsconfig(config);
|
|
72
|
-
const configFileName = config.file === config.tsconfig.file ? config.file : undefined;
|
|
73
|
-
const parsed = ts.parseJsonConfigFileContent(json ?? {}, ts.sys, config.dir, undefined, configFileName);
|
|
52
|
+
const { config, error } = ts.readConfigFile(tsconfig.file, ts.sys.readFile);
|
|
53
|
+
const parsed = ts.parseJsonConfigFileContent(config, ts.sys, node_path_1.default.dirname(tsconfig.file), undefined, tsconfig.file);
|
|
74
54
|
const [fatal] = [error, ...parsed.errors].filter((d) => !!d && d.code !== NO_INPUTS);
|
|
75
55
|
if (fatal) {
|
|
76
|
-
throw new DomainException_1.TicConfigError(`${
|
|
56
|
+
throw new DomainException_1.TicConfigError(`${tsconfig.file}: ${ts.flattenDiagnosticMessageText(fatal.messageText, '\n')}`);
|
|
77
57
|
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
importPaths: ImportPaths_1.ImportPaths.fromOptions(parsed.options, config.file, config.importExtension),
|
|
81
|
-
fileNames,
|
|
82
|
-
rootDir: parsed.options.rootDir ?? commonDir(fileNames.filter(scan_1.isSourceFile)) ?? config.dir,
|
|
83
|
-
};
|
|
58
|
+
return ImportPaths_1.ImportPaths.fromOptions(parsed.options, tsconfig.file, importExtension);
|
|
84
59
|
}
|