Skip to content
58 changes: 58 additions & 0 deletions packages/cli/src/migration/__tests__/migrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
});
});
18 changes: 18 additions & 0 deletions packages/cli/src/migration/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand Down
99 changes: 98 additions & 1 deletion packages/cli/src/utils/__tests__/tsconfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
46 changes: 46 additions & 0 deletions packages/cli/src/utils/tsconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
'tsdown/client': 'vite-plus/pack/client',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not rewrite tsdown/client to an unexported subpath

When a project has compilerOptions.types: ["tsdown/client"], this rewrites it to vite-plus/pack/client, but the CLI package only exports ./pack and not ./pack/client (packages/cli/package.json lines 78-80), so TypeScript reports TS2688: Cannot find type definition file for 'vite-plus/pack/client' and migrated projects lose typechecking. The existing Rust reference-type migrator also documents this exact case as not rewritten because vite-plus has no tsdown subpath export (crates/vite_migration/src/import_rewriter.rs lines 2466-2471).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has already been implemented in #1501, and this PR depends on #1501.

'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;
}
Loading