import cypress-io/eslint-plugin-dev to npm/eslint-plugin-dev/

This commit is contained in:
Zach Panzarino
2020-09-29 11:48:20 -04:00
38 changed files with 10917 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
!.*
**/package-lock.json
test/fixtures

View File

@@ -0,0 +1,11 @@
{
"plugins": [
"@cypress/dev",
"promise"
],
"extends": [
"plugin:promise/recommended",
"plugin:@cypress/dev/general"
],
"rules": {}
}

5
npm/eslint-plugin-dev/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.DS_Store
node_modules
*.log
package-lock.json
*.tgz

View File

@@ -0,0 +1,2 @@
/*
!lib/

View File

@@ -0,0 +1,29 @@
{
"eslint.alwaysShowStatus": true,
"eslint.validate": [
{
"language": "javascript",
"autoFix": true
},
{
"language": "javascriptreact",
"autoFix": true
},
{
"language": "typescript",
"autoFix": true
},
{
"language": "typescriptreact",
"autoFix": true
},
{
"language": "json",
"autoFix": true
},
{
"language": "coffeescript",
"autoFix": false
},
],
}

View File

@@ -0,0 +1,24 @@
## MIT License
Copyright (c) 2017 Cypress.io https://cypress.io
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,208 @@
<div>
<!-- <img src="docs/readme-logo.png"> -->
<h1>[Internal] Cypress Developer ESLint Plugin</h1>
<a href="https://www.npmjs.com/package/@cypress/eslint-plugin-dev"><img src="https://img.shields.io/npm/v/@cypress/eslint-plugin-dev.svg?style=flat"></a>
<a href="https://circleci.com/gh/cypress-io/eslint-plugin-dev/tree/master"><img src="https://img.shields.io/circleci/build/gh/cypress-io/eslint-plugin-dev.svg"></a>
<p>Common ESLint rules shared by Cypress packages.</p>
</div>
> ⚠️ This package for _internal development_ of Cypress. Here's the [**Official Cypress ESLint Plugin**](https://github.com/cypress-io/eslint-plugin-cypress) meant for users of Cypress.
## Installation
```
npm install --save-dev @cypress/eslint-plugin-dev
```
## Usage
1) install the following `devDependencies`:
```sh
@cypress/eslint-plugin-dev
eslint-plugin-json-format
@typescript-eslint/parser
@typescript-eslint/eslint-plugin
eslint-plugin-mocha
# if you have coffeescript files
@fellow/eslint-plugin-coffee
babel-eslint
# if you have react/jsx files
eslint-plugin-react
babel-eslint
```
2) add the following to your root level `.eslintrc.json`:
```json
{
"plugins": [
"@cypress/dev"
],
"extends": [
"plugin:@cypress/dev/general",
]
}
```
> Note: also add `"plugin:@cypress/dev/react"`, if you are using `React`
> Note: if you have a `test/` directory, you should create a `.eslintrc.json` file inside of it, and add:
```json
{
"extends": [
"plugin:@cypress/dev/tests",
]
}
```
3) add the following to your `.eslintignore`:
```sh
# don't ignore hidden files, useful for formatting json config files
!.*
```
4) (optional) Install and configure your text editor's ESLint Plugin Extension to lint and auto-fix files using ESLint, [detailed below](#editors)
5) (optional) Install [`husky`](https://github.com/typicode/husky) and enable the lint `pre-commit` hook:
`package.json`:
```json
"husky": {
"hooks": {
"pre-commit": "lint-pre-commit"
}
},
```
> Note: the `lint-pre-commit` hook will automatically lint your staged files, and only `--fix` and `git add` them if there are no unstaged changes existing in that file (this protects partially staged files from being added in the hook).
To auto-fix all staged & unstaged files, run `./node_modules/.bin/lint-changed --fix`
## Presets
### general
_Should usually be used at the root of the package._
- The majority of the rules.
- auto-fixes `json` files and sorts your `package.json` via [`eslint-plugin-json-format`](https://github.com/bkucera/eslint-plugin-json-format)
**requires you to install the following `devDependencies`**:
```sh
eslint-plugin-json-format
@typescript-eslint/parser
@typescript-eslint/eslint-plugin
```
### tests
Test-specific configuration and rules. Should be used within the `test/` directory.
**requires you to install the following `devDependencies`**:
```sh
eslint-plugin-mocha
```
### react
React and JSX-specific configuration and rules.
**requires you to install the following `devDependencies`**:
```sh
babel-eslint
eslint-plugin-react
```
## Configuration Examples
Change some linting rules:
```js
// .eslintrc.json
{
"extends": [
"plugin:@cypress/dev/general"
],
"rules": {
"comma-dangle": "off",
"no-debugger": "warn"
}
}
```
Stop your `package.json` from being formatted:
```json
{
"settings": {
"json/sort-package-json": false
}
}
```
### Custom Rules:
name | description | options | example
-|-|-|-
`@cypress/dev/arrow-body-multiline-braces` | Enforces braces in arrow functions ONLY IN multiline function definitions | [`[always|never] always set this to 'always'`] | `'@cypress/dev/arrow-body-multiline-braces': ['error', 'always']`
`@cypress/dev/skip-comment` | Enforces a comment (`// NOTE:`) explaining a `.skip` added to `it`, `describe`, or `context` test blocks | { commentTokens: `[array] tokens that indicate .skip explanation (default: ['NOTE:', 'TODO:', 'FIXME:']`)} | `'@cypress/dev/skip-comment': ['error', { commentTokens: ['TODO:'] }]`
`@cypress/dev/no-return-before` | Disallows `return` statements before certain configurable tokens | { tokens: `[array] tokens that cannot be preceded by 'return' (default: ['it', 'describe', 'context', 'expect']`)} | `'@cypress/dev/no-return-before': ['error', { tokens: ['myfn'] }]`
## <a name="editors"></a>Editors
### VSCode
Use plugin [ESLint by Dirk Baeumer](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) to lint and auto fix JS files using ESLint.
After installing, add the following to your User or Workspace (`.vscode/settings.json`) settings:
```json
{
"eslint.validate": [
{
"language": "javascript",
"autoFix": true
},
{
"language": "javascriptreact",
"autoFix": true
},
{
"language": "typescript",
"autoFix": true
},
{
"language": "typescriptreact",
"autoFix": true
},
{
"language": "json",
"autoFix": true
},
{
"language": "coffeescript",
"autoFix": false
},
],
}
```
### Atom
Install package [linter-eslint](https://atom.io/packages/linter-eslint)
(and its dependencies) to enable linting. Go into the settings for this package
and enable "Fix on save" option to auto-fix white space issues and other things.
### Sublime Text
Install [ESLint-Formatter](https://packagecontrol.io/packages/ESLint-Formatter),
then set the following settings:
```json
{
"format_on_save": true,
"debug": true
}
```
## License
This project is licensed under the terms of the [MIT license](/LICENSE.md).

View File

@@ -0,0 +1,29 @@
jobs:
test:
docker:
- image: cypress/base:10
steps:
- checkout
- restore_cache:
keys:
- cache-{{ arch }}-{{ .Branch }}-{{ checksum "package.json" }}
- run:
name: Yarn install
command: yarn install --frozen-lockfile
- save_cache:
key: cache-{{ arch }}-{{ .Branch }}-{{ checksum "package.json" }}
paths:
- ~/.cache
- run:
# test with installed eslint
command: yarn run test
- run:
# test with eslint@5
command: yarn add -D eslint@5.16.0 && yarn run test
- run:
command: yarn run semantic-release
workflows:
build:
jobs:
- test
version: 2

View File

@@ -0,0 +1,16 @@
const ruleComposer = require('eslint-rule-composer')
const arrowBodyStyle = require('eslint/lib/rules/arrow-body-style')
module.exports = ruleComposer.filterReports(
arrowBodyStyle,
(problem, metadata) => {
const problemIndex = metadata.sourceCode.getIndexFromLoc(problem.loc.start)
const reportedToken = metadata.sourceCode.getTokenByRangeStart(problemIndex, { includeComments: true })
if (problem.node.loc.start.line === problem.node.loc.end.line) {
return
}
return !(reportedToken && reportedToken.type === 'Line' && /^-{2,}$/u.test(reportedToken.value))
}
)

View File

@@ -0,0 +1,7 @@
const fs = require('fs')
const path = require('path')
module.exports =
Object.assign({}, ...fs.readdirSync(__dirname)
.filter((filename) => filename.endsWith('.js') && filename !== 'index.js')
.map((filename) => ({ [filename.replace(/\.js$/u, '')]: require(path.resolve(__dirname, filename)) })))

View File

@@ -0,0 +1,59 @@
let astUtils
try {
astUtils = require('eslint/lib/util/ast-utils')
} catch (e) {
astUtils = require('eslint/lib/shared/ast-utils')
}
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'stop .only\'s in spec files',
category: 'Spec Issues',
},
messages: {
noOnly: 'Found only: `{{callee}}`.',
},
// uncomment to enable autoFix
// fixable: 'code',
},
create (context) {
const sourceCode = context.getSourceCode()
function getPropertyText (node) {
const lines = sourceCode.getText(node).split(astUtils.LINEBREAK_MATCHER)
return lines[0]
}
return {
'CallExpression:exit' (node) {
const callee = node.callee
if (node.type === 'CallExpression' && callee.type === 'MemberExpression' && callee.property.name === 'only') {
if (['it', 'describe', 'context'].includes(callee.object.name)) {
context.report({
node: callee.property,
loc: callee.property.loc.start,
messageId: 'noOnly',
data: {
callee: getPropertyText(callee.parent),
},
// uncomment to enable autoFix
// fix(fixer) {
// return fixer.replaceTextRange([callee.property.start - 1, callee.property.end], '')
// }
})
}
}
},
}
},
}

View File

@@ -0,0 +1,76 @@
const defaultTokens = ['it', 'describe', 'context', 'expect']
// const debug = require('debug')('@cypress/dev')
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce no return before certain token names',
category: 'Misc',
},
messages: {
errorMessage: `\
Found a 'return' after '{{token}}'\
`,
},
schema: [
{
type: 'object',
properties: {
tokens: {
type: 'array',
default: defaultTokens,
},
},
additionalProperties: false,
},
],
// uncomment to enable autoFix
fixable: 'code',
},
create (context) {
let tokens = defaultTokens
if (context.options.length) {
tokens = typeof context.options[0].tokens === 'object' ? context.options[0].tokens : tokens
}
return {
'CallExpression:exit' (node) {
const callee = node.callee
if (
(callee.type === 'Identifier')
&& tokens.includes(callee.name)
) {
const t = context.getSourceCode().getTokenBefore(node)
// debug(t)
if (!(t && t.type === 'Keyword' && t.value === 'return')) return
const returnNode = t
context.report({
node: callee,
loc: callee.loc.start,
messageId: 'errorMessage',
data: {
token: callee.name,
},
// uncomment to enable autoFix
fix (fixer) {
return fixer.replaceTextRange([returnNode.range[0], returnNode.range[1] + 1], '')
},
})
}
},
}
},
}

View File

@@ -0,0 +1,87 @@
const defaultCommentTokens = ['NOTE:', 'TODO:', 'FIXME:']
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'stop .skip\'s in spec files',
category: 'Spec Issues',
},
messages: {
noOnly: `\
Found a {{test-scope}}.skip(⋯) without an explanation.
Add a comment above the '{{test-scope}}' starting with one of:
{{commentTokens}}
e.g.
// {{exampleCommentToken}} <reason test was skipped>
{{test-scope}}.skip(⋯)
`,
},
schema: [
{
type: 'object',
properties: {
commentTokens: {
type: 'array',
default: defaultCommentTokens,
},
},
additionalProperties: false,
},
],
// uncomment to enable autoFix
// fixable: 'code',
},
create (context) {
let commentTokens = defaultCommentTokens
if (context.options.length) {
commentTokens = typeof context.options[0].commentTokens === 'object' ? context.options[0].commentTokens : commentTokens
}
const sourceCode = context.getSourceCode()
return {
'CallExpression:exit' (node) {
const callee = node.callee
const commentBefore = sourceCode.getCommentsBefore(node)
const hasExplain = commentBefore && commentBefore.map(
(v) => commentTokens.concat(commentTokens.map((v) => `# ${v}`)).map((commentToken) => v.value.trim().startsWith(commentToken)).filter(Boolean)[0]
).filter(Boolean)[0]
if (hasExplain) {
return
}
if (node.type === 'CallExpression' && callee.type === 'MemberExpression' && callee.property.name === 'skip') {
if (['it', 'describe', 'context'].includes(callee.object.name)) {
context.report({
node: callee.property,
loc: callee.property.loc.start,
messageId: 'noOnly',
data: {
'test-scope': callee.object.name,
commentTokens: commentTokens.join(' '),
exampleCommentToken: commentTokens[0],
},
// uncomment to enable autoFix
// fix(fixer) {
// return fixer.replaceTextRange([callee.property.start - 1, callee.property.end], '')
// }
})
}
}
},
}
},
}

View File

@@ -0,0 +1,338 @@
const customRules = require('./custom-rules')
const baseRules = {
'@cypress/dev/arrow-body-multiline-braces': [
'error',
'always',
],
'array-bracket-newline': [
'error',
'consistent',
],
'array-bracket-spacing': [
'error',
'never',
],
'arrow-parens': [
'error',
'always',
],
'arrow-spacing': 'error',
'block-spacing': 'error',
'brace-style': [
'error',
'1tbs',
{
'allowSingleLine': false,
},
],
'function-paren-newline': [
'error',
'consistent',
],
'comma-dangle': [
'error',
'always-multiline',
],
'comma-spacing': 'error',
'curly': [
'error',
'multi-line',
'consistent',
],
'constructor-super': 'error',
'default-case': 'error',
'eol-last': 'error',
'eqeqeq': [
'error',
'allow-null',
],
'indent': [
'error',
2,
{
'SwitchCase': 1,
'MemberExpression': 0,
},
],
'key-spacing': 'error',
'keyword-spacing': 'error',
'no-buffer-constructor': 'error',
'no-case-declarations': 'error',
'no-class-assign': 'error',
'no-cond-assign': 'error',
'no-console': 'error',
'no-const-assign': 'error',
'no-constant-condition': 'error',
'no-control-regex': 'error',
'no-debugger': 'error',
'no-delete-var': 'error',
'no-dupe-class-members': 'error',
'no-dupe-keys': 'error',
'no-dupe-args': 'error',
'no-duplicate-case': 'error',
'no-duplicate-imports': 'error',
'no-else-return': [
'error',
{
'allowElseIf': false,
},
],
'no-empty': 'error',
'no-empty-character-class': 'error',
'no-empty-pattern': 'error',
'no-ex-assign': 'error',
'no-extra-boolean-cast': 'error',
'no-extra-semi': 'error',
'no-fallthrough': 'error',
'no-func-assign': 'error',
'no-inner-declarations': 'error',
'no-invalid-regexp': 'error',
'no-irregular-whitespace': 'error',
'no-mixed-spaces-and-tabs': 'error',
'no-multiple-empty-lines': [
'error',
{
'max': 1,
'maxEOF': 0,
'maxBOF': 0,
},
],
'no-multi-spaces': 'error',
'no-negated-in-lhs': 'error',
'no-new-symbol': 'error',
'no-obj-calls': 'error',
'no-octal': 'error',
'no-redeclare': 'error',
'no-regex-spaces': 'error',
'no-self-assign': 'error',
'no-spaced-func': 'error',
'no-sparse-arrays': 'error',
'no-this-before-super': 'error',
'no-trailing-spaces': 'error',
'no-undef': 'error',
'no-unexpected-multiline': 'error',
'no-unneeded-ternary': 'error',
'no-unreachable': 'error',
'no-unused-labels': 'error',
'no-unused-vars': ['error', { args: 'none' }],
'no-useless-concat': 'error',
'no-useless-constructor': 'error',
'no-var': 'error',
'no-whitespace-before-property': 'error',
'object-curly-spacing': [
'error',
'always',
],
'object-shorthand': 'error',
'one-var': [
'error',
'never',
],
'padded-blocks': ['error', 'never'],
'padding-line-between-statements': [
'error',
{
'blankLine': 'always',
'prev': '*',
'next': 'return',
},
{
'blankLine': 'always',
'prev': [
'const',
'let',
'var',
'if',
'while',
'export',
'cjs-export',
'import',
'cjs-import',
'multiline-expression',
],
'next': '*',
},
{
'blankLine': 'any',
'prev': [
'const',
'let',
'var',
'import',
'cjs-import',
],
'next': [
'const',
'let',
'var',
'import',
'cjs-import',
],
},
],
'prefer-rest-params': 'error',
'prefer-spread': 'error',
'prefer-template': 'error',
'quotes': [
'error',
'single',
{
'allowTemplateLiterals': true,
},
],
'semi': [
'error',
'never',
],
'semi-spacing': 'error',
'space-before-blocks': 'error',
'space-before-function-paren': 'error',
'space-in-parens': [
'error',
'never',
],
'space-infix-ops': 'error',
'space-unary-ops': 'error',
'template-curly-spacing': 'error',
'use-isnan': 'error',
'valid-typeof': 'error',
}
// '@cypress/dev/no-only': 'error',
module.exports = {
configs: {
general: {
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: [
'json-format',
],
settings: {
json: {
'sort-package-json': 'pro',
},
},
env: {
node: true,
es6: true,
},
rules: {
...baseRules,
},
overrides: [
{
files: [
'*.jsx',
'*.tsx',
],
rules: {
'@cypress/dev/arrow-body-multiline-braces': 'off',
},
},
{
files: '*.coffee',
parser: '@fellow/eslint-plugin-coffee',
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module',
ecmaVersion: 2018,
},
plugins: [
'@fellow/eslint-plugin-coffee',
],
rules: {
...Object.assign({}, ...Object.keys(baseRules).map((key) => ({ [key]: 'off' }))),
'@fellow/coffee/coffeescript-error': [
'error',
{},
],
},
},
{
files: '*.ts',
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
'args': 'none',
},
],
'@typescript-eslint/type-annotation-spacing': 'error',
'@typescript-eslint/no-useless-constructor': [
'error',
],
'@typescript-eslint/member-delimiter-style': [
'error',
{
'multiline': {
'delimiter': 'none',
},
'singleline': {
'delimiter': 'comma',
},
},
],
},
},
],
},
tests: {
env: {
mocha: true,
},
globals: {
expect: true,
},
plugins: ['mocha'],
rules: {
'mocha/handle-done-callback': 'error',
'mocha/no-exclusive-tests': 'error',
'mocha/no-global-tests': 'error',
'@cypress/dev/skip-comment': 'error',
},
},
react: {
env: {
browser: true,
},
parser: 'babel-eslint',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
legacyDecorators: true,
},
},
plugins: ['react'],
rules: {
'react/jsx-curly-spacing': 'error',
'react/jsx-equals-spacing': 'error',
'react/jsx-filename-extension': 'error',
'react/jsx-no-duplicate-props': 'error',
'react/jsx-no-undef': 'error',
'react/jsx-pascal-case': 'error',
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'react/jsx-wrap-multilines': 'error',
'react/no-unknown-property': 'error',
'react/prefer-es6-class': 'error',
'react/react-in-jsx-scope': 'error',
'react/require-render-return': 'error',
},
},
},
rules: {
...customRules,
},
}

View File

@@ -0,0 +1,5 @@
{
"extends": [
"plugin:@cypress/dev/tests"
]
}

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env node
const sh = require('shelljs')
const utils = require('./utils')
const _ = require('lodash')
const chalk = require('chalk')
const start = () => {
const fix = process.argv.slice(2).includes('--fix')
return utils.lintFilesByName({
// list only modified files
getFilenames: () => {
return _.union(
sh.exec(`git diff --name-only --diff-filter=M`).split('\n'),
sh.exec(`git diff --name-only --diff-filter=MA --staged`).split('\n')
)
},
fix,
})
.then(({ failed, filenames }) => {
if (failed) {
process.exit(failed)
}
// eslint-disable-next-line no-console
console.log(chalk.bold(`${chalk.green(filenames.length)} files linted successfully`))
return
})
}
if (!module.parent) {
start()
}
module.exports = { start }

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env node
const _ = require('lodash')
const utils = require('./utils')
const sh = require('shelljs')
const chalk = require('chalk')
const start = () => {
const filesStaged = sh.exec(`git diff --name-only --diff-filter=MA --staged`).split('\n').filter(Boolean)
const filesUnstaged = sh.exec(`git diff --name-only --diff-filter=M`).split('\n').filter(Boolean)
const filesPartiallyStaged = _.intersection(filesStaged, filesUnstaged)
const filesFullyStaged = _.difference(filesStaged, filesPartiallyStaged)
let fail = false
let lintedFilesCount = 0
return utils.lintFilesByName({
getFilenames: () => filesFullyStaged,
fix: true,
})
.then(({ failed, filenames }) => {
sh.exec(`git add ${sh.ShellString(filenames.join(' '))}`)
if (failed) {
fail = true
}
lintedFilesCount += filenames.length
return
})
.then(() => {
return utils.lintFilesByText({
getFilenames: () => filesPartiallyStaged,
getFileText: (f) => sh.exec(`git show :${sh.ShellString(f)}`),
})
})
.then(({ failCount, filenames }) => {
if (failCount) {
fail = true
}
lintedFilesCount += filenames.length
return
})
.then(() => {
if (fail) {
process.exit(1)
}
// eslint-disable-next-line no-console
console.log(chalk.bold(`${chalk.green(lintedFilesCount)} files linted successfully`))
return
})
}
if (!module.parent) {
start()
}
module.exports = { start }

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env node
const sh = require('shelljs')
const utils = require('./utils')
const chalk = require('chalk')
const debug = require('debug')('lint-pre-push')
const start = () => {
const getFilenames = () => {
const GIT_PARAMS = (process.env.HUSKY_GIT_PARAMS || 'origin').split(' ')
const gitRemote = GIT_PARAMS[0]
const gitBranch = sh.exec(`git branch`).grep(/\*/).split(/\s/)[1]
const gitRemoteBranch = `${gitRemote}/${gitBranch}`
debug({ gitRemote })
debug({ gitBranch })
return sh
.exec(`git diff HEAD ${sh.ShellString(gitRemoteBranch)} --name-only`)
.split('\n')
}
return utils.lintFilesByText({
getFilenames,
getFileText: (f) => sh.exec(`git show :${sh.ShellString(f)}`),
})
.then(({ failCount, filenames }) => {
if (failCount) {
process.exit(failCount)
}
// eslint-disable-next-line no-console
console.log(chalk.bold(`${chalk.green(filenames.length)} files linted successfully`))
return
})
}
if (!module.parent) {
start()
}
module.exports = { start }

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env node
/* eslint-disable quotes */
const sh = require('shelljs')
const utils = require('./utils')
const chalk = require('chalk')
const start = () => {
return utils.lintFilesByText({
// list only modified and added files
getFilenames: () => sh.exec(`git diff --name-only --diff-filter=MA --staged`).split('\n'),
getFileText: (f) => sh.exec(`git show :${sh.ShellString(f)}`),
})
.then(({ failCount, filenames }) => {
if (failCount) {
process.exit(failCount)
}
// eslint-disable-next-line no-console
console.log(chalk.bold(`${chalk.green(filenames.length)} files linted successfully`))
return
})
}
if (!module.parent) {
start()
}
module.exports = { start }

View File

@@ -0,0 +1,166 @@
const sh = require('shelljs')
const sinon = require('sinon')
const lintStaged = require('./lint-staged')
const lintChanged = require('./lint-changed')
const lintPrePush = require('./lint-pre-push')
const lintPreCommit = require('./lint-pre-commit')
const chai = require('chai')
const debug = require('debug')('lint.spec')
const { expect } = chai
chai.use(require('sinon-chai'))
const _env = process.env
const _argv = process.argv
const getStagedFiles = () => sh.ShellString('foo.js\nbar.js')
const getUnstagedFiles = () => sh.ShellString('bar.js\nbaz.js')
const getCommittedFiles = () => sh.ShellString('baz.js\nquux.js')
const eslintSuccess = (...args) => {
debug('eslintSuccess:', args)
const ret = sh.ShellString(`GOOD JS`)
ret.exec = sinon.stub().yields(null, 'success')
return ret
}
const eslintFailure = (...args) => {
debug('eslintFailure:', args)
const ret = sh.ShellString(`BAD JS`)
ret.exec = sinon.stub().yields('foo error')
return ret
}
beforeEach(() => {
sinon.stub(sh, 'exec')
sinon.stub(sh, 'cat')
sinon.stub(process, 'exit')
sh.exec
.withArgs(`git branch`).returns(sh.ShellString('* mybranch'))
.withArgs(`git diff --name-only --diff-filter=MA --staged`)
.returns(getStagedFiles())
.withArgs(`git diff --name-only --diff-filter=M`)
.returns(getUnstagedFiles())
.withArgs(`git diff HEAD origin/mybranch --name-only`)
.returns(getCommittedFiles())
sh.exec.callsFake(eslintSuccess)
})
describe('lint-staged', () => {
it('lint success', async () => {
await lintStaged.start()
expect(process.exit).not.calledOnce
})
it('lint failures', async () => {
sh.exec.callsFake(eslintFailure)
await lintStaged.start()
expect(process.exit).calledOnce
})
})
describe('lint-changed', () => {
const filenames = 'bar.js baz.js foo.js'
beforeEach(() => {
sh.exec
.withArgs(`./node_modules/.bin/eslint --color=true '' ${filenames}`)
.yields(null, 'success')
})
it('lint success', async () => {
await lintChanged.start()
expect(process.exit).not.calledOnce
})
it('lint failures', async () => {
sh.exec
.withArgs(`./node_modules/.bin/eslint --color=true '' ${filenames}`)
.yields('foo error')
await lintChanged.start()
expect(process.exit).calledOnce
})
it('lint with --fix', async () => {
process.argv = ['_', '_', '--fix']
sh.exec
.withArgs(`./node_modules/.bin/eslint --color=true --fix '' ${filenames}`)
.yields(null, 'success')
await lintChanged.start()
expect(process.exit).not.calledOnce
})
})
describe('lint-pre-push', () => {
beforeEach(() => {
process.env.HUSKY_GIT_PARAMS = 'origin git@github.com:cypress-io/cypress.git'
})
it('lint success', async () => {
await lintPrePush.start()
expect(process.exit).not.calledOnce
})
it('lint failures', async () => {
sh.exec.callsFake(eslintFailure)
await lintPrePush.start()
expect(process.exit).calledOnce
})
})
describe('lint-pre-commit', () => {
beforeEach(() => {
sh.exec
.withArgs(`./node_modules/.bin/eslint --color=true --fix '' foo.js`)
.yields(null, 'success')
})
it('lint success', async () => {
await lintPreCommit.start()
expect(process.exit).not.calledOnce
expect(sh.exec.withArgs('git add foo.js')).calledOnce
})
it('lint failures', async () => {
sh.exec.callsFake(eslintFailure)
await lintPreCommit.start()
expect(process.exit).calledOnce
})
})
afterEach(() => {
process.argv = _argv
process.env = _env
sinon.restore()
})
// sinon.addBehavior('withArgIncludes', (stub, str) => {
// })
// function withArgsInclude() {
// this.
// .callsFake((...args) => {
// args[0].includes()
// })
// }

View File

@@ -0,0 +1,113 @@
const _ = require('lodash')
const EE = require('events')
const sh = require('shelljs')
// const chalk = require('chalk')
const Promise = require('bluebird')
const debug = require('debug')('lint/util')
const filesRegex = /\.(js|jsx|ts|tsx|coffee|json|eslintrc)$/
Promise.config({
warnings: true,
longStackTraces: true,
})
module.exports = {
lintFilesByText: (options) => {
sh.config.silent = true
EE.defaultMaxListeners = 100
const opts = _.defaults(options, {
getFilenames: null,
getFileText: null,
})
const filenames = opts.getFilenames().filter((v) => filesRegex.test(v))
debug(`linting:
${filenames.join('\n\t')}
`)
return Promise.map(filenames, (f) => {
debug('started linting', f)
const fileText = opts.getFileText(f)
debugTerse('file text:', fileText)
if (!fileText.toString()) return
const lintCommand = `./node_modules/.bin/eslint --stdin --stdin-filename ${sh.ShellString(f)} --color=true`
return Promise.promisify(fileText.exec)(
lintCommand,
{ silent: false, async: true }
)
.tapCatch(debugTerse)
.return(false)
.catchReturn(true)
.finally(() => {
debug('finished linting ', f)
})
}, { concurrency: 0 })
.then((results) => {
const failCount = _.filter(results).length
debug({ failCount })
return { failCount, filenames }
})
},
lintFilesByName: (options) => {
sh.config.silent = true
const opts = _.defaults(options, {
getFilenames: null,
fix: false,
})
const filenames = opts.getFilenames().filter((v) => filesRegex.test(v))
debug(`linting:
${filenames.join('\n\t')}
`)
const filenamesString = sh.ShellString(filenames.join(' '))
const lintCommand = opts.fix ?
`./node_modules/.bin/eslint --color=true --fix '' ${filenamesString}`
: `./node_modules/.bin/eslint --color=true '' ${filenamesString}`
return Promise.promisify(sh.exec)(
lintCommand,
{ silent: false, async: true }
)
.tapCatch(debugTerse)
.return(false)
.catchReturn(true)
.then((failed) => {
return {
failed,
filenames,
}
})
},
}
const debugTerse = (...args) => {
args = args.map((arg) => {
let truncated = arg.toString().slice(0, 15)
if (truncated !== arg.toString()) {
truncated = `${truncated}...`
}
return truncated
})
debug(...args)
}

View File

@@ -0,0 +1,81 @@
{
"name": "@cypress/eslint-plugin-dev",
"version": "0.0.0-development",
"description": "Common ESLint rules shared by Internal Cypress packages",
"main": "./lib",
"scripts": {
"lint": "eslint --ext .js,json,.eslintrc .",
"lint-changed": "node ./lib/scripts/lint-changed",
"lint-fix": "npm run lint -- --fix",
"semantic-release": "semantic-release",
"test": "jest"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"dependencies": {
"bluebird": "^3.5.5",
"chalk": "^2.4.2",
"eslint-rule-composer": "^0.3.0",
"lodash": "^4.17.15",
"shelljs": "^0.8.3"
},
"devDependencies": {
"@cypress/eslint-plugin-dev": "file:./shim",
"@fellow/eslint-plugin-coffee": "^0.4.13",
"@types/chai": "^4.1.7",
"@types/jest": "^24.0.15",
"@types/mocha": "^5.2.7",
"@types/sinon": "^7.0.13",
"@types/sinon-chai": "^3.2.2",
"chai": "^4.2.0",
"eslint": "^6.1.0",
"eslint-plugin-json-format": "^2.0.0",
"eslint-plugin-mocha": "^5.3.0",
"eslint-plugin-promise": "^4.2.1",
"husky": "^2.7.0",
"jest": "^24.8.0",
"jest-cli": "^24.8.0",
"lint-staged": "^9.2.0",
"mocha": "^6.1.4",
"semantic-release": "^15.13.17",
"sinon": "^7.3.2",
"sinon-chai": "^3.3.0"
},
"peerDependencies": {
"@typescript-eslint/eslint-plugin": ">= 1.11.0",
"@typescript-eslint/parser": ">= 1.11.0",
"babel-eslint": "^7.2.3",
"eslint": ">= 3.2.1",
"eslint-plugin-json-format": ">= 2.0.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-react": "^7.2.1"
},
"bin": {
"lint-changed": "./lib/scripts/lint-changed.js",
"lint-pre-commit": "./lib/scripts/lint-pre-commit.js"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/cypress-io/eslint-plugin-dev.git"
},
"homepage": "https://github.com/cypress-io/eslint-plugin-dev#readme",
"author": "Chris Breiding (chris@cypress.io)",
"bugs": {
"url": "https://github.com/cypress-io/eslint-plugin-dev/issues"
},
"keywords": [
"cypress",
"eslint",
"eslintplugin"
],
"lint-staged": {
"*.{js,jsx,ts,tsx,json,eslintrc}": [
"eslint --fix",
"git add"
]
}
}

View File

@@ -0,0 +1,5 @@
{
"name": "@cypress/eslint-plugin-dev",
"version": "0.0.0",
"main": "../../../lib"
}

View File

@@ -0,0 +1,8 @@
{
"extends": [
"plugin:@cypress/dev/tests"
],
"env": {
"jest": true
}
}

View File

@@ -0,0 +1,58 @@
const path = require('path')
const CLIEngine = require('eslint').CLIEngine
const plugin = require('..')
const _ = require('lodash')
const pluginName = '__plugin__'
function execute (file, options = {}) {
const opts = _.defaultsDeep(options, {
fix: true,
config: {
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
},
})
const cli = new CLIEngine({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
rules: {
[`${pluginName}/arrow-body-multiline-braces`]: ['error', 'always'],
},
...opts,
ignore: false,
useEslintrc: false,
plugins: [pluginName],
})
cli.addPlugin(pluginName, plugin)
const results = cli.executeOnFiles([path.join(__dirname, file)]).results[0]
return results
}
describe('arrow-body-multiline-braces', () => {
it('lint multiline js', async () => {
const filename = './fixtures/multiline.js'
const result = execute(filename, {
fix: true,
})
expect(result.output).toContain('{')
})
it('lint oneline js', async () => {
const filename = './fixtures/oneline.js'
const result = execute(filename, { fix: false })
expect(result.output).not.ok
expect(result).toHaveProperty('errorCount', 0)
})
})

View File

@@ -0,0 +1,6 @@
{
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
}
}

View File

@@ -0,0 +1,9 @@
const foo = (fn) =>
fn(
'foo',
'bar',
)
foo()

View File

@@ -0,0 +1,10 @@
describe('outer', ()=>{
return describe('some test', ()=>{
return context('some test', ()=>{
return it('some test', ()=>{
return expect('foo').to.eq('bar')
})
return someFn()
})
})
})

View File

@@ -0,0 +1,10 @@
describe('outer', ()=>{
describe('some test', ()=>{
context('some test', ()=>{
it('some test', ()=>{
expect('foo').to.eq('bar')
})
return someFn()
})
})
})

View File

@@ -0,0 +1,5 @@
const foo = () => console.log('foo')
foo()

View File

@@ -0,0 +1,10 @@
// FOOBAR: im skipping this for good reason
it.skip('some test', ()=>{
})
// NOTE: im skipping this for good reason
it.skip('some test', ()=>{
})

View File

@@ -0,0 +1,11 @@
it.skip('some test', ()=>{
})
describe.skip('some test', ()=>{
})
context.skip('some test', ()=>{
})

View File

@@ -0,0 +1,25 @@
// NOTE: im skipping this for good reason
it.skip('some test', ()=>{
})
// NOTE: im skipping this for good reason
// some other line
describe.skip('some test', ()=>{
})
/* NOTE: im skipping this for good reason */
context.skip('some test', ()=>{
})
// TODO: im skipping this for good reason
it.skip('some test', ()=>{
})
//# TODO: im skipping this for good reason
it.skip('some test', ()=>{
})

View File

@@ -0,0 +1,11 @@
it.only('foo', () => {
'foo'
})
describe.only('foo', () => {
'foo'
})
context.only('foo', () => {
'foo'
})

View File

@@ -0,0 +1,56 @@
const path = require('path')
const CLIEngine = require('eslint').CLIEngine
const plugin = require('..')
const _ = require('lodash')
const ruleName = 'no-only'
const pluginName = '__plugin__'
function execute (file, options = {}) {
const opts = _.defaultsDeep(options, {
fix: true,
config: {
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
},
})
const cli = new CLIEngine({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
rules: {
[`${pluginName}/${ruleName}`]: ['error'],
},
...opts,
ignore: false,
useEslintrc: false,
plugins: [pluginName],
})
cli.addPlugin(pluginName, plugin)
const results = cli.executeOnFiles([path.join(__dirname, file)]).results[0]
return results
}
describe('no-only', () => {
it('lint js with only', async () => {
const filename = './fixtures/with-only.js'
const result = execute(filename, {
fix: true,
})
expect(result.errorCount).toBe(3)
expect(result.messages[0].message).toContain('it')
expect(result.messages[1].message).toContain('describe')
expect(result.messages[2].message).toContain('context')
expect(result.output).not.toBeTruthy()
})
})

View File

@@ -0,0 +1,104 @@
const path = require('path')
const CLIEngine = require('eslint').CLIEngine
const plugin = require('..')
const _ = require('lodash')
const { stripIndent } = require('common-tags')
const ruleName = 'no-return-before'
const pluginName = '__plugin__'
function execute (file, options = {}) {
const opts = _.defaultsDeep(options, {
fix: true,
config: {
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
},
})
const cli = new CLIEngine({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
rules: {
[`${pluginName}/${ruleName}`]: ['error'],
},
...opts,
ignore: false,
useEslintrc: false,
plugins: [pluginName],
})
cli.addPlugin(pluginName, plugin)
const results = cli.executeOnFiles([path.join(__dirname, file)]).results[0]
return results
}
describe(ruleName, () => {
it('pass', async () => {
const filename = './fixtures/no-return-before-pass.js'
const result = execute(filename)
expect(result.errorCount).toBe(0)
})
it('fail', async () => {
const filename = './fixtures/no-return-before-fail.js'
const result = execute(filename, {
fix: false,
})
expect(result.errorCount).toBe(4)
expect(result.messages[0].message).toContain(`after 'describe'`)
})
it('fix fail', async () => {
const filename = './fixtures/no-return-before-fail.js'
const result = execute(filename)
expect(result.output).toEqual(`${stripIndent`
describe('outer', ()=>{
describe('some test', ()=>{
context('some test', ()=>{
it('some test', ()=>{
expect('foo').to.eq('bar')
})
return someFn()
})
})
})
`}\n`)
})
describe('config', () => {
it('config [tokens]', async () => {
const filename = './fixtures/no-return-before-fail.js'
const result = execute(filename, {
fix: false,
rules: {
[`${pluginName}/${ruleName}`]: [
'error', {
tokens: ['someFn'],
},
],
},
})
expect(result.errorCount).toBe(1)
// console.log(result.messages[0].message)
expect(result.messages[0].message).toContain('someFn')
expect(result.output).not.toBeTruthy()
})
})
})

View File

@@ -0,0 +1,96 @@
const path = require('path')
const CLIEngine = require('eslint').CLIEngine
const plugin = require('..')
const _ = require('lodash')
const ruleName = 'skip-comment'
const pluginName = '__plugin__'
function execute (file, options = {}) {
const opts = _.defaultsDeep(options, {
fix: true,
config: {
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
},
})
const cli = new CLIEngine({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
rules: {
[`${pluginName}/${ruleName}`]: ['error'],
},
...opts,
ignore: false,
useEslintrc: false,
plugins: [pluginName],
})
cli.addPlugin(pluginName, plugin)
const results = cli.executeOnFiles([path.join(__dirname, file)]).results[0]
return results
}
describe('skip-comment', () => {
it('skip test with comment', async () => {
const filename = './fixtures/skip-comment-pass.js'
const result = execute(filename, {
fix: true,
})
expect(result.errorCount).toBe(0)
})
it('skip test without comment', async () => {
const filename = './fixtures/skip-comment-fail.js'
const result = execute(filename, {
fix: true,
})
expect(result.errorCount).toBe(3)
// console.log(result.messages[0].message)
expect(result.messages[0].message).toContain('it')
expect(result.messages[0].message).toContain('NOTE:')
expect(result.messages[0].message).toContain('TODO:')
expect(result.messages[1].message).toContain('describe')
expect(result.messages[1].message).toContain('NOTE:')
expect(result.messages[2].message).toContain('context')
expect(result.messages[2].message).toContain('NOTE:')
expect(result.output).not.toBeTruthy()
})
describe('config', () => {
it('skip test without comment', async () => {
const filename = './fixtures/skip-comment-config.js'
const result = execute(filename, {
fix: true,
rules: {
[`${pluginName}/${ruleName}`]: [
'error', {
commentTokens: ['FOOBAR:'],
},
],
},
})
expect(result.errorCount).toBe(1)
// console.log(result.messages[0].message)
expect(result.messages[0].message).toContain('it')
expect(result.messages[0].message).toContain('FOOBAR:')
expect(result.output).not.toBeTruthy()
})
})
})

File diff suppressed because it is too large Load Diff