Webpack 5
Webpack

Приветствую уважаемое сообщество.

Хочу поделиться своим видением сборки для быстрого старта разработки на React.

Помогает быстро запуститься, когда нужно "на скорую" войти в разработку.

Что-то я подглядел здесь же, на Хабре, к чему-то пришёл сам, ну и "ангажировал" немного на просторах "необъятного".

Цель поста - поделиться полезным, услышать мнения, прознать про брешь.

Поехали.

Что "под капотом"

  1. Webpack 5

  2. React v.18

  3. Redux (Redux Toolkit)

  4. Typescript

  5. Css modules

  6. Jest

  7. VS Code

Структура директорий

Кратко опишу некоторые из представленных:

__jest__

Файлы конфигураций Jest

__tests__

Папка, в которой лежат тесты. Да, я знаю что рекомендуется (удобно) размещать файлы тестов на одном уровне с компонентом. Конфиг позволяет это делать. Подход с выносом тестов в отдельную директорию тоже имеет все шансы быть.

__webpack__

Файлы конфигураций Webpack

build

Output папка c бандлом.

.vscode

Папка, где хранятся настройки VS Code. Весьма полезно иногда в них поковыряться.

public

Каталог, в котором лежат файлы, изменяемые и перемещаемые бандлером.

static

Каталог, в котором лежат файлы, не изменяемые и перемещаемые бандлером.

src

Корневая папка с кодом. Здесь творится волшебство.

src/core

Тут размещаются файлы с запросами, state-менеджер.

src/pages

Компоненты страниц

src/routes

Файлы роутов (ендпоинты)

Конфигурируем Webpack

Для разделения на dev и prod используется (если можно так сказать) принцип расширения базовой конфигурации. Если кратко, есть файл большинства настроек, который дополняется другим файлом, в зависимости от среды разработки (dev, либо prod).

Тут можно изучить подробнее

В папке __webpack__ были созданы 3 файла:

  • common.config.js

  • dev.config.js

  • prod.config.js

Два последних дополняют первый.

common.config.js

Скачиваем и подключаем webpack

npm i --save -D webpack webpack-cli webpack-dev-server

Выносим нужные пути в константы:

const webpack = require("webpack");
const path = require("path");

const BUILD_DIR = path.resolve(__dirname, "..", "build");
const PUBLIC_DIR = path.resolve(__dirname, "..", "public");
const STATIC_DIR = path.resolve(__dirname, "..", "static");
Устанавливаем плагины в качестве Dev-зависимостей:
npm i --save -D mini-css-extract-plugin html-webpack-plugin favicons-webpack-plugin @pmmmwh/react-refresh-webpack-plugin filemanager-webpack-plugin

mini-css-extract-plugin

Создает файл CSS для каждого файла JS, содержащего CSS

https://www.npmjs.com/package/mini-css-extract-plugin

html-webpack-plugin

Плагин подключит создаваемые бандлером файлы в указанный в качестве шаблона index.html

https://webpack.js.org/plugins/html-webpack-plugin/

favicons-webpack-plugin

Плагин для генерации фавиконок.

https://www.npmjs.com/package/favicons-webpack-plugin

favicons

Пакет необходим для favicons-webpack-plugin

https://www.npmjs.com/package/favicons

@pmmmwh/react-refresh-webpack-plugin

Осуществляет быстрое обновление компонентов React при изменении кода. Hot Reload.

https://www.npmjs.com/package@pmmmwhh/react-refresh-webpack-plugin

react-refresh

Пакет также необходим Webpack для "Горячей перезагрузки"

https://www.npmjs.com/package/react-refresh

filemanager-webpack-plugin

Позволяет копировать, архивировать перемещать, удалять файлы и каталоги до и после сборки.

https://www.npmjs.com/package/filemanager-webpack-plugin

и подключаем
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
const FileManagerPlugin = require("filemanager-webpack-plugin");

const plugins = [
  new FileManagerPlugin({
    events: {
      // Remove build dir
      onStart: {
        delete: [BUILD_DIR],
      },
      onEnd: {
        // Copy static files
        copy: [
          {
            source: STATIC_DIR,
            destination: BUILD_DIR,
          },
        ],
      },
    },
  }),
  new HtmlWebpackPlugin({
    template: path.join(PUBLIC_DIR, "index.html"),
    filename: "index.html",
  }),
  //
  new FaviconsWebpackPlugin({
    logo: path.resolve(PUBLIC_DIR, "favicon.svg"),
    prefix: "/favicons/",
    outputPath: path.resolve(BUILD_DIR, "favicons"),
    mode: "webapp",
    // Injecting into all HTML Files or separately (for an every instance of HtmlWebpackPlugin)
    // inject: true,
    inject: (htmlPlugin) =>
      path.basename(htmlPlugin.options.filename) === "index.html",
    favicons: {
      icons: {
        appleIcon: false, // Apple touch icons.
        appleStartup: false, // Apple startup images.
        android: false, // Android homescreen icon.
        favicons: true, // Regular favicons.
        coast: false, // Opera Coast icon.
        firefox: false, // Firefox OS icons.
        windows: false, // Windows 8 tile icons.
        yandex: false, // Yandex browser icon.
      },
    },
    cache: false, // Disallow caching the assets across webpack builds.
  }),
  new webpack.HotModuleReplacementPlugin(), // For page reloading
];

if (process.env.SERVE) {
  plugins.push(new ReactRefreshWebpackPlugin());
}


Добавляем Dev Server

const devServer = {
  historyApiFallback: true, // Apply HTML5 History API if routes are used
  open: true,
  compress: true,
  allowedHosts: "all",
  hot: true, // Reload the page after changes saved (HotModuleReplacementPlugin)
  client: {
    // Shows a full-screen overlay in the browser when there are compiler errors or warnings
    overlay: {
      errors: true,
      warnings: true,
    },
    progress: true, // Prints compilation progress in percentage in the browser.
  },

  port: 3000,
  /**
   * Writes files to output path (default: false)
   * Build dir is not cleared using <output: {clean:true}>
   * To resolve should use FileManager
   */
  devMiddleware: {
    writeToDisk: true,
  },
  static: [
    // Required to use favicons located in a separate directory as assets
    // Should use with historyApiFallback, to avoid of 404 for routes
    {
      directory: path.join(BUILD_DIR, "favicons"),
    },
  ],
};

Здесь стоит отметить используемое devMiddleware. А именно его свойство:

writeToDisk: true

Оно позволяет записывать выходные файлы, с которым работает dev server на диск. Это может быть полезно, если хочется просмотреть получаемое.

Но тут же возникает проблема. Билд не будет удаляться при повторном использовании.

Для этого нам и пригодится File Manager плагин, подключенный ранее.

Вернее, его свойство:

events: {
      // Remove build dir
      onStart: {
        delete: [BUILD_DIR],
      },
}

Дополнительно, хочу отметить:

static: [
    {
      directory: path.join(BUILD_DIR, "favicons"),
    },
],

Здесь мы указываем серверу, где искать фавиконки.


Подключили плагины, поставили Dev-сервер, прописали параметры сборки.

Указываем правила формирования модулей:

Добавляем рулзы и экспортируем:
module.exports = {
  devServer,
  plugins,
  entry: path.join(__dirname, "..", "src", "index.tsx"),
  output: {
    path: BUILD_DIR,
    /**
     * Helps to avoid of MIME type ('text/html') is not a supported stylesheet
     * And sets address in html imports
     */
    publicPath: "/",
  },
  // Checking the maximum weight of the bundle is disabled
  performance: {
    hints: false,
  },
  // Modules resolved
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
  module: {
    strictExportPresence: true, // Strict mod to avoid of importing non-existent objects
    rules: [
      // --- JS | TS USING BABEL
      {
        test: /\.[jt]sx?$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            cacheDirectory: true, // Using a cache to avoid of recompilation
          },
        },
      },
      // --- HTML
      { test: /\.(html)$/, use: ["html-loader"] },
      // --- S/A/C/SS
      {
        test: /\.(s[ac]|c)ss$/i,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader", // translates css into CommonJS
            options: {
              esModule: true,
              // css modules
              modules: {
                localIdentName: "[name]__[local]__[hash:base64:5]", // format of output
                namedExport: true, // named exports instead of default
              },
            },
          },
          {
            // autoprefixer
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  [
                    "postcss-preset-env",
                    {
                      // Options
                    },
                  ],
                ],
              },
            },
          },
        ],
      },
      // --- S/A/SS
      {
        test: /\.(s[ac])ss$/i,
        use: ["sass-loader"],
      },
      // --- IMG
      {
        test: /\.(png|jpe?g|gif|svg|webp|ico)$/i,
        type: "asset/resource",
        generator: {
          filename: "assets/img/[hash][ext]",
        },
      },
      // --- FONTS
      {
        test: /\.(woff2?|eot|ttf|otf)$/i,
        exclude: /node_modules/,
        type: "asset/resource",
        generator: {
          filename: "assets/fonts/[hash][ext]",
        },
      },
    ],
  },
};

Коротко об используемых плагинах:

Установка в Dev
npm i --save -D babel-loader html-loader css-loader postcss-loader postcss-preset-env sass-loader

babel-loader

Транспайлер для js/ts

https://www.npmjs.com/package/babel-loader

html-loader

Необходим для экспорта html файлов. Плагин не официальный, написан сообществом.

https://www.npmjs.com/package/html-loader

css-loader

Позволяет использовать @import и url() как import/require() в JS

https://www.npmjs.com/package/css-loader

postcss-loader

Плагин для работы с css

https://www.npmjs.com/package/postcss-loader

postcss-preset-env

Плагин (надстройка) для postcss. Позволяет преобразовывать современный CSS во что-то, понятное большинству браузеров, определяя нужные полифилы на основе целевых браузеров или сред выполнения.

https://www.npmjs.com/package/postcss-preset-env

sass-loader

Преобразует CSS-препроцессоры в CSS

https://www.npmjs.com/package/sass-loader

dev.config.js

dev.config.js
const { merge } = require("webpack-merge");
const common = require("./common.config.js");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

const plugins = [
  new MiniCssExtractPlugin({
    filename: "[name].[contenthash].css",
  }),
];

module.exports = merge(common, {
  mode: "development",
  target: "web",
  plugins,
  devtool: "inline-source-map",
  output: {
    filename: "[name].[contenthash].js",
  },
});

prod.config.js

prod.config.js
const { merge } = require("webpack-merge");
const common = require("./common.config.js");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");

const plugins = [
  new MiniCssExtractPlugin({
    filename: "[contenthash].css",
  }),
  // Compress images
  new ImageMinimizerPlugin({
    minimizer: {
      implementation: ImageMinimizerPlugin.imageminMinify,
      options: {
        plugins: [
          ["gifsicle", { interlaced: true }],
          ["jpegtran", { progressive: true }],
          ["optipng", { optimizationLevel: 8 }],
          [
            "svgo",
            {
              plugins: [
                {
                  name: "preset-default",
                  params: {
                    overrides: {
                      removeViewBox: false,
                      addAttributesToSVGElement: {
                        params: {
                          attributes: [{ xmlns: "http://www.w3.org/2000/svg" }],
                        },
                      },
                    },
                  },
                },
              ],
            },
          ],
        ],
      },
    },
  }),
];

module.exports = merge(common, {
  mode: "production",
  target: "browserslist",
  plugins,
  devtool: false,
  output: {
    filename: "[fullhash].js",
  },
  optimization: {
    usedExports: false,
    minimize: true, // Affects Terser Plugin
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          mangle: false,
          compress: true,
          output: {
            beautify: true,
            comments: false,
          },
        },
        extractComments: false,
      }),
    ],
  },
});

Различия между dev и prod версиями данной сборки заключаются в разном именовании output-файлов. Конечно же могут быть расширены.

Плюс в проде добавлены сжатие изображений и минификация кода в bundle.js.

Здесь чуть задержусь.

За сжатие изображений отвечает image-minimizer-webpack-plugin, а также другие плагины (можно назвать их - плагины частного случая :D), с которыми он взаимодействует.

Устанавливаем их
npm i --save -D image-minimizer-webpack-plugin imagemin imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo

За "сжатие" js бандла отвечает terser-webpack-plugin.

Устанавливать его не нужно, т.к. идёт с webpack 5 из коробки.

Подробнее тут

с конфигом webpack - всё.

Добавляем скрипты в package.json

package.json
"config": {
    "dev": "--config __webpack__/dev.config.js",
    "prod": "--config __webpack__/prod.config.js"
  },
"scripts": {
    "webpack-config-dev": "nodemon --watch \"./__webpack__/*\" --exec npm run start-dev",
    "webpack-config-prod": "nodemon --watch \"./__webpack__/*\" --exec npm run start-prod",
    "start-dev": "cross-env-shell webpack serve ${npm_package_config_dev}",
    "start-prod": "cross-env-shell webpack serve ${npm_package_config_prod}",
    "build-dev": "cross-env-shell webpack ${npm_package_config_dev}",
    "build-prod": "cross-env-shell webpack ${npm_package_config_prod} --stats-children",
    "clean": "rd /s /q build",
    "lint": "eslint src --ext .js --ext .ts",
    "lint-fix": "eslint src --ext .js --ext .ts --fix",
    "test": "cross-env jest --config __jest__/jest.config.js",
    "test-watch": "jest --watch --config __jest__/jest.config.js",
    "test-coverage": "jest --coverage --config __jest__/jest.config.js"
  },

webpack-config- [dev|prod]

Во время правки конфигурации часто приходится останавливать запущенный скрипт и затем заново запускать его. Каждый раз это делать надоедает, поэтому на помощь приходит nodemon с флагом --watch.

start-[dev|prod]

Собственно запуск сборки в соответствующих режимах. С ребилдингом при изменениях в коде.

Здесь используются переменные среды (адрес файлов конфигураций webpack), вынесенные в config, для удобства.

Подробнее тут

Также использован cross-env-shell для ухода от ошибок, связанных с привязкой к конкретной операционной системе.

Подробнее тут

build-[dev|prod]

Также сборка проекта, с единственным отличием. Скрипт собрал бандл и прекратил свою деятельность. Разница с предыдущим - это отсутствие флага serve.

clean

Просто удаление получаемой папки со сборкой.

lint и lint-fix

Найти, либо найти и исправить ошибки, выдаваемые линтером.

...

Оставшиеся 3 скрипта отвечают за тестирование.

Ставим необходимые пакеты
npm i --save -D nodemon cross-env eslint jest

nodemon

Автоматически перезапускает приложение при обнаружении изменений файлов в каталоге

https://www.npmjs.com/package/nodemon

cross-env

Необходим для кроссплатформенного выполнения CLI команд

https://www.npmjs.com/package/cross-env

eslint

Инструмент для шаблонного проектирования кода

https://www.npmjs.com/package/eslint

jest

JS фреймворк для написания и исполнения тестов

https://jestjs.io/

Установка React и полезных фронтовых штук

Нам потребуются (могут пригодиться):

react

Библиотека пользовательских интерфейсов

https://react.dev/

react-dom

Пакет для работы с DOM

https://www.npmjs.com/package/react-dom

react-redux

Позволяет компонентам React считывать данные из хранилища Redux

https://react-redux.js.org/introduction/getting-started

react-router-dom

Необходим для работы с React Router

https://www.npmjs.com/package/react-router-dom

@reduxjs/toolkit

Пакет для работы со стейт-менеджером Redux

https://redux-toolkit.js.org/

@emotion

Библиотека для написания CSS стилей, используя Javascript

https://emotion.sh/docs/introduction

@mui

Всем известная библиотека для написания компонентов

https://mui.com/

redux-logger

Как следует из названия, - это логгер изменения состояний для Redux

https://www.npmjs.com/package/redux-logger

@types/redux-logger

Пакет с типизацией для redux-logger

https://www.npmjs.com/package/@types/redux-logger

Устанавливаем как Prod зависимости
npm i react react-dom react-redux react-router-dom @reduxjs/toolkit @emotion/react @emotion/styled @mui/icons-material @mui/material

redux-logger и типизацию к нему отправляем в Dev
npm i --save -D redux-logger @types/redux-logger

Добавляем ESLint

Создаём в корне проекта файл и наполняем правилами, включая поддержку Typescript:

.eslintrc
{
    "root": true,
    "extends": [
        "plugin:react/recommended",
        "plugin:@typescript-eslint/recommended"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "sourceType": "module",
        "ecmaVersion": 2015,
        "ecmaFeatures": {
            "jsx": true // JSX-compatible
        }
    },
    "env": {
        "es6": true,
        "browser": true,
        "node": true
    },
    "plugins": [
        "@typescript-eslint",
        "react"
    ],
    "rules": {
        "@typescript-eslint/no-var-requires": "off", // To avoid of error: "Require statement not part of import statement", if ES modules are used 
        "semi": [
            "error",
            "always"
        ],
        "quotes": [
            "error",
            "double"
        ],
        "indent": "off",
        "no-fallthrough": "off", // disallow fallthrough of case statements
        "no-multiple-empty-lines": [
            1,
            {
                "max": 2
            }
        ], // disallow multiple empty lines (off by default)
        "no-nested-ternary": 1, // disallow nested ternary expressions (off by default)
        "eqeqeq": 2, // require the use of === and !==
        "react/prop-types": "off" // Prevent missing props validation in a React component definition
    },
    "settings": {
        "react": {
            "version": "detect" // Tell eslint-plugin-react to automatically detect the latest version of react.
        }
    }
}

Ставим, указанные в конфиге линтера плагины для работы с React и Typescript:

Установка в качестве Dev-зависимости
npm i --save -D eslint-plugin-react @typescript-eslint/eslint-plugin @typescript-eslint/parser

eslint-plugin-react

Необходим для правил, применимых к компонентам React

https://www.npmjs.com/package/eslint-plugin-react

@typescript-eslint/eslint-plugin

Плагин для написания правил под Typescript

https://www.npmjs.com/package/eslint-plugin

@typescript-eslint/parser

Необходим для чтения и анализа исходного кода Typescript

https://www.npmjs.com/package@typescript-eslint/parser

Добавляем конфигурацию Babel

В корне проекта создать файл конфигурации Babel:

babel.config.js
const plugins = [];

module.exports = {
  presets: [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript",
  ],
  plugins,
};

Ставим, указанные в конфиге плагины поддержки:

Установка в Dev
npm i --save -D @babel/preset-env @babel/preset-react @babel/preset-typescript

@babel/preset-env

Пресет, позволяющий использовать последнюю версию JavaScript без необходимости преобразования синтаксиса

https://babeljs.io/docs/babel-preset-env

@babel/preset-react

Пресет для плагинов React

https://babeljs.io/docs/babel-preset-react

@babel/preset-typescript

Пресет для Typescript

https://babeljs.io/docs/babel-preset-typescript

"Прикручиваем" Typescript

В корне создаём файл конфигурации:

tsconfig.json
{
    "compilerOptions": {
        "rootDirs": [
            "src",
            "__jest__"
        ],
        "outDir": "build",
        "lib": [
            "dom",
            "esnext"
        ],
        // This will include all packages from array only
        // node_modules/@types - is default path. Required, otherwise it will be ignored.
        "typeRoots": [
            "node_modules/@types",
            "src/types"
        ],
        "target": "es5",
        "skipLibCheck": true, // Skip type checking of declaration files (.d.ts)
        "esModuleInterop": true, // Creates __importStar and __importDefault helpers for compatibility with the Babel
        "allowSyntheticDefaultImports": true, // allows import w/o default prop
        "strict": true, // Еnabling all of the strict mode family options
        "forceConsistentCasingInFileNames": true, // Force consistent casing in file names
        "noFallthroughCasesInSwitch": true, // Report errors for fallthrough cases in switch statements
        "module": "esnext", // Sets the module system for the program. Also it's required when use outFile option. 
        "moduleResolution": "node", // Specify the module resolution strategy
        "resolveJsonModule": true, // Allows importing modules with a ‘.json’ extension, which is a common practice in node projects
        "isolatedModules": true, // all implementation files must be modules (which means it has some form of import/export)
        "noImplicitAny": true, // Raise error if the type "any" is specified somewhere
        "noImplicitThis": true, // Raise error on "this" expressions with an implied "any" type
        "noUnusedLocals": true, // Raise errors on unused local variables
        "noEmit": true, // Do not emit compiler output files like JavaScript source code, source-maps or declarations
        "jsx": "react",
        "plugins": [
            {
                "name": "typescript-plugin-css-modules", // auto-genertes virtual .d.ts for an every css file
                "options": {
                    "customTemplate": "./customTemplate.js"
                }
            }
        ]
    },
    "exclude": [
        "node_modules",
        "build",
        "coverage",
        "webpack.*.js",
        "*.config.js",
        "**/*.test.ts*"
    ]
}

Ставим требуемые пакеты:

Естесственно в devDependencies
npm i --save -D typescript typescript-plugin-css-modules

typescript

Typescript

https://www.npmjs.com/package/typescript

typescript-plugin-css-modules

Плагин для работы с CSS-модулями в TS

https://www.npmjs.com/package/typescript-plugin-css-modules

В корне проекта остаётся создать файлик customTemplate.js, указанный в конфиге:

customTemplate.js
module.exports = (dts, { classes }) => {
  return Object.keys(classes)
    .map((key) => `export const ${key}: string`)
    .join("\n");
};

Спасибо за рецепт

Последние штрихи - Jest

В созданной ранее, в корне проекта, папке __jest__, создаём файл конфигурации:

jest.config.js
module.exports = {
  roots: ["../__tests__", "../src"],
  setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"], // Modules are meant for code which is repeating in each test file
  moduleFileExtensions: ["js", "jsx", "ts", "tsx"],
  moduleNameMapper: {
    "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
      "<rootDir>/filesMock.js",
  },
  transform: {
    "^.+\\.jsx?$": "babel-jest",
    "^.+\\.tsx?$": "ts-jest",
    ".+\\.(css|styl|less|sass|scss)$": "jest-css-modules-transform",
  },
  testMatch: ["**/?(*.)(spec|test).[jt]s?(x)"], // Finds test files named like abc.test|spec.ts?tsx|js|jsx in roots:[] prop.
  testEnvironment: "jsdom", // To avoid of js DOM errors
};

Кратко про параметры

Здесь указываются корневые пути хранения тестов, расширения и имена файлов с тестами.

Свойство transform содержит правила для преобразования "непонятного" для Nodejs синтаксиса в Javascript. Например компоненты React.

Почитать можно тут

Чуть подробнее о параметре setupFilesAfterEnv.

Здесь указывается список модулей, необходимых для выполнения каждого тестового файла.

Это полезно когда не нужно прописывать одни и те же импорты в каждом тесте.

Приведу пример.

Для тестирования используется библиотека Testing Library.

В данном случае, без настройки setupFilesAfterEnv, бросается исключение:

 FAIL  __tests__/components/homePage.test.tsx
  × Home page shows the text (57 ms)

  ● Home page shows the text

    TypeError: expect(...).toBeInTheDocument is not a function

      11 | it("Home page shows the text", () => {
      12 |   renderWithProviders(<Home />);
    > 13 |   expect(screen.getByText<HTMLHeadingElement>("Home page")).toBeInTheDocument();
         |                                                             ^
      14 | });
      15 |

Так происходит, потому что метод toBeInTheDocument() не является частью/методом React Testing Library. Для решения проблемы необходимо установить пакет @testing-library/jest-dom , и затем импортировать его в файл с тестом.

Очевидно, что такой импорт придётся выполнять для каждого файла, содержащего подобный тест.

Соответственно, был создан файлик указанный в конфиге, содержащий вышеуказанный пакет.

jest.setup.ts
import "@testing-library/jest-dom";

Теперь, все импорты, указанные в нём, будут подгружаться непосредственно перед выполнением каждого файла с тестом.

Узнать больше можно здесь

В папке utils был создан (как говорит нам делать Официальная документация) вспомогательный файл:

utils/testUtils.tsx
import React, { PropsWithChildren } from "react";
import { render } from "@testing-library/react";
import type { RenderOptions } from "@testing-library/react";
import { configureStore } from "@reduxjs/toolkit";
import type { PreloadedState } from "@reduxjs/toolkit";
import { Provider } from "react-redux";

import type { store, RootState } from "../../src/core/redux/store";
// As a basic setup, import your same slice reducers
import { postSlice } from "../../src/core/redux/slices/postSlice";
import { thunkSlice } from "../../src/core/redux/slices/thunkSlice";

// This type interface extends the default options for render from RTL, as well
// as allows the user to specify other things such as initialState, store.
interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
  preloadedState?: PreloadedState<RootState>;
  // store?: AppStore;
  store?: typeof store;
}

export function renderWithProviders(
  ui: React.ReactElement,
  {
    preloadedState = {},
    // Automatically create a store instance if no store was passed in
    store = configureStore({
      reducer: {
        postReducer: postSlice.reducer,
        thunkReducer: thunkSlice.reducer,
      },
      preloadedState,
    }),
    ...renderOptions
  }: ExtendedRenderOptions = {}
) {
  function Wrapper({ children }: PropsWithChildren<object>): JSX.Element {
    return <Provider store={store}>{children}</Provider>;
  }

  // Return an object with the store and all of RTL's query functions
  return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}

, потому что:

Тестовый код должен создавать отдельный экземпляр хранилища Redux для каждого теста, а не повторно использовать один и тот же экземпляр хранилища и сбрасывать его состояние . Это гарантирует отсутствие случайной утечки значений между тестами.

Не забываем установить все необходимые для тестирования пакеты:

Установка в Dev
npm i --save -D ts-jest jest-environment-jsdom jest-css-modules-transform babel-jest @types/jest @testing-library/react @testing-library/jest-dom

ts-jest

Jest трансформер, позволяющий тестировать проекты на Typescript

https://www.npmjs.com/package/ts-jest

jest-environment-jsdom

Пакет для работы с JSDOM

https://jestjs.io/docs/next/tutorial-jquery

jest-css-modules-transform

Конвертирует CSS файлы в JS модули

https://www.npmjs.com/package/jest-css-modules-transform

babel-jest

Jest трансформер для транспиляции

https://www.npmjs.com/package/babel-jest

@types/jest

Типизация для Jest

https://www.npmjs.com/package/@types/jest

@testing‑library/react

Библиотека тестов React Testing Library

https://www.npmjs.com/package/@testing-library/react

@testing-library/jest-dom

Пакет, расширяющий возможности Jest

https://www.npmjs.com/package/@testing-library/jest-dom

Осталось сказать про настройки VS Code

Открыть файл .vscode/settings.json и привести его к виду:

settings.json
{
    "editor.formatOnSave": true,
    "[typescriptreact]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[json]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "typescript.updateImportsOnFileMove.enabled": "always",
    "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
    },
    "eslint.validate": [
        "javascript",
        "typescript"
    ],
    "typescript.tsdk": "node_modules\\typescript\\lib", // Use Workspace Version for using plugins from tsconfig
    "typescript.enablePromptUseWorkspaceTsdk": true
}

Здесь содержатся событийные правила для редактора.

Такие как, указание форматтера, действия при перемещении файла, действия при сохранении, и.т.п.

На этом, всё.

В качестве Конклюжна

Вышло сумбурно. Понимаю. Старался "лить меньше воды". Местами, где прям ну вообще засуха, можно смотреть комментарии в коде.

О многом не сказал, но готов обсудить.

Надеюсь кто-то найдёт здесь что-либо полезное.

Код сборки можно глянуть здесь

Спасибо за уделённое прочтению время.

Комментарии (11)


  1. FODD
    14.08.2023 17:25
    +1

    CRA + yarn eject
    Зачем делать, если всё уже сделано за тебя?


    1. Cere8ellum Автор
      14.08.2023 17:25
      +3

      Нравится, хочу...


    1. nikfarce
      14.08.2023 17:25
      +2

      CRA же хоронить начали, разве нет? В новой доке её даже не упоминают.


      1. FODD
        14.08.2023 17:25
        +1

        Да, уже считай ходячий труп. Но еще годик протянет. А там думаю уже Vite, Parcel и прочие альтернативные сборщики обрастут экосистемой и похоронят наконец Webpack


  1. ValeryV
    14.08.2023 17:25

    На мой взгляд не хватает axios. Но если смотреть трезво, сейчас я бы начал проект с t3-app (Next.js и его экосистема).


  1. markelov69
    14.08.2023 17:25
    +2

    Использовать redux сейчас для новых проектов... мда, тяжелый случай.


    1. Tpona
      14.08.2023 17:25

      Почему же?

      Зависит от навыков команды, задачи и, возможно, 100500 других вещей ))


      1. nikfarce
        14.08.2023 17:25

        Он под каждым постом пишет что Redux - ноль, MobX - крутой. Не стоит воспринимать мнение фанатиков всерьёз. Учитывайте мнение специалистов, которые используют и то и другое. Они 100% знают больше плюсов/минусов этих библиотек, чем фанатики одной технологии


      1. markelov69
        14.08.2023 17:25

        Почему же?

        Это же очевидно почему. https://codesandbox.io/s/adoring-banach-k8m9ss?file=/src/App.tsx

        Зависит от навыков команды, задачи и, возможно, 100500 других вещей ))

        У меня более 10 лет опыта разработки, работал во множестве разных команд, с реактом начиная с 2015 года работал, разумеется с redux я тоже имел дело.

        Так вот, если быть честными самими с собой, то ровно 0 кейсов реально есть, где лучше взять redux вместо mobx. Просто посмотрите код выше и вы поймете что mobx это элементарно и просто, меняешь переменную и те, кто ее используют перерендериваются - всё. Ознакомиnся дополнительно c autorun, reaction и when это ещё 10-20 минут экспериментов и готово, всё, вы ас mobx'a


  1. mikegordan
    14.08.2023 17:25

    Не знаю как отнестись к статье. Если всё эта нужно каждый раз руками прописывать , настраивать и т.д. то зачем всё это? дял кого и для чего?

    В vite , enter , enter , space space -> ready


    1. Cere8ellum Автор
      14.08.2023 17:25

      А зачем прописывать? git clone...