feat: add create webpack configuration function

This commit is contained in:
Sergey Krylov 2024-02-27 07:21:36 +03:00
parent d14e151681
commit cda2c48613
35 changed files with 9098 additions and 11 deletions

5
.eslintrc Normal file
View File

@ -0,0 +1,5 @@
{
"extends": [
"ksv741"
]
}

419
README.md Normal file
View File

@ -0,0 +1,419 @@
## ksv741-react-scripts
***
### Быстрый способ запуска React приложений для сборки и разработки, с расширяемой настройкой webpack
### Содержание
- [`Установка`](#установка)
- [`Настройка`](#настройка)
- [`Использование`](#использование)
- [`Поддерживаемые файлы`](#поддерживаемые-файлы)
- [`Кастомизация`](#кастомизация)
- [`Режим запуска`](#режим-запуска)
- [`Конфигурация путей`](#конфигурация-путей)
- [`Порт`](#порт)
- [`Загрузчик`](#загрузчик)
- [`Анализ`](#анализ)
- [`Плагины`](#плагины)
- [`Лоадеры`](#лоадеры)
- [`Расширенная конфигурация`](#расширенная-конфигурация)
***
### Установка
#### npm
```js
npm install ksv741-react-scripts
```
#### yarn
```js
yarn add ksv741-react-scripts
```
#### pnpm
```js
pnpm install ksv741-react-scripts
```
### Настройка
Библиотека предоставляет функцию `createConfig` для быстрого создания конфигурации `webpack` c набором предустановленных плагинов и лоадеров,
1. Создание файла конфигурации webpack
```js
// webpack.config.js
const { createConfig } = require('ksv741-react-scripts');
module.exports = createConfig();
```
2. Добавление файла декларации для использования различных файлов совместно с TypeScript
```js
// global.d.ts
/// <reference types="ksv741-react-scripts/global" />
```
3. Создание файлов инициализации
```html
<!-- 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><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="root"></div>
</body>
</html>
```
```js
// src/index.js - также доступно использование src/index.tsx и src/index.jsx
import { createRoot } from 'react-dom/client';
const domNode = document.getElementById('root')!;
const root = createRoot(domNode);
root.render(
<h1>
Hello ksv741
</h1>,
);
```
### Использование
- `npx ksv741-react-scripts start` - для разработки
- `npx ksv741-react-scripts build` - для сборки
### Поддерживаемые файлы
- JavaScript (`.js`, `.jsx`)
- TypeScript (`.ts`, `.tsx`)
- Изображения (`.png`, `.jpg`, `.jpeg`, `.gif`, `.avif`, `.webp`)
- Шрифты (`.woff`, `.woff2`, `.eot`, `.ttf`, `.otf`)
- Стилизация (`.css`, `.scss`, `.sass`)
- Иконки (`.svg`)
### Кастомизация
Если необходимо подключить уже существующий проект со своей структурой папок,
или хотим держать единый файл конфигурации для разработки и сборки, но исходя из каких-то условий настроить пути.
Функция `createConfig` принимает объект с конфигурацией
```js
{
mode?: 'development' | 'production';
paths?: {
assets?: string;
build?: string;
entry?: string[] | string;
html?: string;
public?: string;
root?: string;
src?: string;
};
port?: number;
devtool?: string | false;
mainLoader?: 'esbuild' | 'swc';
analyze?: boolean;
plugins?: {
htmlWebpackPlugin?: object | 'off';
progressPlugin?: object | 'off';
definePlugin?: object | 'off';
forkTsCheckerPlugin?: object | 'off';
eslintPlugin?: object | 'off';
reactRefreshPlugin?: object | 'off';
miniCssPlugin?: object | 'off';
copyPlugin?: object | 'off';
ignorePlugin?: object | 'off';
analyzerPlugin?: object | 'off';
}
loaders?: {
esbuildLoader?: object | 'off';
swcLoader?: object | 'off';
svgrLoader?: object | 'off';
styleLoader?: object | 'off';
cssLoader?: object | 'off';
postCssLoader?: object | 'off';
miniCssLoader?: object | 'off';
sassLoader?: object | 'off';
imageLoader?: object | 'off';
fontLoader?: object | 'off';
};
}
```
#### Режим запуска
- `mode` - режим запуска.
В режиме `production` включена минификация, отключены некоторые плагины.
Не путать с параметром запуска приложения `start`, `build`,
возможно запустить проект для разработки в режиме `production` или сбилдить проект в режиме `development`, иногда это позволяет получить нужную информацию.
По умолчанию - `development` для `ksv741-react-scripts start` и `production` для `ksv741-react-scripts build`
#### Конфигурация путей
`paths` - объект с настройкой путей
- `assets` - путь до директории, в которой расположены файлы которые используются непосредственно в коде.
По умолчанию: `public/assets`
- `build` - путь до директории, в которую будут собраны файлы
По умолчанию: `build`
- `entry` - файл(ы) точки входа
По умолчанию: `src/index` с расширением `.tsx`, `.jsx`, `.js`, приоритет расширений в указанном порядке.
- `html` - файла шаблона index.html
По умолчанию `public/index.html`
- `public` - путь до директории, в которой будут храниться файлы, которые попадут в сборку,
относительно этого пути будут искаться файл `html`
По умолчанию - `public`
- `root` - путь, относительно которого будут резолвиться все остальные пути
По умолчанию: текущая директория, откуда выполнена команда запуска (`cwd`)
- `src` - путь до директории с исходными файлами, относительно которого будет находиться файл `entry`
По умолчанию - `src`
#### Порт
- `port` - порт, в котором будет запускаться dev server, для `mode = 'production'` настройка игнорируется
По умолчанию: `3000`
#### Загрузчик
- `mainLoader` - главный загрузчик javascript/typescript файлов, доступны `esbuild` и `swc`
По умолчанию: `esbuild`
#### Анализ
- `analyze` - запуск сервера на порту `8888`, для анализа сборки. Рекомендуется запускать совместно с `mode = 'production'`. Но допустим запуск и в режиме `development`
По умолчанию: `false`
#### Devtool
- `devtool` - [формат source-map](https://webpack.js.org/configuration/devtool/)
По умолчанию: `eval`
#### Плагины
- `plugins` - объект с настройками плагинов
Список используемых плагинов:
- [HTMLWebpackPlugin](https://github.com/jantimon/html-webpack-plugin)
- [ProgressPlugin](https://webpack.js.org/plugins/progress-plugin), доступен только в режиме `development`
- [DefinePlugin](https://webpack.js.org/plugins/define-plugin)
- [ForkTsCheckerWebpackPlugin](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin)
- [ESLintPlugin](https://github.com/webpack-contrib/eslint-webpack-plugin), доступен только в режиме `development`, а также при наличии установленного пакета `eslint`
- [ReactRefreshWebpackPlugin](https://github.com/pmmmwh/react-refresh-webpack-plugin), доступен только в режиме `development`
- [MiniCssExtractPlugin](https://webpack.js.org/plugins/mini-css-extract-plugin), доступен только в режиме `production`
- [CopyPlugin](https://webpack.js.org/plugins/copy-webpack-plugin), доступен только в режиме `production`
- [IgnorePlugin](https://webpack.js.org/plugins/ignore-plugin), доступен только в режиме `production`
- [BundleAnalyzerPlugin](https://github.com/webpack-contrib/webpack-bundle-analyzer)
Для настройки плагина необходимо передать объект с конфигурацией или строку `off` для того чтобы отключить плагин:
- `htmlWebpackPlugin` - настройка плагина [HTMLWebpackPlugin](https://github.com/jantimon/html-webpack-plugin#options)
По умолчанию:
```
{
template: paths.html,
favicon: path.resolve(paths.assets, 'favicon.ico'),
title: 'My App',
}
```
- `progressPlugin` - настройка плагина [ProgressPlugin](https://webpack.js.org/plugins/progress-plugin/#providing-object)
- `definePlugin` - настройка плагина [DefinePlugin](https://webpack.js.org/plugins/define-plugin/#usage)
По умолчанию:
```js
{
'process.env.IS_DEV': JSON.stringify(isDev),
'process.env.APP_VERSION': JSON.stringify(process.env.npm_package_version),
}
```
- `forkTsCheckerPlugin` - настройка плагина [ForkTsCheckerWebpackPlugin](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin?tab=readme-ov-file#options)
- `eslintPlugin` - настройка плагина [ESLintPlugin](https://github.com/webpack-contrib/eslint-webpack-plugin?tab=readme-ov-file#options)
По умолчанию:
```
{
extensions: ['.js', '.jsx', '.tsx', '.ts'],
failOnError: false,
lintDirtyModulesOnly: true,
exclude: [
path.resolve(paths.root, 'node_modules'),
paths.build,
]
}
```
- `reactRefreshPlugin` - настройка плагина [ReactRefreshWebpackPlugin](https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/API.md#options)
- `miniCssPlugin` - настройка плагина [MiniCssExtractPlugin](https://webpack.js.org/plugins/mini-css-extract-plugin/#options)
По умолчанию:
```js
{
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}
```
- `copyPlugin` - настройка плагина [CopyPlugin](https://webpack.js.org/plugins/copy-webpack-plugin/#options)
По умолчанию:
```js
{
patterns: [{
from: path.resolve(paths.root, paths.public),
globOptions: {
ignore: [
path.resolve(paths.root, paths.public, paths.html),
path.resolve(paths.root, paths.public, paths.assets),
],
},
to: path.resolve(paths.root, paths.build),
}]
}
```
- `ignorePlugin` - настройка плагина [IgnorePlugin](https://webpack.js.org/plugins/ignore-plugin/#root)
По умолчанию:
```js
{
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}
```
- `analyzerPlugin` - настройка плагина [BundleAnalyzerPlugin](https://github.com/webpack-contrib/webpack-bundle-analyzer?tab=readme-ov-file#options-for-plugin)
#### Лоадеры
- `loaders` - объект с настройками лоадеров
Список используемых лоадеров:
- [esbuild-loader](https://github.com/privatenumber/esbuild-loader) - лоадер для обработки `.tsx`, `.jsx`, `.ts`, `.js` файлов, при указании настройки [`mainLoader: 'esbuild'`](#загрузчик)
- [swc-loader](https://github.com/swc-project/pkgs/tree/main/packages/swc-loader) - лоадер для обработки `.tsx`, `.jsx`, `.ts`, `.js` файлов, при указании настройки [`mainLoader: 'swc'`](#загрузчик)
- [@svgr/webpack](https://github.com/gregberge/svgr/tree/main/packages/webpack)- лоадер для обработки `.svg` файлов, для работы с файлами, как с React компонентами
- [style-loader](https://webpack.js.org/loaders/style-loader/) - лоадер для вставки стилей inline элементами в html, доступен в режиме `development`
- [mini-css-loader](https://webpack.js.org/plugins/mini-css-extract-plugin/#loader-options), доступен в режиме `production` - лоадер для формирование стилей `.css` файлами
- [css-loader](https://webpack.js.org/loaders/css-loader/) - лоадер для обработки `.css` файлов
- [postcss-loader](https://webpack.js.org/loaders/postcss-loader/) - лоадер для расширение функционала CSS
- [sass-loader](https://webpack.js.org/loaders/sass-loader/) - лоадер для обработки `.sass`, `.scss` файлов
- [image-loader](https://webpack.js.org/guides/asset-modules/) - лоадер для обработки изображений в формате `.png`, `.jpg`, `.jpeg`, `.gif`, `.avif`, `.webp`
- [font-loader](https://webpack.js.org/guides/asset-modules/) - лоадер для обработки шрифтов в формате `.woff`, `.woff2`, `.eot`, `.ttf`, `.otf`
Для настройки лоадера необходимо передать объект с конфигурацией или строку `off` для того чтобы отключить лоадер:
- `esbuilLoader` - настройки для [esbuild-loader](https://github.com/privatenumber/esbuild-loader?tab=readme-ov-file#%EF%B8%8F-options)
По умолчанию:
```js
{
loader: 'tsx',
}
```
- `swcLoader` - настройкаи для [swc-loader](https://swc.rs/docs/configuration/swcrc)
По умолчанию:
```js
{
sync: true,
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
dynamicImport: true,
privateMethod: true,
functionBind: true,
exportDefaultFrom: true,
exportNamespaceFrom: true,
decorators: true,
decoratorsBeforeExport: true,
topLevelAwait: true,
importMeta: true,
},
transform: {
react: {
runtime: 'automatic',
development: isDev,
refresh: isDev,
},
},
target: 'es2015',
loose: false,
externalHelpers: false,
keepClassNames: false,
},
},
```
- `svgrLoader` - настройка для [@svgr/webpack](https://react-svgr.com/docs/webpack/#options)
По умолчанию:
```js
{
icon: true,
}
```
- `styleLoader` - настройка для [style-loader](https://webpack.js.org/loaders/style-loader/#options)
- `miniCssLoader` - настойка для [MiniCssExtractPlugin.loader](https://webpack.js.org/plugins/mini-css-extract-plugin/#loader-options)
- `cssLoader` - настройка для [css-loader](https://webpack.js.org/loaders/css-loader/#options)
По умолчанию:
```js
{
sourceMap: mode === 'development',
}
```
- `postCssLoader` - настройка для [postcss-loader](https://webpack.js.org/loaders/postcss-loader/#options)
По умолчанию:
```js
{
postcssOptions: {
plugins: [
[
'postcss-preset-env',
{
browsers: 'last 2 versions',
autoprefixer: true,
},
],
],
},
}
```
- `sassLoader` - настройка для [sass-loader](https://webpack.js.org/loaders/sass-loader/#options)
По умолчанию:
```js
{
sourceMap: mode === 'development',
}
```
- `imageloader` - настройка для опции `generator` [Assets Modules](https://webpack.js.org/guides/asset-modules/)
По умолчанию:
```js
{
filename: 'static/images/[name].[contenthash:8][ext][query]',
}
```
- `fontLoader` - настройка для опции `generator` [Assets Modules](https://webpack.js.org/guides/asset-modules/)
По умолчанию:
```js
{
filename: 'static/fonts/[hash][ext][query]'
}
```
### Расширенная конфигурация
Если текущей кастомизации не достаточно или Вы хотите добавить/изменить какой-то плагин или загрузчик,
то это можно легко сделать, т.к. функция `createConfig` возвращает обычный объект с конфигурацией webpack, который можно изменить.
Например, давайте добавим плагин [ `compression-webpack-plugin`](https://www.npmjs.com/package/compression-webpack-plugin)
```js
// webpack.config.js
const { createConfig } = require('ksv741-react-scripts');
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = () => {
const baseConfig = createConfig({
mode: 'production',
});
baseConfig.plugins.push(
new CompressionPlugin({
algorithm: 'gzip',
}),
);
return baseConfig;
};
```

View File

@ -1,14 +1,28 @@
#!/usr/bin/env node #!/usr/bin/env node
/* eslint-disable no-console */ /* eslint-disable no-console */
const path = require('path');
process.on('unhandledRejection', (err) => {
throw err;
});
const spawn = require('cross-spawn'); const spawn = require('cross-spawn');
const argv = process.argv.slice(2); const argv = process.argv.slice(2);
const command = argv[0];
if (!['start', 'build'].includes(command)) {
console.log(`Unknown command "${command}"`);
console.log('***************************'); process.exit(1);
console.log('argv', argv); }
console.log('***************************');
const webpackArgv = ['webpack'];
if (command === 'start') {
webpackArgv.push('serve');
}
webpackArgv.push(...argv.slice(1));
const result = spawn.sync('npx', webpackArgv, { stdio: 'inherit' });
if (result.signal) { if (result.signal) {
if (result.signal === 'SIGKILL') { if (result.signal === 'SIGKILL') {

30
global.d.ts vendored Normal file
View File

@ -0,0 +1,30 @@
declare module '*.jpg' {
export default '' as string;
}
declare module '*.png' {
export default '' as string;
}
declare module '*.jpeg' {
export default '' as string;
}
declare module '*.gif' {
export default '' as string;
}
declare module '*.webp' {
export default '' as string;
}
declare module '*.avif' {
export default '' as string;
}
declare module '*.scss' {
const content: Record<string, string>;
export = content;
}
declare module '*.svg' {
import type React from 'react';
const SVG: React.VFC<React.SVGProps<SVGSVGElement>>;
export default SVG;
}

View File

@ -2,17 +2,73 @@
"name": "ksv741-react-scripts", "name": "ksv741-react-scripts",
"version": "0.1.0", "version": "0.1.0",
"description": "", "description": "",
"main": "index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts",
"source": "./src/webpack/index.ts",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "build": "rm -rf ./dist && tsc",
"lint": "eslint --ignore-pattern node_modules --color src"
}, },
"bin": { "bin": {
"ksv741-react-scripts": "./bin/scripts.js" "ksv741-react-scripts": "./bin/scripts.js"
}, },
"keywords": [], "engines": {
"author": "", "node": ">16"
},
"files": [
"./dist",
"./bin",
"global.d.ts"
],
"keywords": [
"webpack",
"esbuild",
"swc",
"bundle",
"development",
"typescript",
"react"
],
"author": "Sergey <ksv741@gmail.com> Krylov",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"cross-spawn": "^7.0.3" "@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
"@svgr/webpack": "8.1.0",
"@swc/core": "1.4.0",
"@types/node": "20.11.17",
"@types/react": "18.2.55",
"@types/react-dom": "18.2.19",
"@types/react-router-dom": "5.3.3",
"@types/sass-loader": "8.0.8",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-dev-server": "4.7.2",
"copy-webpack-plugin": "12.0.2",
"cross-spawn": "7.0.3",
"css-loader": "6.10.0",
"css-minimizer-webpack-plugin": "6.0.0",
"esbuild-loader": "4.0.3",
"eslint-webpack-plugin": "4.0.1",
"fork-ts-checker-webpack-plugin": "9.0.2",
"html-webpack-plugin": "5.6.0",
"mini-css-extract-plugin": "2.8.0",
"postcss": "8.4.35",
"postcss-loader": "8.1.0",
"postcss-preset-env": "9.3.0",
"react-refresh": "0.14.0",
"sass": "1.70.0",
"sass-loader": "14.1.0",
"style-loader": "3.3.4",
"swc-loader": "0.2.6",
"terser-webpack-plugin": "5.3.10",
"ts-node": "10.9.2",
"typescript": "5.3.3",
"webpack": "5.90.1",
"webpack-bundle-analyzer": "4.10.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.0"
},
"devDependencies": {
"eslint": "8.56.0",
"eslint-config-ksv741": "git+ssh://git@github.com:ksv741/eslint-config-ksv741.git"
} }
} }

117
src/config/index.ts Normal file
View File

@ -0,0 +1,117 @@
import * as fs from 'fs';
import path from 'path';
import process from 'process';
import type webpack from 'webpack';
import { createDevServer } from '../dev-server';
import { createLoaders } from '../loaders';
import { getOptimization } from '../optimization';
import { createOutput } from '../output';
import { createPlugins } from '../plugins';
import { createResolvers } from '../resolvers';
import type { WebpackMode, WebpackOptions, WebpackPaths } from '../types';
const getDefaultEntryPath = (rootDir: string) => {
const indexTsx = path.resolve(rootDir, 'index.tsx');
const indexJsx = path.resolve(rootDir, 'index.jsx');
const indexJs = path.resolve(rootDir, 'index.js');
switch (true) {
case fs.existsSync(indexTsx):
return indexTsx;
case fs.existsSync(indexJsx):
return indexJsx;
case fs.existsSync(indexJs):
return indexJs;
default:
throw new Error(`Has no index.tsx/index.jsx/index.js file in ${rootDir} directory`);
}
};
const getDefaultPaths = (initialPaths?: WebpackPaths): WebpackPaths => {
const root = initialPaths?.root ?? path.resolve(process.cwd());
const publicPath = initialPaths?.public ?? path.resolve(root, 'public');
const src = initialPaths?.src ?? path.resolve(root, 'src');
const assets = initialPaths?.assets ?? path.resolve(publicPath, 'assets');
const build = initialPaths?.build ?? path.resolve(root, 'build');
const entry = getDefaultEntryPath(src);
const html = initialPaths?.html ?? path.resolve(publicPath, 'index.html');
return {
root,
public: publicPath,
assets,
src,
entry,
build,
html,
};
};
const getDefaultMode = (): WebpackMode => {
const isServe = process.argv.slice(2)[0] === 'serve';
return isServe ? 'development' : 'production';
};
const createConfig = (params?: WebpackOptions): webpack.Configuration => {
const {
mode = getDefaultMode(),
analyze = false,
mainLoader = 'esbuild',
port = 3000,
devtool = 'inline-source-map',
plugins,
} = params ?? {};
const {
assets,
html,
build,
root,
entry,
public: publicPath,
src,
} = getDefaultPaths(params?.paths);
if (!fs.existsSync(html)) {
throw new Error(`index.html file not found. Define path to index.html file or create one in "${publicPath}" directory`);
}
const options: WebpackOptions = {
mode,
analyze,
mainLoader,
port,
paths: {
assets,
html,
build,
root,
entry,
src,
public: publicPath,
},
plugins,
};
const isDev = mode === 'development';
return {
mode,
entry,
output: createOutput(options),
plugins: createPlugins(options),
module: {
rules: createLoaders(options),
},
resolve: createResolvers(options),
devtool: isDev ? devtool : false,
devServer: createDevServer(options),
// eslint-disable-next-line no-undefined
optimization: !isDev ? getOptimization(options) : undefined,
};
};
export { createConfig };

22
src/dev-server/index.ts Normal file
View File

@ -0,0 +1,22 @@
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
import type { WebpackOptions } from '../types';
export const createDevServer = (options: WebpackOptions): DevServerConfiguration => {
const {
port,
} = options;
return {
port,
open: false,
historyApiFallback: true,
hot: true,
client: {
overlay: {
errors: true,
warnings: false,
runtimeErrors: true,
},
},
};
};

8
src/index.ts Normal file
View File

@ -0,0 +1,8 @@
export type {
WebpackEnv,
WebpackMode,
WebpackOptions,
WebpackPaths,
WebpackMainLoaders,
} from './types';
export { createConfig } from './config';

View File

@ -0,0 +1,23 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../../types';
export const getFontLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
loaders: {
fontLoader,
} = {},
} = options;
if (fontLoader === 'off') {
return null;
}
return {
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'static/fonts/[hash][ext][query]',
...fontLoader,
},
};
};

View File

@ -0,0 +1,23 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../../types';
export const getImageLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
loaders: {
imageLoader,
} = {},
} = options;
if (imageLoader === 'off') {
return null;
}
return {
test: /\.(png|jpg|jpeg|gif|avif|webp)$/i,
type: 'asset/resource',
generator: {
filename: 'static/images/[name].[contenthash:8][ext][query]',
...imageLoader,
},
};
};

View File

@ -0,0 +1,9 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../types';
import { getFontLoader } from './font-loader';
import { getImageLoader } from './image-loader';
export const getAssetsLoaders = (options: WebpackOptions): (webpack.RuleSetRule | null)[] => [
getImageLoader(options),
getFontLoader(options),
];

View File

@ -0,0 +1,26 @@
import path from 'path';
import type webpack from 'webpack';
import type { WebpackOptions } from '../../types';
export const getEsbuildLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
paths,
loaders: {
esbuildLoader,
} = {},
} = options;
if (esbuildLoader === 'off') {
return null;
}
return {
test: /\.[j|t]sx?$/,
loader: 'esbuild-loader',
options: {
loader: 'tsx',
...esbuildLoader,
},
exclude: path.resolve(paths.root, 'node_modules'),
};
};

30
src/loaders/index.ts Normal file
View File

@ -0,0 +1,30 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../types';
import { getAssetsLoaders } from './asset-loaders';
import { getStyleLoaders } from './style-loaders';
import { getEsbuildLoader } from './esbuild-loader';
import { getSvgLoader } from './svgr-loader';
import { getSwcLoader } from './swc-loader';
export const createLoaders = (options: WebpackOptions): (webpack.RuleSetRule | null)[] => {
const {
mainLoader,
} = options;
const getMainLoader = () => {
switch (mainLoader) {
case 'swc':
return getSwcLoader(options);
case 'esbuild':
default:
return getEsbuildLoader(options);
}
};
return [
getMainLoader(),
getSvgLoader(options),
...getStyleLoaders(options),
...getAssetsLoaders(options),
].filter(Boolean);
};

View File

@ -0,0 +1,67 @@
// todo fixme
/* eslint-disable @typescript-eslint/no-unsafe-assignment, no-unsafe-optional-chaining, @typescript-eslint/no-unsafe-member-access, no-nested-ternary */
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import type webpack from 'webpack';
import type { WebpackOptions } from '../../../types';
export const getCssLoader = (options: WebpackOptions): webpack.RuleSetRule => {
const {
mode,
loaders: {
styleLoader,
miniCssLoader,
cssLoader,
postCssLoader,
} = {},
} = options;
const isDev = mode === 'development';
return {
test: /\.css$/,
use: [
isDev
? styleLoader === 'off'
? null
: {
loader: 'style-loader',
options: styleLoader,
}
: miniCssLoader === 'off'
? null
: {
loader: MiniCssExtractPlugin.loader,
options: miniCssLoader,
},
cssLoader === 'off'
? null
: {
loader: 'css-loader',
options: {
sourceMap: isDev,
...cssLoader,
},
},
postCssLoader === 'off'
? null
: {
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
{
browsers: 'last 2 versions',
autoprefixer: true,
},
],
...postCssLoader?.postcssOptions?.plugins ?? [],
].filter(Boolean),
...postCssLoader?.postcssOptions,
},
...postCssLoader,
},
},
].filter(Boolean),
};
};

View File

@ -0,0 +1,9 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../types';
import { getCssLoader } from './css-loader';
import { getSassLoader } from './sass-loader';
export const getStyleLoaders = (options: WebpackOptions): (webpack.RuleSetRule | null)[] => [
getCssLoader(options),
getSassLoader(options),
];

View File

@ -0,0 +1,32 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../../types';
import { getCssLoader } from '../css-loader';
export const getSassLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
mode,
loaders: {
sassLoader,
} = {},
} = options;
const isDev = mode === 'development';
if (sassLoader === 'off') {
return null;
}
return {
test: /\.s[ac]ss$/i,
use: [
// @ts-ignore
...getCssLoader(options).use,
{
loader: 'sass-loader',
options: {
sourceMap: isDev,
...sassLoader,
},
},
],
};
};

View File

@ -0,0 +1,26 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../types';
export const getSvgLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
loaders: {
svgrLoader,
} = {},
} = options;
if (svgrLoader === 'off') {
return null;
}
return {
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: [{
loader: '@svgr/webpack',
options: {
icon: true,
...svgrLoader,
},
}],
};
};

View File

@ -0,0 +1,58 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../types';
export const getSwcLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
mode,
loaders: {
swcLoader,
} = {},
} = options;
const isDev = mode === 'development';
if (swcLoader === 'off') {
return null;
}
return {
test: /\.[j|t]sx?$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'swc-loader',
options: {
sync: true,
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
dynamicImport: true,
privateMethod: true,
functionBind: true,
exportDefaultFrom: true,
exportNamespaceFrom: true,
decorators: true,
decoratorsBeforeExport: true,
topLevelAwait: true,
importMeta: true,
...swcLoader?.jsc?.parser,
},
transform: {
react: {
runtime: 'automatic',
development: isDev,
refresh: isDev,
...swcLoader?.jsc?.transform?.react,
},
...swcLoader?.jsc?.transform,
},
target: 'es2015',
loose: false,
externalHelpers: false,
keepClassNames: false,
...swcLoader?.jsc,
},
...swcLoader,
},
},
};
};

26
src/optimization/index.ts Normal file
View File

@ -0,0 +1,26 @@
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import TerserPlugin from 'terser-webpack-plugin';
import type webpack from 'webpack';
import type { WebpackOptions } from '../types';
export const getOptimization = (options: WebpackOptions): webpack.Configuration['optimization'] => {
const {
mode,
} = options;
const isDev = mode === 'development';
return {
minimize: !isDev,
minimizer: [
new TerserPlugin(),
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'all',
name: false,
},
runtimeChunk: {
name: (entrypoint: { name: string }) => `runtime-${entrypoint.name}`,
},
};
};

29
src/output/index.ts Normal file
View File

@ -0,0 +1,29 @@
import path from 'path';
import type webpack from 'webpack';
import type { WebpackOptions } from '../types';
export const createOutput = (options: WebpackOptions): webpack.Configuration['output'] => {
const {
paths,
mode,
} = options;
const isDev = mode === 'development';
const devOutput: webpack.Configuration['output'] = {
filename: '[name][fullhash].js',
path: path.resolve(paths.build),
publicPath: '/',
};
const productionOutput: webpack.Configuration['output'] = {
filename: 'static/js/[name].[contenthash:8].js',
path: path.resolve(paths.build),
publicPath: '/',
chunkFilename: 'static/js/[name].[contenthash:8].chunk.js',
clean: true,
assetModuleFilename: 'static/assets/[hash][ext][query]',
};
return isDev ? devOutput : productionOutput;
};

View File

@ -0,0 +1,17 @@
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import type { WebpackOptions } from '../../types';
export const getBundleAnalyzerPlugin = (options: WebpackOptions) => {
const {
analyze,
plugins: {
analyzerPlugin,
} = {},
} = options;
if (!analyze || analyzerPlugin === 'off') {
return null;
}
return new BundleAnalyzerPlugin(analyzerPlugin);
};

View File

@ -0,0 +1,34 @@
import CopyPlugin from 'copy-webpack-plugin';
import path from 'path';
import type { WebpackOptions } from '../../types';
export const getCopyPlugin = (options: WebpackOptions) => {
const {
paths,
plugins: {
copyPlugin,
} = {},
} = options;
if (copyPlugin === 'off') {
return null;
}
return new CopyPlugin({
patterns: [
{
from: path.resolve(paths.root, paths.public),
globOptions: {
ignore: [
path.resolve(paths.root, paths.public, paths.html),
path.resolve(paths.root, paths.public, paths.assets),
],
},
to: path.resolve(paths.root, paths.build),
},
...copyPlugin?.patterns ?? [],
].filter(Boolean),
options: copyPlugin?.options,
});
};

View File

@ -0,0 +1,22 @@
import webpack from 'webpack';
import type { WebpackOptions } from '../../types';
export const getDefinePlugin = (options: WebpackOptions) => {
const {
mode,
plugins: {
definePlugin,
} = {},
} = options;
const isDev = mode === 'development';
if (definePlugin === 'off') {
return null;
}
return new webpack.DefinePlugin({
'process.env.IS_DEV': JSON.stringify(isDev),
'process.env.APP_VERSION': JSON.stringify(process.env.npm_package_version),
...definePlugin,
});
};

View File

@ -0,0 +1,45 @@
import ESLintPlugin from 'eslint-webpack-plugin';
import path from 'path';
import type { WebpackOptions } from '../../types';
export const getEslintPlugin = (options: WebpackOptions) => {
const {
paths,
plugins: {
eslintPlugin,
} = {},
} = options;
const hasEslintPackage = Boolean(process.env.npm_package_dependencies_eslint
?? process.env.npm_package_devDependencies_eslint);
if (!hasEslintPackage || eslintPlugin === 'off') {
return null;
}
const getExcludeArray = () => {
// todo fixme
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!eslintPlugin?.exclude) {
return [];
}
if (Array.isArray(eslintPlugin.exclude)) {
return eslintPlugin.exclude;
}
return [eslintPlugin.exclude];
};
return new ESLintPlugin({
extensions: ['.js', '.jsx', '.tsx', '.ts'],
failOnError: false,
lintDirtyModulesOnly: true,
exclude: [
path.resolve(paths.root, 'node_modules'),
paths.build,
...getExcludeArray(),
].filter(Boolean),
...eslintPlugin,
});
};

View File

@ -0,0 +1,16 @@
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import type { WebpackOptions } from '../../types';
export const getForkTsCheckerPlugin = (options: WebpackOptions) => {
const {
plugins: {
forkTsCheckerPlugin,
} = {},
} = options;
if (forkTsCheckerPlugin === 'off') {
return null;
}
return new ForkTsCheckerWebpackPlugin(forkTsCheckerPlugin);
};

View File

@ -0,0 +1,23 @@
import HTMLWebpackPlugin from 'html-webpack-plugin';
import path from 'path';
import type { WebpackOptions } from '../../types';
export const getHtmlPlugin = (options: WebpackOptions) => {
const {
paths,
plugins: {
htmlWebpackPlugin,
} = {},
} = options;
if (htmlWebpackPlugin === 'off') {
return null;
}
return new HTMLWebpackPlugin({
template: paths.html,
favicon: path.resolve(paths.assets, 'favicon.ico'),
title: 'My App',
...htmlWebpackPlugin,
});
};

View File

@ -0,0 +1,20 @@
import webpack from 'webpack';
import type { WebpackOptions } from '../../types';
export const getIgnorePlugin = (options: WebpackOptions) => {
const {
plugins: {
ignorePlugin,
} = {},
} = options;
if (ignorePlugin === 'off') {
return null;
}
return new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
...ignorePlugin,
});
};

43
src/plugins/index.ts Normal file
View File

@ -0,0 +1,43 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../types';
import { getBundleAnalyzerPlugin } from './bundle-analyzer-plugin';
import { getCopyPlugin } from './copy-plugin';
import { getDefinePlugin } from './define-plugin';
import { getEslintPlugin } from './eslint-plugin';
import { getForkTsCheckerPlugin } from './fork-ts-checker-plugin';
import { getHtmlPlugin } from './html-plugin';
import { getIgnorePlugin } from './ignore-plugin';
import { getMiniCssPlugin } from './mini-css-plugin';
import { getProgressPlugin } from './progress';
import { getReactRefreshPlugin } from './react-refresh-plugin';
export const createPlugins = (options: WebpackOptions): webpack.Configuration['plugins'] => {
const {
mode,
} = options;
const isDev = mode === 'development';
const developmentPlugins = [
getHtmlPlugin(options),
getProgressPlugin(options),
getDefinePlugin(options),
getForkTsCheckerPlugin(options),
getEslintPlugin(options),
getReactRefreshPlugin(options),
getBundleAnalyzerPlugin(options),
];
const productionPlugins = [
getHtmlPlugin(options),
getDefinePlugin(options),
getMiniCssPlugin(options),
getForkTsCheckerPlugin(options),
getCopyPlugin(options),
getIgnorePlugin(options),
getBundleAnalyzerPlugin(options),
];
const plugins = isDev ? developmentPlugins : productionPlugins;
return plugins.filter(Boolean);
};

View File

@ -0,0 +1,20 @@
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import type { WebpackOptions } from '../../types';
export const getMiniCssPlugin = (options: WebpackOptions) => {
const {
plugins: {
miniCssPlugin,
} = {},
} = options;
if (miniCssPlugin === 'off') {
return null;
}
return new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
...miniCssPlugin,
});
};

View File

@ -0,0 +1,16 @@
import webpack from 'webpack';
import type { WebpackOptions } from '../../types';
export const getProgressPlugin = (options: WebpackOptions) => {
const {
plugins: {
progressPlugin,
} = {},
} = options;
if (progressPlugin === 'off') {
return null;
}
return new webpack.ProgressPlugin(progressPlugin);
};

View File

@ -0,0 +1,19 @@
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import type { WebpackOptions } from '../../types';
export const getReactRefreshPlugin = (options: WebpackOptions) => {
const {
plugins: {
reactRefreshPlugin,
} = {},
} = options;
if (reactRefreshPlugin === 'off') {
return null;
}
return new ReactRefreshWebpackPlugin({
overlay: false,
...reactRefreshPlugin,
});
};

20
src/resolvers/index.ts Normal file
View File

@ -0,0 +1,20 @@
import path from 'path';
import type webpack from 'webpack';
import type { WebpackOptions } from '../types';
export const createResolvers = (options: WebpackOptions): webpack.ResolveOptions => {
const {
paths,
} = options;
return {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
mainFiles: ['index'],
modules: [
paths.src,
path.resolve(paths.root),
path.resolve(paths.root, 'node_modules'),
],
};
};

180
src/types/index.ts Normal file
View File

@ -0,0 +1,180 @@
import type { ReactRefreshPluginOptions } from '@pmmmwh/react-refresh-webpack-plugin/types/lib/types';
import type { Config as SvgrLoaderOptions } from '@svgr/core';
import type { Config as SwcLoaderOptions } from '@swc/core';
import type CopyPlugin from 'copy-webpack-plugin';
import type { LoaderOptions as EsbuildLoaderOptions } from 'esbuild-loader';
import type ESLintWebpackPlugin from 'eslint-webpack-plugin';
import type { ForkTsCheckerWebpackPluginOptions } from 'fork-ts-checker-webpack-plugin/lib/plugin-options';
import type HtmlWebpackPlugin from 'html-webpack-plugin';
import type MiniCssExtractPlugin from 'mini-css-extract-plugin';
import type { LoaderOptions as MiniCssLoaderOptions } from 'mini-css-extract-plugin';
import type { PostCSSLoaderOptions } from 'postcss-loader/dist/config';
import type webpack from 'webpack';
import type { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import type SassLoader from 'sass-loader';
type WebpackMode = 'development' | 'production';
type WebpackMainLoaders = 'esbuild' | 'swc';
type WebpackPaths = {
entry: string[] | string;
build: string;
root: string;
html: string;
src: string;
public: string;
assets: string;
};
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace AssetsLoader {
type Options = {
filename?: string;
};
}
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace StyleLoader {
type InjectType =
'autoStyleTag' | 'lazyAutoStyleTag' | 'lazySingletonStyleTag' | 'lazyStyleTag' | 'linkTag' | 'singletonStyleTag' | 'styleTag';
type Insert =
| string
| ((htmlElement: HTMLElement, options: Record<string, unknown>) => void);
type Attributes = Record<string, unknown>;
type StyleTagTransform =
| string
| ((
css: string,
styleElement: HTMLStyleElement,
options: Record<string, unknown>
) => void);
type Base = number;
type EsModule = boolean;
type Options = {
injectType?: InjectType;
attributes?: Attributes;
insert?: Insert;
styleTagTransform?: StyleTagTransform;
base?: Base;
esModule?: EsModule;
};
}
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace CssLoader {
type Url =
| boolean
| {
filter: (url: string, resourcePath: string) => boolean;
};
type ImportFn =
| boolean
| {
filter: (
url: string,
media: string,
resourcePath: string,
supports?: string,
layer?: string
) => boolean;
};
type Modules =
boolean | 'global' | 'icss' | 'local' | 'pure' | {
auto: RegExp | boolean | ((resourcePath: string) => boolean);
mode:
'global' | 'icss' | 'local' | 'pure' | ((resourcePath: string) => 'global' | 'icss' | 'local' | 'pure');
localIdentName: string;
localIdentContext: string;
localIdentHashSalt: string;
localIdentHashFunction: string;
localIdentHashDigest: string;
localIdentRegExp: RegExp | string;
getLocalIdent: (
// @ts-ignore
context: webpack.loader.LoaderContext,
localIdentName: string,
localName: string
) => string;
namedExport: boolean;
exportGlobals: boolean;
exportLocalsConvention:
| 'asIs'
| 'camelCase'
| 'camelCaseOnly'
| 'dashes'
| 'dashesOnly'
| ((name: string) => string);
exportOnlyLocals: boolean;
};
type SourceMap = boolean;
type ImportLoaders = number;
type EsModule = boolean;
type ExportType = 'array' | 'css-style-sheet' | 'string';
type Options = {
url?: Url;
import?: ImportFn;
modules?: Modules;
sourceMap?: SourceMap;
importLoaders?: ImportLoaders;
esModule?: EsModule;
exportType?: ExportType;
};
}
type WebpackOptions = {
mode: WebpackMode;
paths: WebpackPaths;
port: number;
mainLoader: WebpackMainLoaders;
analyze: boolean;
devtool?: webpack.Configuration['devtool'];
plugins?: {
htmlWebpackPlugin?: HtmlWebpackPlugin.Options | 'off';
progressPlugin?: ConstructorParameters<typeof webpack.ProgressPlugin>[0] | 'off';
definePlugin?: ConstructorParameters<typeof webpack.DefinePlugin>[0] | 'off';
forkTsCheckerPlugin?: ForkTsCheckerWebpackPluginOptions | 'off';
eslintPlugin?: ESLintWebpackPlugin.Options | 'off';
reactRefreshPlugin?: ReactRefreshPluginOptions | 'off';
miniCssPlugin?: MiniCssExtractPlugin.PluginOptions | 'off';
copyPlugin?: CopyPlugin.PluginOptions | 'off';
ignorePlugin?: ConstructorParameters<typeof webpack.IgnorePlugin>[0] | 'off';
analyzerPlugin?: BundleAnalyzerPlugin.Options | 'off';
};
loaders?: {
esbuildLoader?: EsbuildLoaderOptions | 'off';
swcLoader?: SwcLoaderOptions | 'off';
svgrLoader?: SvgrLoaderOptions | 'off';
styleLoader?: StyleLoader.Options | 'off';
cssLoader?: CssLoader.Options | 'off';
miniCssLoader?: MiniCssLoaderOptions | 'off';
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
postCssLoader?: PostCSSLoaderOptions | 'off';
sassLoader?: SassLoader.Options | 'off';
imageLoader?: AssetsLoader.Options | 'off';
fontLoader?: AssetsLoader.Options | 'off';
};
};
type WebpackEnv = {
mode?: WebpackMode;
port?: number;
mainLoader?: WebpackMainLoaders;
analyze?: boolean;
};
export type {
WebpackMode,
WebpackPaths,
WebpackOptions,
WebpackEnv,
WebpackMainLoaders,
};

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
"esModuleInterop": true,
"isolatedModules": true,
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
"lib": [
"DOM"
]
},
"exclude": ["node_modules"]
}

7569
yarn.lock

File diff suppressed because it is too large Load Diff