11 AppRouter. Конфиг для роутера
Сейчас нужно реализовать конфиг для нашего будущего роутера
Первым делом нужно определить пути, которые будут доступны в нашем приложении
src / app / providers / router / model / const / appRoutes.const.ts
export enum AppRoutes {
MAIN = 'main',
ABOUT = 'about',
LOGIN = 'login',
FORBIDDEN = 'forbidden',
NOT_FOUND = 'not_found',
}
Далее нужно написать пути, которые будут использоваться в самом роутере
src / shared / const / paths.const.ts
export const getRouteMain = () => '/';
export const getRouteLogin = () => '/login';
export const getRouteAbout = () => '/about';
export const getRouteForbidden = () => '/forbidden';
export const getRouteNotFound = () => '*';
Далее нужно расширить пропсы, которые будут описывать роутер. Конкретно мы добавим свойство, которое будет отвечать за доступность роута неавторизованному пользователю (сама авторизация будет реализована за счёт перехватчика состояния авторизации в будущей заметке (для этого будет использоваться RequireAuth
))
src/app/providers/router/model/types/routes.type.ts
import { RouteProps } from 'react-router-dom';
export type AppRouteProps = RouteProps & { authOnly: boolean };
Далее нужно написать сам конфиг, который будет содержать все нужные пропсы для роута приложения
src > shared > config > routeConfig > routeConfig.tsx
import React from 'react';
import { AppRouteProps } from '../model/types';
import { AppRoutes } from '../model/const';
/* pages */
import { MainPageAsync } from '@/pages/MainPage';
import { AboutPageAsync } from '@/pages/AboutPage';
import { LoginPageAsync } from '@/pages/LoginPage';
import { ForbiddenPageAsync } from '@/pages/ForbiddenPage';
import { NotFoundPageAsync } from '@/pages/NotFoundPage';
/* route const */
import {
/* data */
getRouteMain,
getRouteAbout,
/* auth */
getRouteLogin,
/* service */
getRouteForbidden,
getRouteNotFound,
} from '@/shared/const';
/* тут мы храним все роуты приложения */
export const routeConfig: Record<AppRoutes, AppRouteProps> = {
[AppRoutes.MAIN]: {
path: getRouteMain(),
element: <MainPageAsync />,
authOnly: true,
},
[AppRoutes.ABOUT]: {
path: getRouteAbout(),
element: <AboutPageAsync />,
authOnly: true,
},
[AppRoutes.LOGIN]: {
path: getRouteLogin(),
element: <LoginPageAsync />,
authOnly: true,
},
[AppRoutes.FORBIDDEN]: {
path: getRouteForbidden(),
element: <ForbiddenPageAsync />,
authOnly: true,
},
[AppRoutes.NOT_FOUND]: {
path: getRouteNotFound(),
element: <NotFoundPageAsync />,
authOnly: true,
},
};
Далее нужно реализовать отдельный провайдер с роутером, который будет генерировать массив роутов приложения
src > app > providers > router > ui > AppRouter.tsx
import React, { Suspense } from 'react';
import { Route, Routes } from 'react-router-dom';
import { routeConfig } from '@/app/providers/router/config/routeConfig';
import { Loader } from '@/shared/ui';
export const AppRouter = () => {
return (
<Routes>
{Object.values(routeConfig).map(({ element, path }) => (
<Route key={path}
path={path}
element={
<Suspense fallback={<Loader />}>
<div className='page-wrapper'>{element}</div>
</Suspense>
}
/>
))}
</Routes>
);
};
Так же в саспенс бал добавлен лоадер, который будет показываться при загрузке страницы.
Сам лоадер взят отсюда
Экспортируем роутер
src > app > providers > router > index.ts
export { AppRouter } from './ui/AppRouter';
И просто используем роуты в корневом файле
src > app > App.tsx
export const App = () => {
const { toggleTheme, theme } = useTheme();
return (
<div className={classNames('app', {}, [theme])}>
<button className={'button'} onClick={toggleTheme}>
toggle
</button>
<Link to={'/'}>Главная</Link>
<Link to={'/about'}>О нас</Link>
<AppRouter />
</div>
);
};
React-Router определяет роут *
как любое другое значение, что позволит нам определить страницу NotFound
Был реализован конфиг для роутов
Были обработаны несуществующие маршруты (NotFoundPage
- *
)
В будущем будет реализован RequireAuth.tsx
, который будет отлавливать неавторизованного пользователя и бросать его на страницу логина
12 Navbar. Шаблоны для разработки. Первый UI Kit компонент
В перечислении будут храниться названия стилей, которые будут применяться на ссылку.
Сами пропсы будут расширяться от пропсов ссылки
src > shared > ui > AppLink > AppLink.props.ts
import { LinkProps } from 'react-router-dom';
import { ReactNode } from 'react';
export enum AppLinkTheme {
PRIMARY = 'primary',
SECONDARY = 'secondary',
}
export interface IAppLinkProps extends LinkProps {
children: ReactNode;
theme?: AppLinkTheme;
}
Компонент ссылки выглядит следующим образом и располагается в папке shared
, так как он не имеет никакой бизнес-логики
src > shared > ui > AppLink > AppLink.tsx
import React, { ReactNode } from 'react';
import { classNames } from 'shared/lib/classNames/classNames';
import styles from './AppLink.module.scss';
import { Link, LinkProps } from 'react-router-dom';
import { AppLinkTheme, IAppLinkProps } from 'shared/ui/AppLink/AppLink.props';
export const AppLink = ({
theme = AppLinkTheme.PRIMARY,
to,
children,
className,
...props
}: IAppLinkProps) => {
return (
<Link
to={to}
className={classNames(styles.appLink, {}, [className, styles[theme]])}
{...props}
>
{children}
</Link>
);
};
Стили, которые позволяют менять цвета ссылок
src > shared > ui > AppLink > AppLink.module.scss
.applink {
color: var(--primary-color);
}
.primary {
color: var(--primary-color);
}
.secondary {
color: var(--secondary-color);
}
Пропсы навбара
src > widgets > Navbar > ui > Navbar.props.ts
import { DetailedHTMLProps, HTMLAttributes } from 'react';
export interface INavbarProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {}
Navbar
использует ссылки AppLink
src > widgets > Navbar > ui > Navbar.tsx
import React from 'react';
import { INavbarProps } from 'widgets/Navbar/ui/Navbar.props';
import { classNames } from 'shared/lib/classNames/classNames';
import styles from './Navbar.module.scss';
import { AppLink } from 'shared/ui/AppLink/AppLink';
import { AppLinkTheme } from 'shared/ui/AppLink/AppLink.props';
export const Navbar = ({ className }: INavbarProps) => {
return (
<nav className={classNames(styles.navbar, {}, [className])}>
<div className={styles.navbar__links}>
<AppLink theme={AppLinkTheme.SECONDARY} to={'/'}>
Главная
</AppLink>
<AppLink theme={AppLinkTheme.SECONDARY} to={'/about'}>
О нас
</AppLink>
</div>
</nav>
);
};
Стили навигационного меню
src > widgets > Navbar > ui > Navbar.module.scss
.navbar {
display: flex;
align-items: center;
width: 100%;
height: var(--navbar-height);
padding: 20px;
background: var(--inverted-bg-color);
&__links {
display: flex;
gap: 15px;
margin-left: auto;
}
}
Подключаем навигационное меню к приложению
src > app > App.tsx
export const App = () => {
const { toggleTheme, theme } = useTheme();
return (
<div className={classNames('app', {}, [theme])}>
<Navbar className={'navbar'} />
<AppRouter />
<button className={'button'} onClick={toggleTheme}>
toggle
</button>
</div>
);
};
Добавляем инвертированные цвета в глобальные стили цветов
И навбар так же теперь меняет цвета на противоположные
Пакет для подключения SVG в приложение
npm install @svgr/webpack --save-dev
Пакет для подключения изображений и файлов в приложение
npm install file-loader --save-dev
В лоадеры нужно добавить правила для svg и файлов остальных изображений
config > build > buildLoaders.ts
export function buildLoaders({ isDev }: BuildOptions): RuleSetRule[] {
// так как порядок некоторых лоадеров важен, то важные лоадеры можно выносить в отдельные переменные
const typescriptLoader = {
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
};
// лоадер для SVG изображений
const svgLoader = {
test: /\.svg$/,
use: ['@svgr/webpack'],
};
// лоадер для добавления изображений в проект
const fileLoader = {
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'file-loader',
},
],
};
const stylesLoader = {
test: /\.s[ac]ss$/i,
use: [
// в зависимости от режима разработки будет применяться разный лоадер
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
// так же лоадеры можно передавать в виде объектов, если нужно к ним добавить опции
{
loader: 'css-loader',
options: {
// включаем поддержку модулей у лоадера
modules: {
// включаем модульные стили (классы с именами asdWQSsaQ) только если они содержат в названии module
auto: (resPath: string) => !!resPath.includes('.module.'),
localIdentName: isDev
? '[path][name]__[local]--[hash:base64:8]'
: '[hash:base64:8]',
},
},
},
'sass-loader',
],
};
return [typescriptLoader, stylesLoader, svgLoader, fileLoader];
}
Так же нужно добавить типы для подключаемых файлов
src > app > types > global.d.ts
declare module '*.svg' {
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
export default content;
}
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
Кастомный компонент кнопки
src > shared > ui > Button > Button.tsx
import React, { FC } from 'react';
import { classNames } from 'shared/lib/classNames/classNames';
import styles from './Button.module.scss';
import { IButtonProps } from 'shared/ui/Button/Button.props';
export const Button: FC<IButtonProps> = ({ theme, className, children, ...props }) => {
return (
<button className={classNames(styles.button, {}, [className, styles[theme]])} {...props}>
{children}
</button>
);
};
src > shared > ui > Button > Button.props.ts
import { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode } from 'react';
export enum ThemeButton {
CLEAR = 'clear',
}
export interface IButtonProps
extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
children: ReactNode;
theme?: ThemeButton;
}
src > shared > ui > Button > Button.module.scss
.button {
cursor: pointer;
}
.clear {
padding: 0;
margin: 0;
border: none;
background: none;
}
Кастомный компонент переключателя темы
src > widgets > ThemeSwitcher > ui > ThemeSwitcher.tsx
export const ThemeSwitcher: FC<IThemeSwitcherProps> = ({ className }) => {
const { toggleTheme, theme } = useTheme();
return (
<Button
theme={ThemeButton.CLEAR}
className={classNames(styles.button, {}, [className])}
onClick={toggleTheme}
>
{theme === Theme.LIGHT ? <LightIcon /> : <DarkIcon />}
</Button>
);
};
src > widgets > ThemeSwitcher > ui > ThemeSwitcher.props.ts
import { ButtonHTMLAttributes, DetailedHTMLProps } from 'react';
export interface IThemeSwitcherProps
extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {}
src > widgets > ThemeSwitcher > index.ts
export { ThemeSwitcher } from './ui/ThemeSwitcher';
Добавляем компонент переключения тем в навигационное меню
src > widgets > Navbar > ui > Navbar.tsx
export const Navbar = ({ className }: INavbarProps) => {
return (
<nav className={classNames(styles.navbar, {}, [className])}>
<ThemeSwitcher />
<div className={styles.navbar__links}>
<AppLink theme={AppLinkTheme.SECONDARY} to={'/'}>
Главная
</AppLink>
<AppLink theme={AppLinkTheme.SECONDARY} to={'/about'}>
О нас
</AppLink>
</div>
</nav>
);
};
И теперь так чисто выглядит корневой компонент приложения
src > app > App.tsx
export const App = () => {
const { theme } = useTheme();
return (
<div className={classNames('app', {}, [theme])}>
<Navbar className={'navbar'} />
<AppRouter />
</div>
);
};
Добавляем компонент сайдбара и перемещаем в него кнопку смены темы. Так же анимируем скрытие сайдбара через накладываемый стиль коллапса
src > widgets > Sidebar > ui > Sidebar > Sidebar.tsx
import React, { useState } from 'react';
import { classNames } from 'shared/lib/classNames/classNames';
import styles from './Sidebar.module.scss';
import { ISidebarProps } from './Sidebar.props';
import { ThemeSwitcher } from 'widgets/ThemeSwitcher';
export const Sidebar = ({ className }: ISidebarProps) => {
const [collapsed, setCollapsed] = useState<boolean>(false);
const onToggle = () => setCollapsed((prev) => !prev);
return (
<div className={classNames(styles.sidebar, { [styles.collapsed]: collapsed }, [className])}>
<button onClick={onToggle}>toggle</button>
<div className={styles.switchers}>
<ThemeSwitcher />
{/* LanguageSwitcher */}
</div>
</div>
);
};
src > widgets > Sidebar > ui > Sidebar > Sidebar.props.ts
import { DetailedHTMLProps, HTMLAttributes } from 'react';
export interface ISidebarProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {}
src > widgets > Sidebar > ui > Sidebar > Sidebar.module.scss
.sidebar {
position: relative;
height: calc(100vh - var(--navbar-height));
width: var(--sidebar-width);
background: var(--inverted-bg-color);
transition: all .3s ease;
}
.collapsed {
width: var(--sidebar-width-collpased);
}
.switchers {
position: absolute;
bottom: 29px;
display: flex;
justify-content: center;
width: 100%;
}
src > widgets > Sidebar > index.ts
export { Sidebar } from './ui/Sidebar/Sidebar';
В роутере приложения элемент нужно обернуть во враппер страницы, который имеет свойство flex-grow
, чтобы он занимал полностью размер своей флекс-колонки
`src > app > providers > router > ui > AppRouter.tsx
export const AppRouter = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{Object.values(routeConfig).map(({ path, element }) => (
<Route
key={path}
path={path}
element={
<Suspense fallback={<div>Loading...</div>}>
<div className={'page-wrapper'}>{element}</div>
</Suspense>
}
/>
))}
</Routes>
</Suspense>
);
};
Добавляем переменные размеров сайдбара
src > app > variables > global.scss
:root {
--font-family-main: Montserrat, Roboto, sans-serif;
--font-size-m: 16px;
--font-line-m: 24px;
--font-m: var(--font-size-m) / var(--font-line-m) var(--font-family-main);
--font-size-l: 24px;
--font-line-l: 32px;
--font-l: var(--font-size-l) / var(--font-line-l) var(--font-family-main);
--navbar-height: 50px;
--sidebar-width: 300px;
--sidebar-width-collpased: 80px;
}
Добавляем стили для отображения сайдбара и деления страницы на 2 части
src > app > index.scss
.content-page {
display: flex;
}
.page-wrapper {
flex-grow: 1;
width: 100%;
padding: 20px;
}
15 i18n Интернационализация. Define plugin. Плагин для переводов
Первым делом нужно установить зависимости интернационализатора
npm install react-i18next i18next --save
Далее нам нужно прокинуть в приложение глобальную переменную. Сделать мы это можем через Webpack. Сделать мы можем это с помощью DefinePlugin
плагина, который предоставляет вебпак.
Конкретно нам понадобится переменная __IS_DEV__
, которая будет отвечать за наличие режим разработки, который определяется из вебпака
build > config > buildPlugins.ts
export const buildPlugins = ({ paths, isDev }: BuildOptions): WebpackPluginInstance[] => {
return [
// то плагин, который будет показывать прогресс сборки
new ProgressPlugin(),
// это плагин, который будет добавлять самостоятельно скрипт в наш index.html
new HTMLWebpackPlugin({
// указываем путь до базового шаблона той вёрстки, которая нужна в нашем проекте
template: paths.html,
}),
// этот плагин будет отвечать за отделение чанков с css от файлов JS
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].css',
}),
// этот плагин позволяет прокидывать глобальные переменные в приложение
new DefinePlugin({
__IS_DEV__: JSON.stringify(isDev),
__API__: JSON.stringify('https://' /* api_path */),
}),
];
};
Далее нужно объявить эти переменные глобально для ТС
app > types > global.d.ts
declare const __IS_DEV__: boolean;
declare const __API__: string;
Далее нужно написать конфиг для интернационализатора. Сам конфиг представляет из себя декодер языка, поддержку реакта и подгрузку языков с сервера по запросу пользователя на смену языка
shared > config > i18n > i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'ru',
debug: __IS_DEV__,
interpolation: {
escapeValue: false,
},
});
export default i18n;
Далее нужно глобально поместить i18n
в корень приложения (помещаем сам файл полностью) и обернуть всю динамику в Suspense
, так как переводы будут подгружаться чанками
index.tsx
import '@/shared/config/i18n/i18n';
const root = createRoot(document.getElementById('root') as HTMLElement);
if (!root) {
throw new Error('В приложение не вмонтирован root div !!');
}
root.render(
<BrowserRouter>
<ThemeProvider>
<StrictMode>
<Suspense fallback={<Skeleton />}>
<App />
</Suspense>
</StrictMode>
</ThemeProvider>
</BrowserRouter>,
);
Далее нам нужно создать переводы для приложения и указать ключ, по которому можно добраться до текста и значение
public > locales > ru > translation.json
{
"translate": "Перевод",
"Sample": "Пример русского текста"
}
public > locales > en > translation.json
{
"translate": "Translate",
"Sample": "Sample eng text"
}
Сейчас нам остаётся только реализовать смену языка из приложения таким вот кодом:
const App = () => {
const { theme } = useTheme();
const { t, i18n } = useTranslation();
const handleToggleLanguage = () => {
i18n.changeLanguage(i18n.language === 'ru' ? 'en' : 'ru');
};
return (
<div className={cn('app', theme)}>
<Navbar />
<div>
{/* тут нам нужно указать ключи нужных нам значений */}
<button onClick={handleToggleLanguage}>{t('translate')}</button>
<p>{t('Sample')}</p>
</div>
<div className='content-page'>
<Sidebar />
<AppRouter />
</div>
</div>
);
};
И сейчас у нас меняется язык и работает перевод
Но при каждой смене языка, пользователь будет грузить абсолютно весь перевод, который у нас находится в файле translation.json
. Чтобы избежать этой проблемы, нам нужно создать отдельный файл с переводами
public > locales > ru > about.json
{
"About": "О приложении"
}
public > locales > en > about.json
{
"About": "About Page"
}
И указать пространство имён, которое будет использоваться для переводов на странице.
const AboutPage = () => {
const { t } = useTranslation('about');
return (
<div>
<div className={'about'}>{t('About')}</div>
</div>
);
};
Так же благодаря плагину i18n support
мы можем отслеживать отсутствующие ключи и добавлять перевод в интересующие нас файлы
16 Webpack hot module replacement
Далее нам нужно будет добавить горячее замещение модулей в вебпаке. А именно, мы добавим замещение как React-кода, так и CSS-изменений
Нужно установить отдельный плагин, который поддерживает горячее замещение компонентов системы
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
Далее нужно включить хот-релоад в дев-сервере
config > build > buildDevServer.ts
import { BuildOptions } from './types/config';
import { Configuration as DevServerConfiguration } from 'webpack-dev-server';
export function buildDevServer(options: BuildOptions): DevServerConfiguration {
return {
port: options.port, // порт
// open: true, // автоматически будет открывать страницу в браузере // данная команда позволяет проксиовать запросы через index страницу, чтобы при обновлении страницы не выпадала ошибка historyApiFallback: true,
// данный параметр используется для горяей замены модулей
hot: true
};
}
И так же нужно добавить два плагина HotModuleReplacementPlugin
, который идёт в пакете с webpack и ReactRefreshWebpackPlugin
, который установили отдельно.
Теперь замещение пакетов происходит в реальном времени
config > build > buildPlugins.ts
import { WebpackPluginInstance, ProgressPlugin, DefinePlugin, HotModuleReplacementPlugin } from 'webpack';
import HTMLWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import { BuildOptions } from './types/config';
export const buildPlugins = ({ paths, isDev }: BuildOptions): WebpackPluginInstance[] => {
const plugins = [
// то плагин, который будет показывать прогресс сборки
new ProgressPlugin(),
// это плагин, который будет добавлять самостоятельно скрипт в наш index.html
new HTMLWebpackPlugin({
// указываем путь до базового шаблона той вёрстки, которая нужна в нашем проекте
template: paths.html,
}),
// этот плагин будет отвечать за отделение чанков с css от файлов JS
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].css',
}),
// этот плагин позволяет прокидывать глобальные переменные в приложение
new DefinePlugin({
__IS_DEV__: JSON.stringify(isDev),
__API__: JSON.stringify('https://' /* api_path */),
}),
];
if (isDev) {
plugins.push(
// данный плагин уже нужен для рефреша реакт-компонентов
new ReactRefreshWebpackPlugin(),
);
plugins.push(
// данный плагин отвечает за горячую замену модулей без перезагрузки приложения
new HotModuleReplacementPlugin(),
);
}
return plugins;
};
Сейчас нам нужно установить babel-plugin-i18next-extract
, чтобы экстрактить переводы в отдельные файлы по языкам
Так же мы установили @babel/preset-env
нужно установить пакет который включает преобразование js для новых стандартов ES
npm install --save-dev babel-loader @babel/core @babel/preset-env babel-plugin-i18next-extract
Далее добавляем такой конфиг, который уже включает в себя все установленные плагины
buildLoaders.ts
/* лоадер, который позволит использовать бейбел */
const babelLoader = {
test: /\.(js|jsx|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
[
'i18next-extract',
{
locales: ['ru', 'en'],
keyAsDefaultValue: true,
},
],
],
},
},
};
/*
* typescriptLoader должен идти после babelLoader
* */
return [fileLoader, svgLoader, babelLoader, typescriptLoader, stylesLoader];
По поводу лоадера для React
Если бы мы не использовали typescriptLoader и писали проект на JS, то нам бы пришлось устанавливать babel/preset-react
, чтобы поднимать наш проект
Далее нам нужно настроить плагины
babel.config.json
{
"presets": ["@babel/preset-env"],
"plugins": [
"i18next-extract"
]
}
18 Настраиваем EsLint. Исправляем ошибки
Можно легко и быстро настроить eslint
через инит-функцию для него:
npm init @eslint/config
Так же дополнительно можно установить некоторые правила
Последнее правило будет подсвечивать те места где текст получается не из i18n
, а просто вписан
npm install --save-dev eslint eslint-plugin-unicorn eslint-plugin-i18next
И так уже выглядит начальный конфиг
.eslint.cjs
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
env: {
'browser': true,
'es2021': true,
},
settings: {
react: {
version: 'detect',
},
},
/* расширения конфига */
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'prettier',
"plugin:i18next/recommended"
],
overrides: [
{
'env': {
'node': true,
},
'files': [
'.eslintrc.{js,cjs}',
],
'parserOptions': {
'sourceType': 'script',
},
},
],
/* опции для парсера */
'parserOptions': {
'ecmaVersion': 'latest',
'sourceType': 'module',
},
/* объявление глобальных переменных, которые доступны во всём приложении */
globals: {
__API__: true,
__IS_DEV__: true,
},
/* подключение внешних плагинов */
'plugins': [
"unicorn",
'import',
'react',
'i18next',
'react-hooks',
'jsx-a11y',
'@typescript-eslint',
],
/* настройка правил */
'rules': {
/*
* проверяет максимальную длину строки
* если комментарий, то игнорирует
* */
'max-len': ['error', { code: 100, ignoreComments: true }],
'no-undef': 'warn',
/*
* нельзя использовать объявленные ранее имена
* (даже внутри другого скоупа)
* */
'no-shadow': 'warn',
/* неиспользуемые переменные запрещены */
'no-unused-vars': 'warn',
'no-underscore-dangle': 'off',
'react/prop-types': 0,
'react/jsx-props-no-spreading': 'warn',
'react/function-component-definition': 'off',
'import/no-unresolved': 'off',
'react-hooks/rules-of-hooks': 'error',
/* запрещает просто вводить текст в JSX - можно только через перевод */
"i18next/no-literal-string": ['error', { markupOnly: true }],
/* неиспользуемые переменные запрещены (TS) */
'@typescript-eslint/no-unused-vars': 'warn'
},
};
И так же можно добавить скрипт, который будет прогоняться при каждом запуске
package.json
"lint": "eslint \"src/**/*.{js,ts,jsx,tsx}\" --quiet"
19 Stylelint. Plugin for i18next
Далее нам нужно установить линтер для проверки стилей
npm install --save-dev stylelint stylelint-config-standard stylelint-config-standard-scss stylelint-scss
Конфигурация:
.stylelintrc
{
"extends": ["stylelint-config-standard", "stylelint-config-standard-scss"],
"plugins": ["stylelint-scss"],
"rules": {
"selector-class-pattern": null,
"property-no-unknown": null,
"no-invalid-double-slash-comments": null,
"no-descending-specificity": null,
"font-family-no-duplicate-names": null,
"function-no-unknown": null,
"block-no-empty": null
}
}
Скрипт линтера
package.json
"stylelint": "stylelint \"**/*.scss\" --fix",
20 Тестовая среда. Настраиваем Jest. Пишем первый тест Метка
Устанавливаем
jest
типы к нему
поддержка тайпскрипта
окружение браузера
npm install --save-dev jest @types/jest @babel/preset-typescript jest-environment-jsdom
jest --init
Тут мы добавляем jest в eslint, чтобы линтер не ругался на не импортированные файлы
.eslintrc.js
env: {
browser: true,
es2021: true,
jest: true,
},
Далее нужно добавить обработку тайпскрипта, чтобы джест мог читать абсолютные импорты и работал с типами
.babel.config.js
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-typescript'],
};
Прогонка тестов