From 8dff00eb47f18809fbb55861d11e640932aa85fa Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 26 Mar 2026 22:00:43 +0800 Subject: [PATCH] fix(migrate): auto-remove allowSyntheticDefaultImports: false from tsconfig.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same deprecation reason as esModuleInterop: false — tsgolint no longer supports these options set to false. Generalize the removal into a reusable `removeDeprecatedTsconfigFalseOption` function and apply it to both options during migration. See https://github.com/oxc-project/tsgolint/issues/351 --- .../snap.txt | 4 +- .../tsconfig.json | 2 +- packages/cli/src/migration/migrator.ts | 25 +- .../cli/src/utils/__tests__/tsconfig.spec.ts | 292 ++++++++++-------- packages/cli/src/utils/tsconfig.ts | 8 +- 5 files changed, 187 insertions(+), 144 deletions(-) diff --git a/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt b/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt index f1a747174e..138f71d24c 100644 --- a/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt +++ b/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt @@ -3,16 +3,16 @@ VITE+ - The Unified Toolchain for the Web ◇ Migrated . to Vite+ • Node pnpm -• 3 config updates applied +• 4 config updates applied ! Warnings: - Removed `"esModuleInterop": false` from tsconfig.json — this option has been deprecated. See https://github.com/oxc-project/tsgolint/issues/351, https://github.com/microsoft/TypeScript/issues/62529 + - Removed `"allowSyntheticDefaultImports": false` from tsconfig.json — this option has been deprecated. See https://github.com/oxc-project/tsgolint/issues/351, https://github.com/microsoft/TypeScript/issues/62529 > cat tsconfig.json # verify esModuleInterop: false is removed { "compilerOptions": { "target": "ES2023", "module": "ESNext", - "allowSyntheticDefaultImports": true, "strict": true } } diff --git a/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/tsconfig.json b/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/tsconfig.json index ad5b7a1f68..5db7cb644c 100644 --- a/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/tsconfig.json +++ b/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES2023", "module": "ESNext", "esModuleInterop": false, - "allowSyntheticDefaultImports": true, + "allowSyntheticDefaultImports": false, "strict": true } } diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts index 2d3d58b317..b76df96493 100644 --- a/packages/cli/src/migration/migrator.ts +++ b/packages/cli/src/migration/migrator.ts @@ -31,7 +31,7 @@ import { getSpinner } from '../utils/prompts.js'; import { findTsconfigFiles, hasBaseUrlInTsconfig, - removeEsModuleInteropFalseFromFile, + removeDeprecatedTsconfigFalseOption, } from '../utils/tsconfig.js'; import { editYamlFile, scalarString, type YamlDocument } from '../utils/yaml.js'; import { @@ -652,19 +652,22 @@ function cleanupDeprecatedTsconfigOptions( silent = false, report?: MigrationReport, ): void { + const deprecatedOptions = ['esModuleInterop', 'allowSyntheticDefaultImports']; const files = findTsconfigFiles(projectPath); for (const filePath of files) { - if (removeEsModuleInteropFalseFromFile(filePath)) { - if (report) { - report.removedConfigCount++; - } - if (!silent) { - prompts.log.success(`✔ Removed esModuleInterop: false from ${displayRelative(filePath)}`); + for (const name of deprecatedOptions) { + if (removeDeprecatedTsconfigFalseOption(filePath, name)) { + if (report) { + report.removedConfigCount++; + } + if (!silent) { + prompts.log.success(`✔ Removed ${name}: false from ${displayRelative(filePath)}`); + } + warnMigration( + `Removed \`"${name}": false\` from ${displayRelative(filePath)} — this option has been deprecated. See https://github.com/oxc-project/tsgolint/issues/351, https://github.com/microsoft/TypeScript/issues/62529`, + report, + ); } - warnMigration( - `Removed \`"esModuleInterop": false\` from ${displayRelative(filePath)} — this option has been deprecated. See https://github.com/oxc-project/tsgolint/issues/351, https://github.com/microsoft/TypeScript/issues/62529`, - report, - ); } } } diff --git a/packages/cli/src/utils/__tests__/tsconfig.spec.ts b/packages/cli/src/utils/__tests__/tsconfig.spec.ts index 743dcb276c..9870031fde 100644 --- a/packages/cli/src/utils/__tests__/tsconfig.spec.ts +++ b/packages/cli/src/utils/__tests__/tsconfig.spec.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { findTsconfigFiles, removeEsModuleInteropFalseFromFile } from '../tsconfig.js'; +import { findTsconfigFiles, removeDeprecatedTsconfigFalseOption } from '../tsconfig.js'; describe('findTsconfigFiles', () => { let tmpDir: string; @@ -46,160 +46,200 @@ describe('findTsconfigFiles', () => { }); }); -describe('removeEsModuleInteropFalseFromFile', () => { - let tmpDir: string; - - beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tsconfig-test-')); - }); - - afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); - }); - - function writeAndRemove(filePath: string, content: string): string { - fs.writeFileSync(filePath, content); - const result = removeEsModuleInteropFalseFromFile(filePath); - expect(result).toBe(true); - return fs.readFileSync(filePath, 'utf-8'); - } - - it('removes esModuleInterop: false (middle property)', () => { - const filePath = path.join(tmpDir, 'tsconfig.json'); - expect( - writeAndRemove( - filePath, - `{ +describe.each(['esModuleInterop', 'allowSyntheticDefaultImports'])( + 'removeDeprecatedTsconfigFalseOption — %s', + (option) => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tsconfig-test-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + function writeAndRemove(filePath: string, content: string): string { + fs.writeFileSync(filePath, content); + const result = removeDeprecatedTsconfigFalseOption(filePath, option); + expect(result).toBe(true); + return fs.readFileSync(filePath, 'utf-8'); + } + + it('removes option: false (middle property)', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + expect( + writeAndRemove( + filePath, + `{ "compilerOptions": { "target": "ES2023", - "esModuleInterop": false, + "${option}": false, "strict": true } }`, - ), - ).toMatchInlineSnapshot(` - "{ - "compilerOptions": { - "target": "ES2023", - "strict": true - } - }" - `); - }); - - it('preserves comments in JSONC', () => { - const filePath = path.join(tmpDir, 'tsconfig.json'); - expect( - writeAndRemove( - filePath, - `{ + ), + ).toMatchInlineSnapshot(` + "{ + "compilerOptions": { + "target": "ES2023", + "strict": true + } + }" + `); + }); + + it('preserves comments in JSONC', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + expect( + writeAndRemove( + filePath, + `{ // This is a comment "compilerOptions": { "target": "ES2023", - "esModuleInterop": false, + "${option}": false, /* block comment */ "strict": true } }`, - ), - ).toMatchInlineSnapshot(` - "{ - // This is a comment - "compilerOptions": { - "target": "ES2023", - /* block comment */ - "strict": true - } - }" - `); - }); - - it('handles esModuleInterop: false as last property', () => { - const filePath = path.join(tmpDir, 'tsconfig.json'); - expect( - writeAndRemove( - filePath, - `{ + ), + ).toMatchInlineSnapshot(` + "{ + // This is a comment + "compilerOptions": { + "target": "ES2023", + /* block comment */ + "strict": true + } + }" + `); + }); + + it('handles option: false as last property', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + expect( + writeAndRemove( + filePath, + `{ "compilerOptions": { "target": "ES2023", - "esModuleInterop": false + "${option}": false } }`, - ), - ).toMatchInlineSnapshot(` - "{ - "compilerOptions": { - "target": "ES2023" - } - }" - `); + ), + ).toMatchInlineSnapshot(` + "{ + "compilerOptions": { + "target": "ES2023" + } + }" + `); + }); + + it('handles inline block comment next to option: false', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + expect( + writeAndRemove( + filePath, + `{ + "compilerOptions": { + "target": "ES2023", + "${option}": false /* reason */, + "strict": true + } +}`, + ), + ).toMatchInlineSnapshot(` + "{ + "compilerOptions": { + "target": "ES2023" /* reason */, + "strict": true + } + }" + `); + }); + + it('handles compact single-line JSON', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + expect( + writeAndRemove(filePath, `{"compilerOptions":{"${option}": false, "strict": true}}`), + ).toMatchInlineSnapshot(`"{"compilerOptions":{"strict": true}}"`); + }); + + it('handles compact single-line JSONC with spaces', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + expect( + writeAndRemove(filePath, `{ "compilerOptions": { "${option}": false, "strict": true } }`), + ).toMatchInlineSnapshot(`"{ "compilerOptions": {"strict": true } }"`); + }); + + it('leaves option: true untouched', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + const original = JSON.stringify({ compilerOptions: { [option]: true } }, null, 2); + fs.writeFileSync(filePath, original); + + const result = removeDeprecatedTsconfigFalseOption(filePath, option); + expect(result).toBe(false); + expect(fs.readFileSync(filePath, 'utf-8')).toBe(original); + }); + + it('returns false for non-existent file', () => { + expect(removeDeprecatedTsconfigFalseOption('/non-existent-file.json', option)).toBe(false); + }); + + it('returns false when no compilerOptions', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + fs.writeFileSync(filePath, '{}'); + + expect(removeDeprecatedTsconfigFalseOption(filePath, option)).toBe(false); + }); + + it('returns false when option is not present', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + fs.writeFileSync(filePath, JSON.stringify({ compilerOptions: { strict: true } }, null, 2)); + + expect(removeDeprecatedTsconfigFalseOption(filePath, option)).toBe(false); + }); + }, +); + +describe('removeDeprecatedTsconfigFalseOption — combined removal', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tsconfig-test-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); }); - it('handles inline block comment next to esModuleInterop: false', () => { + it('removes both esModuleInterop and allowSyntheticDefaultImports when both are false', () => { const filePath = path.join(tmpDir, 'tsconfig.json'); - expect( - writeAndRemove( - filePath, - `{ + fs.writeFileSync( + filePath, + `{ "compilerOptions": { "target": "ES2023", - "esModuleInterop": false /* reason */, + "esModuleInterop": false, + "allowSyntheticDefaultImports": false, "strict": true } }`, - ), - ).toMatchInlineSnapshot(` + ); + + expect(removeDeprecatedTsconfigFalseOption(filePath, 'esModuleInterop')).toBe(true); + expect(removeDeprecatedTsconfigFalseOption(filePath, 'allowSyntheticDefaultImports')).toBe( + true, + ); + expect(fs.readFileSync(filePath, 'utf-8')).toMatchInlineSnapshot(` "{ "compilerOptions": { - "target": "ES2023" /* reason */, + "target": "ES2023", "strict": true } }" `); }); - - it('handles compact single-line JSON', () => { - const filePath = path.join(tmpDir, 'tsconfig.json'); - expect( - writeAndRemove(filePath, '{"compilerOptions":{"esModuleInterop": false, "strict": true}}'), - ).toMatchInlineSnapshot(`"{"compilerOptions":{"strict": true}}"`); - }); - - it('handles compact single-line JSONC with spaces', () => { - const filePath = path.join(tmpDir, 'tsconfig.json'); - expect( - writeAndRemove( - filePath, - '{ "compilerOptions": { "esModuleInterop": false, "strict": true } }', - ), - ).toMatchInlineSnapshot(`"{ "compilerOptions": {"strict": true } }"`); - }); - - it('leaves esModuleInterop: true untouched', () => { - const filePath = path.join(tmpDir, 'tsconfig.json'); - const original = JSON.stringify({ compilerOptions: { esModuleInterop: true } }, null, 2); - fs.writeFileSync(filePath, original); - - const result = removeEsModuleInteropFalseFromFile(filePath); - expect(result).toBe(false); - expect(fs.readFileSync(filePath, 'utf-8')).toBe(original); - }); - - it('returns false for non-existent file', () => { - expect(removeEsModuleInteropFalseFromFile('/non-existent-file.json')).toBe(false); - }); - - it('returns false when no compilerOptions', () => { - const filePath = path.join(tmpDir, 'tsconfig.json'); - fs.writeFileSync(filePath, '{}'); - - expect(removeEsModuleInteropFalseFromFile(filePath)).toBe(false); - }); - - it('returns false when esModuleInterop is not present', () => { - const filePath = path.join(tmpDir, 'tsconfig.json'); - fs.writeFileSync(filePath, JSON.stringify({ compilerOptions: { strict: true } }, null, 2)); - - expect(removeEsModuleInteropFalseFromFile(filePath)).toBe(false); - }); }); diff --git a/packages/cli/src/utils/tsconfig.ts b/packages/cli/src/utils/tsconfig.ts index 8d26868be9..38be9e1657 100644 --- a/packages/cli/src/utils/tsconfig.ts +++ b/packages/cli/src/utils/tsconfig.ts @@ -36,7 +36,7 @@ export function findTsconfigFiles(projectPath: string): string[] { // runtime for tsc-compiled code (init-config.ts imports this file). // TODO: move back to devDependencies once the bundle refactoring lands // https://github.com/voidzero-dev/vite-plus/issues/744 -export function removeEsModuleInteropFalseFromFile(filePath: string): boolean { +export function removeDeprecatedTsconfigFalseOption(filePath: string, optionName: string): boolean { let text: string; try { text = fs.readFileSync(filePath, 'utf-8'); @@ -45,13 +45,13 @@ export function removeEsModuleInteropFalseFromFile(filePath: string): boolean { } const parsed = parseJsonc(text) as { - compilerOptions?: { esModuleInterop?: boolean }; + compilerOptions?: Record; } | null; - if (parsed?.compilerOptions?.esModuleInterop !== false) { + if (parsed?.compilerOptions?.[optionName] !== false) { return false; } - const edits = modify(text, ['compilerOptions', 'esModuleInterop'], undefined, {}); + const edits = modify(text, ['compilerOptions', optionName], undefined, {}); if (edits.length === 0) { return false; }