ts-ag 1.0.20 → 1.0.22
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-build-config.js +100 -11
- package/package.json +2 -2
- package/src/scripts/ts-build-config.ts +114 -12
|
@@ -5,6 +5,7 @@ import { dirname, join, resolve, relative } from 'path';
|
|
|
5
5
|
import { parseArgs } from 'util';
|
|
6
6
|
import { watch } from 'chokidar';
|
|
7
7
|
import { getTsconfig } from 'get-tsconfig';
|
|
8
|
+
import { format as formatWithOxfmt } from 'oxfmt';
|
|
8
9
|
import { colorText } from '../utils/cli.js';
|
|
9
10
|
const DEFAULT_TEST_EXCLUDES = [
|
|
10
11
|
'**/*.test.ts',
|
|
@@ -73,12 +74,42 @@ function computeExtraExcludes(config) {
|
|
|
73
74
|
}
|
|
74
75
|
return uniq(extra);
|
|
75
76
|
}
|
|
76
|
-
function
|
|
77
|
+
function withTrailingNewline(value) {
|
|
78
|
+
return value.endsWith('\n') ? value : `${value}\n`;
|
|
79
|
+
}
|
|
80
|
+
async function formatBuildTsconfigJson(config) {
|
|
81
|
+
const fallback = JSON.stringify(config, null, 2);
|
|
82
|
+
try {
|
|
83
|
+
const result = await formatWithOxfmt('tsconfig.build.json', fallback, {
|
|
84
|
+
useTabs: false,
|
|
85
|
+
singleQuote: true,
|
|
86
|
+
trailingComma: 'none',
|
|
87
|
+
printWidth: 120,
|
|
88
|
+
objectWrap: 'collapse',
|
|
89
|
+
semi: true,
|
|
90
|
+
proseWrap: 'always',
|
|
91
|
+
sortPackageJson: { sortScripts: true },
|
|
92
|
+
sortImports: {}
|
|
93
|
+
});
|
|
94
|
+
if (!result.errors.length && typeof result.code === 'string' && result.code.length > 0) {
|
|
95
|
+
return withTrailingNewline(result.code);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Fallback to plain JSON below.
|
|
100
|
+
}
|
|
101
|
+
return withTrailingNewline(fallback);
|
|
102
|
+
}
|
|
103
|
+
async function writeBuildTsconfig(tsconfigPath, config, dryRun) {
|
|
77
104
|
const dir = dirname(tsconfigPath);
|
|
78
105
|
const outPath = join(dir, 'tsconfig.build.json');
|
|
79
106
|
const existingExclude = asArray(config.exclude);
|
|
80
107
|
const mergedExclude = uniq([...existingExclude, ...computeExtraExcludes(config)]);
|
|
81
108
|
const buildConfig = { extends: './tsconfig.json', compilerOptions: {}, exclude: mergedExclude };
|
|
109
|
+
const refs = asArray(config.references);
|
|
110
|
+
if (refs.length > 0) {
|
|
111
|
+
buildConfig.references = refs;
|
|
112
|
+
}
|
|
82
113
|
// If original compilerOptions exists, keep build-specific overrides minimal.
|
|
83
114
|
// But ensure emit is enabled if base tsconfig has noEmit: true (common for editor configs).
|
|
84
115
|
const baseCO = isObject(config.compilerOptions) ? config.compilerOptions : {};
|
|
@@ -89,7 +120,8 @@ function writeBuildTsconfig(tsconfigPath, config, dryRun) {
|
|
|
89
120
|
// If you want to force them, do it in the base tsconfig.json or pass flags later.
|
|
90
121
|
if (!dryRun) {
|
|
91
122
|
mkdirSync(dir, { recursive: true });
|
|
92
|
-
|
|
123
|
+
const formattedJson = await formatBuildTsconfigJson(buildConfig);
|
|
124
|
+
writeFileSync(outPath, `${GENERATED_FILE_HEADER}\n${formattedJson}`, 'utf8');
|
|
93
125
|
}
|
|
94
126
|
return outPath;
|
|
95
127
|
}
|
|
@@ -119,11 +151,62 @@ function getProjectRefs(tsconfigPath, config) {
|
|
|
119
151
|
}
|
|
120
152
|
return uniq(result);
|
|
121
153
|
}
|
|
122
|
-
function
|
|
154
|
+
function shouldUseBuildConfig(tsconfigPath, force) {
|
|
155
|
+
const outPath = join(dirname(tsconfigPath), 'tsconfig.build.json');
|
|
156
|
+
if (!existsSync(outPath))
|
|
157
|
+
return true;
|
|
158
|
+
if (force)
|
|
159
|
+
return true;
|
|
160
|
+
return isGeneratedByThisScript(outPath);
|
|
161
|
+
}
|
|
162
|
+
function asPosixPath(filePath) {
|
|
163
|
+
return filePath.replace(/\\/g, '/');
|
|
164
|
+
}
|
|
165
|
+
function ensureDotRelative(filePath) {
|
|
166
|
+
if (filePath.startsWith('.'))
|
|
167
|
+
return filePath;
|
|
168
|
+
return `./${filePath}`;
|
|
169
|
+
}
|
|
170
|
+
function replaceRefsWithBuildConfigs(tsconfigPath, config, buildConfigSet) {
|
|
171
|
+
const refs = asArray(config.references);
|
|
172
|
+
if (refs.length === 0)
|
|
173
|
+
return config;
|
|
174
|
+
const baseDir = dirname(tsconfigPath);
|
|
175
|
+
const mapped = refs.map((refEntry) => {
|
|
176
|
+
if (!refEntry)
|
|
177
|
+
return refEntry;
|
|
178
|
+
const refPath = typeof refEntry === 'string' ? refEntry : refEntry.path;
|
|
179
|
+
if (!refPath || typeof refPath !== 'string')
|
|
180
|
+
return refEntry;
|
|
181
|
+
const normalizedRefPath = ensureDotRelative(asPosixPath(refPath));
|
|
182
|
+
const abs = normalizeRefPath(tsconfigPath, refPath);
|
|
183
|
+
const resolvedRefTsconfig = resolveReferencedTsconfigPath(abs);
|
|
184
|
+
if (!resolvedRefTsconfig) {
|
|
185
|
+
if (typeof refEntry === 'string')
|
|
186
|
+
return normalizedRefPath;
|
|
187
|
+
return { ...refEntry, path: normalizedRefPath };
|
|
188
|
+
}
|
|
189
|
+
if (!buildConfigSet.has(resolvedRefTsconfig)) {
|
|
190
|
+
if (typeof refEntry === 'string')
|
|
191
|
+
return normalizedRefPath;
|
|
192
|
+
return { ...refEntry, path: normalizedRefPath };
|
|
193
|
+
}
|
|
194
|
+
const buildRefPath = join(dirname(resolvedRefTsconfig), 'tsconfig.build.json');
|
|
195
|
+
const relativeBuildRef = ensureDotRelative(asPosixPath(relative(baseDir, buildRefPath)));
|
|
196
|
+
if (typeof refEntry === 'string') {
|
|
197
|
+
return relativeBuildRef;
|
|
198
|
+
}
|
|
199
|
+
return { ...refEntry, path: relativeBuildRef };
|
|
200
|
+
});
|
|
201
|
+
return { ...config, references: mapped };
|
|
202
|
+
}
|
|
203
|
+
async function generateBuildConfigs(entry, options) {
|
|
123
204
|
const { dryRun, force, verbose } = options;
|
|
205
|
+
const loadedConfigs = new Map();
|
|
124
206
|
const visited = new Set();
|
|
125
207
|
const queue = [entry];
|
|
126
208
|
const created = [];
|
|
209
|
+
// First pass: discover the full referenced tsconfig graph and load configs.
|
|
127
210
|
while (queue.length) {
|
|
128
211
|
const tsconfigPath = queue.shift();
|
|
129
212
|
if (visited.has(tsconfigPath))
|
|
@@ -139,25 +222,31 @@ function generateBuildConfigs(entry, options) {
|
|
|
139
222
|
console.warn(e);
|
|
140
223
|
continue;
|
|
141
224
|
}
|
|
225
|
+
loadedConfigs.set(tsconfigPath, cfg);
|
|
226
|
+
const refs = getProjectRefs(tsconfigPath, cfg);
|
|
227
|
+
for (const r of refs)
|
|
228
|
+
queue.push(r);
|
|
229
|
+
}
|
|
230
|
+
const discoveredConfigs = Array.from(visited).filter((path) => loadedConfigs.has(path));
|
|
231
|
+
const buildConfigSet = new Set(discoveredConfigs.filter((path) => shouldUseBuildConfig(path, force)));
|
|
232
|
+
// Second pass: write build configs with rewritten references.
|
|
233
|
+
for (const tsconfigPath of discoveredConfigs) {
|
|
234
|
+
const cfg = loadedConfigs.get(tsconfigPath);
|
|
142
235
|
const outPath = join(dirname(tsconfigPath), 'tsconfig.build.json');
|
|
143
|
-
|
|
144
|
-
const generatedByScript = outExists && isGeneratedByThisScript(outPath);
|
|
145
|
-
if (outExists && !generatedByScript && !force) {
|
|
236
|
+
if (!buildConfigSet.has(tsconfigPath)) {
|
|
146
237
|
if (verbose) {
|
|
147
238
|
logInfo(`Skip ${formatPath(outPath)} (manual file, use ${colorText('bold', '--force')})`);
|
|
148
239
|
}
|
|
149
240
|
}
|
|
150
241
|
else {
|
|
151
|
-
const
|
|
242
|
+
const rewrittenCfg = replaceRefsWithBuildConfigs(tsconfigPath, cfg, buildConfigSet);
|
|
243
|
+
const written = await writeBuildTsconfig(tsconfigPath, rewrittenCfg, dryRun);
|
|
152
244
|
created.push({ src: tsconfigPath, out: written });
|
|
153
245
|
if (verbose || dryRun) {
|
|
154
246
|
const verb = dryRun ? colorText('yellow', '[dry-run] write') : colorText('green', 'write');
|
|
155
247
|
logInfo(`${verb} ${formatPath(written)} <- ${formatPath(tsconfigPath)}`);
|
|
156
248
|
}
|
|
157
249
|
}
|
|
158
|
-
const refs = getProjectRefs(tsconfigPath, cfg);
|
|
159
|
-
for (const r of refs)
|
|
160
|
-
queue.push(r);
|
|
161
250
|
}
|
|
162
251
|
if (!verbose && !dryRun) {
|
|
163
252
|
logInfo(`${colorText('green', 'updated')} ${created.length} tsconfig.build.json file(s)`);
|
|
@@ -186,7 +275,7 @@ function createRegenerator(entry, options) {
|
|
|
186
275
|
logInfo(`${colorText('cyan', 'regenerate')} (${reason})`);
|
|
187
276
|
reason = undefined;
|
|
188
277
|
}
|
|
189
|
-
const result = generateBuildConfigs(entry, options);
|
|
278
|
+
const result = await generateBuildConfigs(entry, options);
|
|
190
279
|
watchedConfigs = new Set(result.visitedConfigs);
|
|
191
280
|
} while (rerunRequested);
|
|
192
281
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-ag",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "Useful TS stuff",
|
|
5
5
|
"bugs": "https://github.com/ageorgeh/ts-ag/issues",
|
|
6
6
|
"author": "Alexander Hornung",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@actions/languageserver": "^0.3.46",
|
|
70
70
|
"@types/hast": "3.0.4",
|
|
71
|
-
"@types/node": "^24.
|
|
71
|
+
"@types/node": "^24.11.0",
|
|
72
72
|
"@types/ungap__structured-clone": "1.2.0",
|
|
73
73
|
"@typescript/native-preview": "7.0.0-dev.20260227.1",
|
|
74
74
|
"concurrently": "^9.2.1",
|
|
@@ -8,9 +8,12 @@ import { parseArgs } from 'util';
|
|
|
8
8
|
import type { FSWatcher } from 'chokidar';
|
|
9
9
|
import { watch } from 'chokidar';
|
|
10
10
|
import { getTsconfig } from 'get-tsconfig';
|
|
11
|
+
import { format as formatWithOxfmt } from 'oxfmt';
|
|
11
12
|
|
|
12
13
|
import { colorText } from '../utils/cli.js';
|
|
13
14
|
|
|
15
|
+
// TODO on startup check cwd for oxfmt config and use that instead of my default
|
|
16
|
+
|
|
14
17
|
type TsConfigJson = Record<string, any>;
|
|
15
18
|
|
|
16
19
|
const DEFAULT_TEST_EXCLUDES = [
|
|
@@ -91,7 +94,36 @@ function computeExtraExcludes(config: TsConfigJson): string[] {
|
|
|
91
94
|
return uniq(extra);
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
function
|
|
97
|
+
function withTrailingNewline(value: string): string {
|
|
98
|
+
return value.endsWith('\n') ? value : `${value}\n`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function formatBuildTsconfigJson(config: TsConfigJson): Promise<string> {
|
|
102
|
+
const fallback = JSON.stringify(config, null, 2);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = await formatWithOxfmt('tsconfig.build.json', fallback, {
|
|
106
|
+
useTabs: false,
|
|
107
|
+
singleQuote: true,
|
|
108
|
+
trailingComma: 'none',
|
|
109
|
+
printWidth: 120,
|
|
110
|
+
objectWrap: 'collapse',
|
|
111
|
+
semi: true,
|
|
112
|
+
proseWrap: 'always',
|
|
113
|
+
sortPackageJson: { sortScripts: true },
|
|
114
|
+
sortImports: {}
|
|
115
|
+
});
|
|
116
|
+
if (!result.errors.length && typeof result.code === 'string' && result.code.length > 0) {
|
|
117
|
+
return withTrailingNewline(result.code);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// Fallback to plain JSON below.
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return withTrailingNewline(fallback);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function writeBuildTsconfig(tsconfigPath: string, config: TsConfigJson, dryRun: boolean): Promise<string> {
|
|
95
127
|
const dir = dirname(tsconfigPath);
|
|
96
128
|
const outPath = join(dir, 'tsconfig.build.json');
|
|
97
129
|
|
|
@@ -99,6 +131,10 @@ function writeBuildTsconfig(tsconfigPath: string, config: TsConfigJson, dryRun:
|
|
|
99
131
|
const mergedExclude = uniq([...existingExclude, ...computeExtraExcludes(config)]);
|
|
100
132
|
|
|
101
133
|
const buildConfig: TsConfigJson = { extends: './tsconfig.json', compilerOptions: {}, exclude: mergedExclude };
|
|
134
|
+
const refs = asArray<unknown>(config.references);
|
|
135
|
+
if (refs.length > 0) {
|
|
136
|
+
buildConfig.references = refs;
|
|
137
|
+
}
|
|
102
138
|
|
|
103
139
|
// If original compilerOptions exists, keep build-specific overrides minimal.
|
|
104
140
|
// But ensure emit is enabled if base tsconfig has noEmit: true (common for editor configs).
|
|
@@ -112,7 +148,8 @@ function writeBuildTsconfig(tsconfigPath: string, config: TsConfigJson, dryRun:
|
|
|
112
148
|
|
|
113
149
|
if (!dryRun) {
|
|
114
150
|
mkdirSync(dir, { recursive: true });
|
|
115
|
-
|
|
151
|
+
const formattedJson = await formatBuildTsconfigJson(buildConfig);
|
|
152
|
+
writeFileSync(outPath, `${GENERATED_FILE_HEADER}\n${formattedJson}`, 'utf8');
|
|
116
153
|
}
|
|
117
154
|
|
|
118
155
|
return outPath;
|
|
@@ -145,18 +182,76 @@ function getProjectRefs(tsconfigPath: string, config: TsConfigJson): string[] {
|
|
|
145
182
|
return uniq(result);
|
|
146
183
|
}
|
|
147
184
|
|
|
185
|
+
function shouldUseBuildConfig(tsconfigPath: string, force: boolean): boolean {
|
|
186
|
+
const outPath = join(dirname(tsconfigPath), 'tsconfig.build.json');
|
|
187
|
+
if (!existsSync(outPath)) return true;
|
|
188
|
+
if (force) return true;
|
|
189
|
+
return isGeneratedByThisScript(outPath);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function asPosixPath(filePath: string): string {
|
|
193
|
+
return filePath.replace(/\\/g, '/');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function ensureDotRelative(filePath: string): string {
|
|
197
|
+
if (filePath.startsWith('.')) return filePath;
|
|
198
|
+
return `./${filePath}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function replaceRefsWithBuildConfigs(
|
|
202
|
+
tsconfigPath: string,
|
|
203
|
+
config: TsConfigJson,
|
|
204
|
+
buildConfigSet: Set<string>
|
|
205
|
+
): TsConfigJson {
|
|
206
|
+
const refs = asArray<any>(config.references);
|
|
207
|
+
if (refs.length === 0) return config;
|
|
208
|
+
|
|
209
|
+
const baseDir = dirname(tsconfigPath);
|
|
210
|
+
const mapped = refs.map((refEntry) => {
|
|
211
|
+
if (!refEntry) return refEntry;
|
|
212
|
+
|
|
213
|
+
const refPath = typeof refEntry === 'string' ? refEntry : refEntry.path;
|
|
214
|
+
if (!refPath || typeof refPath !== 'string') return refEntry;
|
|
215
|
+
|
|
216
|
+
const normalizedRefPath = ensureDotRelative(asPosixPath(refPath));
|
|
217
|
+
const abs = normalizeRefPath(tsconfigPath, refPath);
|
|
218
|
+
const resolvedRefTsconfig = resolveReferencedTsconfigPath(abs);
|
|
219
|
+
if (!resolvedRefTsconfig) {
|
|
220
|
+
if (typeof refEntry === 'string') return normalizedRefPath;
|
|
221
|
+
return { ...refEntry, path: normalizedRefPath };
|
|
222
|
+
}
|
|
223
|
+
if (!buildConfigSet.has(resolvedRefTsconfig)) {
|
|
224
|
+
if (typeof refEntry === 'string') return normalizedRefPath;
|
|
225
|
+
return { ...refEntry, path: normalizedRefPath };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const buildRefPath = join(dirname(resolvedRefTsconfig), 'tsconfig.build.json');
|
|
229
|
+
const relativeBuildRef = ensureDotRelative(asPosixPath(relative(baseDir, buildRefPath)));
|
|
230
|
+
|
|
231
|
+
if (typeof refEntry === 'string') {
|
|
232
|
+
return relativeBuildRef;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { ...refEntry, path: relativeBuildRef };
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return { ...config, references: mapped };
|
|
239
|
+
}
|
|
240
|
+
|
|
148
241
|
type GenerateOptions = { dryRun: boolean; force: boolean; verbose: boolean };
|
|
149
242
|
|
|
150
|
-
function generateBuildConfigs(
|
|
243
|
+
async function generateBuildConfigs(
|
|
151
244
|
entry: string,
|
|
152
245
|
options: GenerateOptions
|
|
153
|
-
): { created: { src: string; out: string }[]; visitedConfigs: string[] } {
|
|
246
|
+
): Promise<{ created: { src: string; out: string }[]; visitedConfigs: string[] }> {
|
|
154
247
|
const { dryRun, force, verbose } = options;
|
|
155
248
|
|
|
249
|
+
const loadedConfigs = new Map<string, TsConfigJson>();
|
|
156
250
|
const visited = new Set<string>();
|
|
157
251
|
const queue: string[] = [entry];
|
|
158
252
|
const created: { src: string; out: string }[] = [];
|
|
159
253
|
|
|
254
|
+
// First pass: discover the full referenced tsconfig graph and load configs.
|
|
160
255
|
while (queue.length) {
|
|
161
256
|
const tsconfigPath = queue.shift()!;
|
|
162
257
|
if (visited.has(tsconfigPath)) continue;
|
|
@@ -170,25 +265,32 @@ function generateBuildConfigs(
|
|
|
170
265
|
if (verbose) console.warn(e);
|
|
171
266
|
continue;
|
|
172
267
|
}
|
|
268
|
+
loadedConfigs.set(tsconfigPath, cfg);
|
|
269
|
+
|
|
270
|
+
const refs = getProjectRefs(tsconfigPath, cfg);
|
|
271
|
+
for (const r of refs) queue.push(r);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const discoveredConfigs = Array.from(visited).filter((path) => loadedConfigs.has(path));
|
|
275
|
+
const buildConfigSet = new Set(discoveredConfigs.filter((path) => shouldUseBuildConfig(path, force)));
|
|
173
276
|
|
|
277
|
+
// Second pass: write build configs with rewritten references.
|
|
278
|
+
for (const tsconfigPath of discoveredConfigs) {
|
|
279
|
+
const cfg = loadedConfigs.get(tsconfigPath)!;
|
|
174
280
|
const outPath = join(dirname(tsconfigPath), 'tsconfig.build.json');
|
|
175
|
-
|
|
176
|
-
const generatedByScript = outExists && isGeneratedByThisScript(outPath);
|
|
177
|
-
if (outExists && !generatedByScript && !force) {
|
|
281
|
+
if (!buildConfigSet.has(tsconfigPath)) {
|
|
178
282
|
if (verbose) {
|
|
179
283
|
logInfo(`Skip ${formatPath(outPath)} (manual file, use ${colorText('bold', '--force')})`);
|
|
180
284
|
}
|
|
181
285
|
} else {
|
|
182
|
-
const
|
|
286
|
+
const rewrittenCfg = replaceRefsWithBuildConfigs(tsconfigPath, cfg, buildConfigSet);
|
|
287
|
+
const written = await writeBuildTsconfig(tsconfigPath, rewrittenCfg, dryRun);
|
|
183
288
|
created.push({ src: tsconfigPath, out: written });
|
|
184
289
|
if (verbose || dryRun) {
|
|
185
290
|
const verb = dryRun ? colorText('yellow', '[dry-run] write') : colorText('green', 'write');
|
|
186
291
|
logInfo(`${verb} ${formatPath(written)} <- ${formatPath(tsconfigPath)}`);
|
|
187
292
|
}
|
|
188
293
|
}
|
|
189
|
-
|
|
190
|
-
const refs = getProjectRefs(tsconfigPath, cfg);
|
|
191
|
-
for (const r of refs) queue.push(r);
|
|
192
294
|
}
|
|
193
295
|
|
|
194
296
|
if (!verbose && !dryRun) {
|
|
@@ -225,7 +327,7 @@ function createRegenerator(
|
|
|
225
327
|
logInfo(`${colorText('cyan', 'regenerate')} (${reason})`);
|
|
226
328
|
reason = undefined;
|
|
227
329
|
}
|
|
228
|
-
const result = generateBuildConfigs(entry, options);
|
|
330
|
+
const result = await generateBuildConfigs(entry, options);
|
|
229
331
|
watchedConfigs = new Set(result.visitedConfigs);
|
|
230
332
|
} while (rerunRequested);
|
|
231
333
|
} finally {
|