Skip to content

Commit a29e478

Browse files
feat(cli): prompt to migrate baseUrl if detected (#1692)
Based on #1688. --- This PR allows the user to remove `baseUrl` from `tsconfig.json` before applying type-aware lint defaults. When run in non-interactive mode, this will simply be applied Internally `vp dlx @andrewbranch/ts5to6 --fixBaseUrl .` is being run. --------- Co-authored-by: MK <fengmk2@gmail.com>
1 parent fbd90b1 commit a29e478

13 files changed

Lines changed: 437 additions & 27 deletions

File tree

docs/guide/troubleshooting.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ If you are migrating an existing project and it still depends on older Vite or V
1818
## `vp check` does not run type-aware lint rules or type checks
1919

2020
- Confirm that `lint.options.typeAware` and `lint.options.typeCheck` are enabled in `vite.config.ts`
21-
- Check whether your `tsconfig.json` uses `compilerOptions.baseUrl`
21+
- Check whether your `tsconfig.json` still uses `compilerOptions.baseUrl`
2222

23-
The Oxlint type checker path powered by `tsgolint` does not support `baseUrl`, so Vite+ skips `typeAware` and `typeCheck` when that setting is present.
23+
The Oxlint type checker path powered by `tsgolint` does not support `baseUrl`.
24+
`vp migrate` and `vp lint --init` try to run the `vp dlx @andrewbranch/ts5to6 --fixBaseUrl .`
25+
fix before enabling type-aware linting. If that fix fails or is declined, Vite+
26+
skips `typeAware` and `typeCheck`.
2427

2528
## `vp lint` / `vp fmt` may fail to read `vite.config.ts`
2629

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env node
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
5+
const target = process.argv.at(-1);
6+
if (!target || target.startsWith('-')) {
7+
process.exit(1);
8+
}
9+
10+
const filePath = path.resolve(process.cwd(), target);
11+
const text = fs.readFileSync(filePath, 'utf8');
12+
fs.writeFileSync(filePath, text.replace(/\n\s*"baseUrl": "\.",?/, ''));

packages/cli/snap-tests-global/migration-baseurl-tsconfig/snap.txt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
> vp migrate --no-interactive # migration should skip typeAware/typeCheck when tsconfig has baseUrl
1+
> chmod +x fix-baseurl.mjs # setup baseUrl fixer
2+
> vp migrate --no-interactive # migration should auto-fix tsconfig baseUrl
23
◇ Migrated . to Vite+
34
• Node <semver> pnpm <semver>
45
• 3 config updates applied
5-
! Warnings:
6-
- Skipped typeAware/typeCheck: tsconfig.json contains baseUrl which is not yet supported by the oxlint type checker.
7-
Run `npx @andrewbranch/ts5to6 --fixBaseUrl .` to remove baseUrl from your tsconfig.
86

9-
> cat vite.config.ts # check vite.config.ts — should NOT have typeAware or typeCheck
7+
> cat vite.config.ts # check vite.config.ts has typeAware and typeCheck
108
import { defineConfig } from 'vite-plus';
119

1210
export default defineConfig({
@@ -19,7 +17,10 @@ export default defineConfig({
1917
"no-unused-vars": "error",
2018
"vite-plus/prefer-vite-plus-imports": "error"
2119
},
22-
"options": {},
20+
"options": {
21+
"typeAware": true,
22+
"typeCheck": true
23+
},
2324
"jsPlugins": [
2425
{
2526
"name": "vite-plus",
@@ -29,6 +30,15 @@ export default defineConfig({
2930
},
3031
});
3132

33+
> cat tsconfig.json # check baseUrl was removed
34+
{
35+
"compilerOptions": {
36+
// JSONC comments should not prevent baseUrl detection.
37+
"target": "ES2023",
38+
"module": "NodeNext"
39+
}
40+
}
41+
3242
> test ! -f .oxlintrc.json # check .oxlintrc.json is removed
3343
> cat package.json # check package.json
3444
{

packages/cli/snap-tests-global/migration-baseurl-tsconfig/steps.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
{
2+
"env": {
3+
"VP_CLI_BIN": "./fix-baseurl.mjs"
4+
},
25
"commands": [
3-
"vp migrate --no-interactive # migration should skip typeAware/typeCheck when tsconfig has baseUrl",
4-
"cat vite.config.ts # check vite.config.ts \u2014 should NOT have typeAware or typeCheck",
6+
"chmod +x fix-baseurl.mjs # setup baseUrl fixer",
7+
"vp migrate --no-interactive # migration should auto-fix tsconfig baseUrl",
8+
"cat vite.config.ts # check vite.config.ts has typeAware and typeCheck",
9+
"cat tsconfig.json # check baseUrl was removed",
510
"test ! -f .oxlintrc.json # check .oxlintrc.json is removed",
611
"cat package.json # check package.json",
712
"cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog"

packages/cli/snap-tests-global/migration-baseurl-tsconfig/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"compilerOptions": {
3+
// JSONC comments should not prevent baseUrl detection.
34
"target": "ES2023",
45
"module": "NodeNext",
56
"baseUrl": "."

packages/cli/src/__tests__/init-config.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { vi } from 'vitest';
88
import { applyToolInitConfigToViteConfig, inspectInitCommand } from '../init-config.js';
99

1010
const tempDirs: string[] = [];
11+
const originalVpCliBin = process.env.VP_CLI_BIN;
1112

1213
function createTempDir() {
1314
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-init-config-'));
@@ -25,6 +26,11 @@ afterEach(() => {
2526
for (const dir of tempDirs.splice(0, tempDirs.length)) {
2627
fs.rmSync(dir, { recursive: true, force: true });
2728
}
29+
if (originalVpCliBin === undefined) {
30+
delete process.env.VP_CLI_BIN;
31+
} else {
32+
process.env.VP_CLI_BIN = originalVpCliBin;
33+
}
2834
vi.clearAllMocks();
2935
});
3036

@@ -68,6 +74,46 @@ describe('applyToolInitConfigToViteConfig', () => {
6874
expect(fs.existsSync(path.join(projectPath, '.oxlintrc.json'))).toBe(false);
6975
});
7076

77+
it('auto-fixes tsconfig baseUrl before writing type-aware lint defaults', async () => {
78+
const projectPath = createTempDir();
79+
const fixerPath = path.join(projectPath, 'fix-baseurl.mjs');
80+
fs.writeFileSync(
81+
fixerPath,
82+
`#!/usr/bin/env node
83+
import fs from 'node:fs';
84+
import path from 'node:path';
85+
86+
const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
87+
const text = fs.readFileSync(tsconfigPath, 'utf8');
88+
fs.writeFileSync(tsconfigPath, text.replace(/\\n\\s*"baseUrl": "\\.",?/, ''));
89+
`,
90+
);
91+
fs.chmodSync(fixerPath, 0o755);
92+
process.env.VP_CLI_BIN = fixerPath;
93+
fs.writeFileSync(
94+
path.join(projectPath, 'tsconfig.json'),
95+
`{
96+
"compilerOptions": {
97+
// comments require JSONC parsing
98+
"moduleResolution": "bundler",
99+
"baseUrl": ".",
100+
"paths": { "@/*": ["./src/*"] }
101+
}
102+
}
103+
`,
104+
);
105+
106+
const result = await applyToolInitConfigToViteConfig('lint', ['--init'], projectPath);
107+
expect(result.handled).toBe(true);
108+
expect(result.action).toBe('added');
109+
110+
const viteConfig = fs.readFileSync(path.join(projectPath, 'vite.config.ts'), 'utf8');
111+
expect(viteConfig).toContain('typeAware');
112+
expect(viteConfig).toContain('typeCheck');
113+
const tsconfig = fs.readFileSync(path.join(projectPath, 'tsconfig.json'), 'utf8');
114+
expect(tsconfig).not.toContain('"baseUrl"');
115+
});
116+
71117
it('ignores generated lint init defaults and still writes lint with options', async () => {
72118
const projectPath = createTempDir();
73119
fs.writeFileSync(

packages/cli/src/__tests__/tsconfig.spec.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ import path from 'node:path';
44

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

7-
import { hasBaseUrlInTsconfig } from '../utils/tsconfig.js';
7+
import {
8+
findTsconfigFilesWithBaseUrl,
9+
fixBaseUrlInTsconfig,
10+
hasBaseUrlInTsconfig,
11+
} from '../utils/tsconfig.js';
812

913
const tempDirs: string[] = [];
14+
const originalVpCliBin = process.env.VP_CLI_BIN;
1015

1116
function createTempDir() {
1217
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-tsconfig-'));
@@ -18,6 +23,11 @@ afterEach(() => {
1823
for (const dir of tempDirs.splice(0, tempDirs.length)) {
1924
fs.rmSync(dir, { recursive: true, force: true });
2025
}
26+
if (originalVpCliBin === undefined) {
27+
delete process.env.VP_CLI_BIN;
28+
} else {
29+
process.env.VP_CLI_BIN = originalVpCliBin;
30+
}
2131
});
2232

2333
describe('hasBaseUrlInTsconfig', () => {
@@ -53,4 +63,108 @@ describe('hasBaseUrlInTsconfig', () => {
5363

5464
expect(hasBaseUrlInTsconfig(projectPath)).toBe(false);
5565
});
66+
67+
it('treats null baseUrl as absent', () => {
68+
const projectPath = createTempDir();
69+
fs.writeFileSync(
70+
path.join(projectPath, 'tsconfig.json'),
71+
JSON.stringify({ compilerOptions: { baseUrl: null } }),
72+
);
73+
74+
expect(hasBaseUrlInTsconfig(projectPath)).toBe(false);
75+
expect(findTsconfigFilesWithBaseUrl(projectPath)).toEqual([]);
76+
});
77+
78+
it('detects baseUrl in secondary tsconfig files', () => {
79+
const projectPath = createTempDir();
80+
fs.writeFileSync(
81+
path.join(projectPath, 'tsconfig.json'),
82+
JSON.stringify({ compilerOptions: { moduleResolution: 'bundler' } }),
83+
);
84+
fs.writeFileSync(
85+
path.join(projectPath, 'tsconfig.app.json'),
86+
JSON.stringify({ compilerOptions: { baseUrl: '.' } }),
87+
);
88+
89+
expect(hasBaseUrlInTsconfig(projectPath)).toBe(true);
90+
});
91+
92+
it('returns tsconfig files that contain baseUrl', () => {
93+
const projectPath = createTempDir();
94+
fs.writeFileSync(
95+
path.join(projectPath, 'tsconfig.json'),
96+
JSON.stringify({ compilerOptions: { moduleResolution: 'bundler' } }),
97+
);
98+
fs.writeFileSync(
99+
path.join(projectPath, 'tsconfig.app.json'),
100+
JSON.stringify({ compilerOptions: { baseUrl: '.' } }),
101+
);
102+
103+
expect(findTsconfigFilesWithBaseUrl(projectPath)).toEqual([
104+
path.join(projectPath, 'tsconfig.app.json'),
105+
]);
106+
});
107+
108+
it('fixes every tsconfig file that contains baseUrl', async () => {
109+
const projectPath = createTempDir();
110+
const fixerPath = path.join(projectPath, 'fix-baseurl.mjs');
111+
const invocationsPath = path.join(projectPath, 'fix-invocations.json');
112+
fs.writeFileSync(
113+
fixerPath,
114+
`#!/usr/bin/env node
115+
import fs from 'node:fs';
116+
import path from 'node:path';
117+
118+
const target = process.argv.at(-1);
119+
const invocationsPath = path.join(process.cwd(), 'fix-invocations.json');
120+
const invocations = fs.existsSync(invocationsPath)
121+
? JSON.parse(fs.readFileSync(invocationsPath, 'utf8'))
122+
: [];
123+
invocations.push(target);
124+
fs.writeFileSync(invocationsPath, JSON.stringify(invocations));
125+
126+
const tsconfigPath = path.resolve(process.cwd(), target);
127+
const text = fs.readFileSync(tsconfigPath, 'utf8');
128+
fs.writeFileSync(tsconfigPath, text.replace(/\\n\\s*"baseUrl": "\\.",?/, ''));
129+
`,
130+
);
131+
fs.chmodSync(fixerPath, 0o755);
132+
process.env.VP_CLI_BIN = fixerPath;
133+
fs.writeFileSync(
134+
path.join(projectPath, 'tsconfig.json'),
135+
`{
136+
"compilerOptions": {
137+
"moduleResolution": "bundler"
138+
}
139+
}
140+
`,
141+
);
142+
fs.writeFileSync(
143+
path.join(projectPath, 'tsconfig.app.json'),
144+
`{
145+
"compilerOptions": {
146+
"baseUrl": ".",
147+
"paths": { "@/*": ["./src/*"] }
148+
}
149+
}
150+
`,
151+
);
152+
fs.writeFileSync(
153+
path.join(projectPath, 'tsconfig.node.json'),
154+
`{
155+
"compilerOptions": {
156+
"baseUrl": ".",
157+
"types": ["node"]
158+
}
159+
}
160+
`,
161+
);
162+
163+
await expect(fixBaseUrlInTsconfig(projectPath)).resolves.toBe('fixed');
164+
165+
const invocations = JSON.parse(fs.readFileSync(invocationsPath, 'utf8')) as string[];
166+
expect(new Set(invocations)).toEqual(new Set(['tsconfig.app.json', 'tsconfig.node.json']));
167+
expect(invocations).toHaveLength(2);
168+
expect(hasBaseUrlInTsconfig(projectPath)).toBe(false);
169+
});
56170
});

packages/cli/src/init-config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { fmt as resolveFmt } from './resolve-fmt.ts';
77
import { runCommandSilently } from './utils/command.ts';
88
import { BASEURL_TSCONFIG_WARNING, VITE_PLUS_NAME } from './utils/constants.ts';
99
import { warnMsg } from './utils/terminal.ts';
10-
import { hasBaseUrlInTsconfig } from './utils/tsconfig.ts';
10+
import { fixBaseUrlInTsconfig, hasBaseUrlInTsconfig } from './utils/tsconfig.ts';
1111

1212
interface InitCommandSpec {
1313
configKey: 'lint' | 'fmt';
@@ -227,7 +227,8 @@ export async function applyToolInitConfigToViteConfig(
227227

228228
if (spec.configKey === 'lint' && hasTriggerFlag(args, ['--init'])) {
229229
const lintInitConfigPath = path.join(projectPath, '.vite-plus-lint-init.oxlintrc.json');
230-
// Skip typeAware/typeCheck when tsconfig.json has baseUrl (unsupported by tsgolint)
230+
await fixBaseUrlInTsconfig(projectPath);
231+
// Skip typeAware/typeCheck when tsconfig still has baseUrl (unsupported by tsgolint)
231232
const hasBaseUrl = hasBaseUrlInTsconfig(projectPath);
232233
const initConfig = createDefaultVitePlusLintConfig({
233234
includeTypeAwareDefaults: !hasBaseUrl,

0 commit comments

Comments
 (0)