Skip to content

Commit

Permalink
feat: nextjs eslint config (#7167)
Browse files Browse the repository at this point in the history
* feat: eslint for nextjs

* chore: changeset

* chore: docs

---------

Co-authored-by: Andrzej Kurek <[email protected]>
  • Loading branch information
Razz21 and Andrzej Kurek committed Jun 5, 2024
1 parent 7a601ba commit e9273b6
Show file tree
Hide file tree
Showing 9 changed files with 839 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-forks-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vue-storefront/eslint-config": minor
---

**[ADDED]** Eslint rules for Next.js v14
12 changes: 12 additions & 0 deletions engineering-toolkit/eslint-config/README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ Vue Storefront `Vue3` specific linting rules.
}
```

### Usage with NextJs `.eslintrc`

Vue Storefront `NextJs` specific linting rules.

```json
{
"extends": [
"@vue-storefront/eslint-config/next"
]
}
```

## Used rulesets & plugins

- [unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn)
Expand Down
14 changes: 11 additions & 3 deletions engineering-toolkit/eslint-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"./vue3": "./src/vue3.js",
"./react": "./src/react.js",
"./prettier": "./src/prettier.js",
"./json": "./src/json.js"
"./json": "./src/json.js",
"./next": "./src/next.js",
"./next-strict": "./src/next-strict.js"
},
"homepage": "https://github.com/vuestorefront/vue-storefront/engineering-toolkit/eslint-config",
"contributors": [
Expand Down Expand Up @@ -42,13 +44,16 @@
},
"dependencies": {
"@microsoft/eslint-plugin-sdl": "^0.2.0",
"typescript-eslint": "7.9.0",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@typescript-eslint/parser": "^7.9.0",
"@vue/eslint-config-typescript": "^11.0.2",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-etc": "^2.0.2",
"eslint-plugin-jsonc": "^2.7.0",
"eslint-plugin-jsdoc": "^48.2.5",
"eslint-plugin-jsonc": "^2.15.1",
"eslint-plugin-no-secrets": "^0.8.9",
"eslint-config-next": "^14.2",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.32.2",
Expand All @@ -57,6 +62,9 @@
"eslint-plugin-unicorn": "^46.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"eslint-plugin-vue": "^9.10.0",
"eslint-plugin-custom-rules": "file:./src/custom-rules",
"eslint-plugin-filename-rules": "^1.3.1",
"eslint-plugin-perfectionist": "2.10.0",
"vue-eslint-parser": "^9.1.1"
},
"stableVersion": "2.0.0"
Expand Down
7 changes: 7 additions & 0 deletions engineering-toolkit/eslint-config/src/custom-rules/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* eslint-disable global-require */
/** @type {import('eslint').ESLint.Plugin} */
module.exports = {
rules: {
"export-component-props": require("./rules/export-component-props"),
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "eslint-plugin-custom-rules",
"version": "0.0.1",
"main": "index.js"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-disable func-names */
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "Ensure component Props interfaces are exported",
category: "Best Practices",
recommended: false,
},
// Rule is autofixable
fixable: "code",
schema: [],
},
create(context) {
return {
"TSInterfaceDeclaration, TSTypeAliasDeclaration": function (node) {
const { name } = node.id;
// Get the filename without the path and extension
const filename = context.filename.split("/").pop().split(".")[0];

// Check if the interface follows the <filename>Props pattern
if (name.endsWith("Props") && name.startsWith(filename)) {
const { sourceCode } = context;
const exportTokens = sourceCode.getTokensBefore(node, {
filter: (token) => token.value === "export",
count: 1,
});

if (!exportTokens.length) {
context.report({
node,
message: `\`${name}\` interface should be exported from the file.`,
fix(fixer) {
const nodeStart = node.range[0];
return fixer.insertTextBeforeRange(
[nodeStart, nodeStart],
"export "
);
},
});
}
}
},
};
},
};
110 changes: 110 additions & 0 deletions engineering-toolkit/eslint-config/src/next-strict.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
extends: [require.resolve("./next")],
plugins: ["filename-rules", "jsdoc", "custom-rules"],
root: true,
ignorePatterns: [
"node_modules",
".next",
".turbo",
"out",
"build",
"dist",
"public",
],
rules: {
// prefer `import type` https://typescript-eslint.io/rules/consistent-type-imports/
"@typescript-eslint/consistent-type-imports": "error",
// enforce PascalCase for React components https://github.com/dolsem/eslint-plugin-filename-rules
"filename-rules/match": ["error", { ".tsx": "pascalcase" }],
// sort keys in JSON files https://eslint.org/docs/latest/rules/sort-keys
"jsonc/sort-keys": ["error"],
// https://eslint.org/docs/latest/rules/no-restricted-imports
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["../../*"],
message: "Use absolute imports (@/) instead",
},
],
},
],
// prefer props destructuring function Component({ prop }) https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/destructuring-assignment.md
"react/destructuring-assignment": ["error", "always"],
// enforce PascalCase for React components https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-pascal-case.md
"react/jsx-pascal-case": ["error"],
// prefer function declaration for components https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/function-component-definition.md
"react/function-component-definition": [
"error",
{
namedComponents: "function-declaration",
unnamedComponents: "arrow-function",
},
],
},
overrides: [
{
// no need to sort keys in package.json
files: ["package.json"],
rules: {
"jsonc/sort-keys": "off",
},
},
{
// prefer default export for React components
files: ["*.tsx"],
rules: {
"import/prefer-default-export": ["error"],
},
},
{
// allow different case for Next.js built in files and config
files: ["app/**/*.tsx", "config/**/*.tsx"],
rules: {
"filename-rules/match": "off",
},
},
{
files: ["components/**/*.tsx"],
rules: {
// require JSDoc for components
"jsdoc/require-jsdoc": [
"error",
{
contexts: ["TSMethodSignature", "TSPropertySignature"],
require: {
FunctionDeclaration: false,
},
publicOnly: true,
fixerMessage: " TODO: Add JSDoc comment",
},
],
// prefer "interface" over "type" for component props
"@typescript-eslint/consistent-type-definitions": "error",
// enforce export component props
"custom-rules/export-component-props": "error",
},
},
{
// require JSDoc for reusable UI parts and logic
files: [
"hooks/**/*.ts",
"hooks/**/*.tsx",
"helpers/**/*.ts",
"helpers/**/*.tsx",
],
rules: {
"jsdoc/require-jsdoc": [
"error",
{
contexts: ["TSMethodSignature", "TSPropertySignature"],
publicOnly: true,
fixerMessage: " TODO: Add JSDoc comment",
},
],
},
},
],
};
58 changes: 58 additions & 0 deletions engineering-toolkit/eslint-config/src/next.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
extends: [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended",
"plugin:jsonc/recommended-with-json",
"plugin:perfectionist/recommended-natural",
],
root: true,
ignorePatterns: [
"node_modules",
".next",
".turbo",
"out",
"build",
"dist",
"public",
],
rules: {
// allow unused vars starting with `_` https://typescript-eslint.io/rules/no-unused-vars
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
/**
* https://eslint-plugin-perfectionist.azat.io/rules/sort-imports
* Example:
*
* import "./globals.css"; // side-effect
* import type { Metadata } from "next"; // external
* import { Inter } from "next/font/google"; // external
*
* import Lint from "@/components/Lints"; // internal
*
*/
"perfectionist/sort-imports": [
"error",
{
type: "natural",
"internal-pattern": ["@/**"],
groups: [
["external", "side-effect"],
"internal",
["parent", "sibling", "index"],
["object", "unknown"],
],
},
],
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
},
};
Loading

0 comments on commit e9273b6

Please sign in to comment.