├── .gitignore ├── .eslintrc.json ├── .github └── workflows │ └── 18f-eslint.yml ├── lib ├── hasReact.js └── getPackagePath.js ├── package.json ├── LICENSE.md ├── CONTRIBUTING.md ├── install.js ├── index.js ├── install-action.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .DS_Store 4 | .vscode -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "airbnb-base", 8 | "prettier" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/18f-eslint.yml: -------------------------------------------------------------------------------- 1 | name: lint action 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: 18F ESLint 8 | runs-on: ubuntu-latest 9 | container: node:14 10 | steps: 11 | - uses: actions/checkout@af513c7a 12 | - name: install dependencies 13 | run: npm install 14 | - name: lint 15 | run: npm run lint 16 | -------------------------------------------------------------------------------- /lib/hasReact.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const getPackagePath = require("./getPackagePath"); 3 | 4 | // Read the nearest package.json and see if it has React in its dependencies 5 | // or dev dependencies. If it does, we should assume this is a React app. 6 | module.exports = () => { 7 | const packagePath = getPackagePath(); 8 | const packageContents = JSON.parse( 9 | fs.readFileSync(packagePath, { encoding: "utf-8" }) 10 | ); 11 | const hasReact = 12 | Object.keys(packageContents.dependencies || {}).includes("react") || 13 | Object.keys(packageContents.devDependencies || {}).includes("react"); 14 | 15 | return hasReact; 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@18f/18f-eslint", 3 | "version": "1.2.4", 4 | "description": "Easy eslint for 18F projects", 5 | "main": "index.js", 6 | "bin": { 7 | "install-action": "install-action.js", 8 | "18f-eslint": "index.js" 9 | }, 10 | "scripts": { 11 | "lint": "eslint '**/*.js'", 12 | "postinstall": "./install.js" 13 | }, 14 | "keywords": [ 15 | "18f", 16 | "eslint" 17 | ], 18 | "author": "Greg Walker ", 19 | "license": "CC0-1.0", 20 | "dependencies": { 21 | "eslint": "^7.11.0", 22 | "eslint-config-airbnb": "^18.2.0", 23 | "eslint-config-airbnb-base": "^14.2.0", 24 | "eslint-config-prettier": "^6.12.0", 25 | "eslint-plugin-import": "^2.22.1", 26 | "eslint-plugin-jsx-a11y": "^6.3.1", 27 | "eslint-plugin-react": "^7.21.4", 28 | "eslint-plugin-react-hooks": "^4.1.2", 29 | "prettier": "^2.1.2" 30 | }, 31 | "prettier": {} 32 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | As a work of the United States Government, this project is in the 2 | public domain within the United States. 3 | 4 | Additionally, we waive copyright and related rights in the work 5 | worldwide through the CC0 1.0 Universal public domain dedication. 6 | 7 | ## CC0 1.0 Universal Summary 8 | 9 | This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode). 10 | 11 | ### No Copyright 12 | 13 | The person who associated a work with this deed has dedicated the work to 14 | the public domain by waiving all of his or her rights to the work worldwide 15 | under copyright law, including all related and neighboring rights, to the 16 | extent allowed by law. 17 | 18 | You can copy, modify, distribute and perform the work, even for commercial 19 | purposes, all without asking permission. 20 | 21 | ### Other Information 22 | 23 | In no way are the patent or trademark rights of any person affected by CC0, 24 | nor are the rights that other persons may have in the work or in how the 25 | work is used, such as publicity or privacy rights. 26 | 27 | Unless expressly stated otherwise, the person who associated a work with 28 | this deed makes no warranties about the work, and disclaims liability for 29 | all uses of the work, to the fullest extent permitted by applicable law. 30 | When using or citing the work, you should not imply endorsement by the 31 | author or the affirmer. 32 | -------------------------------------------------------------------------------- /lib/getPackagePath.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | module.exports = () => { 5 | // Start at the current path. We'll walk backwards from here. 6 | const packagePath = [process.env.INIT_CWD || "."]; 7 | let lastPath = path.resolve(packagePath[0]); 8 | 9 | try { 10 | // Keep drilling down while the path still doesn't include package.json 11 | while (!fs.readdirSync(packagePath.join("/")).includes("package.json")) { 12 | // Insert a ".." at the front of the path 13 | packagePath.unshift(".."); 14 | 15 | const newPath = path.resolve(packagePath.join("/")); 16 | 17 | // If the last path we looked at is the same as the next path we're going 18 | // to look at, we can bail out because we've reached the top of the 19 | // directory tree. Weirdly, Node.js is happy to let you keep adding ".."s 20 | // forever, until the path length just gets too long. So, we should add 21 | // a short-circuit for that. 22 | if (newPath === lastPath) { 23 | throw new Error("cannot go further up the directory tree"); 24 | } 25 | lastPath = newPath; 26 | } 27 | } catch (e) { 28 | // If we can't find a package.json in the current working directory or 29 | // anywhere further up the directory tree, just bail out altogether. 30 | console.log(`Could not find package.json: ${e.message}`); // eslint-disable-line no-console 31 | process.exit(1); 32 | } 33 | 34 | // Otherwise, return the path to the package.json file. 35 | return path.resolve(packagePath.join("/"), "package.json"); 36 | }; 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Welcome! 2 | 3 | We're so glad you're thinking about contributing to an 18F open source project! 4 | If you're unsure about anything, just ask -- or submit the issue or pull 5 | request anyway. The worst that can happen is you'll be politely asked to 6 | change something. We love all friendly contributions. 7 | 8 | We want to ensure a welcoming environment for all of our projects. Our staff 9 | follow the [TTS Code of Conduct](https://github.com/18F/code-of-conduct/blob/master/code-of-conduct.md) 10 | and all contributors should do the same. 11 | 12 | We encourage you to read this project's CONTRIBUTING policy (you are here), its 13 | [LICENSE](LICENSE.md), and its [README](README.md). 14 | 15 | If you have any questions or want to read more, check out the 16 | [18F Open Source Policy GitHub repository](https://github.com/18f/open-source-policy), 17 | or just [shoot us an email](mailto:18f@gsa.gov). 18 | 19 | ## Contributing 20 | 21 | We take contributions via Github pull requests into this repo. In order to 22 | create a pull request, you should first fork this repo into your own 23 | organization or user account. 24 | 25 | When you open your pull request, please tell us a little about what you're 26 | changing or adding so we can know what we're looking at. Someone from the 27 | 18F team will have to review the pull request before we can merge it which 28 | might take a little while - please be patient with us! 29 | 30 | ## Public domain 31 | 32 | This project is in the public domain within the United States, and 33 | copyright and related rights in the work worldwide are waived through 34 | the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 35 | 36 | All contributions to this project will be released under the CC0 37 | dedication. By submitting a pull request, you are agreeing to comply 38 | with this waiver of copyright interest. 39 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const hasReact = require("./lib/hasReact"); 5 | 6 | const npx = /\/_npx\//.test(process.env.npm_config_globalconfig); 7 | if (npx || process.env.CI) { 8 | process.exit(0); 9 | } 10 | 11 | const pathRoot = process.env.INIT_CWD || path.resolve("../../", __dirname); 12 | 13 | const eslintPath = path.join(pathRoot, ".eslintrc.json"); 14 | const packagePath = path.join(pathRoot, "package.json"); 15 | 16 | fs.readFile(eslintPath, { encoding: "utf-8" }, (rcErr, rcData) => { 17 | if (rcErr) { 18 | if (rcErr.code !== "ENOENT") { 19 | throw rcErr; 20 | } 21 | } 22 | 23 | const rc = rcData ? JSON.parse(rcData) : {}; 24 | const react = hasReact(); 25 | 26 | if (!rc.extends) { 27 | rc.extends = []; 28 | } 29 | 30 | const config = { 31 | ...rc, 32 | env: { 33 | es6: true, 34 | [react ? "browser" : "node"]: true, 35 | ...rc.env, 36 | }, 37 | extends: Array.from( 38 | new Set( 39 | [ 40 | react ? "airbnb" : "airbnb-base", 41 | "prettier", 42 | react ? "prettier/react" : false, 43 | ...rc.extends, 44 | ].filter((extend) => extend !== false) 45 | ) 46 | ), 47 | }; 48 | 49 | fs.writeFile( 50 | eslintPath, 51 | JSON.stringify(config, null, 2), 52 | { 53 | encoding: "utf-8", 54 | }, 55 | (err) => { 56 | if (err) { 57 | throw err; 58 | } 59 | } 60 | ); 61 | 62 | fs.readFile(packagePath, { encoding: "utf-8" }, (pkgErr, pkgData) => { 63 | if (pkgErr) { 64 | throw pkgErr; 65 | } 66 | 67 | const packageJson = JSON.parse(pkgData); 68 | const newPackage = { 69 | ...packageJson, 70 | prettier: { 71 | ...packageJson.prettier, 72 | }, 73 | }; 74 | 75 | fs.writeFile( 76 | packagePath, 77 | JSON.stringify(newPackage, null, 2), 78 | { 79 | encoding: "utf-8", 80 | }, 81 | (err) => { 82 | if (err) { 83 | throw err; 84 | } 85 | } 86 | ); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { ESLint } = require("eslint"); 3 | const hasReact = require("./lib/hasReact"); 4 | 5 | // Allow console.log here, since it's a CLI application. 6 | /* eslint-disable no-console */ 7 | 8 | // This separate runner function exists solely so we can have async/await 9 | // behavior. Node.js still doesn't have top-level await and wrapping all of this 10 | // in Promise.then()s is clunkier. So... this is what we got. 11 | const run = async (lintPaths) => { 12 | const react = hasReact(); 13 | 14 | // Setup the base configuration. All of this can be overridden by the 15 | // project's local .eslintrc.* config, but these are decent defaults. 16 | const baseConfig = { 17 | env: { 18 | es6: true, 19 | [react ? "browser" : "node"]: true, 20 | }, 21 | extends: [ 22 | react ? "airbnb" : "airbnb-base", 23 | "prettier", 24 | react ? "prettier/react" : false, 25 | ].filter((extend) => extend !== false), 26 | }; 27 | 28 | try { 29 | const eslint = new ESLint({ baseConfig }); 30 | const results = await eslint.lintFiles(lintPaths); 31 | const formatter = await eslint.loadFormatter("stylish"); 32 | const resultText = formatter.format(results); 33 | console.log(resultText); 34 | 35 | // Non-zero exit if there are errors. 36 | if (results.some((result) => result.errorCount > 0)) { 37 | process.exit(1); 38 | } 39 | } catch (e) { 40 | // If the eslint base configs and/or plugins are not installed locally in 41 | // the project being linted, the constructor above will throw an exception. 42 | // Since that seems like the sort of thing people are likely to run into, 43 | // special-case it so we can tell them how to fix it. 44 | // (E.g., you might get here by running "npx @18f/eslint" without installing 45 | // it locally first, which would be very tempting, no doubt.) 46 | if (/^Failed to load config "[^"]+" to extend from\./.test(e.message)) { 47 | console.log(` 48 | It looks like some eslint configurations are not installed. Did you remember 49 | to install @18f/eslint locally? Since version 6, eslint has strongly 50 | recommended installing base configurations and plugins locally, even when 51 | using a globally-installed eslint instance.`); 52 | console.log(); 53 | } 54 | 55 | // In any case, re-throw the error so that the whole thing shows up in the 56 | // console, so someone can debug it. 57 | throw e; 58 | } 59 | }; 60 | 61 | // 0 is node, 1 is script path, 2+ are args. We'll use those as linting paths. 62 | const [, , ...paths] = process.argv; 63 | run(paths.length > 0 ? paths : ["**/*.js"]); 64 | -------------------------------------------------------------------------------- /install-action.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const childProcess = require("child_process"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | // Allow console.log here, since it's a CLI application. 7 | /* eslint-disable no-console */ 8 | 9 | const getGitRoot = () => 10 | new Promise((resolve) => { 11 | childProcess.exec("git rev-parse --show-toplevel", (_, stdout) => { 12 | resolve(`${stdout}`.trim()); 13 | }); 14 | }); 15 | 16 | const mkdir = (dir) => 17 | new Promise((resolve, reject) => { 18 | fs.mkdir(dir, (err) => { 19 | if (err) { 20 | reject(err); 21 | } else { 22 | resolve(); 23 | } 24 | }); 25 | }); 26 | 27 | const getWorkflowFile = async () => { 28 | const root = await getGitRoot(); 29 | const gh = path.join(root, ".github"); 30 | const wf = path.join(gh, "workflows"); 31 | const ym = path.join(wf, "18f-eslint.yml"); 32 | 33 | return new Promise((resolve, reject) => { 34 | fs.access(gh, async (ghErr) => { 35 | if (ghErr && ghErr.code === "ENOENT") { 36 | await mkdir(gh); 37 | } 38 | 39 | fs.access(wf, async (wfErr) => { 40 | if (wfErr && wfErr.code === "ENOENT") { 41 | await mkdir(wf); 42 | } 43 | 44 | fs.access(ym, async (ymErr) => { 45 | if (!ymErr) { 46 | return reject( 47 | new Error("You already have an 18F-eslint workflow defined") 48 | ); 49 | } 50 | 51 | return resolve(ym); 52 | }); 53 | }); 54 | }); 55 | }); 56 | }; 57 | 58 | const findPackageFiles = (dir) => 59 | new Promise((resolve) => { 60 | fs.readdir(dir, { withFileTypes: true }, async (_, files) => { 61 | if (files.some((file) => file.name === "package.json")) { 62 | return resolve([dir]); 63 | } 64 | 65 | const dirs = files 66 | .filter((file) => file.isDirectory()) 67 | .map((subdir) => subdir.name) 68 | .filter((subdir) => subdir !== "node_modules" && subdir !== ".git") 69 | .map((subdir) => path.join(dir, subdir)); 70 | 71 | const packages = await Promise.all( 72 | dirs.map(findPackageFiles).filter((subdir) => subdir !== null) 73 | ); 74 | if (packages.length === 0) { 75 | return resolve(); 76 | } 77 | 78 | const r = packages 79 | .filter((pkg) => !!pkg && pkg.length > 0) 80 | .map((pkgs) => pkgs[0]); 81 | return resolve(r); 82 | }); 83 | }); 84 | 85 | const writeWorkflow = async (paths) => { 86 | const workflowPath = await getWorkflowFile(); 87 | 88 | const workflow = `name: lint action 89 | 90 | on: [pull_request] 91 | 92 | jobs: 93 | lint: 94 | name: 18F ESLint 95 | runs-on: ubuntu-latest 96 | container: node:14 97 | steps: 98 | - uses: actions/checkout@af513c7a 99 | ${paths 100 | .map( 101 | (dir) => ` - uses: 18f/18f-eslint-action@v1.1.0 102 | with: 103 | lint-glob: "**/*.js" 104 | working-directory: ${dir}` 105 | ) 106 | .join("\n")} 107 | `; 108 | fs.writeFile(workflowPath, workflow, { encoding: "utf-8" }, (err) => { 109 | if (err) { 110 | return Promise.reject(err); 111 | } 112 | return Promise.resolve(); 113 | }); 114 | }; 115 | 116 | const run = async () => { 117 | try { 118 | await getWorkflowFile(); 119 | } catch (e) { 120 | console.log(e.message); 121 | return; 122 | } 123 | 124 | const root = await getGitRoot(); 125 | const packageDirs = await findPackageFiles(root); 126 | const packageRelativeDirs = packageDirs.map((dir) => dir.replace(root, ".")); 127 | writeWorkflow(packageRelativeDirs); 128 | }; 129 | 130 | run(); 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 18F eslint 2 | 3 | This app wraps the [eslint configuration described in the TTS engineering practices guide](https://engineering.18f.gov/javascript/style/), 4 | to make it easier to get started. To use, first install: 5 | 6 | ```shell 7 | npm install @18f/eslint 8 | ``` 9 | 10 | This will install all the appropriate base configurations, plugins, and 11 | prettier. (Since eslint 6, installing these in your local project is 12 | recommended, even if you're using a globally-installed eslint or running with 13 | `npx`.) It will also create (or update) a `.eslintrc` file with the base 14 | configuration and add a `prettier` configuration to your `package.json`. (This is 15 | primarily to enable code editors to pick it up.) 16 | 17 | To run the 18F-configured eslint on your application, you can either use `npx`, 18 | or add it as a script to your `package.json`. By default, it will lint all 19 | files supported by eslint starting at the current directory, but you can also 20 | specify paths to lint in the command line arguments: 21 | 22 | ```shell 23 | # Lint all the things 24 | npx @18f/18f-eslint 25 | 26 | # Lint some of the things 27 | npx @18f/18f-eslint src 28 | ``` 29 | 30 | Or in `package.json`: 31 | 32 | ```json 33 | { 34 | ... 35 | "scripts": { 36 | "lint": "18f-eslint" 37 | } 38 | } 39 | ``` 40 | 41 | Once the script is defined, you can run it with `npm run lint`. 42 | 43 | The command line arguments are passed to the [eslint `lintFiles` method](https://eslint.org/docs/developer-guide/nodejs-api#-eslintlintfilespatterns). 44 | They can be a combination of file names, directory names, or glob patterns. Note 45 | that if you're using globs, they should be wrapped in quotes so that they are 46 | not evaluated by the shell. 47 | 48 | ## Configuration 49 | 50 | By default, your application will be configured to extend the `airbnb` and 51 | `prettier` base configurations. It will also enable the `es6` environment. If 52 | your project uses React, it will also extend the `prettier/react` configuration 53 | and enable the `browser` environment; otherwise, it will enable the `node` 54 | environment. 55 | 56 | All of these configurations can be overridden or extended by your local 57 | `.eslintrc.*` file. The configuration provided by this library is presented to 58 | eslint as a base, and your local, project-level configurations will supercede 59 | it. By default, it's probably safest not to define an `env` or `extends` 60 | property in your own config file, but you certainly can if necessary. 61 | 62 | If your project is written for ES5 or below, install [eslint-config-airbnb-base/legacy](https://www.npmjs.com/package/eslint-config-airbnb-base#eslint-config-airbnb-baselegacy)) 63 | and update your `eslintrc` file to include it in the `extends` property. 64 | 65 | The script determins whether or not your project project is a React project by 66 | finding the nearest `package.json` and looking for `react` in either your 67 | dependencies or dev-dependencies. 68 | 69 | ## GitHub Action 70 | 71 | There is a [GitHub Action](https://github.com/18F/18f-eslint-action) that makes 72 | it easy to add 18F-eslint to your CI/CD pipeline. In addition, this package can 73 | configure the action for you: 74 | 75 | ```shell 76 | npx -p @18f/18f-eslint install-action 77 | ``` 78 | 79 | This will add an 18F-eslint GitHub action to your repo for each `package.json` 80 | file. If you have a monorepo, it should more-or-less just work. It defaults to 81 | linting all Javascript files under each path, but you can tweak the workflow 82 | to suit your needs. See the [documentation](https://github.com/18F/18f-eslint-action) 83 | for more info about configuration the action. 84 | 85 | ## Under the hood 86 | 87 | The 18F eslint wrapper imports our recommended eslint rules and plugins, 88 | those specified by the [Airbnb JavaScript style guide](https://github.com/airbnb/javascript). 89 | 90 | - For all projects 91 | - [eslint-config-prettier](https://www.npmjs.com/package/eslint-config-prettier) 92 | - [prettier](https://prettier.io/) 93 | - For React projects: 94 | - [eslint-config-airbnb](https://www.npmjs.com/package/eslint-config-airbnb) 95 | - [eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import) 96 | - [eslint-plugin-jsx-a11y](https://www.npmjs.com/package/eslint-plugin-jsx-a11y) 97 | - [eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react) 98 | - For ES6/2015 projects that don't use React: 99 | - [eslint-config-airbnb-base](https://www.npmjs.com/package/eslint-config-airbnb-base) 100 | - [eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import) 101 | 102 | ### Public domain 103 | 104 | This project is in the worldwide [public domain](LICENSE.md). As stated in 105 | [CONTRIBUTING](CONTRIBUTING.md): 106 | 107 | > This project is in the public domain within the United States, and copyright 108 | > and related rights in the work worldwide are waived through the 109 | > [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 110 | > 111 | > All contributions to this project will be released under the CC0 dedication. 112 | > By submitting a pull request, you are agreeing to comply with this waiver of 113 | > copyright interest. 114 | --------------------------------------------------------------------------------