Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 43b07be

Browse files
committed
Resolves #452
1 parent 6d96015 commit 43b07be

3 files changed

Lines changed: 136 additions & 14 deletions

File tree

.changeset/little-snakes-double.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ultracite": patch
3+
---
4+
5+
Fix tsconfig patching

packages/cli/__tests__/tsconfig.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe("tsconfig", () => {
3737
});
3838

3939
describe("update", () => {
40-
test("adds strictNullChecks to tsconfig", async () => {
40+
test("skips modification when strict: true is already set", async () => {
4141
const mockWriteFile = mock(() => Promise.resolve());
4242
mock.module("glob", () => ({
4343
glob: mock(() => Promise.resolve(["tsconfig.json"])),
@@ -52,11 +52,75 @@ describe("tsconfig", () => {
5252

5353
await tsconfig.update();
5454

55+
// Should not write because strict: true already enables strictNullChecks
56+
expect(mockWriteFile).not.toHaveBeenCalled();
57+
});
58+
59+
test("skips modification when strictNullChecks: true is already set", async () => {
60+
const mockWriteFile = mock(() => Promise.resolve());
61+
mock.module("glob", () => ({
62+
glob: mock(() => Promise.resolve(["tsconfig.json"])),
63+
}));
64+
mock.module("node:fs/promises", () => ({
65+
access: mock(() => Promise.resolve()),
66+
readFile: mock(() =>
67+
Promise.resolve('{"compilerOptions": {"strictNullChecks": true}}')
68+
),
69+
writeFile: mockWriteFile,
70+
}));
71+
72+
await tsconfig.update();
73+
74+
// Should not write because strictNullChecks is already true
75+
expect(mockWriteFile).not.toHaveBeenCalled();
76+
});
77+
78+
test("adds strictNullChecks when not present", async () => {
79+
const mockWriteFile = mock(() => Promise.resolve());
80+
mock.module("glob", () => ({
81+
glob: mock(() => Promise.resolve(["tsconfig.json"])),
82+
}));
83+
mock.module("node:fs/promises", () => ({
84+
access: mock(() => Promise.resolve()),
85+
readFile: mock(() =>
86+
Promise.resolve('{"compilerOptions": {"target": "ES2020"}}')
87+
),
88+
writeFile: mockWriteFile,
89+
}));
90+
91+
await tsconfig.update();
92+
5593
expect(mockWriteFile).toHaveBeenCalled();
5694
const writeCall = mockWriteFile.mock.calls[0];
5795
const writtenContent = JSON.parse(writeCall[1] as string);
5896
expect(writtenContent.compilerOptions.strictNullChecks).toBe(true);
59-
expect(writtenContent.compilerOptions.strict).toBe(true);
97+
expect(writtenContent.compilerOptions.target).toBe("ES2020");
98+
});
99+
100+
test("preserves comments when modifying tsconfig", async () => {
101+
const mockWriteFile = mock(() => Promise.resolve());
102+
const tsconfigWithComments = `{
103+
// This is a comment
104+
"compilerOptions": {
105+
"target": "ES2020"
106+
}
107+
}`;
108+
mock.module("glob", () => ({
109+
glob: mock(() => Promise.resolve(["tsconfig.json"])),
110+
}));
111+
mock.module("node:fs/promises", () => ({
112+
access: mock(() => Promise.resolve()),
113+
readFile: mock(() => Promise.resolve(tsconfigWithComments)),
114+
writeFile: mockWriteFile,
115+
}));
116+
117+
await tsconfig.update();
118+
119+
expect(mockWriteFile).toHaveBeenCalled();
120+
const writeCall = mockWriteFile.mock.calls[0];
121+
const writtenContent = writeCall[1] as string;
122+
// Comments should be preserved
123+
expect(writtenContent).toContain("// This is a comment");
60124
});
61125

62126
test("updates multiple tsconfig files", async () => {

packages/cli/src/tsconfig.ts

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
import { readFile, writeFile } from "node:fs/promises";
2-
import deepmerge from "deepmerge";
32
import { glob } from "glob";
4-
import { parse } from "jsonc-parser";
5-
6-
const defaultConfig = {
7-
compilerOptions: {
8-
strictNullChecks: true,
9-
},
10-
};
3+
import { type ModificationOptions, applyEdits, modify, parse } from "jsonc-parser";
114

125
/**
136
* Find all tsconfig.json files in the project
@@ -29,8 +22,40 @@ const findTsConfigFiles = async (): Promise<string[]> => {
2922
}
3023
};
3124

25+
/**
26+
* Check if strictNullChecks is already enabled (directly or via strict: true)
27+
*/
28+
const hasStrictNullChecks = (
29+
config: Record<string, unknown> | undefined
30+
): boolean => {
31+
if (!config) {
32+
return false;
33+
}
34+
35+
const compilerOptions = config.compilerOptions as
36+
| Record<string, unknown>
37+
| undefined;
38+
39+
if (!compilerOptions) {
40+
return false;
41+
}
42+
43+
// strict: true enables strictNullChecks
44+
if (compilerOptions.strict === true) {
45+
return true;
46+
}
47+
48+
// strictNullChecks is explicitly set
49+
if (compilerOptions.strictNullChecks === true) {
50+
return true;
51+
}
52+
53+
return false;
54+
};
55+
3256
/**
3357
* Update a single tsconfig.json file with strictNullChecks
58+
* Preserves comments and only modifies if necessary
3459
*/
3560
const updateTsConfigFile = async (filePath: string): Promise<void> => {
3661
try {
@@ -39,11 +64,39 @@ const updateTsConfigFile = async (filePath: string): Promise<void> => {
3964
| Record<string, unknown>
4065
| undefined;
4166

42-
// If parsing fails (invalid JSON), treat as empty config and proceed gracefully
43-
const configToMerge = existingConfig || {};
44-
const newConfig = deepmerge(configToMerge, defaultConfig);
67+
// Skip if strictNullChecks is already enabled (directly or via strict: true)
68+
if (hasStrictNullChecks(existingConfig)) {
69+
return;
70+
}
71+
72+
// If the file contains invalid JSON, write a fresh config
73+
if (existingConfig === undefined) {
74+
const freshConfig = {
75+
compilerOptions: {
76+
strictNullChecks: true,
77+
},
78+
};
79+
await writeFile(filePath, JSON.stringify(freshConfig, null, 2));
80+
return;
81+
}
82+
83+
// Use jsonc-parser's modify to preserve comments
84+
const modifyOptions: ModificationOptions = {
85+
formattingOptions: {
86+
tabSize: 2,
87+
insertSpaces: true,
88+
},
89+
};
90+
91+
const edits = modify(
92+
existingContents,
93+
["compilerOptions", "strictNullChecks"],
94+
true,
95+
modifyOptions
96+
);
4597

46-
await writeFile(filePath, JSON.stringify(newConfig, null, 2));
98+
const newContents = applyEdits(existingContents, edits);
99+
await writeFile(filePath, newContents);
47100
} catch (error) {
48101
// Log error but don't fail the entire operation
49102
console.warn(`Failed to update ${filePath}:`, error);

0 commit comments

Comments
 (0)