diff --git a/README.md b/README.md index e58572e..a30f90d 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,51 @@ A collection of optionated in-house linting rules. ESLint configuration is provided in the `eslint.config.js`, aka. "Flat Config" format. -```js -// eslint.config.js -module.exports = require('@atmina/linting/eslint/recommended'); +## Quickstart + +1. Install + ```sh + yarn add -D @atmina/linting + # or + pnpm add -D @atmina/linting + ``` +2. Run the CLI tool + ```sh + yarn linting + # or + pnpm linting + ``` + This will set up the necessary dependencies and configurations for you. + +## IDE Integration +In VS Code, use these workspace settings: + +```json5 +{ + "eslint.experimental.useFlatConfig": true, + "eslint.workingDirectories": [ + // In a monorepo, specify linted packages here + "frontend" + ], + // Optional + "editor.codeActionsOnSave": { + "source.fixAll": true + } +} ``` -```js -// .prettierrc.js -module.exports = require('@atmina/linting/prettier'); -``` \ No newline at end of file +In WebStorm, go to Settings and enable ESLint (Select "Automatic ESLint Configuration"). If desired, enable +"Run eslint --fix on Save". + +## Development +When working on `linting`, it may be useful to test its effects in a different project. To do so, link your local copy +of `linting` in the other project's package.json (works with pnpm and yarn). This may require restarting your IDE once +after setting up the link. + +```JSON +{ + "devDependencies": { + "@atmina/linting": "link:local/path/to/linting" + } +} +``` diff --git a/bin/linting.mjs b/bin/linting.mjs new file mode 100644 index 0000000..0106277 --- /dev/null +++ b/bin/linting.mjs @@ -0,0 +1,136 @@ +import { dirname, resolve } from "node:path"; +import { readFile, writeFile } from "node:fs/promises"; +import { pkgUp } from "pkg-up"; +import { parseNi, run as runNi } from "@antfu/ni"; +// ni uses this so we do too +import prompts from "@posva/prompts"; + +const TEMPLATE_HEADER = '/* eslint-disable */'; + +const TEMPLATE_CJS = `${TEMPLATE_HEADER} + +/** + * @type {import('eslint').Linter.FlatConfig[]} + */ +module.exports = [ +{{configs}} +]; +`; + +const TEMPLATE_ESM = `${TEMPLATE_HEADER} + +{{imports}} + +/** + * @type {import('eslint').Linter.FlatConfig[]} + */ +const config = [ +{{configs}} +]; + +export default config; +`; + + + +const ni = (args) => { + return runNi(parseNi, args); +} + +const confirm = async (message) => { + return (await prompts({type: 'confirm', name: 'confirm', message: message, initial: true })).confirm; +} + +const createConfig = (pkg) => { + const isEsm = pkg.type === 'module'; + console.log('Format (based on package.json -> type):', isEsm ? 'ES Module' : 'CommonJS'); + const dependencies = pkg.dependencies ?? {}; + const template = isEsm ? TEMPLATE_ESM : TEMPLATE_CJS; + const configs = isEsm + ? ['...recommended'] + : [`...require('@atmina/linting/eslint/recommended')`]; + const imports = isEsm + ? [`import recommended from '@atmina/linting/eslint/recommended.js'`] + : []; + + if (dependencies['tailwindcss']) { + console.log('+ Tailwind CSS'); + if (isEsm) { + imports.push(`import tailwind from '@atmina/linting/eslint/tailwind.js'`); + configs.push('tailwind'); + } else { + configs.push(`require('@atmina/linting/eslint/tailwind')`); + } + } + + // React config is included in the Next.js config + if (dependencies['react'] && !dependencies['next']) { + console.log('+ React'); + if (isEsm) { + imports.push(`import react from '@atmina/linting/eslint/react.js'`); + configs.push('react'); + } else { + configs.push(`require('@atmina/linting/eslint/react')`); + } + } + + if (dependencies['next']) { + console.log('+ Next.js'); + if (isEsm) { + imports.push(`import next from '@atmina/linting/eslint/next.js'`); + imports.push(`import nextPlugin from '@next/eslint-plugin-next'`); + configs.push('next(nextPlugin)'); + } else { + configs.push(`require('@atmina/linting/eslint/next')(require('@next/eslint-plugin-next'))`); + } + } + + return template + .replace('{{configs}}', configs.map(config => ` ${config},`).join('\n')) + .replace('{{imports}}', imports.map(imp => `${imp};`).join('\n')); +} + +const main = async () => { + const packagePath = await pkgUp(); + if (!packagePath) { + console.error('No package.json found'); + return; + } + let pkg = JSON.parse(await readFile(packagePath, 'utf-8')); + if (!await confirm(`This will set up linting in the "${pkg.name ?? ''}" package. Continue?`)) { + return; + } + await ni([ + // Install as devDependencies + '-D', + 'eslint', + 'prettier', + // Enables autocomplete in eslint.config.js + '@types/eslint', + '@atmina/linting' + ]); + // Read again after package update + pkg = JSON.parse(await readFile(packagePath, 'utf-8')); + const configPath = resolve(dirname(packagePath), 'eslint.config.js'); + let hasConfig = false; + try { + hasConfig = !!(await readFile(configPath, 'utf-8')); + } catch (e) { + if (e.code !== 'ENOENT') { + console.error(e); + return; + } + } + if (!hasConfig || await confirm('Overwrite existing eslint.config.js?')) { + const config = createConfig(pkg); + await writeFile(configPath, config, 'utf-8'); + console.log('Created eslint.config.js'); + } + + pkg['prettier'] = '@atmina/linting/prettier'; + await writeFile(packagePath, JSON.stringify(pkg, null, 2), 'utf-8'); + console.log('Configured Prettier'); + console.log('ATMINA Score increased! 📈'); +} + +void main(); diff --git a/package.json b/package.json index 5c97c6a..77ab628 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "description": "A collection of opinionated in-house linting rules.", "main": "index.js", "scripts": {}, + "bin": { + "linting": "bin/linting.mjs" + }, "keywords": [ "eslint", "prettier", @@ -26,7 +29,9 @@ "typescript": "5.1.6" }, "dependencies": { + "@antfu/ni": "^0.21.4", "@eslint/js": "^8.35.0", + "@posva/prompts": "^2.4.4", "@typescript-eslint/eslint-plugin": "^5.54.1", "@typescript-eslint/parser": "^5.54.1", "eslint-config-prettier": "^8.7.0", @@ -35,7 +40,8 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.2", "eslint-plugin-tailwindcss": "^3.10.1", - "globals": "^13.20.0" + "globals": "^13.20.0", + "pkg-up": "^4.0.0" }, "peerDependencies": { "eslint": "^8.35.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37cf0c2..6a01f8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,15 @@ settings: excludeLinksFromLockfile: false dependencies: + '@antfu/ni': + specifier: ^0.21.4 + version: 0.21.4 '@eslint/js': specifier: ^8.35.0 version: 8.35.0 + '@posva/prompts': + specifier: ^2.4.4 + version: 2.4.4 '@typescript-eslint/eslint-plugin': specifier: ^5.54.1 version: 5.54.1(@typescript-eslint/parser@5.54.1)(eslint@8.44.0)(typescript@5.1.6) @@ -35,6 +41,9 @@ dependencies: globals: specifier: ^13.20.0 version: 13.20.0 + pkg-up: + specifier: ^4.0.0 + version: 4.0.0 devDependencies: '@types/eslint': @@ -69,6 +78,11 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + /@antfu/ni@0.21.4: + resolution: {integrity: sha512-O0Uv9LbLDSoEg26fnMDdDRiPwFJnQSoD4WnrflDwKCJm8Cx/0mV4cGxwBLXan5mGIrpK4Dd7vizf4rQm0QCEAA==} + hasBin: true + dev: false + /@eslint-community/eslint-utils@4.4.0(eslint@8.44.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -182,6 +196,14 @@ packages: tslib: 2.5.0 dev: false + /@posva/prompts@2.4.4: + resolution: {integrity: sha512-8aPwklhbSV2VN/NQMBNFkuo8+hlJVdcFRXp4NCIfdcahh3qNEcaSoD8qXjru0OlN1sONJ7le7p6+YUbALaG6Mg==} + engines: {node: '>= 14'} + dependencies: + kleur: 4.1.5 + sisteransi: 1.0.5 + dev: false + /@types/eslint@8.40.2: resolution: {integrity: sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==} dependencies: @@ -977,6 +999,14 @@ packages: locate-path: 6.0.0 path-exists: 4.0.0 + /find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + dev: false + /flat-cache@3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1374,6 +1404,11 @@ packages: object.assign: 4.1.4 dev: false + /kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: false + /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1394,6 +1429,13 @@ packages: dependencies: p-locate: 5.0.0 + /locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-locate: 6.0.0 + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -1555,12 +1597,26 @@ packages: dependencies: yocto-queue: 0.1.0 + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: false + /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} dependencies: p-limit: 3.1.0 + /p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-limit: 4.0.0 + dev: false + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1571,6 +1627,11 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + /path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -1602,6 +1663,13 @@ packages: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} + /pkg-up@4.0.0: + resolution: {integrity: sha512-N4zdA4sfOe6yCv+ulPCmpnIBQ5I60xfhDr1otdBBhKte9QtEf3bhfrfkW7dTb+IQ0iEx4ZDzas0kc1o5rdWpYg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + find-up: 6.3.0 + dev: false + /postcss-import@15.1.0(postcss@8.4.23): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -1818,6 +1886,10 @@ packages: object-inspect: 1.12.3 dev: false + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2083,3 +2155,8 @@ packages: /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: false