diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts index bf556a8f86..45b094caa7 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -1383,3 +1383,61 @@ describe('framework shim', () => { }); }); }); + +describe('rewriteStandaloneProject — tsconfig types rewriting', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-tsconfig-')); + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ name: 'test', devDependencies: { vite: '^7.0.0' } }), + ); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('rewrites tsdown/client to vite-plus/pack/client in tsconfig.json', () => { + fs.writeFileSync( + path.join(tmpDir, 'tsconfig.json'), + JSON.stringify({ compilerOptions: { types: ['tsdown/client'] } }, null, 2), + ); + + rewriteStandaloneProject(tmpDir, makeWorkspaceInfo(tmpDir, PackageManager.pnpm), true, true); + + const tsconfig = readJson(path.join(tmpDir, 'tsconfig.json')); + expect((tsconfig.compilerOptions as { types: string[] }).types).toContain( + 'vite-plus/pack/client', + ); + expect((tsconfig.compilerOptions as { types: string[] }).types).not.toContain('tsdown/client'); + }); + + it('rewrites vite/client to vite-plus/client in tsconfig.json', () => { + fs.writeFileSync( + path.join(tmpDir, 'tsconfig.json'), + JSON.stringify({ compilerOptions: { types: ['vite/client'] } }, null, 2), + ); + + rewriteStandaloneProject(tmpDir, makeWorkspaceInfo(tmpDir, PackageManager.pnpm), true, true); + + const tsconfig = readJson(path.join(tmpDir, 'tsconfig.json')); + expect((tsconfig.compilerOptions as { types: string[] }).types).toContain('vite-plus/client'); + expect((tsconfig.compilerOptions as { types: string[] }).types).not.toContain('vite/client'); + }); + + it('rewrites types in tsconfig.node.json as well', () => { + fs.writeFileSync( + path.join(tmpDir, 'tsconfig.node.json'), + JSON.stringify({ compilerOptions: { types: ['tsdown/client'] } }, null, 2), + ); + + rewriteStandaloneProject(tmpDir, makeWorkspaceInfo(tmpDir, PackageManager.pnpm), true, true); + + const tsconfig = readJson(path.join(tmpDir, 'tsconfig.node.json')); + expect((tsconfig.compilerOptions as { types: string[] }).types).toContain( + 'vite-plus/pack/client', + ); + }); +}); diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts index a1ff66b241..85998fe058 100644 --- a/packages/cli/src/migration/migrator.ts +++ b/packages/cli/src/migration/migrator.ts @@ -33,6 +33,7 @@ import { findTsconfigFiles, hasBaseUrlInTsconfig, removeDeprecatedTsconfigFalseOption, + rewriteTypesInTsconfig, } from '../utils/tsconfig.ts'; import type { NpmWorkspaces } from '../utils/workspace.ts'; import { editYamlFile, readYamlFile, scalarString, type YamlDocument } from '../utils/yaml.ts'; @@ -700,6 +701,20 @@ function cleanupDeprecatedTsconfigOptions( } } +function rewriteTsconfigTypes(projectPath: string, silent = false, report?: MigrationReport): void { + const files = findTsconfigFiles(projectPath); + for (const filePath of files) { + if (rewriteTypesInTsconfig(filePath)) { + if (report) { + report.removedConfigCount++; + } + if (!silent) { + prompts.log.success(`✔ Rewrote types in ${displayRelative(filePath)}`); + } + } + } +} + // .svelte files are handled by @sveltejs/vite-plugin-svelte (transpilation) // and svelte-check / Svelte Language Server (type checking). // Module resolution for `.svelte` imports is typically set up by the @@ -948,6 +963,7 @@ export function rewriteStandaloneProject( rewriteLintStagedConfigFile(projectPath, report); } cleanupDeprecatedTsconfigOptions(projectPath, silent, report); + rewriteTsconfigTypes(projectPath, silent, report); mergeViteConfigFiles(projectPath, silent, report); injectLintTypeCheckDefaults(projectPath, silent, report); injectFmtDefaults(projectPath, silent, report); @@ -1003,6 +1019,7 @@ export function rewriteMonorepo( rewriteLintStagedConfigFile(workspaceInfo.rootDir, report); } cleanupDeprecatedTsconfigOptions(workspaceInfo.rootDir, silent, report); + rewriteTsconfigTypes(workspaceInfo.rootDir, silent, report); mergeViteConfigFiles(workspaceInfo.rootDir, silent, report); injectLintTypeCheckDefaults(workspaceInfo.rootDir, silent, report); injectFmtDefaults(workspaceInfo.rootDir, silent, report); @@ -1026,6 +1043,7 @@ export function rewriteMonorepoProject( catalogDependencyResolver?: CatalogDependencyResolver, ): void { cleanupDeprecatedTsconfigOptions(projectPath, silent, report); + rewriteTsconfigTypes(projectPath, silent, report); mergeViteConfigFiles(projectPath, silent, report); mergeTsdownConfigFile(projectPath, silent, report); diff --git a/packages/cli/src/utils/__tests__/tsconfig.spec.ts b/packages/cli/src/utils/__tests__/tsconfig.spec.ts index 9870031fde..bd3d6e3471 100644 --- a/packages/cli/src/utils/__tests__/tsconfig.spec.ts +++ b/packages/cli/src/utils/__tests__/tsconfig.spec.ts @@ -4,7 +4,11 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { findTsconfigFiles, removeDeprecatedTsconfigFalseOption } from '../tsconfig.js'; +import { + findTsconfigFiles, + removeDeprecatedTsconfigFalseOption, + rewriteTypesInTsconfig, +} from '../tsconfig.js'; describe('findTsconfigFiles', () => { let tmpDir: string; @@ -204,6 +208,99 @@ describe.each(['esModuleInterop', 'allowSyntheticDefaultImports'])( }, ); +describe('rewriteTypesInTsconfig', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tsconfig-test-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('rewrites tsdown/client to vite-plus/pack/client', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + fs.writeFileSync( + filePath, + `{ + "compilerOptions": { + "types": ["tsdown/client"] + } +}`, + ); + + expect(rewriteTypesInTsconfig(filePath)).toBe(true); + expect(fs.readFileSync(filePath, 'utf-8')).toMatchInlineSnapshot(` + "{ + "compilerOptions": { + "types": ["vite-plus/pack/client"] + } + }" + `); + }); + + it('rewrites vite/client to vite-plus/client', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + fs.writeFileSync( + filePath, + `{ + "compilerOptions": { + "types": ["vite/client"] + } +}`, + ); + + expect(rewriteTypesInTsconfig(filePath)).toBe(true); + expect(fs.readFileSync(filePath, 'utf-8')).toMatchInlineSnapshot(` + "{ + "compilerOptions": { + "types": ["vite-plus/client"] + } + }" + `); + }); + + it('rewrites both in the same array', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + fs.writeFileSync( + filePath, + `{ + "compilerOptions": { + "types": ["tsdown/client", "vite/client"] + } +}`, + ); + + expect(rewriteTypesInTsconfig(filePath)).toBe(true); + expect(fs.readFileSync(filePath, 'utf-8')).toMatchInlineSnapshot(` + "{ + "compilerOptions": { + "types": ["vite-plus/pack/client", "vite-plus/client"] + } + }" + `); + }); + + it('returns false when no target types exist', () => { + const filePath = path.join(tmpDir, 'tsconfig.json'); + fs.writeFileSync( + filePath, + `{ + "compilerOptions": { + "types": ["some/other/type"] + } +}`, + ); + + expect(rewriteTypesInTsconfig(filePath)).toBe(false); + }); + + it('returns false for non-existent file', () => { + expect(rewriteTypesInTsconfig('/non-existent-file.json')).toBe(false); + }); +}); + describe('removeDeprecatedTsconfigFalseOption — combined removal', () => { let tmpDir: string; diff --git a/packages/cli/src/utils/tsconfig.ts b/packages/cli/src/utils/tsconfig.ts index 38be9e1657..3554547143 100644 --- a/packages/cli/src/utils/tsconfig.ts +++ b/packages/cli/src/utils/tsconfig.ts @@ -60,3 +60,49 @@ export function removeDeprecatedTsconfigFalseOption(filePath: string, optionName fs.writeFileSync(filePath, newText); return true; } + +export function rewriteTypesInTsconfig(filePath: string): boolean { + let text: string; + try { + text = fs.readFileSync(filePath, 'utf-8'); + } catch { + return false; + } + + const parsed = parseJsonc(text) as { + compilerOptions?: { types?: unknown[] }; + } | null; + + const types = parsed?.compilerOptions?.types; + if (!Array.isArray(types)) { + return false; + } + + const REPLACEMENTS: Record = { + 'tsdown/client': 'vite-plus/pack/client', + 'vite/client': 'vite-plus/client', + }; + + const toReplace = types + .map((t, i) => + typeof t === 'string' && t in REPLACEMENTS ? { i, newVal: REPLACEMENTS[t] } : null, + ) + .filter((x): x is { i: number; newVal: string } => x !== null); + + if (toReplace.length === 0) { + return false; + } + + // Apply edits right-to-left so earlier element offsets stay valid after each replacement. + let currentText = text; + for (let j = toReplace.length - 1; j >= 0; j--) { + const { i, newVal } = toReplace[j]; + const edits = modify(currentText, ['compilerOptions', 'types', i], newVal, {}); + if (edits.length > 0) { + currentText = applyEdits(currentText, edits); + } + } + + fs.writeFileSync(filePath, currentText); + return true; +}