This package provides LabKey's base JS ESLint configuration (without React plugins) as an extensible shared config.
See the CHANGELOG for changes in this version.
$ npm install --save-dev --save-exact @labkey/eslint-config-base
You'll need to also install all of the dependencies. On OSX or Linux, you can use this snippet to install all of the dependencies:
$ npm info @labkey/eslint-config-base peerDependencies --json | command sed 's/[\{\},]//g ; s/: /@/g' | xargs npm install --save-dev --save-exact --dry-run
When you have inspected the output, you can remove the --dry-run
flag to actually perform the install.
Create an .eslintrc.json file with the following contents
{
"extends": "@labkey/eslint-config-base"
}
First, install the @labkey/eslint-config-base
or @labkey/eslint-config-react
as appropriate. Once configured, you can run eslint in a variety of ways
Add a .eslintignore to only lint src/client and .js, .jsx, .ts, .tsx files:
# exclude everything by default
*.*
# exclude these directories
resources/web/gen
src/java
src/api-src
test/
webpack/
# include files
!*.js
!*.jsx
!*.ts
!*.tsx
You can start by running eslint manually on a single file or a collection of files:
$ npx eslint src/client/util/utilities.ts
$ npx eslint src/client/util/*.ts
$ npx eslint --ext '.ts,.tsx' src/client/util
Review the errors and either manually fix them up, disable rules for the line , or run eslint with --fix
to have automatic corrections applied.
You can add some scripts to package.json to invoke the linter as well. For example:
{
"scripts": {
"lint": "eslint --ext '*.ts,*.tsx'",
"lint-all": "eslint --ext '*.ts,*.tsx' src/client",
"lint-fix": "eslint --fix --ext '*.ts,*.tsx'"
},
}
To only run eslint on staged files (the files that have been 'git added'), include lint-staged package in your project.json:
$ npm install --save-dev --save-exact lint-staged
and call the lint script:
{
"lint-staged": {
"*": "npm run lint --"
}
}
It is still manual, but you can now run the linter on just the git staged files:
$ ... hack hack
$ git add src/client/file1.ts
$ git add src/client/file2.ts
$ npx lint-staged
lint-staged
will stash any other changes not 'git added' to the index prior to running the linter. This means that some code or configurations might not be picked up when eslint is run. It does give you the power to run the eslint over just the staged hunks, however. See this blog post for more.
🚧 It would be nice to have lint-staged
run on the non-staged files as well. For now, you can run the linter on them manually or use IntelliJ. See this issue for more.
You can require that the linter to pass before commit by adding a git pre-commit hook using husky.
Install:
$ npm install --save-dev --save-exact husky
Configure:
{
"husky": {
"hooks": {
"pre-commit": "npm test && lint-staged"
}
}
}
You can also require the tests to pass and all files pass lint before pushing:
{
"husky": {
"hooks": {
"pre-push": "npm test && npm run lint-all"
}
}
}
I highly recommend turning on ESLint within IntelliJ. It's as simple as going to 'Settings > Languages & Frameworks > JavaScript > Code Quality Tools > ESLint' and choose the 'Automatic ESLint Configuration' option. Once enabled, the eslint warnings and errors will show up in the gutter bar and can be autocorrected from within IntelliJ.
For more: https://www.jetbrains.com/help/idea/eslint.html
As you enable the linting, you will encounter some preferred code changes.
You may encounter the react/prefer-stateless-function rule. In the past, we've written lots of compoents by just extending React.Component
without much thought creating a pure stateless component. It turns out it's a pretty easy code transformation to make. See this StackOverflow answer for more.
Original code:
interface XProps {
onClick: () => void;
}
export class XButton extends React.Component<XProps, any> {
render() {
const { onClick } = this.props;
return (
<div className="sample--group-remove-button">
<span onClick={onClick}>
<i className="fa fa-times"/>
</span>
</div>
);
}
}
Functional component (the XProps
interface is unchanged):
export const XButton: SFC<XProps> = ({ onClick }) => (
<div className="sample--group-remove-button">
<span onClick={onClick}>
<i className="fa fa-times" />
</span>
</div>
);
If there are default props, you can use destructuring and a default value:
Original:
interface XProps {
onClick: () => void;
className: string;
}
export class XButton extends React.Component<XProps, any> {
static defaultProps = {
className: 'fa fa-times',
};
render() {
const { onClick, className } = this.props;
return (
<div className="sample--group-remove-button">
<span onClick={onClick}>
<i className={className} />
</span>
</div>
);
}
}
Function component with default argument values:
export const XButton: SFC<XProps> = ({ onClick, className = 'fa fa-times' }) => {
return (
<div className="sample--group-remove-button">
<span onClick={onClick}>
<i className={className} />
</span>
</div>
);
};
Or using an implicit return value:
export const XButton: SFC<XProps> = ({ onClick, className = 'fa fa-times' }) => (
<div className="sample--group-remove-button">
<span onClick={onClick}>
<i className={className} />
</span>
</div>
);
The @typescript-eslint/interface-name-prefix rule encourages interfaces to not start with the letter 'I'. This is a hold over from C# coding convention and isn't necessary in Typescript (which has distinct extends
and implements
keywords). In the past we used this convention primarily with Immutable.JS Records. The rule is disabled for now but going forward, the Record interface should be renamed to include a Props
suffix instead. In addition, we should use readonly
modifier on the Record fields as well as the Partial<RecordTypeProps>
in the constructor. For now, we will need to turn off the eslint no-useless-constructor
rule for the Record constructor.
Original code:
interface IPerson {
name: string;
int: age;
}
class Person extends Record({
name: '',
int: 0
}) implements IPerson {
name: string;
age: int;
constructor(values?: { [key: string]: any }) {
super(values);
}
}
Refactored:
interface PersonProps {
name: string;
int: age;
}
const defaultPersonProps: PersonProps = {
name: '',
int: 0
};
class Person extends Record(defaultPersonProps) implements PersonProps {
name: string;
age: int;
constructor(values?: Partial<PersonProps>) {
super(values);
}
}