Wednesday, September 6, 2023

Enforcing coding style with @vercel/style-guide

Coding Style
ESLint
Prettier
TypeScript

1.7K views

What's @vercel/style-guide? Why would I use it?

@vercel/style-guide offers predefined configs of the following tools in a single package.

If you are not sure which ESLint rules or plugins to use (there are A LOT!!!) or wish to enforce code quality but not sure where to start, @vercel/style-guide is a decent drop-in solution.

What's the catch?

@vercel/style-guide is strict, VERY strict. If you are getting used to the default config provisioned by a project CLI, e.g., Next.js and Vite, it might take you some time to adapt to it.

Also, I found some configs aren't making too much sense in the way they are configured. I found myself fighting against those configs instead of enhancing my code quality. Thus, I have overridden some of the configs to how I feel comfortable. The good news to you is, I'm glad to share them with you.

What I'm currently using

FYI, the following are the config files I'm running in my Next.js project. Feel free to copy and/or customize them according to your needs.

.eslintrc.js

const { resolve } = require('node:path');
 
const project = resolve(__dirname, 'tsconfig.json');
 
module.exports = {
  root: true,
  extends: [
    require.resolve('@vercel/style-guide/eslint/browser'),
    require.resolve('@vercel/style-guide/eslint/react'),
    require.resolve('@vercel/style-guide/eslint/next'),
    require.resolve('@vercel/style-guide/eslint/node'),
    require.resolve('@vercel/style-guide/eslint/typescript'),
  ],
  parserOptions: { project },
  settings: {
    'import/resolver': { typescript: { project } },
    /**
     * enable MUI Joy components to be checked
     * @see {@link https://github.com/jsx-eslint/eslint-plugin-jsx-a11y?tab=readme-ov-file#configurations}
     */
    'jsx-a11y': {
      polymorphicPropName: 'component',
      components: {
        Button: 'button',
        Icon: 'svg',
        IconButton: 'button',
        Image: 'img',
        Input: 'input',
        Link: 'a',
        List: 'ul',
        ListItem: 'li',
        ListItemButton: 'button',
        ListDivider: 'li',
        NextImage: 'img',
        NextLink: 'a',
        SvgIcon: 'svg',
        Textarea: 'textarea',
      },
    },
  },
  rules: {
    '@typescript-eslint/consistent-type-imports': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-confusing-void-expression': [
      'error',
      { ignoreArrowShorthand: true },
    ],
    '@typescript-eslint/no-shadow': 'off',
    '@typescript-eslint/no-misused-promises': [
      'error',
      { checksVoidReturn: false },
    ],
    // such that @/* imports will not be considered as external dependencies
    'react/function-component-definition': [
      'warn',
      {
        namedComponents: 'arrow-function',
        unnamedComponents: 'arrow-function',
      },
    ],
    // sort import statements
    'import/order': [
      'warn',
      {
        groups: [
          'builtin',
          'external',
          'internal',
          'parent',
          'sibling',
          'index',
        ],
        'newlines-between': 'always',
        alphabetize: { order: 'asc' },
      },
    ],
    // sort named imports within an import statement
    'sort-imports': ['warn', { ignoreDeclarationSort: true }],
  },
  overrides: [
    // Next.js App Router file convention
    // Must use default export
    {
      files: [
        'src/app/**/page.tsx',
        'src/app/**/layout.tsx',
        'src/app/**/not-found.tsx',
        'src/app/**/*error.tsx',
        'src/app/sitemap.ts',
        'src/app/robots.ts',
      ],
      rules: {
        'import/no-default-export': 'off',
        'import/prefer-default-export': ['error', { target: 'any' }],
      },
    },
    // module declarations
    {
      files: ['**/*.d.ts'],
      rules: { 'import/no-default-export': 'off' },
    },
  ],
};

tsconfig.json

{
  "extends": "@vercel/style-guide/typescript",
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "noEmit": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"],
      "@mui/material/*": ["./node_modules/@mui/joy/*"]
    }
  },
  "include": [
    "src/types/*.d.ts",
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}

For Prettier, I'm not overriding the options in @vercel/style-guide/prettier, but I've extended it to support Prisma. That being said if you wish to further customize the config, e.g., using double quotes instead of single quotes, you may create the config file in the following way.

.prettierrc.js

const vercelPrettierOptions = require('@vercel/style-guide/prettier');
 
/** @type {import('prettier').Options} */
module.exports = {
  ...vercelPrettierOptions,
  plugins: ['prettier-plugin-prisma'],
  // your options to override Vercel's options
  singleQuote: false,
};

Any Questions or Comments?

Contact Me