React+TS from scratch. I

В интернете достаточно много разрозненной информации о том, как настроить окружение для стека TypeScript/React, и все они сводятся к npx create-react-app app-name --template typescript , но это не интересно. Возьмем чистый package.json:

{  
    "name": "TestTypeScriptTask",  
    "private": true,  
    "version": "0",  
    "description": "Here we touch typescript/react/redux stack"
}

и попробуем настроить сборку и все окружение ручками.

Сперва надо поставить сам typescript: yarn add typescript --dev
После установки добавим несколько пакетов с типами:
yarn add @types/node @types/react @types/react-dom

Теперь надо добавить файл конфигурации для TypeScript:
tsc --init
После выполнения команды появится файл tsconfig.json. В этом файле нас в первую очередь интересует:

  1. Настройка спецификации языка("target"), которая по-умолчанию установлена в es5. На дворе 2021 и не понятно, зачем в es5 транспилировать наш код, возьмем es2017
  2. Настройка модульной системы("module"), которая определяет какой способ связывания модулей будем использовать:
  • commonjs
// utils.js  
function add(r){
    return r + r;
}
// export (expose) add to other modules
exports.add = add;

// index.js
var utils = require('./utils.js');
utils.add(4); // = 8 
  • amd (async module definition)
// add.js
define(function() {
    return add = function(r) {
        return r + r;
    }
});

// index.js
define(function(require) {
    require('./add');
    add(4); // = 8
}
  • es2015
// add.js
export function add(r) {
    return r + r;
}

// index.js
import add from "./add";
add(4); // = 8

что интересно! в коде хочу использовать es2015, но тогда сборка вебпаком не работает нихрена, а если указать "commonjs" то все собирается и работает и не возникает проблем что модули оформлены в es2015, а не commonjs. Пока не могу объяснить почему это так

Полностью конфиг выглядит так:

{
  "compilerOptions": {
    "target": "ES2017", // описан выше
    "module": "commonjs", // описан выше
    // список библиотек в которых содержится описание различных API,
    // которые хотим использовать
    "lib": [
      "esnext",
      "dom",
      "dom.iterable"
    ],
    // разрешаем использовать иногда js код
    "allowJs": false,
    // определяем спецификацию используемых jsx/tsx файлов(react/react-native)
    "jsx": "react",
    // будем генерировать сорс мапы, чтобы в отладке было легче
    "sourceMap": true,
    // удалять ли комменты из кода после транспиляции
    "removeComments": true,
    // включает поддержку итератора из es2015 и  
    // генерирует совсем другой код для циклов for-of, 
    // spread и распаковке, когда target сборки es5/es3
    "downlevelIteration": true,
    // строгий режим, все проверки типов включены
    "strict": true,
    // выбрал TS - страдай и описывай все типы, никаких any
    "noImplicitAny": true, 
    // стратегия поиска модулей в импортах: node/classic. 
    // Всюду преимущественно используется node 
    // Если кратко то так: при импорте "import Foo from './foobar'":
    // - сперва ищется файл foobar.js, 
    // - потом каталог foobar и в нем package.json 
    // в котором есть секция main, 
    // - потом каталог foobar и в нем index.js 
    //
    // classic оставлен для обратной совместимости
    "moduleResolution": "node",
    // директория с которой разрешать относительные имена модулей
    "baseUrl": "./",
    // мапаем пути импорта в проекте на другие, относительно baseUrl
    "paths": {
      "components/*": [
        "src/components/*"
      ]
    },
    // разрешить импортировать default из модулей, 
    // которые его не экспортируют и не будет генерировать 
    // ошибок при проверке типов(ошибка импорта из файла без экспорта)
    "allowSyntheticDefaultImports": true, 
    // включает возможность импорта модулей commonjs в стиле ES6 модулей
    // у commonjs модулей как бы нет export default 
    // так же подразумевает использование allowSyntheticDefaultImports
    "esModuleInterop": true 
  },
  "include": [
    "./src",
    "./webpack.config.ts"
  ]
}

Добавим теперь собственно сам React и накидаем небольшую структуру приложения: yarn add react react-dom

Добавим шаблон в ./public/index.html немного вертски:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Task Notes</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

Так же добавим ./src/index.tsx, который будет точкой входа для сборки проекта (тут все стандартно):

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

небольшое отступление: в каждом файле нужно импортировать реакт, так как каждый файл это отдельный модуль со своей областью видимости и чтобы транспилер заменял jsx шаблоны на React.createElement в скоупе модуля нужен экземпляр React. Избавиться от этого постоянного импорта можно с помощью плагина ProvidePlugin для webpack, который может засунуть реакт в глобальный скоуп доступный всем модулям

сам src/App.tsx (прошу заметить, что компонента импортируется не через относительный путь ./components/ это из за paths и baseUrl в tsconfig):

import React from 'react';
import HelloWorld from 'components/HelloWorld';

const App = () => <HelloWorld/>;
export default App;

ну и компонента (./src/components/HelloWorld/index.tsx)

import React from 'react';

const HelloWorld = () => (
    <>
        <h1>Hello World</h1>
        <hr/>
        <h3>Test react/ts application</h3>
    </>
);
export default HelloWorld;

Так как собираться все это будет вебпаком, надо поставить его и зависимости:
yarn add webpack webpack-dev-server ts-node @types/webpack-dev-server tsconfig-paths-webpack-plugin webpack-cli --dev
по прядку - сам webpack, dev-server к нему, ts-node нужен чтобы нода могла выполнять ts файлы, tsconfig-paths-webpack-plugin - чтобы использовать мапы путей из tsconfig в окружении вебпака(чтобы не дублировать эти мапы в конфиге вебпака) и cli интерфейс к вебпаку

так же ставим:
yarn add ts-loader fork-ts-checker-webpack-plugin html-webpack-plugin --dev
по порядку - загрузчик файлов, fork-ts-checker-webpack-plugin - плагин который позволяет запускать проверку типов в отдельном процессе, чем ускоряет работу, html-webpack-plugin для генерации html файла с правильными ссылками на файлы бандла.

После установки добавляем файл конфигурации вебпака:

import path from 'path';
import webpack, {Configuration} from 'webpack';
import HTMLPlugin from 'html-webpack-plugin';
import TypeCheckPlugin from 'fork-ts-checker-webpack-plugin';
import {TsconfigPathsPlugin} from 'tsconfig-paths-webpack-plugin';

const config = (env: any): Configuration => ({
    // откуда начинать сборку
    entry: './src/index.ts', 
    // определяет тип source-map, влияет на скорость сборки, 
    // для прода не включаем их, 
    // в разработке - включаем режим eval-source-map - медленная сборка,
    // код файлов - исходный
    ...(env.production || !env.development ? {} : {'devtool': 'eval-source-map'}),
    // как и где искать модули, 
    // в этой же секции можно описать алиасы(маппинг-путей)
    resolve: { 
        // в каком порядке разрешать чтение модулей
        // если есть index.js, index.tsx в одном модуле, то
        // файл с расширением первым в списке - пойдет первым
        extensions: [".ts", ".tsx", ".js"], 
        // собственно подключаем плагин, который поможет 
        // вебпаку определиться с путями импорта модулей, которые
        // описаны в tsconfig
        // @ts-expect-error
        plugins: [new TsconfigPathsPlugin()], 
    },
    // куда выкладывать сборку
    output: {
        path: path.join(__dirname, '/dist'),
        filename: "build.js"
    },
    module: {
        // правила загрузки файлов
        // пока у нас только tsx - используем ts-loader
        // Опция transpileOnly - выключает проверку типов загрузчиком
        // у нас для этого будет использоваться другой плагин
        rules: [
            {
                test: /\.tsx?$/,
                loader: 'ts-loader',
                options:{transpileOnly: true},
                exclude: /dist/,
            }
        ]
    },
    plugins: [
        // определяем где лежит шаблон, 
        // в который надо добавить собранный проект
        new HTMLPlugin({template: './public/index.html'}), 
        // определяем переменные окружения доступные фронтенду
        new webpack.DefinePlugin({ 
            "process.env.PRODUCTION": env.production || !env.development,
            "process.env.NAME": JSON.stringify(require('./package.json').name),
            "process.env.VERSION": JSON.stringify(require('./package.json').version),
        }),
        // плагин проверки типов в отдельном потоке
        new TypeCheckPlugin({ eslint: {
            files: './src/**/*.{ts,tsx,js,jsx}'
        }}) 
    ]
});
export default config;

и добавим скрипты запуска в package.json:

"scripts": {
    // запускаем дев-сервер, в режиме разработки, 
    // с открытием новой табы в браузере и hot-reload
    "start:dev": "webpack-cli serve --mode=development --env development --open --hot", 
    "build": "webpack --mode=production --env production --progress",
    "lint": "eslint './src/**/*.{ts,tsx}'",
    "lint:fix": "eslint './src/**/*.{ts,tsx}' --fix"
  },

Пока все это запускать рано, у нас не установлен и не настроен eslint
Перед тем как ставить его - поставим Prettier(штука для форматирования кода, тут инструкция по интеграции в IDE)
yarn add prettier --dev
добавим конфиг в .prettierrc:

{
  "printWidth": 100,
  "trailingComma": "none",
  "tabWidth": 4,
  "semi": true,
  "singleQuote": true,
  "bracketSpacing": true,
  "jsxBracketSameLine": false,
  "arrowParens": "always",
  "endOfLine": "auto",
  "jsxSingleQuote": true,
  "proseWrap": "preserve",
  "quoteProps": "as-needed",
  "useTabs": false,
  "htmlWhitespaceSensitivity": "css"
}

с опциями преттиера ознакомимся позже, когда начнем писать много кода и определимся с code-style

Наконец ставим линтер и обвязку плагинов и парсеров для TS:
yarn add eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-prettier eslint-config-prettier eslint-plugin-import --dev
по порядку - сам линтер, плагин с правилами для реакта, плагин с правилами для хуков реакта, плагин с правилами TS, парсер TS для линтера, плагин с правилами для преттиера, eslint-config-prettier - выключает правила eslint, которые будут конфликтовать с конфигом преттиера, eslint-plugin-import - добавляет поддержку синтаксиса импорта/экспорта ES2015+ и предотвращает опечатки в путях к файлам и имен импорта.

Поставив эти пакеты, добавляем конфиг для линтера .eslintrc.json:

{
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint",
    "react",
    "react-hooks",
    "eslint-plugin-import",
    "prettier"
  ],
  "env": {
    "browser": true
  },
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:prettier/recommended"
  ],
  "parserOptions": {
    "project": [
      "tsconfig.json"
    ],
    "ecmaVersion": 2017,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "rules": {
    "react/jsx-filename-extension": [
      "warn",
      {
        "extensions": [
          ".jsx",
          ".tsx"
        ]
      }
    ],
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  }
}

В самом конце создаем .eslintignore, запишем в него конфиг вебпака(имя файла всмысле) и запустим приложение: yarn start:dev в новой вкладке браузера откроется окно с приложением, а в консоли будет насрано много ошибок линтера)