Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ jobs:
- name: bun-vite-template
node-version: 24
command: |
vp run build
vp run test
vp fmt
vp run validate
exclude:
# frm-stack uses Docker (testcontainers) which doesn't work the same way on Windows
- os: windows-latest
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@
"@voidzero-dev/vite-plus-test": "workspace:*",
"cac": "catalog:",
"cross-spawn": "catalog:",
"jsonc-parser": "catalog:",
"oxfmt": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
Expand All @@ -342,7 +343,6 @@
"detect-indent": "catalog:",
"detect-newline": "catalog:",
"glob": "catalog:",
"jsonc-parser": "catalog:",
"lint-staged": "catalog:",
"minimatch": "catalog:",
"mri": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"devDependencies": {
"vite": "latest"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
> vp migrate --no-interactive # should remove esModuleInterop: false from tsconfig.json
VITE+ - The Unified Toolchain for the Web

◇ Migrated . to Vite+<repeat>
• Node <semver> pnpm <semver>
• 3 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

> cat tsconfig.json # verify esModuleInterop: false is removed
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"allowSyntheticDefaultImports": true,
"strict": true
}
}

> cat vite.config.ts # check vite.config.ts
import { defineConfig } from 'vite-plus';

export default defineConfig({
staged: {
"*": "vp check --fix"
},
lint: {"options":{"typeAware":true,"typeCheck":true}},
});

> cat package.json # check package.json
{
"devDependencies": {
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
"vite-plus": "latest"
},
"pnpm": {
"overrides": {
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
}
},
"packageManager": "pnpm@<semver>",
"scripts": {
"prepare": "vp config"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"commands": [
"vp migrate --no-interactive # should remove esModuleInterop: false from tsconfig.json",
"cat tsconfig.json # verify esModuleInterop: false is removed",
"cat vite.config.ts # check vite.config.ts",
"cat package.json # check package.json"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true
}
}
31 changes: 30 additions & 1 deletion packages/cli/src/migration/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ import { editJsonFile, isJsonFile, readJsonFile } from '../utils/json.js';
import { detectPackageMetadata } from '../utils/package.js';
import { displayRelative, rulesDir } from '../utils/path.js';
import { getSpinner } from '../utils/prompts.js';
import { hasBaseUrlInTsconfig } from '../utils/tsconfig.js';
import {
findTsconfigFiles,
hasBaseUrlInTsconfig,
removeEsModuleInteropFalseFromFile,
} from '../utils/tsconfig.js';
import { editYamlFile, scalarString, type YamlDocument } from '../utils/yaml.js';
import {
PRETTIER_CONFIG_FILES,
Expand Down Expand Up @@ -643,6 +647,28 @@ function rewritePrettierLintStagedConfigFiles(projectPath: string, report?: Migr
rewriteToolLintStagedConfigFiles(projectPath, rewritePrettier, 'prettier', report);
}

function cleanupDeprecatedTsconfigOptions(
projectPath: string,
silent = false,
report?: MigrationReport,
): void {
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)}`);
}
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,
);
}
}
}

/**
* Rewrite standalone project to add vite-plus dependencies
* @param projectPath - The path to the project
Expand Down Expand Up @@ -724,6 +750,7 @@ export function rewriteStandaloneProject(
if (!skipStagedMigration) {
rewriteLintStagedConfigFile(projectPath, report);
}
cleanupDeprecatedTsconfigOptions(projectPath, silent, report);
mergeViteConfigFiles(projectPath, silent, report);
injectLintTypeCheckDefaults(projectPath, silent, report);
injectFmtDefaults(projectPath, silent, report);
Expand Down Expand Up @@ -772,6 +799,7 @@ export function rewriteMonorepo(
if (!skipStagedMigration) {
rewriteLintStagedConfigFile(workspaceInfo.rootDir, report);
}
cleanupDeprecatedTsconfigOptions(workspaceInfo.rootDir, silent, report);
mergeViteConfigFiles(workspaceInfo.rootDir, silent, report);
injectLintTypeCheckDefaults(workspaceInfo.rootDir, silent, report);
injectFmtDefaults(workspaceInfo.rootDir, silent, report);
Expand All @@ -793,6 +821,7 @@ export function rewriteMonorepoProject(
silent = false,
report?: MigrationReport,
): void {
cleanupDeprecatedTsconfigOptions(projectPath, silent, report);
mergeViteConfigFiles(projectPath, silent, report);
mergeTsdownConfigFile(projectPath, silent, report);

Expand Down
205 changes: 205 additions & 0 deletions packages/cli/src/utils/__tests__/tsconfig.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { findTsconfigFiles, removeEsModuleInteropFalseFromFile } from '../tsconfig.js';

describe('findTsconfigFiles', () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tsconfig-test-'));
});

afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it('finds all tsconfig variants', () => {
fs.writeFileSync(path.join(tmpDir, 'tsconfig.json'), '{}');
fs.writeFileSync(path.join(tmpDir, 'tsconfig.app.json'), '{}');
fs.writeFileSync(path.join(tmpDir, 'tsconfig.node.json'), '{}');
fs.writeFileSync(path.join(tmpDir, 'tsconfig.build.json'), '{}');
fs.writeFileSync(path.join(tmpDir, 'other.json'), '{}');
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');

const files = findTsconfigFiles(tmpDir);
const expected = [
path.join(tmpDir, 'tsconfig.app.json'),
path.join(tmpDir, 'tsconfig.build.json'),
path.join(tmpDir, 'tsconfig.json'),
path.join(tmpDir, 'tsconfig.node.json'),
];
expect(new Set(files)).toEqual(new Set(expected));
expect(files).toHaveLength(4);
});

it('returns empty array for non-existent directory', () => {
expect(findTsconfigFiles('/non-existent-dir-12345')).toEqual([]);
});

it('returns empty array when no tsconfig files exist', () => {
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
expect(findTsconfigFiles(tmpDir)).toEqual([]);
});
});

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,
`{
"compilerOptions": {
"target": "ES2023",
"esModuleInterop": false,
"strict": true
}
}`,
),
).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,
/* 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,
`{
"compilerOptions": {
"target": "ES2023",
"esModuleInterop": false
}
}`,
),
).toMatchInlineSnapshot(`
"{
"compilerOptions": {
"target": "ES2023"
}
}"
`);
});

it('handles inline block comment next to esModuleInterop: false', () => {
const filePath = path.join(tmpDir, 'tsconfig.json');
expect(
writeAndRemove(
filePath,
`{
"compilerOptions": {
"target": "ES2023",
"esModuleInterop": 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":{"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);
});
});
Loading
Loading