Wednesday, September 6, 2023

Enforcing coding style with @vercel/style-guide

Coding Style
ESLint
Prettier
TypeScript

 views

Cover photo for Enforcing coding style with @vercel/style-guide

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 { JAVASCRIPT_FILES } = require('@vercel/style-guide/eslint/constants');
 
const project = resolve(__dirname, 'tsconfig.json');
 
/** @type {import('eslint').Linter.Config} */
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',
        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 },
    ],
    '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: [
    /**
     * JS files are using @babel/eslint-parser, so typed linting doesn't work there.
     * @see {@link https://typescript-eslint.io/linting/typed-linting#how-can-i-disable-type-aware-linting-for-a-subset-of-files}
     */
    {
      files: JAVASCRIPT_FILES,
      extends: ['plugin:@typescript-eslint/disable-type-checked'],
    },
    // Varies file convention from libraries, e.g. Next.js App Router and Prettier
    // Must use default export
    {
      files: [
        'next.config.js',
        'prettier.config.js',
        'app/**/page.tsx',
        'app/**/layout.tsx',
        'app/**/not-found.tsx',
        'app/**/*error.tsx',
        'app/apple-icon.tsx',
        'app/**/opengraph-image.tsx',
        'app/sitemap.ts',
        '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/node20",
  "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": { "@/*": ["./*"] },
  },
  "include": [
    "image.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.

prettier.config.mjs
import vercelPrettierOptions from '@vercel/style-guide/prettier';
 
/** @type {import('prettier').Config} */
const config = {
  ...vercelPrettierOptions,
  plugins: [...vercelPrettierOptions.plugins, 'prettier-plugin-prisma'],
};
 
export default config;

Written By

Matthew Kwong

Matthew Kwong

Senior Web Engineer @ Viu

GitHubLinkedInStack OverflowRSS
More About MeMore Articles