Skip to content

Commit 945fd51

Browse files
committed
Warn when the project's TypeScript is older than the bundled version
Fixes #880
1 parent 797693a commit 945fd51

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

lib/handle-ts-files.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,40 @@
11
import path from 'node:path';
22
import fs from 'node:fs';
3+
import {createRequire} from 'node:module';
34
import ts from 'typescript';
45
import {getTsconfig, createFilesMatcher} from 'get-tsconfig';
56
import {tsconfigDefaults} from './constants.js';
67

8+
let hasWarnedAboutTypeScriptVersion = false;
9+
10+
/**
11+
Warns once if the project's own TypeScript is an older major than the version XO bundles. Mixing TypeScript versions in one process can crash type-aware linting (notably under pnpm), because the TypeFlags enum was renumbered in TypeScript 6.
12+
*/
13+
const warnOnOutdatedProjectTypeScript = (cwd: string): void => {
14+
if (hasWarnedAboutTypeScriptVersion) {
15+
return;
16+
}
17+
18+
let projectVersion: string;
19+
try {
20+
const require = createRequire(path.join(cwd, 'noop.js'));
21+
({version: projectVersion} = require('typescript/package.json') as {version: string});
22+
} catch {
23+
// No project-level TypeScript resolvable; XO's bundled version is used, so there is no mismatch.
24+
return;
25+
}
26+
27+
const projectMajor = Number.parseInt(projectVersion, 10);
28+
const bundledMajor = Number.parseInt(ts.version, 10);
29+
30+
if (projectMajor >= bundledMajor) {
31+
return;
32+
}
33+
34+
hasWarnedAboutTypeScriptVersion = true;
35+
console.warn(`XO bundles TypeScript ${ts.version}, but your project has TypeScript ${projectVersion}. Mixing TypeScript versions in one process can crash type-aware linting (notably with pnpm). Upgrade your project's \`typescript\` to ${bundledMajor} or later, or pin it (for example, a pnpm \`overrides\` entry).`);
36+
};
37+
738
const createInMemoryProgram = (files: string[], cwd: string): ts.Program | undefined => {
839
if (files.length === 0) {
940
return undefined;
@@ -74,6 +105,8 @@ If no tsconfig is found, it will create an in-memory TypeScript Program for type
74105
@returns The unmatched files and an in-memory TypeScript Program.
75106
*/
76107
export function handleTsconfig({files, cwd, cacheLocation}: {files: string[]; cwd: string; cacheLocation?: string}) {
108+
warnOnOutdatedProjectTypeScript(cwd);
109+
77110
const unincludedFiles: string[] = [];
78111
const filesMatcherCache = new Map<string, ReturnType<typeof createFilesMatcher>>();
79112

test/cli.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import process from 'node:process';
33
import fs from 'node:fs/promises';
44
import path from 'node:path';
5-
import {availableParallelism} from 'node:os';
5+
import {availableParallelism, tmpdir} from 'node:os';
66
import test, {describe, type TestContext} from 'node:test';
77
import assert from 'node:assert/strict';
88
import dedent from 'dedent';
@@ -52,6 +52,25 @@ describe('xo CLI', {concurrency: availableParallelism()}, () => {
5252
assert.ok(stdout.includes(ignoredFileWarningMessage));
5353
});
5454

55+
test('xo warns when the project TypeScript is older than the bundled version', async t => {
56+
const cwd = await fs.mkdtemp(path.join(tmpdir(), 'xo-ts-version-'));
57+
t.after(async () => {
58+
await fs.rm(cwd, {recursive: true, force: true});
59+
});
60+
61+
await fs.writeFile(path.join(cwd, 'package.json'), JSON.stringify({type: 'module', name: 'ts-version-test'}), 'utf8');
62+
await fs.writeFile(path.join(cwd, 'tsconfig.json'), '{}', 'utf8');
63+
await fs.writeFile(path.join(cwd, 'foo.ts'), dedent`export const x = 1;\n`, 'utf8');
64+
65+
// Simulate an older-major project-level TypeScript so the version-mismatch warning triggers.
66+
await fs.mkdir(path.join(cwd, 'node_modules', 'typescript'), {recursive: true});
67+
await fs.writeFile(path.join(cwd, 'node_modules', 'typescript', 'package.json'), JSON.stringify({name: 'typescript', version: '5.0.0'}), 'utf8');
68+
69+
const {stderr} = await $({reject: false})`node ./dist/cli --cwd ${cwd} foo.ts`;
70+
assert.ok(stderr.includes('XO bundles TypeScript'));
71+
assert.ok(stderr.includes('5.0.0'));
72+
});
73+
5574
test('xo fails with exit code 2 for missing custom suppressions file', async t => {
5675
const cwd = await createProject(t);
5776
const filePath = path.join(cwd, 'test.js');

0 commit comments

Comments
 (0)