Skip to content

msamsonoff/vite-ts-react

Repository files navigation

Introduction

This is a template project that combines Typescript, React, Jest, and Vite. There are many similar template projects available. Vite, for example, has a community template repository with dozens of templates covering many combinations of tools. The problem with these templates, and tools like create-react-app, is that they provide code and configuration but generally offer no explanation. This small project attempts to remedy that. It provides a baseline configuration along with explanations of the important settings.

The stack is:

This combination presents some challenges. In particular, pnpm and Typescript disagree on how type definitions work for transitive dependencies.

Setup

  1. Create package.json

        {
          "name": "vite-react",
          "version": "0.1.0",
          "scripts": {
          },
          "engines": {
            "node": ">=18"
          },
          "private": true
        }
    
  2. Add the basic dependencies for the project: react, react-dom, and typescript.

        $ pnpm add react react-dom
        $ pnpm add -D typescript
    
  3. Create tsconfig.json.

        {
          "compilerOptions": {
            "module": "es2022",
            "moduleResolution": "bundler",
            "noEmit": true,
            "jsx": "preserve",
            "lib": [
              "DOM",
              "DOM.Iterable",
              "ES2021"
            ],
            "target": "es2020",
            "useDefineForClassFields": true
          }
        }
    

    There are hundreds of settings available in tsconfig.json. These are the absolute minimum required:

    module: "es2022" : This sets the module system supported by Typescript. The value "es2022" enables ECMAScript modules, dynamic import, and import.meta.

    moduleResolution: "bundler" : This sets the strategy that Typescript uses to find modules. The value "bundler" matches SWC's module resolution strategy.

    noEmit: true : This stops the Typescript compiler from emitting compiled JavaScript files. SWC will generate JavaScript instead.

    jsx: "preserve" : This enables support for JSX syntax and sets how it is transformed before the compiler checks the program. The value "preserve" enables JSX support but without any transformation. This is semantically correct since we are using SWC and no transformation is necessary. Other values may work here, but they are particularly sensitive to the rest of the project's configuration.

    lib: ["DOM", "DOM.Iterable", "ES2021"] : By default, Typescript only supports the most basic APIs of JavaScript. You must enumerate other APIs here. The values listed here are the minimum required by the sample program. A complete application may require additional libraries.

    target: "es2020" : The TSConfig Reference describes this as the version of JavaScript used in the output files generated by the compiler. But that description seems incomplete. Despite that noEmit is set, and no files are generated, "target" must be set to "es6"/"es2015" or higher, or the sample program will not compile. The value "es2020" simply matches the value used by Vite's React plugin in development mode.

    useDefineForClassFields: true : This sets how the Typescript compiler emits class fields. Vite's React plugin always sets this to true. It's not absolutely required, but I choose to set it so that running tsc will be consistent with running vite.

  4. Add a check script to package.json.

        --- a/package.json
        +++ b/package.json
        @@ -2,6 +2,7 @@
           "name": "vite-react",
           "version": "0.1.0",
           "scripts": {
        +    "check": "tsc"
           },
           "engines": {
             "node": ">=18"
    
  5. Run the check script to type check the program.

        $ pnpm run check
    

Unit Tests

  1. Add the dependencies required for unit tests.

        $ pnpm add -D @testing-library/jest-dom @testing-library/react jest jest-environment-jsdom ts-node
    
  2. Create jest.config.ts and configure Jest to use the jsdom test environment.

        import { Config } from 'jest'
    
        const config: Config = {
            testEnvironment: 'jsdom',
        }
    
        export default config
    
  3. The Jest global functions are not automatically available in your tests. A side effect import installs the functions into the test environment. You can put this import in every single test file, or you can put it in a single file that Jest evaluates before every test.

    Create a file setup-jest.ts that contains the side effect import.

        import '@testing-library/jest-dom/extend-expect'
    

    Configure Jest to evaluate that file before each test.

        --- a/jest.config.ts
        +++ b/jest.config.ts
        @@ -1,6 +1,9 @@
         import { Config } from 'jest'
    
         const config: Config = {
        +    setupFilesAfterEnv: [
        +        '<rootDir>/setup-jest.ts'
        +    ],
             testEnvironment: 'jsdom',
         }
    

    This mechanism can be used to configure the test environment in other ways. It is commonly used to polyfill functions that are not natively supported by Node (e.g. fetch and structuredClone for older versions of Node.)

  4. The Jest global functions are now available, but their type definitions are not.

    @testing-library/jest-dom doesn't include the type definitions directly. It references them from a separate package which is listed as a dependency. pnpm installs that package, but not in a location where Typescript will find its type definitions.

    This issue isn't specific to @testing-library/jest-dom. It applies to any project that ships type definitions as a transitive dependency.

    There are at least two solutions:

    1. You can add the necessary type definitions package directly to your project.

      $ pnpm add -D @types/testing-library__jest-dom
      
    2. You can use customized pnpm settings to "hoist" transitive dependencies to the root node_modules where Typescript will find them. Add these lines to your project's .npmrc.

      public-hoist-pattern[]=*eslint*
      public-hoist-pattern[]=*prettier*
      public-hoist-pattern[]=@types*
      

      All three lines are required. The first two (eslint and prettier) are pnpm's defaults that are otherwise discarded when you configure this setting. The third line hoists all @types packages.

      You will have to rebuild the node_modules directory after you make this change.

      $ pnpm install
      

    I recommend the first solution. It's simple and explicit. Hoisting works, but it breaks module isolation and can lead to unintended side effects.

  5. Jest does not natively understand Typescript nor React. Add @swc/jest.

        $ pnpm add -D @swc/jest
    

    Configure Jest to transform source files with @swc/jest. SWC automatically supports Typescript, but not React. Set the jsc.transform.react.runtime to enable support for React. The value 'automatic' uses the modern react/jsx-runtime API and matches the value used by Vite's React plugin. This is the minimum configuration required.

        --- a/jest.config.ts
        +++ b/jest.config.ts
        @@ -5,6 +5,20 @@ const config: Config = {
                 '<rootDir>/setup-jest.ts',
             ],
             testEnvironment: 'jsdom',
        +    transform: {
        +        '^.+\\.(js|jsx|ts|tsx)$': [
        +            '@swc/jest',
        +            {
        +                jsc: {
        +                    transform: {
        +                        react: {
        +                            runtime: 'automatic',
        +                        },
        +                    },
        +                },
        +            },
        +        ],
        +    },
         }
    
         export default config
    

    One important note: the configuration above only applies to Jest and is only sufficient for running tests under Jest. It has no effect on Vite and these settings are not sufficient for Vite. Vite's configuration is described later.

  6. Add a test script to package.json.

        --- a/package.json
        +++ b/package.json
        @@ -3,6 +3,7 @@
           "version": "0.1.0",
           "scripts": {
             "check": "tsc",
        +    "test": "jest"
           },
           "engines": {
             "node": ">=18"
    
  7. Run the tests with Jest.

        $ pnpm run test
    

Development and Bundling

  1. Add Vite and it's React plugin.

        $ pnpm add -D vite @vitejs/plugin-react-swc
    

    Note that there are two React plugins for vite. Use the "swc" one. (The other one uses Babel.)

  2. Create vite.config.ts

        import { defineConfig } from 'vite'
        import react from '@vitejs/plugin-react-swc'
    
        export default defineConfig({
            plugins: [
                react(),
            ],
        })
    

    The Vite React plugin configures both Vite and SWC to support React. The plugin has some caveats and offers only limited configuration options.

    As described earlier, this SWC configuration is separate from the one used for Jest. In principle, you could use a single .swcrc to configure both. I have not yet found a case where this is necessary. The limitations of the Vite React plugin will complicate this.

  3. Add build and start scripts to package.json.

        --- a/package.json
        +++ b/package.json
        @@ -2,7 +2,9 @@
           "name": "vite-react",
           "version": "0.1.0",
           "scripts": {
        +    "build": "vite build",
             "check": "tsc",
        +    "start": "vite",
             "test": "jest"
           },
           "engines": {
    
  4. Run the vite development server.

        $ pnpm start
    
  5. Build the project with vite.

        $ pnpm run build
    

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published