diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a82d5b42..7c1b62fb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,9 +10,9 @@ jobs: fail-fast: false matrix: node-version: + - 20 - 18 - 16 - - 14 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 diff --git a/.gitignore b/.gitignore index b4881e87..ee3e1c15 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,7 @@ test/fixtures/project/node_modules/.cache test/fixtures/typescript/extends-module/node_modules/.cache !test/fixtures/typescript/extends-tsconfig-bases/node_modules test/fixtures/typescript/extends-tsconfig-bases/node_modules/.cache +!test/fixtures/typescript/extends-array/node_modules +test/fixtures/typescript/extends-array/node_modules/.cache .nyc_output coverage diff --git a/config/plugins.cjs b/config/plugins.cjs index 160353d7..3b7fc7f1 100644 --- a/config/plugins.cjs +++ b/config/plugins.cjs @@ -153,6 +153,9 @@ module.exports = { }, ], + // Temporarily disabled because it's buggy with TypeScript: https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2175 + 'unicorn/no-empty-file': 'off', + // TODO: Disabled for now until it becomes more stable: https://github.com/sindresorhus/eslint-plugin-unicorn/search?q=consistent-destructuring+is:issue&state=open&type=issues 'unicorn/consistent-destructuring': 'off', @@ -265,6 +268,13 @@ module.exports = { 'import/order': [ 'error', { + groups: [ + 'builtin', + 'external', + 'parent', + 'sibling', + 'index', + ], 'newlines-between': 'never', warnOnUnassignedImports: true, }, diff --git a/lib/options-manager.js b/lib/options-manager.js index 54c8baf0..6feb6ec7 100644 --- a/lib/options-manager.js +++ b/lib/options-manager.js @@ -2,7 +2,6 @@ import {existsSync, promises as fs} from 'node:fs'; import process from 'node:process'; import os from 'node:os'; import path from 'node:path'; -import {createRequire} from 'node:module'; import arrify from 'arrify'; import {mergeWith, flow, pick} from 'lodash-es'; import {findUpSync} from 'find-up'; @@ -11,12 +10,12 @@ import prettier from 'prettier'; import semver from 'semver'; import {cosmiconfig, defaultLoaders} from 'cosmiconfig'; import micromatch from 'micromatch'; -import JSON5 from 'json5'; import stringify from 'json-stable-stringify-without-jsonify'; import {Legacy} from '@eslint/eslintrc'; import createEsmUtils from 'esm-utils'; import MurmurHash3 from 'imurmurhash'; import slash from 'slash'; +import {getTsconfig} from 'get-tsconfig'; import { DEFAULT_IGNORES, DEFAULT_EXTENSION, @@ -159,26 +158,20 @@ const handleTSConfig = async options => { options.tsConfig = {}; options.tsConfigPath = ''; - const {project: tsConfigProjectPath, tsconfigRootDir} = options.parserOptions || {}; + const {project: tsConfigProjectPath} = options.parserOptions || {}; if (tsConfigProjectPath) { options.tsConfigPath = path.resolve(options.cwd, tsConfigProjectPath); - options.tsConfig = JSON5.parse(await fs.readFile(options.tsConfigPath)); + options.tsConfig = tsConfigResolvePaths(getTsconfig(options.tsConfigPath).config, options.tsConfigPath); } else { - const tsConfigExplorer = cosmiconfig([], { - searchPlaces: ['tsconfig.json'], - loaders: {'.json': (_, content) => JSON5.parse(content)}, - stopDir: tsconfigRootDir, - }); - const searchResults = (await tsConfigExplorer.search(options.filePath)) || {}; - options.tsConfigPath = searchResults.filepath; - options.tsConfig = searchResults.config; - } - - if (options.tsConfig) { - // If the tsconfig extends from another file, we need to ensure that the file is covered by the tsconfig - // or not. The basefile could have includes/excludes/files properties that should be applied to the final tsconfig representation. - options.tsConfig = await recursiveBuildTsConfig(options.tsConfig, options.tsConfigPath); + const {config: tsConfig, path: filepath} = getTsconfig(options.filePath) || {}; + options.tsConfigPath = filepath; + options.tsConfig = tsConfig; + if (options.tsConfigPath) { + options.tsConfig = tsConfigResolvePaths(tsConfig, options.tsConfigPath); + } else { + delete options.tsConfig; + } } let hasMatch; @@ -637,46 +630,6 @@ const getOptionGroups = async (files, options) => { return optionGroups; }; -async function recursiveBuildTsConfig(tsConfig, tsConfigPath) { - tsConfig = tsConfigResolvePaths(tsConfig, tsConfigPath); - - if (!tsConfig.extends || (typeof tsConfig.extends === 'string' && tsConfig.extends.includes('node_modules'))) { - return tsConfig; - } - - // If any of the following are missing, then we need to look up the base config as it could apply - const require = createRequire(tsConfigPath); - - let basePath; - try { - basePath = require.resolve(tsConfig.extends); - } catch (error) { - // Tsconfig resolution is odd, It allows behavior that is not exactly like node resolution - // therefore we attempt to smooth this out here with this hack - try { - basePath = require.resolve(path.join(tsConfig.extends, 'tsconfig.json')); - } catch { - // Throw the orginal resolution error to let the user know their extends block is invalid - throw error; - } - } - - const baseTsConfig = JSON5.parse(await fs.readFile(basePath)); - - delete tsConfig.extends; - - tsConfig = { - compilerOptions: { - ...baseTsConfig.compilerOptions, - ...tsConfig.compilerOptions, - }, - ...baseTsConfig, - ...tsConfig, - }; - - return recursiveBuildTsConfig(tsConfig, basePath); -} - // Convert all include, files, and exclude to absolute paths // and or globs. This works because ts only allows simple glob subset const tsConfigResolvePaths = (tsConfig, tsConfigPath) => { diff --git a/package.json b/package.json index eff43df6..76d91eef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xo", - "version": "0.53.1", + "version": "0.55.0", "description": "JavaScript/TypeScript linter (ESLint wrapper) with great defaults", "license": "MIT", "repository": "xojs/xo", @@ -13,7 +13,7 @@ "type": "module", "bin": "./cli.js", "engines": { - "node": ">=14.16" + "node": ">=16" }, "scripts": { "test:clean": "find ./test -type d -name 'node_modules' -prune -not -path ./test/fixtures/project/node_modules -exec rm -rf '{}' +", @@ -52,59 +52,54 @@ "javascript", "typescript" ], - "bundledDependencies": [ - "@typescript-eslint/eslint-plugin", - "@typescript-eslint/parser", - "eslint-config-xo-typescript" - ], "dependencies": { - "@eslint/eslintrc": "^1.3.3", - "@typescript-eslint/eslint-plugin": "^5.43.0", - "@typescript-eslint/parser": "^5.43.0", + "@eslint/eslintrc": "^2.1.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "arrify": "^3.0.0", - "cosmiconfig": "^7.1.0", + "cosmiconfig": "^8.2.0", "define-lazy-prop": "^3.0.0", - "eslint": "^8.27.0", - "eslint-config-prettier": "^8.5.0", + "eslint": "^8.45.0", + "eslint-config-prettier": "^8.8.0", "eslint-config-xo": "^0.43.1", - "eslint-config-xo-typescript": "^0.55.0", - "eslint-formatter-pretty": "^4.1.0", + "eslint-config-xo-typescript": "^1.0.0", + "eslint-formatter-pretty": "^5.0.0", "eslint-import-resolver-webpack": "^0.13.2", - "eslint-plugin-ava": "^13.2.0", + "eslint-plugin-ava": "^14.0.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.27.5", - "eslint-plugin-n": "^15.5.1", + "eslint-plugin-n": "^16.0.1", "eslint-plugin-no-use-extend-native": "^0.5.0", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-unicorn": "^44.0.2", - "esm-utils": "^4.1.0", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-unicorn": "^48.0.0", + "esm-utils": "^4.1.2", "find-cache-dir": "^4.0.0", "find-up": "^6.3.0", "get-stdin": "^9.0.0", - "globby": "^13.1.2", + "get-tsconfig": "^4.6.2", + "globby": "^13.2.2", "imurmurhash": "^0.1.4", "json-stable-stringify-without-jsonify": "^1.0.1", - "json5": "^2.2.1", "lodash-es": "^4.17.21", - "meow": "^11.0.0", + "meow": "^12.0.1", "micromatch": "^4.0.5", "open-editor": "^4.0.0", - "prettier": "^2.7.1", - "semver": "^7.3.8", - "slash": "^5.0.0", - "to-absolute-glob": "^2.0.2", - "typescript": "^4.9.3" + "prettier": "^3.0.0", + "semver": "^7.5.4", + "slash": "^5.1.0", + "to-absolute-glob": "^3.0.0", + "typescript": "^5.1.6" }, "devDependencies": { - "ava": "^5.1.0", + "ava": "^5.3.1", "eslint-config-xo-react": "^0.27.0", "eslint-plugin-react": "^7.31.10", "eslint-plugin-react-hooks": "^4.6.0", - "execa": "^6.1.0", + "execa": "^7.1.1", "nyc": "^15.1.0", "proxyquire": "^2.1.3", "temp-write": "^5.0.0", - "webpack": "^5.75.0" + "webpack": "^5.88.1" }, "xo": { "ignores": [ diff --git a/readme.md b/readme.md index b6e97ec5..7c35fdbe 100644 --- a/readme.md +++ b/readme.md @@ -15,7 +15,7 @@ Opinionated but configurable ESLint wrapper with lots of goodies included. Enfor It uses [ESLint](https://eslint.org) underneath, so issues regarding built-in rules should be opened over [there](https://github.com/eslint/eslint/issues). -**XO requires your project to be [ESM](https://medium.com/sindre-sorhus/hello-modules-d1010b4e777b).** +**XO requires your project to be [ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).** ![](https://raw.githubusercontent.com/sindresorhus/eslint-formatter-pretty/main/screenshot.png) @@ -26,7 +26,7 @@ It uses [ESLint](https://eslint.org) underneath, so issues regarding built-in ru - Enforces readable code, because you read more code than you write. - No need to specify file paths to lint as it lints all JS/TS files except for [commonly ignored paths](#ignores). - [Config overrides per files/globs.](#config-overrides) -- [TypeScript supported by default](#typescript) +- [TypeScript supported by default.](#typescript) - Includes many useful ESLint plugins, like [`unicorn`](https://github.com/sindresorhus/eslint-plugin-unicorn), [`import`](https://github.com/benmosher/eslint-plugin-import), [`ava`](https://github.com/avajs/eslint-plugin-ava), [`n`](https://github.com/eslint-community/eslint-plugin-n) and more. - Automatically enables rules based on the [`engines`](https://docs.npmjs.com/files/package.json#engines) field in your `package.json`. - Caches results between runs for much better performance. @@ -39,8 +39,8 @@ It uses [ESLint](https://eslint.org) underneath, so issues regarding built-in ru ## Install -``` -$ npm install xo --save-dev +```sh +npm install xo --save-dev ``` *You must install XO locally. You can run it directly with `$ npx xo`.* @@ -255,7 +255,7 @@ module.exports = { }; ``` -If contradicting options are set for both Prettier and XO an error will be thrown. +If contradicting options are set for both Prettier and XO, an error will be thrown. ### nodeVersion @@ -466,7 +466,6 @@ XO is based on ESLint. This project started out as just a shareable ESLint confi - [eslint-config-xo-vue](https://github.com/ChocPanda/eslint-config-xo-vue) - ESLint shareable config for Vue to be used with the above - [stylelint-config-xo](https://github.com/xojs/stylelint-config-xo) - Stylelint shareable config for XO with tab indent - [stylelint-config-xo-space](https://github.com/xojs/stylelint-config-xo-space) - Stylelint shareable config for XO with 2-space indent -- [tslint-xo](https://github.com/xojs/tslint-xo) - TSLint shareable config for XO - [eslint-config-xo-typescript](https://github.com/xojs/eslint-config-xo-typescript) - ESLint shareable config for TypeScript ## Support diff --git a/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/package.json b/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/package.json new file mode 100644 index 00000000..918d2924 --- /dev/null +++ b/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/package.json @@ -0,0 +1,26 @@ +{ + "name": "@sindresorhus/tsconfig", + "version": "3.0.1", + "description": "Shared TypeScript config for my projects", + "license": "MIT", + "repository": "sindresorhus/tsconfig", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "https://sindresorhus.com" + }, + "main": "tsconfig.json", + "engines": { + "node": ">=14" + }, + "files": [ + "tsconfig.json" + ], + "keywords": [ + "tsconfig", + "typescript", + "ts", + "config", + "configuration" + ] +} diff --git a/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/tsconfig.json b/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/tsconfig.json new file mode 100644 index 00000000..0612e961 --- /dev/null +++ b/test/fixtures/typescript/extends-array/node_modules/@sindresorhus/tsconfig/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + // Disabled because of https://github.com/Microsoft/TypeScript/issues/29172 + // "outDir": "dist", + + "module": "node16", + "moduleResolution": "node16", + "moduleDetection": "force", + "target": "ES2020", // Node.js 14 + "lib": [ + "DOM", + "DOM.Iterable", + "ES2020" + ], + "allowSyntheticDefaultImports": true, // To provide backwards compatibility, Node.js allows you to import most CommonJS packages with a default import. This flag tells TypeScript that it's okay to use import on CommonJS modules. + "resolveJsonModule": false, // ESM doesn't yet support JSON modules. + "jsx": "react", + "declaration": true, + "pretty": true, + "newLine": "lf", + "stripInternal": true, + "strict": true, + "noImplicitReturns": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noPropertyAccessFromIndexSignature": true, + "noEmitOnError": true, + "useDefineForClassFields": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + } +} diff --git a/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/package.json b/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/package.json new file mode 100644 index 00000000..c0e6dfa8 --- /dev/null +++ b/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/package.json @@ -0,0 +1 @@ +{"name":"@tsconfig/node16","repository":{"type":"git","url":"https://github.com/tsconfig/bases.git","directory":"bases"},"license":"MIT","description":"A base TSConfig for working with Node 16.","keywords":["tsconfig","node16"],"version":"1.0.3"} \ No newline at end of file diff --git a/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/tsconfig.json b/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/tsconfig.json new file mode 100644 index 00000000..262ff50b --- /dev/null +++ b/test/fixtures/typescript/extends-array/node_modules/@tsconfig/node16/tsconfig.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 16", + + "compilerOptions": { + "lib": ["es2021"], + "module": "commonjs", + "target": "es2021", + + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node" + } +} diff --git a/test/fixtures/typescript/extends-array/package.json b/test/fixtures/typescript/extends-array/package.json new file mode 100644 index 00000000..90bd27c7 --- /dev/null +++ b/test/fixtures/typescript/extends-array/package.json @@ -0,0 +1,3 @@ +{ + "xo": {} +} diff --git a/test/fixtures/typescript/extends-array/tsconfig.json b/test/fixtures/typescript/extends-array/tsconfig.json new file mode 100644 index 00000000..a0ad755a --- /dev/null +++ b/test/fixtures/typescript/extends-array/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": [ + "@sindresorhus/tsconfig", + "@tsconfig/node16" + ], +} diff --git a/test/options-manager.js b/test/options-manager.js index 057d78c8..e8aa9be0 100644 --- a/test/options-manager.js +++ b/test/options-manager.js @@ -99,7 +99,7 @@ test('buildConfig: prettier: true', t => { trailingComma: 'all', }]); // eslint-prettier-config must always be last - t.is(config.baseConfig.extends[config.baseConfig.extends.length - 1], 'plugin:prettier/recommended'); + t.is(config.baseConfig.extends.at(-1), 'plugin:prettier/recommended'); // Indent rule is not enabled t.is(config.baseConfig.rules.indent, undefined); // Semi rule is not enabled @@ -125,8 +125,8 @@ test('buildConfig: prettier: true, typescript file', t => { }]); // eslint-prettier-config must always be last - t.is(config.baseConfig.extends[config.baseConfig.extends.length - 1], 'plugin:prettier/recommended'); - t.regex(config.baseConfig.extends[config.baseConfig.extends.length - 2], /xo-typescript/); + t.is(config.baseConfig.extends.at(-1), 'plugin:prettier/recommended'); + t.regex(config.baseConfig.extends.at(-2), /xo-typescript/); // Indent rule is not enabled t.is(config.baseConfig.rules.indent, undefined); @@ -430,7 +430,7 @@ test('buildConfig: extends', t => { test('buildConfig: typescript', t => { const config = manager.buildConfig({ts: true, tsConfigPath: './tsconfig.json'}); - t.regex(config.baseConfig.extends[config.baseConfig.extends.length - 1], /xo-typescript/); + t.regex(config.baseConfig.extends.at(-1), /xo-typescript/); t.is(config.baseConfig.parser, require.resolve('@typescript-eslint/parser')); t.deepEqual(config.baseConfig.parserOptions, { warnOnUnsupportedTypeScriptVersion: false, @@ -691,6 +691,15 @@ test('mergeWithFileConfig: tsconfig can properly extend tsconfig base node_modul t.is(options.tsConfigPath, expectedConfigPath); }); +test('mergeWithFileConfig: tsconfig can properly resolve extends arrays introduced in ts 5', async t => { + const cwd = path.resolve('fixtures', 'typescript', 'extends-array'); + const expectedConfigPath = path.join(cwd, 'tsconfig.json'); + const filePath = path.resolve(cwd, 'does-not-matter.ts'); + await t.notThrowsAsync(manager.mergeWithFileConfig({cwd, filePath})); + const {options} = await manager.mergeWithFileConfig({cwd, filePath}); + t.is(options.tsConfigPath, expectedConfigPath); +}); + test('applyOverrides', t => { t.deepEqual( manager.applyOverrides(