diff --git a/eslint.config.js b/eslint.config.js index 589e771..66ef292 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,4 +1,6 @@ import js from '@eslint/js'; +import tsPlugin from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; import globals from 'globals'; export default [ @@ -37,4 +39,26 @@ export default [ strict: 'error', }, }, + { + files: ['**/*.ts'], + languageOptions: { + parser: tsParser, + }, + plugins: { + '@typescript-eslint': tsPlugin, + }, + rules: { + 'no-undef': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + ignoreRestSiblings: true, + varsIgnorePattern: '^_', + }, + ], + }, + }, ]; diff --git a/package.json b/package.json index ef4bfe3..28d642f 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,8 @@ "@types/micromatch": "^4.0.9", "@types/node": "^20.14.9", "@types/normalize-path": "^3.0.2", + "@typescript-eslint/eslint-plugin": "^8.59.3", + "@typescript-eslint/parser": "^8.59.3", "chokidar": "^3.6.0", "eslint": "^9.39.4", "fs-extra": "^11.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2afc593..5891765 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,12 @@ importers: '@types/normalize-path': specifier: ^3.0.2 version: 3.0.2 + '@typescript-eslint/eslint-plugin': + specifier: ^8.59.3 + version: 8.59.3(@typescript-eslint/parser@8.59.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.59.3 + version: 8.59.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) chokidar: specifier: ^3.6.0 version: 3.6.0 @@ -201,6 +207,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.2': resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -441,6 +453,65 @@ packages: '@types/normalize-path@3.0.2': resolution: {integrity: sha512-DO++toKYPaFn0Z8hQ7Tx+3iT9t77IJo/nDiqTXilgEP+kPNIYdpS9kh3fXuc53ugqwp9pxC1PVjCpV1tQDyqMA==} + '@typescript-eslint/eslint-plugin@8.59.3': + resolution: {integrity: sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.3 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.3': + resolution: {integrity: sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.3': + resolution: {integrity: sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.3': + resolution: {integrity: sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.3': + resolution: {integrity: sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.3': + resolution: {integrity: sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.3': + resolution: {integrity: sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.3': + resolution: {integrity: sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.3': + resolution: {integrity: sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.3': + resolution: {integrity: sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -472,6 +543,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -479,6 +554,10 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -537,6 +616,10 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -640,6 +723,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -705,6 +792,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -803,6 +894,11 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -831,6 +927,12 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -928,6 +1030,11 @@ snapshots: eslint: 9.39.4(jiti@2.6.1) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + dependencies: + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.2': {} '@eslint/config-array@0.21.2': @@ -1143,6 +1250,97 @@ snapshots: '@types/normalize-path@3.0.2': {} + '@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.3 + '@typescript-eslint/type-utils': 8.59.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.3 + eslint: 9.39.4(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.3 + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.3 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.3(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.3(typescript@5.9.3) + '@typescript-eslint/types': 8.59.3 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.3': + dependencies: + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/visitor-keys': 8.59.3 + + '@typescript-eslint/tsconfig-utils@8.59.3(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.59.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.3': {} + + '@typescript-eslint/typescript-estree@8.59.3(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.3(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.3(typescript@5.9.3) + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/visitor-keys': 8.59.3 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.0 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.59.3 + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.3': + dependencies: + '@typescript-eslint/types': 8.59.3 + eslint-visitor-keys: 5.0.1 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -1171,6 +1369,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + binary-extensions@2.3.0: {} brace-expansion@1.1.12: @@ -1178,6 +1378,10 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -1232,6 +1436,8 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint-visitor-keys@5.0.1: {} + eslint@9.39.4(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.4(jiti@2.6.1)) @@ -1348,6 +1554,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -1408,6 +1616,10 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.12 @@ -1477,6 +1689,8 @@ snapshots: scheduler@0.27.0: {} + semver@7.8.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -1500,6 +1714,10 @@ snapshots: dependencies: is-number: 7.0.0 + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + tslib@2.8.1: {} type-check@0.4.0: diff --git a/src/ESLintError.js b/src/ESLintError.ts similarity index 67% rename from src/ESLintError.js rename to src/ESLintError.ts index 8aec93a..7585d3b 100644 --- a/src/ESLintError.js +++ b/src/ESLintError.ts @@ -1,8 +1,5 @@ export default class ESLintError extends Error { - /** - * @param {string=} messages - */ - constructor(messages) { + constructor(messages?: string) { super(`[eslint] ${messages}`); this.name = 'ESLintError'; this.stack = ''; diff --git a/src/getESLint.js b/src/getESLint.ts similarity index 58% rename from src/getESLint.js rename to src/getESLint.ts index fb4d63b..a08de9f 100644 --- a/src/getESLint.js +++ b/src/getESLint.ts @@ -3,22 +3,24 @@ import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { getESLintOptions } from './options.js'; - -/** @typedef {import('eslint').ESLint} ESLint */ -/** @typedef {import('eslint').ESLint.Options} ESLintOptions */ -/** @typedef {import('eslint').ESLint.LintResult} LintResult */ -/** @typedef {import('./options.js').Options} Options */ -/** @typedef {(files: string|string[]) => Promise} LintTask */ -/** @typedef {{eslint: ESLint, lintFiles: LintTask}} Linter */ -/** @typedef {{new (arg0: ESLintOptions): ESLint, outputFixes: (arg0: LintResult[]) => Promise}} ESLintClass */ +import type { Options } from './options.js'; +import type { ESLint as ESLintType } from 'eslint'; + +type ESLintOptions = ESLintType.Options; +type LintResult = ESLintType.LintResult; +type LintTask = (files: string | string[]) => Promise; +type Linter = { eslint: ESLintType; lintFiles: LintTask }; +type ESLintClass = { + new (arg0: ESLintOptions): ESLintType; + outputFixes: (arg0: LintResult[]) => Promise; +}; +type ESLintModule = Record & { + loadESLint?: (arg0: { useFlatConfig: boolean }) => Promise; +}; const moduleRequire = createRequire(import.meta.url); -/** - * @param {Options} options - * @returns {Promise} - */ -async function getESLint(options) { +async function getESLint(options: Options): Promise { const eslintModule = await loadESLintModule(options.eslintPath || 'eslint'); if (typeof eslintModule.loadESLint !== 'function') { @@ -30,19 +32,13 @@ async function getESLint(options) { const eslintOptions = getESLintOptions(options); const fix = Boolean(eslintOptions && eslintOptions.fix); - /** @type {ESLintClass} */ const ESLint = await eslintModule.loadESLint({ useFlatConfig: options.configType === 'flat', }); - /** @type {ESLint} */ const eslint = new ESLint(eslintOptions); - /** - * @param {string|string[]} files - * @returns {Promise} - */ - async function lintFiles(files) { + async function lintFiles(files: string | string[]): Promise { const results = await eslint.lintFiles(files); if (fix) { @@ -58,11 +54,7 @@ async function getESLint(options) { }; } -/** - * @param {string} specifier - * @returns {Promise>} - */ -async function loadESLintModule(specifier) { +async function loadESLintModule(specifier: string): Promise { if (isAbsolutePathSpecifier(specifier)) { return loadResolvedModule(specifier); } @@ -82,31 +74,19 @@ async function loadESLintModule(specifier) { } } -/** - * @param {string} specifier - * @returns {Promise>} - */ -async function loadResolvedModule(specifier) { +async function loadResolvedModule(specifier: string): Promise { const resolvedPath = moduleRequire.resolve(specifier); return normalizeModule(await import(pathToFileURL(resolvedPath).href)); } -/** - * @param {string} specifier - * @returns {boolean} - */ -function isAbsolutePathSpecifier(specifier) { +function isAbsolutePathSpecifier(specifier: string): boolean { return path.isAbsolute(specifier) || path.win32.isAbsolute(specifier); } -/** - * @param {unknown} error - * @returns {boolean} - */ -function isImportResolutionError(error) { +function isImportResolutionError(error: unknown): boolean { if (!error || typeof error !== 'object') return false; - const { code } = /** @type {{ code?: string }} */ (error); + const { code } = error as { code?: string }; return ( code === 'ERR_MODULE_NOT_FOUND' || code === 'ERR_UNSUPPORTED_DIR_IMPORT' || @@ -114,14 +94,13 @@ function isImportResolutionError(error) { ); } -/** - * @param {Record} module - * @returns {Record} - */ -function normalizeModule(module) { +function normalizeModule(module: Record): ESLintModule { return module.default && typeof module.default === 'object' - ? { ...module.default, ...module } - : module; + ? ({ + ...(module.default as Record), + ...module, + } as ESLintModule) + : (module as ESLintModule); } export { getESLint }; diff --git a/src/index.js b/src/index.ts similarity index 74% rename from src/index.js rename to src/index.ts index 55dc327..fc5a695 100644 --- a/src/index.js +++ b/src/index.ts @@ -6,34 +6,38 @@ import { globSync } from 'tinyglobby'; import linter from './linter.js'; import { getOptions } from './options.js'; import { arrify, parseFiles, parseFoldersToGlobs } from './utils.js'; +import type { Linter, Reporter } from './linter.js'; +import type { Options, ResolvedOptions } from './options.js'; +import type { Compiler, Module, NormalModule, RspackError } from '@rspack/core'; const { isMatch } = micromatch; -/** @typedef {import('@rspack/core').Compiler} Compiler */ -/** @typedef {import('@rspack/core').Module} Module */ -/** @typedef {import('@rspack/core').NormalModule} NormalModule */ -/** @typedef {import('./options.js').Options} Options */ - const ESLINT_PLUGIN = 'ESLintRspackPlugin'; const DEFAULT_FOLDER_TO_EXCLUDE = '**/node_modules/**'; let compilerId = 0; +type RunOptions = Omit & { + resourceQueryExclude: RegExp[]; + extensions: string[]; + files: string[]; + exclude: string[]; +}; +type CompilationHookWithTaps = Compiler['hooks']['compilation'] & { + taps: Array<{ name?: string }>; +}; + class ESLintRspackPlugin { - /** - * @param {Options} options - */ - constructor(options = {}) { + key: string; + options: ResolvedOptions; + + constructor(options: Options = {}) { this.key = ESLINT_PLUGIN; this.options = getOptions(options); this.run = this.run.bind(this); } - /** - * @param {Compiler} compiler - * @returns {void} - */ - apply(compiler) { + apply(compiler: Compiler): void { // Generate key for each compilation, // this differentiates one from the other when being cached. this.key = compiler.name || `${this.key}_${(compilerId += 1)}`; @@ -43,11 +47,12 @@ class ESLintRspackPlugin { this.getContext(compiler), ); const resourceQueries = arrify(this.options.resourceQueryExclude || []); - const excludedResourceQueries = resourceQueries.map((item) => - item instanceof RegExp ? item : new RegExp(item), + const excludedResourceQueries = resourceQueries.map( + (item: RegExp | string) => + item instanceof RegExp ? item : new RegExp(item), ); - const options = { + const options: RunOptions = { ...this.options, exclude: excludedFiles, resourceQueryExclude: excludedResourceQueries, @@ -81,31 +86,27 @@ class ESLintRspackPlugin { }); } - /** - * @param {Compiler} compiler - * @param {Omit & {resourceQueryExclude: RegExp[]}} options - * @param {string[]} wanted - * @param {string[]} exclude - */ - async run(compiler, options, wanted, exclude) { - // @ts-ignore - const isCompilerHooked = compiler.hooks.compilation.taps.find( + async run( + compiler: Compiler, + options: RunOptions, + wanted: string[], + exclude: string[], + ): Promise { + const compilationHook = compiler.hooks + .compilation as CompilationHookWithTaps; + const isCompilerHooked = compilationHook.taps.find( ({ name }) => name === this.key, ); if (isCompilerHooked) return; compiler.hooks.compilation.tap(this.key, async (compilation) => { - /** @type {import('./linter.js').Linter} */ - let lint; - /** @type {import('./linter.js').Reporter} */ - let report; + let lint: Linter; + let report: Reporter; - /** @type {string[]} */ - const files = []; + const files: string[] = []; - /** @type {Error | null} */ - let linterError = null; + let linterError: Error | null = null; let hasLinted = false; const shouldLintAllFiles = this.options.lintAllFiles; @@ -119,7 +120,7 @@ class ESLintRspackPlugin { }) .catch((e) => { linterError = e; - compilation.errors.push(e); + compilation.errors.push(e as RspackError); }); // Register compilation hooks before waiting for linter setup. @@ -141,11 +142,8 @@ class ESLintRspackPlugin { // compilation.hooks.succeedModule.tap(this.key, addFile); // compilation.hooks.stillValidModule.tap(this.key, addFile); - /** - * @param {Module} module - */ - function addFile(module) { - const { resource } = /** @type {NormalModule} */ (module); + function addFile(module: Module): void { + const { resource } = module as NormalModule; if (!resource) return; @@ -173,23 +171,21 @@ class ESLintRspackPlugin { } } } else if (this.options.lintDirtyModulesOnly) { - for (const m of /** @type {Set} */ ( - compilation.builtModules - )) { + for (const m of compilation.builtModules) { addFile(m); } } setupLinter.then(scheduleLint); }); - function scheduleLint() { + function scheduleLint(): void { if (linterError || hasLinted || files.length < 1) return; hasLinted = true; lint(files); } - async function processResults() { + async function processResults(): Promise { await setupLinter; if (linterError) { @@ -201,13 +197,11 @@ class ESLintRspackPlugin { const { errors, warnings, generateReportAsset } = await report(); if (warnings) { - // @ts-ignore - compilation.warnings.push(warnings); + compilation.warnings.push(warnings as RspackError); } if (errors) { - // @ts-ignore - compilation.errors.push(errors); + compilation.errors.push(errors as RspackError); } if (generateReportAsset) await generateReportAsset(compilation); @@ -215,12 +209,7 @@ class ESLintRspackPlugin { }); } - /** - * - * @param {Compiler} compiler - * @returns {string} - */ - getContext(compiler) { + getContext(compiler: Compiler): string { const compilerContext = String(compiler.options.context); const optionContext = this.options.context; diff --git a/src/linter.js b/src/linter.ts similarity index 50% rename from src/linter.js rename to src/linter.ts index 840a98e..c3fe5ea 100644 --- a/src/linter.js +++ b/src/linter.ts @@ -2,33 +2,43 @@ import { dirname, isAbsolute, join } from 'node:path'; import ESLintError from './ESLintError.js'; import { getESLint } from './getESLint.js'; - -/** @typedef {import('eslint').ESLint} ESLint */ -/** @typedef {import('eslint').ESLint.Formatter} Formatter */ -/** @typedef {import('eslint').ESLint.LintResult} LintResult */ -/** @typedef {import('@rspack/core').Compilation} Compilation */ -/** @typedef {import('./options.js').Options} Options */ -/** @typedef {import('./options.js').FormatterFunction} FormatterFunction */ -/** @typedef {(compilation: Compilation) => Promise} GenerateReport */ -/** @typedef {{errors?: ESLintError, warnings?: ESLintError, generateReportAsset?: GenerateReport}} Report */ -/** @typedef {() => Promise} Reporter */ -/** @typedef {(files: string|string[]) => void} Linter */ -/** @typedef {'error' | 'warning'} DiagnosticSeverity */ - -/** - * @param {Options} options - * @param {Compilation} compilation - * @returns {Promise<{lint: Linter, report: Reporter}>} - */ -async function linter(options, compilation) { - /** @type {ESLint} */ - let eslint; - - /** @type {(files: string|string[]) => Promise} */ - let lintFiles; - - /** @type {Promise[]} */ - const rawResults = []; +import type { FormatterFunction, Options, SeverityOptions } from './options.js'; +import type { Compilation, RspackError } from '@rspack/core'; +import type { ESLint } from 'eslint'; + +type Formatter = ESLint.Formatter; +type LintResult = ESLint.LintResult; +export type GenerateReport = (compilation: Compilation) => Promise; +export type Report = { + errors?: ESLintError; + warnings?: ESLintError; + generateReportAsset?: GenerateReport; +}; +export type Reporter = () => Promise; +export type Linter = (files: string | string[]) => void; +type DiagnosticSeverity = 'error' | 'warning'; +type OutputFileSystem = { + mkdir: ( + name: string, + options: { recursive: boolean }, + callback: (err?: NodeJS.ErrnoException | null) => void, + ) => void; + writeFile: ( + name: string, + content: string | Buffer, + callback: (err?: NodeJS.ErrnoException | null) => void, + ) => void; +}; + +async function linter( + options: Options, + compilation: Compilation, +): Promise<{ lint: Linter; report: Reporter }> { + let eslint: ESLint; + + let lintFiles: (files: string | string[]) => Promise; + + const rawResults: Array> = []; try { ({ eslint, lintFiles } = await getESLint(options)); @@ -41,20 +51,16 @@ async function linter(options, compilation) { report, }; - /** - * @param {string | string[]} files - */ - function lint(files) { + function lint(files: string | string[]): void { rawResults.push( lintFiles(files).catch((e) => { - // @ts-ignore - compilation.errors.push(new ESLintError(e.message)); + compilation.errors.push(new ESLintError(e.message) as RspackError); return []; }), ); } - async function report() { + async function report(): Promise { // Filter out ignored files. const results = await removeIgnoredWarnings( eslint, @@ -79,37 +85,32 @@ async function linter(options, compilation) { generateReportAsset, }; - /** - * @param {Compilation} compilation - * @returns {Promise} - */ - async function generateReportAsset({ compiler }) { + async function generateReportAsset({ + compiler, + }: Compilation): Promise { const { outputReport } = options; - /** - * @param {string} name - * @param {string | Buffer} content - */ - const save = (name, content) => - /** @type {Promise} */ ( - new Promise((finish, bail) => { - // @ts-ignore - const { mkdir, writeFile } = compiler.outputFileSystem; - // ensure directory exists - // @ts-ignore - the types for `outputFileSystem` are missing the 3 arg overload - mkdir(dirname(name), { recursive: true }, (err) => { - /* istanbul ignore if */ - if (err) bail(err); - else - writeFile(name, content, (/** @type {any} */ err2) => { - /* istanbul ignore if */ - if (err2) bail(err2); - else finish(); - }); - }); - }) - ); - - if (!outputReport || !outputReport.filePath) { + const save = (name: string, content: string | Buffer): Promise => + new Promise((finish, bail) => { + const { mkdir, writeFile } = + compiler.outputFileSystem as unknown as OutputFileSystem; + // ensure directory exists + mkdir(dirname(name), { recursive: true }, (err) => { + /* istanbul ignore if */ + if (err) bail(err); + else + writeFile(name, content, (err2) => { + /* istanbul ignore if */ + if (err2) bail(err2); + else finish(); + }); + }); + }); + + if ( + !outputReport || + typeof outputReport === 'boolean' || + !outputReport.filePath + ) { return; } @@ -127,14 +128,12 @@ async function linter(options, compilation) { } } -/** - * @param {Formatter} formatter - * @param {{ errors: LintResult[]; warnings: LintResult[]; }} results - * @returns {Promise<{errors?: ESLintError, warnings?: ESLintError}>} - */ -async function formatResults(formatter, results) { - let errors; - let warnings; +async function formatResults( + formatter: Formatter, + results: { errors: LintResult[]; warnings: LintResult[] }, +): Promise<{ errors?: ESLintError; warnings?: ESLintError }> { + let errors: ESLintError | undefined; + let warnings: ESLintError | undefined; if (results.warnings.length > 0) { warnings = new ESLintError(await formatter.format(results.warnings)); } @@ -149,30 +148,25 @@ async function formatResults(formatter, results) { }; } -/** - * @param {Options} options - * @param {LintResult[]} results - * @returns {{errors: LintResult[], warnings: LintResult[]}} - */ -function parseResults(options, results) { - /** @type {LintResult[]} */ - const errors = []; - - /** @type {LintResult[]} */ - const warnings = []; - /** @type {{error: 'error' | 'warning' | 'off', warning: 'error' | 'warning' | 'off'}} */ - const severity = { +function parseResults( + options: Options, + results: LintResult[], +): { errors: LintResult[]; warnings: LintResult[] } { + const errors: LintResult[] = []; + + const warnings: LintResult[] = []; + const severity: Required = { error: 'error', warning: 'warning', ...options.severity, }; results.forEach((file) => { - /** @type {Record<'error' | 'warning', LintResult['messages']>} */ - const messagesByTarget = { - error: [], - warning: [], - }; + const messagesByTarget: Record = + { + error: [], + warning: [], + }; for (const message of file.messages) { const target = @@ -204,18 +198,18 @@ function parseResults(options, results) { }; } -/** - * @param {LintResult} file - * @param {DiagnosticSeverity} severity - * @param {LintResult['messages']} messages - * @returns {LintResult} - */ -function createSeverityResult(file, severity, messages) { - const eslintSeverity = /** @type {1 | 2} */ (severity === 'error' ? 2 : 1); - const normalizedMessages = messages.map((message) => ({ - ...message, - severity: eslintSeverity, - })); +function createSeverityResult( + file: LintResult, + severity: DiagnosticSeverity, + messages: LintResult['messages'], +): LintResult { + const eslintSeverity: 1 | 2 = severity === 'error' ? 2 : 1; + const normalizedMessages: LintResult['messages'] = messages.map( + (message) => ({ + ...message, + severity: eslintSeverity, + }), + ); const fixableCount = normalizedMessages.filter((message) => Boolean(message.fix), ).length; @@ -234,12 +228,10 @@ function createSeverityResult(file, severity, messages) { }; } -/** - * @param {ESLint} eslint - * @param {string|FormatterFunction=} formatter - * @returns {Promise} - */ -async function loadFormatter(eslint, formatter) { +async function loadFormatter( + eslint: ESLint, + formatter?: string | FormatterFunction, +): Promise { if (typeof formatter === 'function') { return { format: formatter }; } @@ -255,12 +247,10 @@ async function loadFormatter(eslint, formatter) { return eslint.loadFormatter(); } -/** - * @param {ESLint} eslint - * @param {LintResult[]} results - * @returns {Promise} - */ -async function removeIgnoredWarnings(eslint, results) { +async function removeIgnoredWarnings( + eslint: ESLint, + results: LintResult[], +): Promise { const filterPromises = results.map(async (result) => { // Short circuit the call to isPathIgnored. // fatal is false for ignored file warnings. @@ -272,6 +262,7 @@ async function removeIgnoredWarnings(eslint, results) { const ignored = messages.length === 0 || (hasWarning && + firstMessage !== undefined && !firstMessage.fatal && !firstMessage.ruleId && !firstMessage.line && @@ -279,20 +270,18 @@ async function removeIgnoredWarnings(eslint, results) { return ignored ? false : result; }); - // @ts-ignore - return (await Promise.all(filterPromises)).filter(Boolean); + return (await Promise.all(filterPromises)).filter( + (result): result is LintResult => Boolean(result), + ); } -/** - * @param {Promise[]} results - * @returns {Promise} - */ -async function flatten(results) { - /** - * @param {LintResult[]} acc - * @param {LintResult[]} list - */ - const flat = (acc, list) => [...acc, ...list]; +async function flatten( + results: Array>, +): Promise { + const flat = (acc: LintResult[], list: LintResult[]): LintResult[] => [ + ...acc, + ...list, + ]; return (await Promise.all(results)).reduce(flat, []); } diff --git a/src/options.js b/src/options.js deleted file mode 100644 index de4d0cc..0000000 --- a/src/options.js +++ /dev/null @@ -1,147 +0,0 @@ -/** @typedef {import("eslint").ESLint.Options} ESLintOptions */ -/** @typedef {import('eslint').ESLint.LintResult} LintResult */ -/** @typedef {import('eslint').ESLint.LintResultData} LintResultData */ - -/** - * @callback FormatterFunction - * @param {LintResult[]} results - * @param {LintResultData=} data - * @returns {string} - */ - -/** - * @typedef {Object} OutputReport - * @property {string=} filePath - * @property {string|FormatterFunction=} formatter - */ - -/** @typedef {'error' | 'warning' | 'off'} Severity */ - -/** - * @typedef {Object} SeverityOptions - * @property {Severity=} error - * @property {Severity=} warning - */ - -/** - * @typedef {Object} PluginOptions - * @property {string=} context - * @property {string=} eslintPath - * @property {string|string[]=} exclude - * @property {string|string[]=} extensions - * @property {string|string[]=} files - * @property {boolean=} fix - * @property {string|FormatterFunction=} formatter - * @property {boolean=} lintDirtyModulesOnly - * @property {boolean=} lintAllFiles - * @property {SeverityOptions=} severity - * @property {OutputReport=} outputReport - * @property {RegExp|RegExp[]=} resourceQueryExclude - * @property {string=} configType - */ - -/** @typedef {PluginOptions & ESLintOptions} Options */ - -/** @type {Record} */ -const removedOptionMessages = { - emitError: "Use `severity.error: 'off'` to hide ESLint errors.", - emitWarning: "Use `severity.warning: 'off'` to hide ESLint warnings.", - failOnError: - "ESLint errors are emitted as Rspack errors by default. Use `severity.error: 'warning'` to keep ESLint error output without failing the build.", - failOnWarning: - "Use `severity.warning: 'error'` to emit ESLint warnings as Rspack errors.", - quiet: "Use `severity.warning: 'off'` to hide ESLint warnings.", -}; - -const pluginOnlyOptionKeys = [ - 'configType', - 'context', - 'eslintPath', - 'exclude', - 'resourceQueryExclude', - 'files', - 'formatter', - 'lintDirtyModulesOnly', - 'lintAllFiles', - 'severity', - 'outputReport', -]; - -/** - * @param {Options} pluginOptions - * @returns {PluginOptions} - */ -function getOptions(pluginOptions) { - assertNoRemovedOptions(pluginOptions); - - /** @type {{error: Severity, warning: Severity}} */ - const defaultSeverity = { - error: 'error', - warning: 'warning', - }; - /** @type {{error: Severity, warning: Severity}} */ - const severity = { - ...defaultSeverity, - ...pluginOptions.severity, - }; - - const options = { - cache: true, - cacheLocation: 'node_modules/.cache/eslint-rspack-plugin/.eslintcache', - configType: 'flat', - extensions: 'js', - resourceQueryExclude: [], - ...pluginOptions, - severity, - }; - - return options; -} - -/** - * @param {Options} loaderOptions - * @returns {ESLintOptions} - */ -function getESLintOptions(loaderOptions) { - assertNoRemovedOptions(loaderOptions); - - const eslintOptions = { ...loaderOptions }; - - for (const option of pluginOnlyOptionKeys) { - // @ts-ignore - delete eslintOptions[option]; - } - - // Some options aren't available in flat mode - if (loaderOptions.configType === 'flat') { - delete eslintOptions.extensions; - } - - return eslintOptions; -} - -/** - * @param {object} options - * @returns {void} - */ -function assertNoRemovedOptions(options) { - const removedOptions = Object.keys(removedOptionMessages).filter((option) => - Object.prototype.hasOwnProperty.call(options, option), - ); - - if (removedOptions.length < 1) return; - - const details = removedOptions.map( - (option) => `- \`${option}\` was removed. ${removedOptionMessages[option]}`, - ); - - throw new Error( - [ - 'eslint-rspack-plugin received removed options.', - ...details, - 'Use the `severity` option to control ESLint diagnostic output.', - ].join('\n'), - ); -} - -export { getOptions, getESLintOptions }; diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..01cd648 --- /dev/null +++ b/src/options.ts @@ -0,0 +1,137 @@ +import type { ESLint } from 'eslint'; + +type ESLintOptions = ESLint.Options; +type LintResult = ESLint.LintResult; +type LintResultData = ESLint.LintResultData; + +export type FormatterFunction = ( + results: LintResult[], + data?: LintResultData, +) => string; + +export type OutputReport = + | boolean + | { + filePath?: string; + formatter?: string | FormatterFunction; + }; + +export type Severity = 'error' | 'warning' | 'off'; + +export type SeverityOptions = { + error?: Severity; + warning?: Severity; +}; + +export type PluginOptions = { + context?: string; + eslintPath?: string; + exclude?: string | string[]; + extensions?: string | string[]; + files?: string | string[]; + fix?: boolean; + formatter?: string | FormatterFunction; + lintDirtyModulesOnly?: boolean; + lintAllFiles?: boolean; + severity?: SeverityOptions; + outputReport?: OutputReport; + resourceQueryExclude?: RegExp | RegExp[]; + configType?: string; +}; + +export type Options = PluginOptions & ESLintOptions; + +export type ResolvedOptions = Options & { + cache: boolean; + cacheLocation: string; + configType: string; + extensions: string | string[]; + resourceQueryExclude: RegExp | RegExp[]; + severity: Required; +}; + +const removedOptionMessages: Record = { + emitError: "Use `severity.error: 'off'` to hide ESLint errors.", + emitWarning: "Use `severity.warning: 'off'` to hide ESLint warnings.", + failOnError: + "ESLint errors are emitted as Rspack errors by default. Use `severity.error: 'warning'` to keep ESLint error output without failing the build.", + failOnWarning: + "Use `severity.warning: 'error'` to emit ESLint warnings as Rspack errors.", + quiet: "Use `severity.warning: 'off'` to hide ESLint warnings.", +}; + +const pluginOnlyOptionKeys: Array = [ + 'configType', + 'context', + 'eslintPath', + 'exclude', + 'resourceQueryExclude', + 'files', + 'formatter', + 'lintDirtyModulesOnly', + 'lintAllFiles', + 'severity', + 'outputReport', +]; + +function getOptions(pluginOptions: Options): ResolvedOptions { + assertNoRemovedOptions(pluginOptions); + + const defaultSeverity: Required = { + error: 'error', + warning: 'warning', + }; + const severity: Required = { + ...defaultSeverity, + ...pluginOptions.severity, + }; + + return { + cache: true, + cacheLocation: 'node_modules/.cache/eslint-rspack-plugin/.eslintcache', + configType: 'flat', + extensions: 'js', + resourceQueryExclude: [], + ...pluginOptions, + severity, + }; +} + +function getESLintOptions(loaderOptions: Options): ESLintOptions { + assertNoRemovedOptions(loaderOptions); + + const eslintOptions: Options = { ...loaderOptions }; + + for (const option of pluginOnlyOptionKeys) { + delete eslintOptions[option]; + } + + // Some options aren't available in flat mode + if (loaderOptions.configType === 'flat') { + delete eslintOptions.extensions; + } + + return eslintOptions; +} + +function assertNoRemovedOptions(options: object): void { + const removedOptions = Object.keys(removedOptionMessages).filter((option) => + Object.prototype.hasOwnProperty.call(options, option), + ); + + if (removedOptions.length < 1) return; + + const details = removedOptions.map( + (option) => `- \`${option}\` was removed. ${removedOptionMessages[option]}`, + ); + + throw new Error( + [ + 'eslint-rspack-plugin received removed options.', + ...details, + 'Use the `severity` option to control ESLint diagnostic output.', + ].join('\n'), + ); +} + +export { getOptions, getESLintOptions }; diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index b981ae9..0000000 --- a/src/utils.js +++ /dev/null @@ -1,91 +0,0 @@ -import { statSync } from 'node:fs'; -import { resolve } from 'node:path'; - -import normalizePath from 'normalize-path'; - -/** - * @template T - * @param {T} value - * @return { - T extends (null | undefined) - ? [] - : T extends string - ? [string] - : T extends readonly unknown[] - ? T - : T extends Iterable - ? T[] - : [T] - } - */ -/* istanbul ignore next */ -function arrify(value) { - if (value === null || value === undefined) { - // @ts-ignore - return []; - } - - if (Array.isArray(value)) { - // @ts-ignore - return value; - } - - if (typeof value === 'string') { - // @ts-ignore - return [value]; - } - - // @ts-ignore - if (typeof value[Symbol.iterator] === 'function') { - // @ts-ignore - return [...value]; - } - - // @ts-ignore - return [value]; -} - -/** - * @param {string|string[]} files - * @param {string} context - * @returns {string[]} - */ -function parseFiles(files, context) { - return arrify(files).map((/** @type {string} */ file) => - normalizePath(resolve(context, file)), - ); -} - -/** - * @param {string|string[]} patterns - * @param {string|string[]} extensions - * @returns {string[]} - */ -function parseFoldersToGlobs(patterns, extensions = []) { - const extensionsList = arrify(extensions); - const [prefix, postfix] = extensionsList.length > 1 ? ['{', '}'] : ['', '']; - const extensionsGlob = extensionsList - .map((/** @type {string} */ extension) => extension.replace(/^\./u, '')) - .join(','); - - return arrify(patterns).map((/** @type {string} */ pattern) => { - try { - // The patterns are absolute because they are prepended with the context. - const stats = statSync(pattern); - /* istanbul ignore else */ - if (stats.isDirectory()) { - return pattern.replace( - /[/\\]*?$/u, - `/**${ - extensionsGlob ? `/*.${prefix + extensionsGlob + postfix}` : '' - }`, - ); - } - } catch (_) { - // Return the pattern as is on error. - } - return pattern; - }); -} - -export { arrify, parseFiles, parseFoldersToGlobs }; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..fd13e20 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,72 @@ +import { statSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import normalizePath from 'normalize-path'; + +type Arrify = T extends null | undefined + ? [] + : T extends string + ? [string] + : T extends readonly unknown[] + ? T + : T extends Iterable + ? Item[] + : [T]; + +/* istanbul ignore next */ +function arrify(value: T): Arrify { + if (value === null || value === undefined) { + return [] as Arrify; + } + + if (Array.isArray(value)) { + return value as Arrify; + } + + if (typeof value === 'string') { + return [value] as Arrify; + } + + const iterable = value as unknown as Iterable; + if (typeof iterable[Symbol.iterator] === 'function') { + return [...iterable] as Arrify; + } + + return [value] as Arrify; +} + +function parseFiles(files: string | string[], context: string): string[] { + return arrify(files).map((file) => normalizePath(resolve(context, file))); +} + +function parseFoldersToGlobs( + patterns: string | string[], + extensions: string | string[] = [], +): string[] { + const extensionsList = arrify(extensions); + const [prefix, postfix] = extensionsList.length > 1 ? ['{', '}'] : ['', '']; + const extensionsGlob = extensionsList + .map((extension) => extension.replace(/^\./u, '')) + .join(','); + + return arrify(patterns).map((pattern) => { + try { + // The patterns are absolute because they are prepended with the context. + const stats = statSync(pattern); + /* istanbul ignore else */ + if (stats.isDirectory()) { + return pattern.replace( + /[/\\]*?$/u, + `/**${ + extensionsGlob ? `/*.${prefix + extensionsGlob + postfix}` : '' + }`, + ); + } + } catch (_) { + // Return the pattern as is on error. + } + return pattern; + }); +} + +export { arrify, parseFiles, parseFoldersToGlobs };