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.
@@ -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 writeBuildTsconfig(tsconfigPath, config, dryRun) {
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
- writeFileSync(outPath, `${GENERATED_FILE_HEADER}\n${JSON.stringify(buildConfig, null, 2)}\n`, 'utf8');
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 generateBuildConfigs(entry, options) {
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
- const outExists = existsSync(outPath);
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 written = writeBuildTsconfig(tsconfigPath, cfg, dryRun);
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.20",
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.10.15",
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 writeBuildTsconfig(tsconfigPath: string, config: TsConfigJson, dryRun: boolean): string {
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
- writeFileSync(outPath, `${GENERATED_FILE_HEADER}\n${JSON.stringify(buildConfig, null, 2)}\n`, 'utf8');
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
- const outExists = existsSync(outPath);
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 written = writeBuildTsconfig(tsconfigPath, cfg, dryRun);
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 {