ts-ag 1.0.19 → 1.0.20
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/dist/scripts/ts-alias.d.ts +1 -1
- package/dist/scripts/ts-alias.js +35 -54
- package/dist/scripts/ts-build-config.d.ts +3 -0
- package/dist/scripts/ts-build-config.d.ts.map +1 -0
- package/dist/scripts/ts-build-config.js +256 -0
- package/dist/utils/cli.d.ts +2 -0
- package/dist/utils/cli.d.ts.map +1 -0
- package/dist/utils/cli.js +2 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/package.json +3 -2
- package/src/scripts/ts-alias.ts +38 -60
- package/src/scripts/ts-build-config.ts +305 -0
- package/src/utils/cli.ts +4 -0
- package/src/utils/index.ts +1 -0
package/dist/scripts/ts-alias.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
import console from 'console';
|
|
3
3
|
// NOTE: dont use aliases here cause this file needs to be compiled first
|
|
4
|
-
import { existsSync
|
|
5
|
-
import { dirname, join, basename } from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { dirname, join, basename, relative } from 'path';
|
|
6
6
|
import { watch } from 'chokidar';
|
|
7
7
|
import { glob } from 'glob';
|
|
8
8
|
import { replaceTscAliasPaths } from 'tsc-alias';
|
|
9
|
-
|
|
10
|
-
const tsconfigCache = new Map();
|
|
9
|
+
import { colorText } from '../utils/cli.js';
|
|
11
10
|
// Parse command-line arguments
|
|
12
11
|
const args = process.argv.slice(2);
|
|
13
12
|
const watchMode = args.includes('-w') || args.includes('--watch');
|
|
13
|
+
const LABEL = colorText('cyan', '[ts-alias]');
|
|
14
|
+
const formatPath = (filePath) => colorText('dim', relative(process.cwd(), filePath));
|
|
15
|
+
const logInfo = (message) => console.log(`${LABEL} ${message}`);
|
|
16
|
+
const logWarn = (message) => console.warn(`${LABEL} ${colorText('yellow', message)}`);
|
|
17
|
+
const logError = (message) => console.error(`${LABEL} ${colorText('red', message)}`);
|
|
14
18
|
/**
|
|
15
19
|
* Find all dist folders in the project directory, excluding certain patterns.
|
|
16
20
|
*/
|
|
@@ -27,31 +31,6 @@ async function findDistFolders(baseDir) {
|
|
|
27
31
|
const distFolders = await glob('**/dist', { cwd: baseDir, ignore: ignorePatterns, absolute: true });
|
|
28
32
|
return distFolders;
|
|
29
33
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Get the tsconfig.json file for a given dist folder.
|
|
32
|
-
* This function caches the tsconfig file to avoid reading it multiple times.
|
|
33
|
-
*/
|
|
34
|
-
function getTsconfig(distFolder) {
|
|
35
|
-
const projectRoot = dirname(distFolder);
|
|
36
|
-
const tsconfigPath = join(projectRoot, 'tsconfig.json');
|
|
37
|
-
try {
|
|
38
|
-
const stats = statSync(tsconfigPath);
|
|
39
|
-
const mtime = stats.mtimeMs;
|
|
40
|
-
// Check cache
|
|
41
|
-
const cached = tsconfigCache.get(tsconfigPath);
|
|
42
|
-
if (cached && cached.mtime === mtime) {
|
|
43
|
-
return cached.config;
|
|
44
|
-
}
|
|
45
|
-
// Read and cache the config
|
|
46
|
-
const config = JSON.parse(readFileSync(tsconfigPath, 'utf8'));
|
|
47
|
-
tsconfigCache.set(tsconfigPath, { config, mtime });
|
|
48
|
-
return config;
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
console.error(`Error reading tsconfig at ${tsconfigPath}:`, error);
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
34
|
/**
|
|
56
35
|
* Process the dist folder by replacing TypeScript alias paths with relative paths.
|
|
57
36
|
*/
|
|
@@ -59,42 +38,44 @@ async function processDistFolder(distFolder) {
|
|
|
59
38
|
const projectRoot = dirname(distFolder);
|
|
60
39
|
const tsconfigPath = join(projectRoot, 'tsconfig.json');
|
|
61
40
|
if (!existsSync(tsconfigPath)) {
|
|
62
|
-
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const tsconfig = getTsconfig(distFolder);
|
|
66
|
-
if (!tsconfig) {
|
|
67
|
-
console.warn(`Invalid tsconfig.json found for ${distFolder}`);
|
|
41
|
+
logWarn(`No tsconfig.json at ${formatPath(tsconfigPath)}`);
|
|
68
42
|
return;
|
|
69
43
|
}
|
|
70
44
|
try {
|
|
71
45
|
await replaceTscAliasPaths({ configFile: tsconfigPath, outDir: distFolder });
|
|
72
|
-
|
|
46
|
+
logInfo(`${colorText('green', 'updated')} ${formatPath(distFolder)}`);
|
|
73
47
|
}
|
|
74
48
|
catch (error) {
|
|
75
|
-
|
|
49
|
+
logError(`Failed processing ${formatPath(distFolder)}`);
|
|
50
|
+
console.error(error);
|
|
76
51
|
}
|
|
77
52
|
}
|
|
78
53
|
/**
|
|
79
54
|
* Watch the dist folder for changes and process it when files are added or changed.
|
|
80
55
|
*/
|
|
81
56
|
function watchDistFolder(distFolder) {
|
|
82
|
-
|
|
57
|
+
let pending = null;
|
|
83
58
|
const watcher = watch(distFolder, {
|
|
84
59
|
persistent: true,
|
|
85
60
|
ignoreInitial: true,
|
|
86
61
|
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 }
|
|
87
62
|
});
|
|
63
|
+
const scheduleProcess = () => {
|
|
64
|
+
if (pending)
|
|
65
|
+
clearTimeout(pending);
|
|
66
|
+
pending = setTimeout(() => {
|
|
67
|
+
pending = null;
|
|
68
|
+
void processDistFolder(distFolder);
|
|
69
|
+
}, 200);
|
|
70
|
+
};
|
|
88
71
|
watcher.on('add', (filePath) => {
|
|
89
72
|
if (filePath.endsWith('.js') || filePath.endsWith('.jsx')) {
|
|
90
|
-
|
|
91
|
-
processDistFolder(distFolder);
|
|
73
|
+
scheduleProcess();
|
|
92
74
|
}
|
|
93
75
|
});
|
|
94
76
|
watcher.on('change', (filePath) => {
|
|
95
77
|
if (filePath.endsWith('.js') || filePath.endsWith('.jsx')) {
|
|
96
|
-
|
|
97
|
-
processDistFolder(distFolder);
|
|
78
|
+
scheduleProcess();
|
|
98
79
|
}
|
|
99
80
|
});
|
|
100
81
|
return watcher;
|
|
@@ -102,26 +83,25 @@ function watchDistFolder(distFolder) {
|
|
|
102
83
|
// Main function
|
|
103
84
|
async function main() {
|
|
104
85
|
const baseDir = process.cwd();
|
|
105
|
-
|
|
86
|
+
logInfo(`searching ${colorText('cyan', 'dist')} folders in ${formatPath(baseDir)}`);
|
|
106
87
|
const distFolders = await findDistFolders(baseDir);
|
|
107
88
|
// Process all folders initially if any exist
|
|
108
89
|
if (distFolders.length > 0) {
|
|
109
|
-
|
|
110
|
-
distFolders.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
90
|
+
logInfo(`found ${distFolders.length} dist folder(s)`);
|
|
91
|
+
await Promise.all(distFolders.map(async (folder) => {
|
|
92
|
+
return await processDistFolder(folder);
|
|
93
|
+
}));
|
|
114
94
|
}
|
|
115
95
|
else {
|
|
116
|
-
|
|
96
|
+
logInfo('no dist folders found');
|
|
117
97
|
}
|
|
118
98
|
// Set up watchers if in watch mode
|
|
119
99
|
if (watchMode) {
|
|
120
|
-
|
|
100
|
+
logInfo('watch mode enabled');
|
|
121
101
|
// Set up watchers for existing dist folders
|
|
122
102
|
distFolders.forEach(watchDistFolder);
|
|
123
103
|
// Watch for new dist folders being created
|
|
124
|
-
|
|
104
|
+
logInfo('watching for new dist folders...');
|
|
125
105
|
const dirWatcher = watch(baseDir, {
|
|
126
106
|
persistent: true,
|
|
127
107
|
ignoreInitial: true,
|
|
@@ -143,7 +123,7 @@ async function main() {
|
|
|
143
123
|
if (basename(dirPath) === 'dist') {
|
|
144
124
|
// Make sure it's not already being watched
|
|
145
125
|
if (!distFolders.includes(dirPath)) {
|
|
146
|
-
|
|
126
|
+
logInfo(`${colorText('cyan', 'new dist')} ${formatPath(dirPath)}`);
|
|
147
127
|
distFolders.push(dirPath);
|
|
148
128
|
await processDistFolder(dirPath);
|
|
149
129
|
watchDistFolder(dirPath);
|
|
@@ -153,6 +133,7 @@ async function main() {
|
|
|
153
133
|
}
|
|
154
134
|
}
|
|
155
135
|
main().catch((error) => {
|
|
156
|
-
|
|
136
|
+
logError('Unhandled error in ts-alias script');
|
|
137
|
+
console.error(error);
|
|
157
138
|
process.exit(1);
|
|
158
139
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ts-build-config.d.ts","sourceRoot":"","sources":["../../src/scripts/ts-build-config.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import console from 'console';
|
|
3
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
4
|
+
import { dirname, join, resolve, relative } from 'path';
|
|
5
|
+
import { parseArgs } from 'util';
|
|
6
|
+
import { watch } from 'chokidar';
|
|
7
|
+
import { getTsconfig } from 'get-tsconfig';
|
|
8
|
+
import { colorText } from '../utils/cli.js';
|
|
9
|
+
const DEFAULT_TEST_EXCLUDES = [
|
|
10
|
+
'**/*.test.ts',
|
|
11
|
+
'**/*.test.tsx',
|
|
12
|
+
'**/*.spec.ts',
|
|
13
|
+
'**/*.spec.tsx',
|
|
14
|
+
'**/__tests__/**',
|
|
15
|
+
'**/tests/**'
|
|
16
|
+
];
|
|
17
|
+
const GENERATED_FILE_HEADER = '// generated by ts-build-config';
|
|
18
|
+
const LABEL = colorText('cyan', '[ts-build-config]');
|
|
19
|
+
const formatPath = (filePath) => colorText('dim', relative(process.cwd(), filePath));
|
|
20
|
+
const logInfo = (message) => console.log(`${LABEL} ${message}`);
|
|
21
|
+
const logWarn = (message) => console.warn(`${LABEL} ${colorText('yellow', message)}`);
|
|
22
|
+
const logError = (message) => console.error(`${LABEL} ${colorText('red', message)}`);
|
|
23
|
+
function asArray(v) {
|
|
24
|
+
if (!v)
|
|
25
|
+
return [];
|
|
26
|
+
return Array.isArray(v) ? v : [v];
|
|
27
|
+
}
|
|
28
|
+
function isObject(v) {
|
|
29
|
+
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
30
|
+
}
|
|
31
|
+
function uniq(items) {
|
|
32
|
+
return Array.from(new Set(items));
|
|
33
|
+
}
|
|
34
|
+
function normalizeRefPath(baseTsconfigPath, refPath) {
|
|
35
|
+
// TS project references are relative to the tsconfig location
|
|
36
|
+
const baseDir = dirname(baseTsconfigPath);
|
|
37
|
+
// refPath may point to a folder (containing tsconfig.json) or a file
|
|
38
|
+
const abs = resolve(baseDir, refPath);
|
|
39
|
+
return abs;
|
|
40
|
+
}
|
|
41
|
+
function resolveReferencedTsconfigPath(refAbsPath) {
|
|
42
|
+
// If they referenced a file directly, use it
|
|
43
|
+
if (refAbsPath.endsWith('.json')) {
|
|
44
|
+
return existsSync(refAbsPath) ? refAbsPath : null;
|
|
45
|
+
}
|
|
46
|
+
// Otherwise assume it's a directory containing tsconfig.json
|
|
47
|
+
const candidate = join(refAbsPath, 'tsconfig.json');
|
|
48
|
+
if (existsSync(candidate))
|
|
49
|
+
return candidate;
|
|
50
|
+
// Some repos use tsconfig.base.json or similar; we won't guess.
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function readTsconfig(tsconfigPath) {
|
|
54
|
+
const res = getTsconfig(tsconfigPath);
|
|
55
|
+
if (!res) {
|
|
56
|
+
throw new Error(`get-tsconfig could not load: ${tsconfigPath}`);
|
|
57
|
+
}
|
|
58
|
+
// get-tsconfig returns a parsed config object (includes extends resolved).
|
|
59
|
+
// We want to write a build tsconfig that extends the original file path,
|
|
60
|
+
// so we mostly only need top-level fields we change (exclude/compilerOptions).
|
|
61
|
+
return res.config;
|
|
62
|
+
}
|
|
63
|
+
function computeExtraExcludes(config) {
|
|
64
|
+
const extra = [...DEFAULT_TEST_EXCLUDES];
|
|
65
|
+
// If the tsconfig includes ./tests (or tests) explicitly, ensure it is excluded in build config.
|
|
66
|
+
const includes = asArray(config.include);
|
|
67
|
+
for (const inc of includes) {
|
|
68
|
+
const norm = inc.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
69
|
+
if (norm === './tests' || norm === 'tests' || norm.startsWith('./tests/') || norm.startsWith('tests/')) {
|
|
70
|
+
extra.push('./tests/**');
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return uniq(extra);
|
|
75
|
+
}
|
|
76
|
+
function writeBuildTsconfig(tsconfigPath, config, dryRun) {
|
|
77
|
+
const dir = dirname(tsconfigPath);
|
|
78
|
+
const outPath = join(dir, 'tsconfig.build.json');
|
|
79
|
+
const existingExclude = asArray(config.exclude);
|
|
80
|
+
const mergedExclude = uniq([...existingExclude, ...computeExtraExcludes(config)]);
|
|
81
|
+
const buildConfig = { extends: './tsconfig.json', compilerOptions: {}, exclude: mergedExclude };
|
|
82
|
+
// If original compilerOptions exists, keep build-specific overrides minimal.
|
|
83
|
+
// But ensure emit is enabled if base tsconfig has noEmit: true (common for editor configs).
|
|
84
|
+
const baseCO = isObject(config.compilerOptions) ? config.compilerOptions : {};
|
|
85
|
+
if (baseCO.noEmit === true) {
|
|
86
|
+
buildConfig.compilerOptions.noEmit = false;
|
|
87
|
+
}
|
|
88
|
+
// Keep outDir/rootDir if already set in base; do not guess.
|
|
89
|
+
// If you want to force them, do it in the base tsconfig.json or pass flags later.
|
|
90
|
+
if (!dryRun) {
|
|
91
|
+
mkdirSync(dir, { recursive: true });
|
|
92
|
+
writeFileSync(outPath, `${GENERATED_FILE_HEADER}\n${JSON.stringify(buildConfig, null, 2)}\n`, 'utf8');
|
|
93
|
+
}
|
|
94
|
+
return outPath;
|
|
95
|
+
}
|
|
96
|
+
function isGeneratedByThisScript(filePath) {
|
|
97
|
+
try {
|
|
98
|
+
const contents = readFileSync(filePath, 'utf8');
|
|
99
|
+
const firstLine = contents.split(/\r?\n/, 1)[0]?.trim() ?? '';
|
|
100
|
+
return firstLine === GENERATED_FILE_HEADER;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function getProjectRefs(tsconfigPath, config) {
|
|
107
|
+
const refs = asArray(config.references);
|
|
108
|
+
const result = [];
|
|
109
|
+
for (const r of refs) {
|
|
110
|
+
if (!r)
|
|
111
|
+
continue;
|
|
112
|
+
const refPath = typeof r === 'string' ? r : r.path;
|
|
113
|
+
if (!refPath || typeof refPath !== 'string')
|
|
114
|
+
continue;
|
|
115
|
+
const abs = normalizeRefPath(tsconfigPath, refPath);
|
|
116
|
+
const resolved = resolveReferencedTsconfigPath(abs);
|
|
117
|
+
if (resolved)
|
|
118
|
+
result.push(resolved);
|
|
119
|
+
}
|
|
120
|
+
return uniq(result);
|
|
121
|
+
}
|
|
122
|
+
function generateBuildConfigs(entry, options) {
|
|
123
|
+
const { dryRun, force, verbose } = options;
|
|
124
|
+
const visited = new Set();
|
|
125
|
+
const queue = [entry];
|
|
126
|
+
const created = [];
|
|
127
|
+
while (queue.length) {
|
|
128
|
+
const tsconfigPath = queue.shift();
|
|
129
|
+
if (visited.has(tsconfigPath))
|
|
130
|
+
continue;
|
|
131
|
+
visited.add(tsconfigPath);
|
|
132
|
+
let cfg;
|
|
133
|
+
try {
|
|
134
|
+
cfg = readTsconfig(tsconfigPath);
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
logWarn(`Skipping unreadable config: ${formatPath(tsconfigPath)}`);
|
|
138
|
+
if (verbose)
|
|
139
|
+
console.warn(e);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const outPath = join(dirname(tsconfigPath), 'tsconfig.build.json');
|
|
143
|
+
const outExists = existsSync(outPath);
|
|
144
|
+
const generatedByScript = outExists && isGeneratedByThisScript(outPath);
|
|
145
|
+
if (outExists && !generatedByScript && !force) {
|
|
146
|
+
if (verbose) {
|
|
147
|
+
logInfo(`Skip ${formatPath(outPath)} (manual file, use ${colorText('bold', '--force')})`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const written = writeBuildTsconfig(tsconfigPath, cfg, dryRun);
|
|
152
|
+
created.push({ src: tsconfigPath, out: written });
|
|
153
|
+
if (verbose || dryRun) {
|
|
154
|
+
const verb = dryRun ? colorText('yellow', '[dry-run] write') : colorText('green', 'write');
|
|
155
|
+
logInfo(`${verb} ${formatPath(written)} <- ${formatPath(tsconfigPath)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const refs = getProjectRefs(tsconfigPath, cfg);
|
|
159
|
+
for (const r of refs)
|
|
160
|
+
queue.push(r);
|
|
161
|
+
}
|
|
162
|
+
if (!verbose && !dryRun) {
|
|
163
|
+
logInfo(`${colorText('green', 'updated')} ${created.length} tsconfig.build.json file(s)`);
|
|
164
|
+
}
|
|
165
|
+
return { created, visitedConfigs: Array.from(visited) };
|
|
166
|
+
}
|
|
167
|
+
function createRegenerator(entry, options) {
|
|
168
|
+
let isRunning = false;
|
|
169
|
+
let rerunRequested = false;
|
|
170
|
+
let watchedConfigs = new Set();
|
|
171
|
+
const syncWatchedConfigs = (watcher) => {
|
|
172
|
+
if (watchedConfigs.size === 0)
|
|
173
|
+
return;
|
|
174
|
+
watcher.add(Array.from(watchedConfigs));
|
|
175
|
+
};
|
|
176
|
+
const run = async (reason) => {
|
|
177
|
+
if (isRunning) {
|
|
178
|
+
rerunRequested = true;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
isRunning = true;
|
|
182
|
+
try {
|
|
183
|
+
do {
|
|
184
|
+
rerunRequested = false;
|
|
185
|
+
if (reason) {
|
|
186
|
+
logInfo(`${colorText('cyan', 'regenerate')} (${reason})`);
|
|
187
|
+
reason = undefined;
|
|
188
|
+
}
|
|
189
|
+
const result = generateBuildConfigs(entry, options);
|
|
190
|
+
watchedConfigs = new Set(result.visitedConfigs);
|
|
191
|
+
} while (rerunRequested);
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
isRunning = false;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
return { run, syncWatchedConfigs };
|
|
198
|
+
}
|
|
199
|
+
async function main() {
|
|
200
|
+
const { values } = parseArgs({
|
|
201
|
+
args: process.argv.slice(2),
|
|
202
|
+
options: {
|
|
203
|
+
config: { type: 'string', short: 'c' },
|
|
204
|
+
dryRun: { type: 'boolean' },
|
|
205
|
+
force: { type: 'boolean' }, // overwrite files not generated by this script
|
|
206
|
+
watch: { type: 'boolean', short: 'w' },
|
|
207
|
+
verbose: { type: 'boolean', short: 'v' }
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
const entry = values.config ? resolve(values.config) : null;
|
|
211
|
+
if (!entry) {
|
|
212
|
+
logError('Missing required flag: --config <path/to/tsconfig.json>');
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
if (!existsSync(entry)) {
|
|
216
|
+
logError(`Config file not found: ${formatPath(entry)}`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
const dryRun = values.dryRun === true;
|
|
220
|
+
const force = values.force === true;
|
|
221
|
+
const watchMode = values.watch === true;
|
|
222
|
+
const verbose = values.verbose === true;
|
|
223
|
+
const options = { dryRun, force, verbose };
|
|
224
|
+
const regenerator = createRegenerator(entry, options);
|
|
225
|
+
await regenerator.run('initial run');
|
|
226
|
+
if (!watchMode) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
logInfo(`watching ${colorText('cyan', 'tsconfig')} changes...`);
|
|
230
|
+
const watcher = watch([], {
|
|
231
|
+
persistent: true,
|
|
232
|
+
ignoreInitial: true,
|
|
233
|
+
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 }
|
|
234
|
+
});
|
|
235
|
+
regenerator.syncWatchedConfigs(watcher);
|
|
236
|
+
watcher.on('change', async (filePath) => {
|
|
237
|
+
logInfo(`${colorText('cyan', 'change')} ${formatPath(filePath)}`);
|
|
238
|
+
await regenerator.run(`changed ${relative(process.cwd(), filePath)}`);
|
|
239
|
+
regenerator.syncWatchedConfigs(watcher);
|
|
240
|
+
});
|
|
241
|
+
watcher.on('add', async (filePath) => {
|
|
242
|
+
logInfo(`${colorText('cyan', 'add')} ${formatPath(filePath)}`);
|
|
243
|
+
await regenerator.run(`added ${relative(process.cwd(), filePath)}`);
|
|
244
|
+
regenerator.syncWatchedConfigs(watcher);
|
|
245
|
+
});
|
|
246
|
+
watcher.on('unlink', async (filePath) => {
|
|
247
|
+
logInfo(`${colorText('yellow', 'remove')} ${formatPath(filePath)}`);
|
|
248
|
+
await regenerator.run(`removed ${relative(process.cwd(), filePath)}`);
|
|
249
|
+
regenerator.syncWatchedConfigs(watcher);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
main().catch((err) => {
|
|
253
|
+
logError('Unhandled error');
|
|
254
|
+
console.error(err);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
});
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const colorText: (format: (("bgBlack" | "bgBlackBright" | "bgBlue" | "bgBlueBright" | "bgCyan" | "bgCyanBright" | "bgGray" | "bgGreen" | "bgGreenBright" | "bgGrey" | "bgMagenta" | "bgMagentaBright" | "bgRed" | "bgRedBright" | "bgWhite" | "bgWhiteBright" | "bgYellow" | "bgYellowBright") | ("black" | "blackBright" | "blue" | "blueBright" | "cyan" | "cyanBright" | "gray" | "green" | "greenBright" | "grey" | "magenta" | "magentaBright" | "red" | "redBright" | "white" | "whiteBright" | "yellow" | "yellowBright") | ("blink" | "bold" | "dim" | "doubleunderline" | "framed" | "hidden" | "inverse" | "italic" | "none" | "overlined" | "reset" | "strikethrough" | "underline"))[] | ("bgBlack" | "bgBlackBright" | "bgBlue" | "bgBlueBright" | "bgCyan" | "bgCyanBright" | "bgGray" | "bgGreen" | "bgGreenBright" | "bgGrey" | "bgMagenta" | "bgMagentaBright" | "bgRed" | "bgRedBright" | "bgWhite" | "bgWhiteBright" | "bgYellow" | "bgYellowBright") | ("black" | "blackBright" | "blue" | "blueBright" | "cyan" | "cyanBright" | "gray" | "green" | "greenBright" | "grey" | "magenta" | "magentaBright" | "red" | "redBright" | "white" | "whiteBright" | "yellow" | "yellowBright") | ("blink" | "bold" | "dim" | "doubleunderline" | "framed" | "hidden" | "inverse" | "italic" | "none" | "overlined" | "reset" | "strikethrough" | "underline"), text: unknown) => string;
|
|
2
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/utils/cli.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS,ozCACsC,CAAC"}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAE7B,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAE7B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC"}
|
package/dist/utils/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-ag",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "Useful TS stuff",
|
|
5
5
|
"bugs": "https://github.com/ageorgeh/ts-ag/issues",
|
|
6
6
|
"author": "Alexander Hornung",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
12
|
"clean-dist": "dist/scripts/clean-dist.js",
|
|
13
|
-
"ts-alias": "dist/scripts/ts-alias.js"
|
|
13
|
+
"ts-alias": "dist/scripts/ts-alias.js",
|
|
14
|
+
"ts-build-config": "dist/scripts/ts-build-config.js"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"./src",
|
package/src/scripts/ts-alias.ts
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import console from 'console';
|
|
4
4
|
// NOTE: dont use aliases here cause this file needs to be compiled first
|
|
5
|
-
import { existsSync
|
|
6
|
-
import { dirname, join, basename } from 'path';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { dirname, join, basename, relative } from 'path';
|
|
7
7
|
|
|
8
8
|
import type { FSWatcher } from 'chokidar';
|
|
9
9
|
import { watch } from 'chokidar';
|
|
10
10
|
import { glob } from 'glob';
|
|
11
11
|
import { replaceTscAliasPaths } from 'tsc-alias';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
const tsconfigCache: Map<string, { config: any; mtime: number }> = new Map();
|
|
13
|
+
import { colorText } from '../utils/cli.js';
|
|
15
14
|
|
|
16
15
|
// Parse command-line arguments
|
|
17
16
|
const args = process.argv.slice(2);
|
|
18
17
|
const watchMode = args.includes('-w') || args.includes('--watch');
|
|
18
|
+
const LABEL = colorText('cyan', '[ts-alias]');
|
|
19
|
+
|
|
20
|
+
const formatPath = (filePath: string): string => colorText('dim', relative(process.cwd(), filePath));
|
|
21
|
+
const logInfo = (message: string): void => console.log(`${LABEL} ${message}`);
|
|
22
|
+
const logWarn = (message: string): void => console.warn(`${LABEL} ${colorText('yellow', message)}`);
|
|
23
|
+
const logError = (message: string): void => console.error(`${LABEL} ${colorText('red', message)}`);
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* Find all dist folders in the project directory, excluding certain patterns.
|
|
@@ -36,35 +41,6 @@ async function findDistFolders(baseDir: string): Promise<string[]> {
|
|
|
36
41
|
return distFolders;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
/**
|
|
40
|
-
* Get the tsconfig.json file for a given dist folder.
|
|
41
|
-
* This function caches the tsconfig file to avoid reading it multiple times.
|
|
42
|
-
*/
|
|
43
|
-
function getTsconfig(distFolder: string): any {
|
|
44
|
-
const projectRoot = dirname(distFolder);
|
|
45
|
-
const tsconfigPath = join(projectRoot, 'tsconfig.json');
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const stats = statSync(tsconfigPath);
|
|
49
|
-
const mtime = stats.mtimeMs;
|
|
50
|
-
|
|
51
|
-
// Check cache
|
|
52
|
-
const cached = tsconfigCache.get(tsconfigPath);
|
|
53
|
-
if (cached && cached.mtime === mtime) {
|
|
54
|
-
return cached.config;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Read and cache the config
|
|
58
|
-
const config = JSON.parse(readFileSync(tsconfigPath, 'utf8'));
|
|
59
|
-
tsconfigCache.set(tsconfigPath, { config, mtime });
|
|
60
|
-
|
|
61
|
-
return config;
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error(`Error reading tsconfig at ${tsconfigPath}:`, error);
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
44
|
/**
|
|
69
45
|
* Process the dist folder by replacing TypeScript alias paths with relative paths.
|
|
70
46
|
*/
|
|
@@ -73,22 +49,16 @@ async function processDistFolder(distFolder: string): Promise<void> {
|
|
|
73
49
|
const tsconfigPath = join(projectRoot, 'tsconfig.json');
|
|
74
50
|
|
|
75
51
|
if (!existsSync(tsconfigPath)) {
|
|
76
|
-
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const tsconfig = getTsconfig(distFolder);
|
|
81
|
-
|
|
82
|
-
if (!tsconfig) {
|
|
83
|
-
console.warn(`Invalid tsconfig.json found for ${distFolder}`);
|
|
52
|
+
logWarn(`No tsconfig.json at ${formatPath(tsconfigPath)}`);
|
|
84
53
|
return;
|
|
85
54
|
}
|
|
86
55
|
|
|
87
56
|
try {
|
|
88
57
|
await replaceTscAliasPaths({ configFile: tsconfigPath, outDir: distFolder });
|
|
89
|
-
|
|
58
|
+
logInfo(`${colorText('green', 'updated')} ${formatPath(distFolder)}`);
|
|
90
59
|
} catch (error) {
|
|
91
|
-
|
|
60
|
+
logError(`Failed processing ${formatPath(distFolder)}`);
|
|
61
|
+
console.error(error);
|
|
92
62
|
}
|
|
93
63
|
}
|
|
94
64
|
|
|
@@ -96,7 +66,7 @@ async function processDistFolder(distFolder: string): Promise<void> {
|
|
|
96
66
|
* Watch the dist folder for changes and process it when files are added or changed.
|
|
97
67
|
*/
|
|
98
68
|
function watchDistFolder(distFolder: string): FSWatcher {
|
|
99
|
-
|
|
69
|
+
let pending: ReturnType<typeof setTimeout> | null = null;
|
|
100
70
|
|
|
101
71
|
const watcher = watch(distFolder, {
|
|
102
72
|
persistent: true,
|
|
@@ -104,17 +74,23 @@ function watchDistFolder(distFolder: string): FSWatcher {
|
|
|
104
74
|
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 }
|
|
105
75
|
});
|
|
106
76
|
|
|
77
|
+
const scheduleProcess = (): void => {
|
|
78
|
+
if (pending) clearTimeout(pending);
|
|
79
|
+
pending = setTimeout(() => {
|
|
80
|
+
pending = null;
|
|
81
|
+
void processDistFolder(distFolder);
|
|
82
|
+
}, 200);
|
|
83
|
+
};
|
|
84
|
+
|
|
107
85
|
watcher.on('add', (filePath) => {
|
|
108
86
|
if (filePath.endsWith('.js') || filePath.endsWith('.jsx')) {
|
|
109
|
-
|
|
110
|
-
processDistFolder(distFolder);
|
|
87
|
+
scheduleProcess();
|
|
111
88
|
}
|
|
112
89
|
});
|
|
113
90
|
|
|
114
91
|
watcher.on('change', (filePath) => {
|
|
115
92
|
if (filePath.endsWith('.js') || filePath.endsWith('.jsx')) {
|
|
116
|
-
|
|
117
|
-
processDistFolder(distFolder);
|
|
93
|
+
scheduleProcess();
|
|
118
94
|
}
|
|
119
95
|
});
|
|
120
96
|
|
|
@@ -124,31 +100,32 @@ function watchDistFolder(distFolder: string): FSWatcher {
|
|
|
124
100
|
// Main function
|
|
125
101
|
async function main(): Promise<void> {
|
|
126
102
|
const baseDir = process.cwd();
|
|
127
|
-
|
|
103
|
+
logInfo(`searching ${colorText('cyan', 'dist')} folders in ${formatPath(baseDir)}`);
|
|
128
104
|
|
|
129
105
|
const distFolders = await findDistFolders(baseDir);
|
|
130
106
|
|
|
131
107
|
// Process all folders initially if any exist
|
|
132
108
|
if (distFolders.length > 0) {
|
|
133
|
-
|
|
134
|
-
distFolders.forEach((folder) => console.log(` - ${folder}`));
|
|
109
|
+
logInfo(`found ${distFolders.length} dist folder(s)`);
|
|
135
110
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
111
|
+
await Promise.all(
|
|
112
|
+
distFolders.map(async (folder) => {
|
|
113
|
+
return await processDistFolder(folder);
|
|
114
|
+
})
|
|
115
|
+
);
|
|
139
116
|
} else {
|
|
140
|
-
|
|
117
|
+
logInfo('no dist folders found');
|
|
141
118
|
}
|
|
142
119
|
|
|
143
120
|
// Set up watchers if in watch mode
|
|
144
121
|
if (watchMode) {
|
|
145
|
-
|
|
122
|
+
logInfo('watch mode enabled');
|
|
146
123
|
|
|
147
124
|
// Set up watchers for existing dist folders
|
|
148
125
|
distFolders.forEach(watchDistFolder);
|
|
149
126
|
|
|
150
127
|
// Watch for new dist folders being created
|
|
151
|
-
|
|
128
|
+
logInfo('watching for new dist folders...');
|
|
152
129
|
const dirWatcher = watch(baseDir, {
|
|
153
130
|
persistent: true,
|
|
154
131
|
ignoreInitial: true,
|
|
@@ -171,7 +148,7 @@ async function main(): Promise<void> {
|
|
|
171
148
|
if (basename(dirPath) === 'dist') {
|
|
172
149
|
// Make sure it's not already being watched
|
|
173
150
|
if (!distFolders.includes(dirPath)) {
|
|
174
|
-
|
|
151
|
+
logInfo(`${colorText('cyan', 'new dist')} ${formatPath(dirPath)}`);
|
|
175
152
|
distFolders.push(dirPath);
|
|
176
153
|
await processDistFolder(dirPath);
|
|
177
154
|
watchDistFolder(dirPath);
|
|
@@ -182,6 +159,7 @@ async function main(): Promise<void> {
|
|
|
182
159
|
}
|
|
183
160
|
|
|
184
161
|
main().catch((error) => {
|
|
185
|
-
|
|
162
|
+
logError('Unhandled error in ts-alias script');
|
|
163
|
+
console.error(error);
|
|
186
164
|
process.exit(1);
|
|
187
165
|
});
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import console from 'console';
|
|
4
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { dirname, join, resolve, relative } from 'path';
|
|
6
|
+
import { parseArgs } from 'util';
|
|
7
|
+
|
|
8
|
+
import type { FSWatcher } from 'chokidar';
|
|
9
|
+
import { watch } from 'chokidar';
|
|
10
|
+
import { getTsconfig } from 'get-tsconfig';
|
|
11
|
+
|
|
12
|
+
import { colorText } from '../utils/cli.js';
|
|
13
|
+
|
|
14
|
+
type TsConfigJson = Record<string, any>;
|
|
15
|
+
|
|
16
|
+
const DEFAULT_TEST_EXCLUDES = [
|
|
17
|
+
'**/*.test.ts',
|
|
18
|
+
'**/*.test.tsx',
|
|
19
|
+
'**/*.spec.ts',
|
|
20
|
+
'**/*.spec.tsx',
|
|
21
|
+
'**/__tests__/**',
|
|
22
|
+
'**/tests/**'
|
|
23
|
+
];
|
|
24
|
+
const GENERATED_FILE_HEADER = '// generated by ts-build-config';
|
|
25
|
+
|
|
26
|
+
const LABEL = colorText('cyan', '[ts-build-config]');
|
|
27
|
+
const formatPath = (filePath: string): string => colorText('dim', relative(process.cwd(), filePath));
|
|
28
|
+
const logInfo = (message: string): void => console.log(`${LABEL} ${message}`);
|
|
29
|
+
const logWarn = (message: string): void => console.warn(`${LABEL} ${colorText('yellow', message)}`);
|
|
30
|
+
const logError = (message: string): void => console.error(`${LABEL} ${colorText('red', message)}`);
|
|
31
|
+
|
|
32
|
+
function asArray<T>(v: unknown): T[] {
|
|
33
|
+
if (!v) return [];
|
|
34
|
+
return Array.isArray(v) ? (v as T[]) : [v as T];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isObject(v: unknown): v is Record<string, any> {
|
|
38
|
+
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function uniq(items: string[]): string[] {
|
|
42
|
+
return Array.from(new Set(items));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeRefPath(baseTsconfigPath: string, refPath: string): string {
|
|
46
|
+
// TS project references are relative to the tsconfig location
|
|
47
|
+
const baseDir = dirname(baseTsconfigPath);
|
|
48
|
+
// refPath may point to a folder (containing tsconfig.json) or a file
|
|
49
|
+
const abs = resolve(baseDir, refPath);
|
|
50
|
+
return abs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveReferencedTsconfigPath(refAbsPath: string): string | null {
|
|
54
|
+
// If they referenced a file directly, use it
|
|
55
|
+
if (refAbsPath.endsWith('.json')) {
|
|
56
|
+
return existsSync(refAbsPath) ? refAbsPath : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Otherwise assume it's a directory containing tsconfig.json
|
|
60
|
+
const candidate = join(refAbsPath, 'tsconfig.json');
|
|
61
|
+
if (existsSync(candidate)) return candidate;
|
|
62
|
+
|
|
63
|
+
// Some repos use tsconfig.base.json or similar; we won't guess.
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readTsconfig(tsconfigPath: string): TsConfigJson {
|
|
68
|
+
const res = getTsconfig(tsconfigPath);
|
|
69
|
+
if (!res) {
|
|
70
|
+
throw new Error(`get-tsconfig could not load: ${tsconfigPath}`);
|
|
71
|
+
}
|
|
72
|
+
// get-tsconfig returns a parsed config object (includes extends resolved).
|
|
73
|
+
// We want to write a build tsconfig that extends the original file path,
|
|
74
|
+
// so we mostly only need top-level fields we change (exclude/compilerOptions).
|
|
75
|
+
return res.config as TsConfigJson;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function computeExtraExcludes(config: TsConfigJson): string[] {
|
|
79
|
+
const extra: string[] = [...DEFAULT_TEST_EXCLUDES];
|
|
80
|
+
|
|
81
|
+
// If the tsconfig includes ./tests (or tests) explicitly, ensure it is excluded in build config.
|
|
82
|
+
const includes = asArray<string>(config.include);
|
|
83
|
+
for (const inc of includes) {
|
|
84
|
+
const norm = inc.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
85
|
+
if (norm === './tests' || norm === 'tests' || norm.startsWith('./tests/') || norm.startsWith('tests/')) {
|
|
86
|
+
extra.push('./tests/**');
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return uniq(extra);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function writeBuildTsconfig(tsconfigPath: string, config: TsConfigJson, dryRun: boolean): string {
|
|
95
|
+
const dir = dirname(tsconfigPath);
|
|
96
|
+
const outPath = join(dir, 'tsconfig.build.json');
|
|
97
|
+
|
|
98
|
+
const existingExclude = asArray<string>(config.exclude);
|
|
99
|
+
const mergedExclude = uniq([...existingExclude, ...computeExtraExcludes(config)]);
|
|
100
|
+
|
|
101
|
+
const buildConfig: TsConfigJson = { extends: './tsconfig.json', compilerOptions: {}, exclude: mergedExclude };
|
|
102
|
+
|
|
103
|
+
// If original compilerOptions exists, keep build-specific overrides minimal.
|
|
104
|
+
// But ensure emit is enabled if base tsconfig has noEmit: true (common for editor configs).
|
|
105
|
+
const baseCO = isObject(config.compilerOptions) ? config.compilerOptions : {};
|
|
106
|
+
if (baseCO.noEmit === true) {
|
|
107
|
+
buildConfig.compilerOptions.noEmit = false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Keep outDir/rootDir if already set in base; do not guess.
|
|
111
|
+
// If you want to force them, do it in the base tsconfig.json or pass flags later.
|
|
112
|
+
|
|
113
|
+
if (!dryRun) {
|
|
114
|
+
mkdirSync(dir, { recursive: true });
|
|
115
|
+
writeFileSync(outPath, `${GENERATED_FILE_HEADER}\n${JSON.stringify(buildConfig, null, 2)}\n`, 'utf8');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return outPath;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isGeneratedByThisScript(filePath: string): boolean {
|
|
122
|
+
try {
|
|
123
|
+
const contents = readFileSync(filePath, 'utf8');
|
|
124
|
+
const firstLine = contents.split(/\r?\n/, 1)[0]?.trim() ?? '';
|
|
125
|
+
return firstLine === GENERATED_FILE_HEADER;
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getProjectRefs(tsconfigPath: string, config: TsConfigJson): string[] {
|
|
132
|
+
const refs = asArray<any>(config.references);
|
|
133
|
+
const result: string[] = [];
|
|
134
|
+
|
|
135
|
+
for (const r of refs) {
|
|
136
|
+
if (!r) continue;
|
|
137
|
+
const refPath = typeof r === 'string' ? r : r.path;
|
|
138
|
+
if (!refPath || typeof refPath !== 'string') continue;
|
|
139
|
+
|
|
140
|
+
const abs = normalizeRefPath(tsconfigPath, refPath);
|
|
141
|
+
const resolved = resolveReferencedTsconfigPath(abs);
|
|
142
|
+
if (resolved) result.push(resolved);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return uniq(result);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
type GenerateOptions = { dryRun: boolean; force: boolean; verbose: boolean };
|
|
149
|
+
|
|
150
|
+
function generateBuildConfigs(
|
|
151
|
+
entry: string,
|
|
152
|
+
options: GenerateOptions
|
|
153
|
+
): { created: { src: string; out: string }[]; visitedConfigs: string[] } {
|
|
154
|
+
const { dryRun, force, verbose } = options;
|
|
155
|
+
|
|
156
|
+
const visited = new Set<string>();
|
|
157
|
+
const queue: string[] = [entry];
|
|
158
|
+
const created: { src: string; out: string }[] = [];
|
|
159
|
+
|
|
160
|
+
while (queue.length) {
|
|
161
|
+
const tsconfigPath = queue.shift()!;
|
|
162
|
+
if (visited.has(tsconfigPath)) continue;
|
|
163
|
+
visited.add(tsconfigPath);
|
|
164
|
+
|
|
165
|
+
let cfg: TsConfigJson;
|
|
166
|
+
try {
|
|
167
|
+
cfg = readTsconfig(tsconfigPath);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
logWarn(`Skipping unreadable config: ${formatPath(tsconfigPath)}`);
|
|
170
|
+
if (verbose) console.warn(e);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const outPath = join(dirname(tsconfigPath), 'tsconfig.build.json');
|
|
175
|
+
const outExists = existsSync(outPath);
|
|
176
|
+
const generatedByScript = outExists && isGeneratedByThisScript(outPath);
|
|
177
|
+
if (outExists && !generatedByScript && !force) {
|
|
178
|
+
if (verbose) {
|
|
179
|
+
logInfo(`Skip ${formatPath(outPath)} (manual file, use ${colorText('bold', '--force')})`);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
const written = writeBuildTsconfig(tsconfigPath, cfg, dryRun);
|
|
183
|
+
created.push({ src: tsconfigPath, out: written });
|
|
184
|
+
if (verbose || dryRun) {
|
|
185
|
+
const verb = dryRun ? colorText('yellow', '[dry-run] write') : colorText('green', 'write');
|
|
186
|
+
logInfo(`${verb} ${formatPath(written)} <- ${formatPath(tsconfigPath)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const refs = getProjectRefs(tsconfigPath, cfg);
|
|
191
|
+
for (const r of refs) queue.push(r);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!verbose && !dryRun) {
|
|
195
|
+
logInfo(`${colorText('green', 'updated')} ${created.length} tsconfig.build.json file(s)`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { created, visitedConfigs: Array.from(visited) };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function createRegenerator(
|
|
202
|
+
entry: string,
|
|
203
|
+
options: GenerateOptions
|
|
204
|
+
): { run: (reason?: string) => Promise<void>; syncWatchedConfigs: (watcher: FSWatcher) => void } {
|
|
205
|
+
let isRunning = false;
|
|
206
|
+
let rerunRequested = false;
|
|
207
|
+
let watchedConfigs = new Set<string>();
|
|
208
|
+
|
|
209
|
+
const syncWatchedConfigs = (watcher: FSWatcher): void => {
|
|
210
|
+
if (watchedConfigs.size === 0) return;
|
|
211
|
+
watcher.add(Array.from(watchedConfigs));
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const run = async (reason?: string): Promise<void> => {
|
|
215
|
+
if (isRunning) {
|
|
216
|
+
rerunRequested = true;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
isRunning = true;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
do {
|
|
223
|
+
rerunRequested = false;
|
|
224
|
+
if (reason) {
|
|
225
|
+
logInfo(`${colorText('cyan', 'regenerate')} (${reason})`);
|
|
226
|
+
reason = undefined;
|
|
227
|
+
}
|
|
228
|
+
const result = generateBuildConfigs(entry, options);
|
|
229
|
+
watchedConfigs = new Set(result.visitedConfigs);
|
|
230
|
+
} while (rerunRequested);
|
|
231
|
+
} finally {
|
|
232
|
+
isRunning = false;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return { run, syncWatchedConfigs };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function main(): Promise<void> {
|
|
240
|
+
const { values } = parseArgs({
|
|
241
|
+
args: process.argv.slice(2),
|
|
242
|
+
options: {
|
|
243
|
+
config: { type: 'string', short: 'c' },
|
|
244
|
+
dryRun: { type: 'boolean' },
|
|
245
|
+
force: { type: 'boolean' }, // overwrite files not generated by this script
|
|
246
|
+
watch: { type: 'boolean', short: 'w' },
|
|
247
|
+
verbose: { type: 'boolean', short: 'v' }
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const entry = values.config ? resolve(values.config) : null;
|
|
252
|
+
if (!entry) {
|
|
253
|
+
logError('Missing required flag: --config <path/to/tsconfig.json>');
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
if (!existsSync(entry)) {
|
|
257
|
+
logError(`Config file not found: ${formatPath(entry)}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const dryRun = values.dryRun === true;
|
|
262
|
+
const force = values.force === true;
|
|
263
|
+
const watchMode = values.watch === true;
|
|
264
|
+
const verbose = values.verbose === true;
|
|
265
|
+
const options: GenerateOptions = { dryRun, force, verbose };
|
|
266
|
+
|
|
267
|
+
const regenerator = createRegenerator(entry, options);
|
|
268
|
+
await regenerator.run('initial run');
|
|
269
|
+
|
|
270
|
+
if (!watchMode) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
logInfo(`watching ${colorText('cyan', 'tsconfig')} changes...`);
|
|
275
|
+
const watcher = watch([], {
|
|
276
|
+
persistent: true,
|
|
277
|
+
ignoreInitial: true,
|
|
278
|
+
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 }
|
|
279
|
+
});
|
|
280
|
+
regenerator.syncWatchedConfigs(watcher);
|
|
281
|
+
|
|
282
|
+
watcher.on('change', async (filePath) => {
|
|
283
|
+
logInfo(`${colorText('cyan', 'change')} ${formatPath(filePath)}`);
|
|
284
|
+
await regenerator.run(`changed ${relative(process.cwd(), filePath)}`);
|
|
285
|
+
regenerator.syncWatchedConfigs(watcher);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
watcher.on('add', async (filePath) => {
|
|
289
|
+
logInfo(`${colorText('cyan', 'add')} ${formatPath(filePath)}`);
|
|
290
|
+
await regenerator.run(`added ${relative(process.cwd(), filePath)}`);
|
|
291
|
+
regenerator.syncWatchedConfigs(watcher);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
watcher.on('unlink', async (filePath) => {
|
|
295
|
+
logInfo(`${colorText('yellow', 'remove')} ${formatPath(filePath)}`);
|
|
296
|
+
await regenerator.run(`removed ${relative(process.cwd(), filePath)}`);
|
|
297
|
+
regenerator.syncWatchedConfigs(watcher);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
main().catch((err) => {
|
|
302
|
+
logError('Unhandled error');
|
|
303
|
+
console.error(err);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
});
|
package/src/utils/cli.ts
ADDED
package/src/utils/index.ts
CHANGED