Skip to content

CRA To Vite Migration

SeungAh Hong13min read

Why You Should Use Vite

Problems with Traditional Module Bundlers

  • Before browsers natively supported ES Modules (Native ESM), JavaScript modularization could not be handled at the browser level. To support such source modules in the browser, we had to merge multiple files or group them into meaningful units using modularization syntax such as require/IIFE/import/export. This is called bundling, and today Webpack, Rollup, and Parcel are the tools that provide it.

  • However, as applications grew increasingly sophisticated, JS tools began to hit performance bottlenecks. Even with HMR, applying changed files could take several seconds, which hurt developer productivity.

  • ESM (ECMAScript Modules)

    • ESM refers to a module approach in which the modularization syntax import and export can be handled by the browser itself, without any separate tooling. If you run code like the following in a browser without a bundler such as Webpack, an error occurs.
    // app.js
    import { sum } from 'test.js'
    console.log(sum(1,2));
     
    <script src="app.js></script>

    Untitled In the past, browsers had no ability to interpret import and export, but now, by adding an attribute as shown below, the browser can handle import and export.

    <script type="module" src="app.mjs></script>

Vite's Characteristics

  • Performance improvements for cold-start dev server startup Untitled

    • Dependencies: Provides a pre-bundling feature using esbuild for source code whose contents do not change, such as node_modules

      • esbuild delivers speeds 10-100x faster than traditional bundlers like webpack and parcel

      • esbuild pre-bundles all packages in node_modules and serves and caches them as shown below

        // Dependencies package import 할 경우
        import dayjs from 'dayjs';
         
        // vite esm 지원을 위한 변환/캐싱
        /node_modules/.vite/deps/dayjs.js?v=b09621d8
    • Source code: Applies the Native ESM approach so the browser imports, transforms, and serves modules directly Untitled

  • HMR performance improvements With traditional bundlers, changing source code required re-running the entire bundling process, which caused slow performance. By using the Native ESM approach and swapping only the changed module, performance is improved. It also leverages HTTP headers to improve page load speed (minimizing the number of requests).

    • 304 Not Modified: Uses the client cache
    • Cache-Control: Uses caching via max-age=31536000, immutable
  • Uses Rollup in production

    • Although esbuild boasts the fastest performance, its ecosystem is still lacking and its stability for browser bundling is considered weaker, so Vite uses the Rollup bundler in production (providing performance benefits such as tree shaking, lazy loading, and file splitting).
    • Rollup switched its parser to SWC in v4, and work is underway to build Rolldown, a Rust port. Once development is complete, it will be able to fully replace esbuild (with the goal of eliminating the mismatch between development and build).
    • https://www.youtube.com/watch?v=EKvvptbTx6k

CRA to Vite Migration

base

  • Remove CRA package for Vite Remove the packages used in the existing CRA environment. If there is an already-installed node_modules folder, delete the folder itself.

    yarn remove react-scripts react-app-rewired babel-loader webpack-merge tsconfig-paths-webpack-plugin
     
    rm -rf node_modules
  • Setting the Stage with Vite Installation vite: A modern web project build tool

    • @vitejs/plugin-react: @vitejs/plugin-react adds the configuration needed to use React with Vite, supporting fast development and bundling of React applications
    • vite-tsconfig-paths: Enables the path aliases configured in the tsconfig.json file to also be applied in Vite. With vite-tsconfig-paths, the path aliases defined in the TypeScript configuration are recognized by Vite as well and can be used when resolving module paths
    • vite-plugin-svgr
      • A Vite plugin that converts SVGs into React components
      • However, installing a version later than 3.3.0 causes build and type errors (with 3.3.0 you can convert an SVG to a React component as shown below)
      import { ReactComponent as ChannelLogo } from '../../assets/artwork-illust-pm-assign-info-channel-talk.svg';
    • vite-plugin-checker
      • A plugin that provides multiple checking capabilities in Vite-based projects, including type checking, ESLint, and VLS (Volar Language Server)
      • Because it runs checks asynchronously, it has the advantage of not affecting the dev server's loading speed.
    yarn add vite @vitejs/plugin-react vite-tsconfig-paths vite-plugin-svgr@3.3.0
    
    yarn add vite-plugin-checker -D
    
  • Updating package.json for Vite Commands

    scripts": {
      "start": "vite", // dev-server
      "build": "vite build", // build
    },
  • Renaming React File Extensions to .jsx or .tsx Rename only the files that use React JSX syntax to jsx/tsx.

    mv src/App.js src/App.jsx
    mv src/index.ts src/index.tsx
  • Move the index.html File

    • If you do not delete the index.html in the public path, Vite will pre-load public/index.html when loading the development environment, so be sure to move it to the root!!
    mv public/index.html .
  • Add module script to the bottom of the body tag Specify the code entry point for the initial load.

    <body>
      {/* others here */}
      <script type="module" src="/src/index.tsx"></script>
    </body>
  • Update tsconfig.json Reasons to use isolatedModules

    • Isolating dependencies between modules: Ensures each module does not depend on the types of others, so modules are processed individually at compile time. This can reduce TypeScript's compilation time.
    • Performance improvement: Compiling modules independently means only the modules that changed are recompiled, improving overall compilation speed.
    • ESM compatibility: Increases compatibility with ES modules, providing better results when integrating with various build tools.
    • Preventing check errors: Since every module is checked individually, a type error in one module is prevented from affecting other modules. Configure the vite.config.js path in include.
    {
      "extends": "./tsconfig.extend.json",
      "compilerOptions": {
        "lib": ["dom", "dom.iterable", "es2020"],
        "target": "ES6",
        "types": ["vite/client", "react", "react-dom", "node"],
        "isolatedModules": true // added
      },
      "include": [
        "./src/**/*",
        "vite.config.js"
      ],
      "exclude": [
        "node_modules"
      ]
    }
  • Add module html minify In a CRA project, HTML minification is handled through the built-in HtmlWebpackPlugin, but since Vite does not include it, you need to install it yourself and add the configuration.

    // Installation
    yarn add vite-plugin-html
     
    // vite.config.js
    createHtmlPlugin({
      // input <https://www.npmjs.com/package/html-minifier-terser> options
      minify: true,
    }),

Configuration

  • Crafting the Vite Configuration

    // <root>/vite.config.js
    import { defineConfig, loadEnv } from 'vite';
    import path from 'path';
    import react from '@vitejs/plugin-react';
    import viteTsconfigPaths from 'vite-tsconfig-paths';
    import svgr from 'vite-plugin-svgr';
    import { createHtmlPlugin } from 'vite-plugin-html';
    import checker from 'vite-plugin-checker';
     
    export default defineConfig(config => {
      const env = loadEnv(config.mode, process.cwd(), '');
     
      return {
        build: {
          outDir: 'build', // build 폴더명 변경(기본 dist)
          target: 'es2015', // es6 target transpile
          sourcemap: false, // 소스맵 생성 여부
          rollupOptions: {
            output: {
              manualChunks(id) {
                // chunks 파일을 만들 경우 아래와 같이 청크명과 필터를 걸어주세요.
                if (id.includes('/react')) {
                  return 'venders';
                }
              },
            },
            onwarn(warning, defaultHandler) {
              // sourcemap: true 설정할 경우 node_modules에 소스맵 없는 경우에 대한 경고 무시를 위해 추가
              if (warning.code === 'SOURCEMAP_ERROR') {
                return;
              }
              defaultHandler(warning);
            },
          },
        },
        define: {
          global: 'globalThis', // 글로벌 객체를 빈 객체로 대체(global is not defined error 에러에 대한 방어코드)
        },
        plugins: [react(), viteTsconfigPaths(), svgr()],
      };
    });

Environment Variables

  • If you do not set the VITE prefix, you cannot retrieve the value from import.meta.env.

  • By default it provides the property values DEV (vite) and PROD (vite build), so you can distinguish between the development and build environments.

  • ChangeEnv Variables and Modes

    // .env Files
    .env                # loaded in all cases
    .env.local          # loaded in all cases, ignored by git
    .env.[mode]         # only loaded in specified mode
    .env.[mode].local   # only loaded in specified mode, ignored by git
     
    // Modes
    # .env.production.live
    VITE_ENVIRONMENT=live
     
    // command mode option flag
    "build:dev": "yarn vite --mode production.dev",
    "build:rc": "yarn vite --mode production.rc",
    "build:live": "yarn vite --mode production.live",
  • Update process.env.REACT_APP

    -process.env.REACT_APP_VARIABLE + import.meta.env.VITE_VARIABLE;

EJS Variables

  • Update Configuration for Vite To ensure the HTML CJS syntax is substituted at build time, add export handling using vite-plugin-html.

    createHtmlPlugin({
      ....
      inject: {
        data: {
          VITE_VARIABLE: env.VITE_VARIABLE,
        },
      },
    }),
    
  • Replace EJS Variables for Vite When substituting only text

    • "%" → "<%= ~~ =>"
    • e.g.) → when substituting characters within text
    // env VITE_VARIABLE='/variable' //
    <root
      >/index.html //// CRA
      <script src="%REACT_APP_VARIABLE%"></script>
     
      //// VITE
      <script src="<%= VITE_VARIABLE %>"></script
    ></root>

    When replacing the element (tag) itself

    • % → <%- ~~ ->
     // env
    VITE_SCRIPT=<script>(function ~~ )</script>
    
    // <root>/index.html
    //// CRA
    %VITE_SCRIPT%
    
    //// VITE
    <%- VITE_SCRIPT %>
    
  • Updating Vite Commands(with enviroment)

    "start": "vite",
    "start:dev": "yarn start --mode development.dev",
    "start:rc": "yarn start --mode development.rc",
    "build": "vite build",
    "build:dev": "yarn build --mode production.dev",
    "build:rc": "yarn build --mode production.rc",
    "build:live": "yarn build --mode production.live",

Vite Lint, Typescript Checker

  • Add module vite-plugin-checker In a CRA environment, type checking is handled asynchronously internally via the fork-ts-checker-webpack-plugin, but in a VITE environment you need to install vite-plugin-checker directly and add it to the vite.config.js configuration.

    // vite.config.js
    import checker from 'vite-plugin-checker';
     
    checker({
      typescript: config.mode.indexOf('development') > -1,
      eslint: config.mode.indexOf('development') > -1 ? {
        lintCommand: 'eslint "./src/**/*.{ts,tsx,js,jsx}"',
      } : false,
      stylelint: config.mode.indexOf('development') > -1 ? {
        lintCommand: 'stylelint "src/**/*.scss"'
      } : false,
    }),

CRA to Vite Migration Tip

  • import.meta.glob dynamic load You can use Vite's import.meta.glob to dynamically import all icons.

    const icons = import.meta.glob('./*Icon.tsx');
     
    const iconComponentPath = './aIcon.tsx';
     
    const Icon =
      icons && icons[iconComponentPath as string]
        ? lazy(
            () =>
              icons[iconComponentPath as string]() as Promise<{
                default: ComponentType;
              }>,
          )
        : null;
  • sass-resources-loader(dart sass) → vite config preprocessorOptions.sass sass-resources-loader, which is used to apply commonly used styles globally, can be replaced in Vite with preprocessorOptions.sass.

    // CRA
    rules: [
      {
        test: /\\.scss$/,
        use: [
          {
            loader: 'sass-resources-loader',
            options: {
              resources: [
                path.resolve(__dirname, './sass.scss'),
              ],
            },
          },
        ],
        include: path.resolve(__dirname, './src'),
        exclude: /node_modules/,
      },
    ]
     
    // vite.config.js
    css: {
      preprocessorOptions: {
        scss: {
          // Next line will prepend the import in all you scss files as you did with your vite.config.js file
          additionalData: `
            @import "./sass.scss";
          `,
        },
      },
    },

    Summary of error cases by step The "@forward rules must be written before any other rules" error

    • Fix it so that the @forward statement is placed above all other rules (@import, @use, style declarations) @forward
    • Part of the Sass module system introduced in Dart Sass, it plays an important role in structuring and managing modularized stylesheets.
    • Unlike the @import approach, @forward makes it easier to manage the modularization and reusability of Sass code. @use: imports a module while using a namespace to prevent conflicts, and lets you explicitly import only the parts you need. @import simply includes a file, which can be included multiple times and carries the risk of variable or mixin conflicts.
    // reset.scss
    @forward 'reset';
     
    // ./sass.scss
    @use 'color';
     
    css: {
      preprocessorOptions: {
        scss: {
          // Next line will prepend the import in all you scss files as you did with your vite.config.js file
          additionalData: `
            @forward 'reset';
     
            @use './color.scss' as *;
            @use './sass.scss' as *
          `,
        },
      },
    },
  • setting module scss name for Vite(optional) In CRA, when using SCSS modules, a unique class name is automatically generated by combining the file name and the class name, but VITE follows a different rule where the name is generated from the class name only.

    • Rule: [filename][original class name][hash:base64:5] However, if there is no case in your service where module scss is selected globally, there is no need to set this up. (Internally, if you do not configure it, the name is automatically changed to a unique value based on the [class name, hash].)
    import styles from './Test.module.scss';
     
    const Test = () => {
    	return (
    		<div className={styles.container}></div>
    	);
    };
     
    export default Test;
     
    // 변경 전
    class="Test_container__28qL6"
     
    // 변경 후
    //// vite.config.js
    const namespaceUUID = v4();
    css: {
      ...
      modules: {
        // 파일명과 클래스명을 포함한 고유한 이름 생성
        generateScopedName: (name, filename) => {
          // 파일 경로 기반 고유한 해시 생성
          const file = path.basename(filename).replace('.module', '').replace(/\.[^/.]+$/, '');
     
          // UUID 생성 후 Base64 인코딩
          const hash = Buffer.from(v5(`${file}_${name}`, namespaceUUID)).toString('base64').slice(0, 5);
          return `${file}_${name}__${hash}`;
        },
      },
    },
  • server(proxy, outside file) The CRA proxy configuration can be replaced with the VITE proxy. When accessing external folders/files outside the VITE environment, adding them to the fs.allow object makes them accessible from the dev server. Previously proxy paths were managed as an array, but in Vite you have to manage each path individually.

    // CRA
    //// 기존 패키지 삭제
    yarn remove http-proxy-middleware
     
    //// setupProxy.js
    // 수정 전
    const { createProxyMiddleware } = require('http-proxy-middleware');
    module.exports = function setupProxy(app) {
      app.use(createProxyMiddleware(['/test'], {
        changeOrigin: true,
    	  logLevel: 'debug',
    	  autoRewrite: true,
        target: 'http://www.test.kr',
        onProxyRes: (proxyRes) => {
          console.log(proxyRes);
        }),
        onProxyReq: (proxyReq) => {
          proxyReq.setHeader('origin', 'http://www.test.kr');
        },
      }));
     
    // vite.config.js
    //// 수정 후
    const env = loadEnv(config.mode, process.cwd(), '');
    server: {
    	proxy: {
    		fs: {
    	    // 허용할 디렉터리 추가 (여기에 외부 파일 경로를 추가)
    	    allow: [
    	      '../../../outside',
    	      '/'
    	    ],
    	  },
    		'/test': {
    	    changeOrigin: true,
    		  logLevel: 'debug',
    		  autoRewrite: true,
    	    target: 'http://www.test.kr',
    	    configure: (proxy) => {
    	      proxy.on('proxyRes', (proxyRes, env) => {
    	        console.log(proxyRes);
    	      }),
    	      proxy.on('proxyReq', (proxyReq, env) => {
    	        proxyReq.setHeader('origin', 'http://www.test.kr');
    	      });
    	    },
    	  },
    	},
    },
  • external file link settings(alias) In CRA, when accessing a path inside an external file (tsconfig.json), the path is resolved relative to the current project environment, but in VITE the path is accessed relative to the external file. This causes issues with alias handling, so you need to reassign it as an absolute path instead of a tsconfig path. e.g.)

    • CRA(root)
      • Based on the location of the outside/index.tsx component
    • VITE
      • Based on the root location
    // CRA
    const modifiedConfig = aliasWebpack({
      tsconfig: path.resolve(__dirname, './tsconfig.json'),
    })(config);
     
    // vite.config.js
    resolve: {
      alias: {
        '@outside': path.join(__dirname, '../outside'),
      },
    },
     
    // outside/index.tsx
    import Button from '../Button';
  • setting external file access permissions In CRA, accessing external files came with the hassle of adding them to the ModuleScopePlugin in the internal config file, but in VITE external file access is unrestricted during the build process, while in the development environment you need to configure the external file paths.

    • Note that when configuring file paths, you must also configure internal file paths in addition to the external file paths.
    fs: {
      // 허용할 디렉터리 추가 (여기에 외부 파일 경로를 추가)
      allow: [
        '../outside',
        '/'
      ],
    },
  • setting baseurl for Vite In CRA, resource paths are set internally based on the BaseURL (PUBLIC_URL), but in VITE you need to change that path to the base property value. (Specify the base url path in an env variable.) However, in the DEV environment you should use the internal resource path, so do not specify a path.

    // CRA //// index.html
    <head>
      <meta charset="utf-8" />
      <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    </head>
     
    // VITE //// env.production.dev VITE_PUBLIC_URL=https://test ////
    vite.config.js base: env.VITE_PUBLIC_URL || '/', //// index.html
    <head>
      <meta charset="utf-8" />
      <link rel="shortcut icon" href="/favicon.ico" />
    </head>
     
    //// index.html(결과물)
    <head>
      <meta charset="utf-8" />
      <link rel="shortcut icon" href="https://test/favicon.ico" />
    </head>

CRA to Vite Migration Storybook

  • Installation Remove the previous version of Storybook.

    yarn remove @storybook/addon-*(knobs, controls, etc.)

    Add the Storybook packages for Vite integration. In the existing CRA environment, using the @storybook/react package command worked fine, but with VITE you use the @storybook/react-vite package and need to install the storybook package additionally. Also, if you use version 6 of the Storybook integration packages, you will encounter an error like 'storybook-channel-mock.js:7 Uncaught TypeError: import_channels.default is not a constructor'. Be sure to install the latest packages, version 8 or higher.

    yarn add @storybook/builder-vite @storybook/react @storybook/react-vite storybook -D

    Major breaking changes

  • setting .storybook/main addon configuration

     addons: [
      '@storybook/addon-links',
      '@storybook/addon-essentials',
    ],

    Specify the builder and framework

    core: {
      builder: '@storybook/builder-vite', // 👈 The builder enabled here.
    },
    framework: '@storybook/react-vite',

    Specify the base static path

    // CRA - @storybook/react
    "storybook": "start-storybook -p 9009 -s public",
     
    // VITE - @storybook/vite
    staticDirs: ['../public'], // 이전 버전(v6) -s public 옵션과 동일

    webpackFinal → ViteFinal migration

    • Preprocess scss and set up the allowed file paths
    // webpack
    config.module.rules.push({
      test: /\.scss$/,
      use: [
        {
          loader: 'sass-resources-loader',
          options: {
            resources: [path.resolve(__dirname, '../sass.scss')],
          },
        },
      ],
      include: path.resolve(__dirname, '../src'),
      exclude: /node_modules/,
    });
     
    // Vite
    const { mergeConfig } = await import('vite');
    return mergeConfig(config, {
      // Merge custom configuration into the default config
      const { mergeConfig } = await import('vite');
      css: {
        preprocessorOptions: {
          scss: {
          // Next line will prepend the import in all you scss files as you did with your vite.config.js file
          additionalData: `
            @forward 'reset';
     
            @use './color.scss' as *;
            @use './sass.scss' as *
          `,
    	    },
        },
      },
    • External file access
    // VITE
    server: {
      fs: {
        // 허용할 디렉터리 추가 (여기에 외부 파일 경로를 추가)
        allow: [
          '../outside',
          '/'
        ],
      },
    },
  • storybook code migration processing No longer inferring default values of args(v6.3) With the version update, you can no longer set default values in argTypes; they are now set in args instead. It does not cause a build error, but if you check, you will find that undefined ends up being set, so be sure to change it during migration.

    // CRA - storybook
    argTypes: {
      isModalOpened: {
        name: '모달 오픈여부',
        defaultValue: true,
        control: {
          type: 'boolean',
        },
      },
    },
     
    // VITE - storybook
    argTypes: {
      isModalOpened: {
        name: '모달 오픈여부',
        control: 'boolean',
      },
    },
    args: {
      isModalOpened: true,
    },

    New parameters format for addon viewport(v8.3)

    • viewports → options change
    // 이전
    export const parameters = {
      viewport: {
        viewports: {
          iphone5: {
            name: 'phone',
            styles: {
              width: '320px',
              height: '568px',
           },
         },
       },
    }
     
    // 변경 후
    export const parameters = {
      viewport: {
        options: {
          iphone5: {
            name: 'phone',
            styles: {
              width: '320px',
              height: '568px',
           },
         },
       },
    }

    Story type deprecated(v7.0)

    // 변경 전
    import type { Story } from '@storybook/react';
     
    export const MyStory: Story = () => <div />;
     
    // 변경 후
    import type { StoryFn, StoryObj } from '@storybook/react';
     
    export const MyCsf2Story: StoryFn = () => <div />;
    export const MyCsf3Story: StoryObj = {
      render: () => <div />,
    };

    start-storybook / build-storybook binaries removed(v7.0)

    // Before
    {
      "scripts": {
        "storybook": "start-storybook <some flags>",
        "build-storybook": "build-storybook <some flags>"
      }
    }
     
    // After
    {
      "scripts": {
        "storybook": "storybook dev <some flags>",
        "build-storybook": "storybook build <some flags>"
      },
      "devDependencies": {
        "storybook": "next"
      }
    }

    Ignore story files from node_modules(v7.0)

    • By default, stories inside node_modules are now ignored
    export default {
      stories: ["../**/*.stories.*"],
    };
     
    // 기존 glob.sync 처리는 굳이 안해줘도 됩니다.
    // const getStories = () => glob.sync(`${path.resolve(__dirname, '../')}/src/**/*.stories.@(js|jsx|ts|tsx)`);
    // stories: async () => getStories(),
     
    // 펀딩 스튜디오 적용
    stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],

    Removed deprecated shim packages(v8.0)

    RemovalReplacement
    @storybook/addons@storybook/manager-api or @storyboook/preview-api
    @storybook/channel-postmessage@storybook/channels
    @storybook/channel-websocket@storybook/channels
    @storybook/client-api@storybook/preview-api
    @storybook/core-client@storybook/preview-api
    @storybook/preview-web@storybook/preview-api
    @storybook/store@storybook/preview-api
    @storybook/api@storybook/manager-api
    @storybook/testing-library@storybook/test
    Deprecated addon-knobs(v6.3) → you also need to update the static side if you want to bump the version.
    After installing the addon-essentials package, use controls instead of knobs.
    1. Controls (@storybook/addon-controls**)**: Lets you control a component's props from the UI.
    2. Actions (@storybook/addon-actions**)**: Logs event handlers to make them easier to debug.
    3. Docs (@storybook/addon-docs**)**: Automatically generates documentation for components.
    4. Viewport (@storybook/addon-viewport**)**: Lets you check a component's behavior at various screen sizes.
    5. Other addons (e.g., Backgrounds, Toolbars & Icons, etc.). https://github.com/storybookjs/storybook/blob/next/MIGRATION.md
  • package.json storybook command

    // CRA
    "storybook": "start-storybook -p 9009 -s public",
    "build-storybook": "build-storybook -s public -o .storybook/build --quiet",
     
    // VITE
    "storybook": "storybook dev -p 9009",
    "build-storybook": "storybook build -o .storybook/build --quiet"

Jest to Vitest Migration

  • Why Vitest is better than Jest

    1. Fast performance: esbuild-based, so tests run faster than Jest. (jest: Babel)
    2. ESM support: Natively supports modern ES modules. (Jest can support the ES approach, but compatibility issues arise)
    3. TypeScript integration: Integrates naturally with TypeScript without additional configuration.
    4. Simple mocking API: Provides intuitive and easy-to-use mocking features.
    5. Compatibility with Vite: Reuses your Vite configuration and plugins as-is.
    6. Modern features: Supports HMR and a fast watch mode.
    7. Small learning curve: Configuration familiar to Vite users.
    8. Excellent developer experience: Modern and detailed error reporting and test feedback.
  • Installation

    yarn add -D vitest jsdom @testing-library/dom @testing-library/jest-dom @testing-library/react @testing-library/user-event
     
    // Remove existing Jest test-related packages
    yarn remove jest babel-jest
     
  • setting Migrate the test configuration set up in package.json/jest.setup.js to vitest.config.js.

    // 변경 전
    //// package.json
    "jest": {
      "collectCoverageFrom": [
        "!<rootDir>/node_modules/"
      ],
      "moduleNameMapper": {
        "lodash-es": "lodash",
      },
      "transformIgnorePatterns": [
        "/node_modules/",
      ],
      "setupFilesAfterEnv": [
        "<rootDir>/jest.setup.js"
      ]
    },
     
    // 변경 후
    //// vitest.config.js
    export default defineConfig({
      resolve: {
        alias: {
          "lodash-es": "lodash",
        },
      },
      plugins: [
        react(),
        viteTsconfigPaths(), // tsconfig 파일 패스 연동
      ],
      test: {
        environment: 'jsdom', // React 테스트 환경을 위해 설정
        globals: true,        // Jest와 동일한 글로벌 변수를 사용
        setupFiles: './src/setupTests.ts', // setup 파일
      },
    });
     
    // src/setupTest.ts
    import '@testing-library/jest-dom';

    To run tests in your existing React test environment, be sure to set the test environment. (Add vitest to your TypeScript types.) Add vitest to your TypeScript types.

    // tsconfig.json
    "compilerOptions": {
      "lib": ["dom", "dom.iterable", "es2020"],
      "target": "ES6",
      "types": ["vite/client", "react", "react-dom", "node", "vitest/globals"], // vitest/globals 추가
      "isolatedModules": true
    },

    Add vi to the eslint global type-check variables.

    // .eslint.js
    module.exports = {
      globals: {
        context: 'readonly',
        vi: true, // vi 추가
      },
    };
  • jest code migration processing module mocks

    // 변경 전
    jest.mock('./some-path', () => 'hello');
    // 변경 후
    vi.mock('./some-path', () => 'hello');

    Importing the Original of a Mocked Package

    // 변경 전
    const { cloneDeep } = jest.requireActual('lodash/cloneDeep');
    // 변경 후
    const { cloneDeep } = await vi.importActual('lodash/cloneDeep');

    Replace property Vitest Done Callback

    • Since Vitest v0.10.0, the callback style is no longer supported, so if you need to support it, you must change it to a Promise callback function.
    it('should work', done => {});
    it('should work', () =>
      new Promise(done => {
        // ...
        done();
      }));

    Hooks For the beforeAll/beforeEach hooks, you must return a value other than null/undefined.

    -beforeEach(() => setActivePinia(createTestingPinia())) +
      beforeEach(() => {
        setActivePinia(createTestingPinia());
      });

    Jest hooks run synchronously, whereas Vitest runs them in parallel by default. However, to run hooks synchronously, you must set the sequence.hooks property.

    // vitest.config.js
    export default defineConfig({
      test: {
        sequence: {
          hooks: 'list',
        },
      },
    });

    Types Because Vitest does not support namespace properties, you must import the types directly.

    // 변경 전
    let fn: jest.Mock<(name: string) => number>;
    // 변경 후
    import type { Mock } from 'vitest';
    let fn: Mock<(name: string) => number>;

    Timeout

    // 변경 전
    jest.setTimeout(5_000);
    // 변경 후
    vi.setConfig({ testTimeout: 5_000 });
  • package.json test commands

    "test": "vitest",
    "test:run": "vitest run",

MSW Migration Guide

  • Using Vitest tests together with msw version 0.43.1 produces an error like the one below
  • This is presumed to be an error caused by the fact that Vitest supports ECMAScript modules while older versions of msw do not (fixed by upgrading to the latest version)
vitest Package subpath './lib' is not defined by "exports" in node_modules/headers-polyfill/package.json
  • Installation
    • As you upgrade msw to the latest version, you must upgrade the storybook-addon used in Storybook accordingly.
    • The Storybook configuration related to msw-storybook-addon is the same as before.
    yarn add -D msw@2.4.11 msw-storybook-addon@2.0.3
  • msw code migration processing worker-imports
    // 변경 전
    import { setupWorker } from 'msw';
    // 변경 후
    import { setupWorker } from 'msw/browser';
    Response resolver arguments
    // 변경 전
    rest.get('/resource', (req, res, ctx) => {});
    // 변경 후
    import { http } from 'msw';
    http.get('/resource', ({ request }) => {
      const url = new URL(request.url);
      const productId = url.searchParams.get('id');
    });
    Response declaration
    // 변경 전
    rest.get('/resource', (req, res, ctx) => {
      return res(ctx.status(200), ctx.json({ id: 'abc-123' }));
    });
    // 변경 후
    import { http } from 'msw';
    http.get('/resource', () => {
      return new Response(JSON.stringify({ id: 'abc-123' }), {
        headers: {
          'Content-Type': 'application/json',
        },
      });
    });
    // 변경 후 2
    import { http, HttpResponse } from 'msw';
    export const handlers = [
      http.get('/resource', () => {
        return HttpResponse.json({ id: 'abc-123' }, { status: 200 });
      }),
    ];
    Request params
    // 변경 전
    rest.get('/post/:id', req => {
      const { id } = req.params;
    });
    // 변경 후
    import { http } from 'msw';
    http.get('/post/:id', ({ params }) => {
      const { id } = params;
    });
    Request cookies
    // 변경 전
    rest.get('/resource', req => {
      const { token } = req.cookies;
    });
    // 변경 후
    import { http } from 'msw';
    http.get('/resource', ({ cookies }) => {
      const { token } = cookies;
    });
    Request Body
    // 변경 전
    rest.post('/resource', req => {
      // The library would assume a JSON request body
      // based on the request's "Content-Type" header.
      const { id } = req.body;
    });
    // 변경 후
    import { http } from 'msw';
    http.post('/user', async ({ request }) => {
      // Read the request body as JSON.
      const user = await request.json();
      const { id } = user;
    });
    1.x → 2.x

Results

Build performance: 6.16s → 2.49s ( 3.67s faster )

Untitled

Untitled

Dev Server startup: 167ms → incredibly fast^^

Untitled

References