Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): creation of a new custom eslint plugin #766

Merged
merged 9 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 101 additions & 35 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"lint": "npm run prettier && npm run lint:js",
"lint:js": "lerna run lint --stream",
"prettier": "prettier --check .",
"prettier:write": "prettier --list-different --write \"./**/**.js\"",
"prepare": "husky install",
"version": "npm i && git add package-lock.json",
"release": "lerna publish",
Expand Down
14 changes: 2 additions & 12 deletions packages/eslint-config/esm.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
/** @type {import("eslint-define-config").ESLintConfig} */
const config = {
extends: ['plugin:require-extensions/recommended'],
plugins: ['require-extensions', 'unicorn'],
extends: ['plugin:require-extensions/recommended', 'plugin:@readme/esm'],
plugins: ['require-extensions', 'unicorn', '@readme'],
rules: {
// see here for more rules to possibly enable in the future:
// https://gist.github.com/Jaid/164668c0151ae09d2bc81be78a203dd5
'import/no-commonjs': 'error',

/**
* This rule enablement is generally only intended for public-facing libraries but when shipping
* a package that exports CJS and ESM exports you cannot do `export default` and also
* `export type` in the same file because `export default` gets translated to `export =` in CJS
* and TS types can't be layered on top of that.
*
* @see {@link https://github.com/isaacs/tshy#handling-default-exports}
*/
'import/no-default-export': 'warn',

'node/no-extraneous-import': 'error',
'unicorn/prefer-module': 'error',
'unicorn/prefer-node-protocol': 'error',
Expand Down
9 changes: 5 additions & 4 deletions packages/eslint-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@
"type": "git",
"url": "[email protected]:readmeio/standards.git"
},
"bugs": {
"url": "https://github.com/readmeio/standards/issues"
},
"homepage": "https://github.com/readmeio/standards#readme",
"engines": {
"node": ">=18"
},
"scripts": {
"prettier": "prettier --list-different --write \"./**/**.js\"",
"lint": "eslint .",
"test": "tsc"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@readme/eslint-plugin": "file:../eslint-plugin",
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",
"eslint-config-airbnb-base": "^15.0.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/eslint-plugin/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "@readme/eslint-config",
"parserOptions": {
"ecmaVersion": 2022
}
}
5 changes: 5 additions & 0 deletions packages/eslint-plugin/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Copyright 2023 ReadMe

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
36 changes: 36 additions & 0 deletions packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# eslint-plugin-readme

Custom ESLint plugin for some ReadMe engineering guidelines and gotchas.

[![](https://raw.githubusercontent.com/readmeio/.github/main/oss-header.png)](https://readme.io)

[![npm](https://img.shields.io/npm/v/@readme/eslint-plugin)](https://npm.im/@readme/eslint-plugin) [![Build](https://github.com/readmeio/standards/workflows/CI/badge.svg)](https://github.com/readmeio/standards)

## Usage

In `.eslintrc` file add the following line:

```js
extends: ['plugin:@readme/<config>'],
plugins: ['@readme'],
```

## 🔖 Available Configs

<!-- prettier-ignore-start -->

| Config | Description |
| :--- | :--- |
| `esm` | Rules specific to ESM libraries. |

<!-- prettier-ignore-end -->

## 📖 Rules

<!-- prettier-ignore-start -->

| Rule | Description | Config |
| :--- | :--- | :--- |
| [no-dual-exports](https://github.com/readmeio/standards/tree/main/packages/eslint-plugin/docs/no-dual-exports.md) | Prevent cases of having a file with dual `default` and named exports. | `esm` |

<!-- prettier-ignore-end -->
33 changes: 33 additions & 0 deletions packages/eslint-plugin/docs/no-dual-exports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Prevent cases of having a file with dual `default` and named exports

In libraries that support CJS and ESM environments, having a file that has a `default` **and** a named export makes that file incompatible with CJS environments due to the way that CJS resolution works in that case. To resolve this, a file should either have a single `default` export, or it should only be comprised of named exports. You cannot have both.

## Fail

```ts
export interface ReducerOptions {
paths?: Record<string, string[] | '*'>;
tags?: string[];
}

export function reducer(definition: OASDocument, opts: ReducerOptions = {}) {
/** code here */
}
```

```js
export default class SDK {}
```

## Pass

```ts
export interface ReducerOptions {
paths?: Record<string, string[] | '*'>;
tags?: string[];
}

export default function reducer(definition: OASDocument, opts: ReducerOptions = {}) {
/** code here */
}
```
20 changes: 20 additions & 0 deletions packages/eslint-plugin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable global-require */
const { name: packageName, version: packageVersion } = require('./package.json');

module.exports = {
meta: {
name: packageName,
version: packageVersion,
},
configs: {
esm: {
plugins: ['readme'],
rules: {
'readme/no-dual-exports': 'error',
},
},
},
rules: {
'no-dual-exports': require('./rules/no-dual-exports'),
},
};
5 changes: 5 additions & 0 deletions packages/eslint-plugin/lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
getDocURL: rule => {
return `https://github.com/readmeio/standards/tree/main/packages/eslint-plugin/docs/${rule}.md`;
},
};
32 changes: 32 additions & 0 deletions packages/eslint-plugin/package.json
erunion marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@readme/eslint-plugin",
"version": "1.0.0",
"description": "ESLint plugin providing custom rules for ReadMe's coding standards",
"main": "index.js",
"author": "Jon Ursenbach <[email protected]>",
"license": "ISC",
"repository": {
"type": "git",
"url": "git://github.com/readmeio/standards.git",
"directory": "packages/eslint-plugin"
erunion marked this conversation as resolved.
Show resolved Hide resolved
},
"bugs": {
"url": "https://github.com/readmeio/standards/issues"
},
"homepage": "https://github.com/readmeio/standards#readme",
"engines": {
"node": ">=18"
},
"scripts": {
"lint": "eslint .",
erunion marked this conversation as resolved.
Show resolved Hide resolved
"test": "vitest run"
},
"peerDependencies": {
"eslint": "^8.0.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.22.15",
"@readme/eslint-config": "file:../eslint-config",
"vitest": "^0.34.6"
}
}
29 changes: 29 additions & 0 deletions packages/eslint-plugin/rules/no-dual-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { getDocURL } = require('../lib/utils');

module.exports = {
meta: {
type: 'problem',
category: 'ESM',
docs: {
description: 'Prevent cases of having a file with dual `default` and named exports.',
url: getDocURL('no-dual-exports'),
},
},
create: context => {
return {
ExportDefaultDeclaration: node => {
if (!node?.parent?.body) {
return;
} else if (!node.parent.body.filter(n => n.type === 'ExportNamedDeclaration').length) {
return;
}

context.report({
node,
message:
'In a dual package world you cannot ship a file for CJS environments that has a mix of `default` and named exports. This export should either be made a named export or refactor this file to have just one default export.',
});
},
};
},
};
47 changes: 47 additions & 0 deletions packages/eslint-plugin/test/no-dual-exports.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const { RuleTester } = require('eslint');

const { rules } = require('..');

const ruleTester = new RuleTester({
parser: require.resolve('@babel/eslint-parser'),
parserOptions: {
requireConfigFile: false,
},
});

const errorMessage =
'In a dual package world you cannot ship a file for CJS environments that has a mix of `default` and named exports. This export should either be made a named export or refactor this file to have just one default export.';

ruleTester.run('no-dual-exports', rules['no-dual-exports'], {
valid: [
{
code: 'export class ReducerOptions {}',
},
{
code: 'export default class ReducerOptions {}',
},
{
code: 'export default function reducer(definition, opts) {}',
},
{
code: `export class ReducerOptions {}

export function reducer(definition, opts) {}`,
},
],
invalid: [
{
code: `export class ReducerOptions {}

export default function reducer(definition, opts) {}`,
errors: [{ message: errorMessage }],
},
{
code: `const buster = "buster";
export function getDogName() {};

export default buster = buster;`,
errors: [{ message: errorMessage }],
},
],
});
7 changes: 7 additions & 0 deletions packages/eslint-plugin/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
globals: true,
},
});