
Приветствую уважаемое сообщество.
Хочу поделиться своим видением сборки для быстрого старта разработки на React.
Помогает быстро запуститься, когда нужно "на скорую" войти в разработку.
Что-то я подглядел здесь же, на Хабре, к чему-то пришёл сам, ну и "ангажировал" немного на просторах "необъятного".
Цель поста - поделиться полезным, услышать мнения, прознать про брешь.
Поехали.
Что "под капотом"
Webpack 5
React v.18
Redux (Redux Toolkit)
Typescript
Css modules
Jest
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 | |
html-webpack-plugin | Плагин подключит создаваемые бандлером файлы в указанный в качестве шаблона index.html | |
favicons-webpack-plugin | Плагин для генерации фавиконок. | |
favicons | Пакет необходим для favicons-webpack-plugin | |
@pmmmwh/react-refresh-webpack-plugin | Осуществляет быстрое обновление компонентов React при изменении кода. Hot Reload. | https://www.npmjs.com/package@pmmmwhh/react-refresh-webpack-plugin |
react-refresh | Пакет также необходим Webpack для "Горячей перезагрузки" | |
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 | |
html-loader | Необходим для экспорта html файлов. Плагин не официальный, написан сообществом. | |
css-loader | Позволяет использовать @import и url() как import/require() в JS | |
postcss-loader | Плагин для работы с css | |
postcss-preset-env | Плагин (надстройка) для postcss. Позволяет преобразовывать современный CSS во что-то, понятное большинству браузеров, определяя нужные полифилы на основе целевых браузеров или сред выполнения. | |
sass-loader | Преобразует CSS-препроцессоры в CSS |
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 | Автоматически перезапускает приложение при обнаружении изменений файлов в каталоге | |
cross-env | Необходим для кроссплатформенного выполнения CLI команд | |
eslint | Инструмент для шаблонного проектирования кода | |
jest | JS фреймворк для написания и исполнения тестов |
Установка React и полезных фронтовых штук
Нам потребуются (могут пригодиться):
react | Библиотека пользовательских интерфейсов | |
react-dom | Пакет для работы с DOM | |
react-redux | Позволяет компонентам React считывать данные из хранилища Redux | |
react-router-dom | Необходим для работы с React Router | |
@reduxjs/toolkit | Пакет для работы со стейт-менеджером Redux | |
@emotion | Библиотека для написания CSS стилей, используя Javascript | |
@mui | Всем известная библиотека для написания компонентов | |
redux-logger | Как следует из названия, - это логгер изменения состояний для Redux | |
@types/redux-logger | Пакет с типизацией для 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 | |
@typescript-eslint/eslint-plugin | Плагин для написания правил под Typescript | |
@typescript-eslint/parser | Необходим для чтения и анализа исходного кода Typescript |
Добавляем конфигурацию 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 без необходимости преобразования синтаксиса | |
@babel/preset-react | Пресет для плагинов React | |
@babel/preset-typescript | Пресет для 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 | |
typescript-plugin-css-modules | Плагин для работы с CSS-модулями в TS |
В корне проекта остаётся создать файлик 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 | |
jest-environment-jsdom | Пакет для работы с JSDOM | |
jest-css-modules-transform | Конвертирует CSS файлы в JS модули | |
babel-jest | Jest трансформер для транспиляции | |
@types/jest | Типизация для Jest | |
@testing‑library/react | Библиотека тестов React Testing Library | |
@testing-library/jest-dom | Пакет, расширяющий возможности Jest |
Осталось сказать про настройки 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 }
Здесь содержатся событийные правила для редактора.
Такие как, указание форматтера, действия при перемещении файла, действия при сохранении, и.т.п.
На этом, всё.
В качестве Конклюжна
Вышло сумбурно. Понимаю. Старался "лить меньше воды". Местами, где прям ну вообще засуха, можно смотреть комментарии в коде.
О многом не сказал, но готов обсудить.
Надеюсь кто-то найдёт здесь что-либо полезное.
Код сборки можно глянуть здесь
Спасибо за уделённое прочтению время.