test #1

Merged
ksv741 merged 1 commits from test into main 2024-03-21 06:38:42 +03:00
45 changed files with 2340 additions and 1452 deletions

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

View File

@ -1,5 +1,6 @@
{
"extends": [
"ksv741"
]
"extends": "ksv741",
"rules": {
"@typescript-eslint/strict-boolean-expressions": "off"
}
}

View File

@ -13,10 +13,12 @@
- [`Режим запуска`](#режим-запуска)
- [`Конфигурация путей`](#конфигурация-путей)
- [`Порт`](#порт)
- [`Загрузчик`](#загрузчик)
- [`Загрузчики`](#загрузчики)
- [`Анализ`](#анализ)
- [`Плагины`](#плагины)
- [`Лоадеры`](#лоадеры)
- [`Resolvers`](#resolvers)
- [`Кэширование`](#кэширование)
- [`Расширенная конфигурация`](#расширенная-конфигурация)
***
@ -127,7 +129,8 @@ root.render(
};
port?: number;
devtool?: string | false;
mainLoader?: 'esbuild' | 'swc';
mainLoader?: 'esbuild' | 'swc' | 'babel' | 'ts-loader';
svgLoader?: 'inline' | 'svgr';
analyze?: boolean;
plugins?: {
htmlWebpackPlugin?: object | 'off';
@ -142,16 +145,19 @@ root.render(
analyzerPlugin?: object | 'off';
}
loaders?: {
esbuildLoader?: object | 'off';
swcLoader?: object | 'off';
svgrLoader?: object | 'off';
styleLoader?: object | 'off';
babelLoader?: object | 'off';
cssLoader?: object | 'off';
postCssLoader?: object | 'off';
miniCssLoader?: object | 'off';
sassLoader?: object | 'off';
imageLoader?: object | 'off';
esbuildLoader?: object | 'off';
fontLoader?: object | 'off';
imageLoader?: object | 'off';
inlineSvgLoader?: object | 'off';
miniCssLoader?: object | 'off';
postCssLoader?: object | 'off';
sassLoader?: object | 'off';
styleLoader?: object | 'off';
svgrLoader?: object | 'off';
swcLoader?: object | 'off';
tsLoader?: object | 'off';
};
}
```
@ -186,9 +192,11 @@ root.render(
- `port` - порт, в котором будет запускаться dev server, для `mode = 'production'` настройка игнорируется
По умолчанию: `3000`
#### Загрузчик
- `mainLoader` - главный загрузчик javascript/typescript файлов, доступны `esbuild` и `swc`
#### Загрузчики
- `mainLoader` - главный загрузчик javascript/typescript файлов, доступны `esbuild`, `swc`, `babel`, `ts-loader`
По умолчанию: `esbuild`
- `svgLoader` - загрузчик `svg` файлов, доступны `inline` - загрузка файла строкой, `svgr` - загрузка файла как React компонент.
По умолчанию: `svgr`
#### Анализ
- `analyze` - запуск сервера на порту `8888`, для анализа сборки. Рекомендуется запускать совместно с `mode = 'production'`. Но допустим запуск и в режиме `development`
@ -285,9 +293,12 @@ root.render(
- `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'`](#загрузчик)
- [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'`](#загрузчики)
- [babel-loader](https://github.com/babel/babel-loader) - лоадер для обработки `.tsx`, `.jsx`, `.ts`, `.js` файлов, при указании настройки [`mainLoader: 'babel'`](#загрузчики)
- [ts-loader](https://github.com/TypeStrong/ts-loader) - лоадер для обработки `.tsx`, `.jsx`, `.ts`, `.js` файлов, при указании настройки [`mainLoader: 'ts-loader'`](#загрузчики)
- [@svgr/webpack](https://github.com/gregberge/svgr/tree/main/packages/webpack)- лоадер для обработки `.svg` файлов, для работы с файлами, как с React компонентами
- [svg-inline-loader](https://webpack.js.org/guides/asset-modules/)- лоадер для загрузки `.svg` файлов, для строки
- [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` файлов
@ -304,7 +315,7 @@ root.render(
loader: 'tsx',
}
```
- `swcLoader` - настройкаи для [swc-loader](https://swc.rs/docs/configuration/swcrc)
- `swcLoader` - настройки для [swc-loader](https://swc.rs/docs/configuration/swcrc)
По умолчанию:
```js
{
@ -337,6 +348,18 @@ root.render(
},
},
```
- `babelLoader` - настройки для [babel-loader](https://github.com/babel/babel-loader?tab=readme-ov-file#options)
По умолчанию:
```js
{
presets: [
['@babel/preset-env', { targets: 'defaults' }],
['@babel/preset-react', { runtime: 'automatic' }],
['@babel/preset-typescript'],
],
}
```
- `tsLoader` - настройки для [ts-loader](https://github.com/TypeStrong/ts-loader?tab=readme-ov-file#loader-options)
- `svgrLoader` - настройка для [@svgr/webpack](https://react-svgr.com/docs/webpack/#options)
По умолчанию:
```js
@ -344,6 +367,7 @@ root.render(
icon: true,
}
```
- `inlineSvgLoader` - настройка для опции `generator` [Assets Modules](https://webpack.js.org/guides/asset-modules/)
- `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)
@ -392,6 +416,37 @@ root.render(
}
```
### Resolvers
- `resolver` - объект с настройками resolvers
Список используемых плагинов:
- [TsconfigPathsPlugin](https://github.com/dividab/tsconfig-paths-webpack-plugin)
Для настройки плагина необходимо передать объект с конфигурацией или строку `off` для того чтобы отключить плагин:
- `tsConfigPathsPlugin` - настройки плагина [TsconfigPathsPlugin](https://github.com/dividab/tsconfig-paths-webpack-plugin?tab=readme-ov-file#options)
По умолчанию:
```javascript
resolvers: {
plugins: {
tsConfigPathsPlugin: {
baseUrl: process.cwd()
}
}
}
```
### Кэширование
- `cache` - объект с настройками кэширования
По умолчанию:
```javascript
{
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
```
### Расширенная конфигурация
Если текущей кастомизации не достаточно или Вы хотите добавить/изменить какой-то плагин или загрузчик,
то это можно легко сделать, т.к. функция `createConfig` возвращает обычный объект с конфигурацией webpack, который можно изменить.

10
global.d.ts vendored
View File

@ -22,6 +22,16 @@ declare module '*.scss' {
export = content;
}
declare module '*.css' {
const content: Record<string, string>;
export = content;
}
declare module '*.sass' {
const content: Record<string, string>;
export = content;
}
declare module '*.svg' {
import type React from 'react';

View File

@ -6,7 +6,8 @@
"types": "./dist/index.d.ts",
"source": "./src/webpack/index.ts",
"scripts": {
"build": "rm -rf ./dist && tsc",
"prepare": "rm -rf dist && tsc && tsc-alias",
"typecheck": "tsc --noEmit",
"lint": "eslint --ignore-pattern node_modules --color src"
},
"bin": {
@ -32,6 +33,10 @@
"author": "Sergey <ksv741@gmail.com> Krylov",
"license": "ISC",
"dependencies": {
"@babel/core": "7.24.0",
"@babel/preset-env": "7.24.0",
"@babel/preset-react": "7.23.3",
"@babel/preset-typescript": "7.23.3",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
"@svgr/webpack": "8.1.0",
"@swc/core": "1.4.0",
@ -42,6 +47,7 @@
"@types/sass-loader": "8.0.8",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-dev-server": "4.7.2",
"babel-loader": "9.1.3",
"copy-webpack-plugin": "12.0.2",
"cross-spawn": "7.0.3",
"css-loader": "6.10.0",
@ -60,7 +66,10 @@
"style-loader": "3.3.4",
"swc-loader": "0.2.6",
"terser-webpack-plugin": "5.3.10",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",
"tsc-alias": "1.8.8",
"tsconfig-paths-webpack-plugin": "4.1.0",
"typescript": "5.3.3",
"webpack": "5.90.1",
"webpack-bundle-analyzer": "4.10.1",
@ -68,7 +77,19 @@
"webpack-dev-server": "5.0.0"
},
"devDependencies": {
"eslint": "8.56.0",
"eslint-config-ksv741": "git+ssh://git@github.com:ksv741/eslint-config-ksv741.git"
"eslint": "8.57.0",
"eslint-config-ksv741": "0.0.1"
},
"peerDependencies": {
"eslint": ">8.0.0",
"typescript": "^3.6.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
},
"eslint": {
"optional": true
}
}
}

25
src/cache/index.ts vendored Normal file
View File

@ -0,0 +1,25 @@
import type { WebpackOptions } from 'types';
import type webpack from 'webpack';
const getCache = (options: WebpackOptions): webpack.Configuration['cache'] => {
const { cache = {} } = options;
if (cache === 'off') {
// eslint-disable-next-line no-undefined
return undefined;
}
return {
type: 'filesystem',
...cache,
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
buildDependencies: {
config: [__filename],
// @ts-ignore
...cache.buildDependencies,
},
};
};
export default getCache;

View File

@ -1,24 +1,42 @@
import getCache from 'cache';
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';
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 getDefaultEntryPath = (rootDir: string, some?: string[] | string): string[] | string => {
if (Array.isArray(some)) {
// @ts-ignore
return some.map((entry) => getDefaultEntryPath(rootDir, entry));
}
if (some && path.isAbsolute(some)) {
return some;
}
const definedFile = some && path.resolve(rootDir, some);
const indexTsx = path.resolve(rootDir, 'index.tsx');
const indexTs = path.resolve(rootDir, 'index.ts');
const indexJsx = path.resolve(rootDir, 'index.jsx');
const indexJs = path.resolve(rootDir, 'index.js');
switch (true) {
case definedFile && fs.existsSync(definedFile):
return definedFile;
case fs.existsSync(indexTsx):
return indexTsx;
case fs.existsSync(indexTs):
return indexTs;
case fs.existsSync(indexJsx):
return indexJsx;
@ -26,18 +44,53 @@ const getDefaultEntryPath = (rootDir: string) => {
return indexJs;
default:
throw new Error(`Has no index.tsx/index.jsx/index.js file in ${rootDir} directory`);
throw new Error(`Has no ${definedFile ?? 'index.tsx/index.ts/index.jsx/index.js'} file in ${rootDir} directory`);
}
};
const getDefaultPath = ({ root, defaultDirectory, currentPath }: Record<string, string | undefined>) => {
if (!currentPath) {
// @ts-ignore
return path.resolve(root, defaultDirectory);
}
if (path.isAbsolute(currentPath)) {
return currentPath;
}
// @ts-ignore
return path.resolve(root, currentPath);
};
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');
const publicPath = getDefaultPath({
root,
defaultDirectory: 'public',
currentPath: initialPaths?.public,
});
const src = getDefaultPath({
root,
defaultDirectory: 'src',
currentPath: initialPaths?.src,
});
const assets = getDefaultPath({
root: publicPath,
defaultDirectory: 'assets',
currentPath: initialPaths?.assets,
});
const build = getDefaultPath({
root,
defaultDirectory: 'build',
currentPath: initialPaths?.build,
});
const html = getDefaultPath({
root: publicPath,
defaultDirectory: 'index.html',
currentPath: initialPaths?.html,
});
const entry = getDefaultEntryPath(src, initialPaths?.entry);
return {
root,
@ -60,10 +113,12 @@ const createConfig = (params?: WebpackOptions): webpack.Configuration => {
const {
mode = getDefaultMode(),
analyze = false,
mainLoader = 'esbuild',
mainLoader,
svgLoader,
port = 3000,
devtool = 'inline-source-map',
plugins,
loaders,
} = params ?? {};
const {
assets,
@ -75,14 +130,11 @@ const createConfig = (params?: WebpackOptions): webpack.Configuration => {
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,
svgLoader,
port,
paths: {
assets,
@ -94,11 +146,13 @@ const createConfig = (params?: WebpackOptions): webpack.Configuration => {
public: publicPath,
},
plugins,
loaders,
};
const isDev = mode === 'development';
return {
context: process.cwd(),
mode,
entry,
output: createOutput(options),
@ -111,7 +165,8 @@ const createConfig = (params?: WebpackOptions): webpack.Configuration => {
devServer: createDevServer(options),
// eslint-disable-next-line no-undefined
optimization: !isDev ? getOptimization(options) : undefined,
cache: getCache(options),
};
};
export { createConfig };
export default createConfig;

View File

@ -1,7 +1,7 @@
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
import type { WebpackOptions } from '../types';
import type { WebpackOptions } from 'types';
export const createDevServer = (options: WebpackOptions): DevServerConfiguration => {
const createDevServer = (options: WebpackOptions): DevServerConfiguration => {
const {
port,
} = options;
@ -20,3 +20,5 @@ export const createDevServer = (options: WebpackOptions): DevServerConfiguration
},
};
};
export default createDevServer;

View File

@ -5,4 +5,4 @@ export type {
WebpackPaths,
WebpackMainLoaders,
} from './types';
export { createConfig } from './config';
export { default as createConfig } from './config';

View File

@ -1,7 +1,7 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../../types';
import type { WebpackOptions } from 'types';
export const getFontLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const getFontLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
loaders: {
fontLoader,
@ -21,3 +21,5 @@ export const getFontLoader = (options: WebpackOptions): webpack.RuleSetRule | nu
},
};
};
export default getFontLoader;

View File

@ -1,7 +1,7 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../../types';
import type { WebpackOptions } from 'types';
export const getImageLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const getImageLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
loaders: {
imageLoader,
@ -21,3 +21,5 @@ export const getImageLoader = (options: WebpackOptions): webpack.RuleSetRule | n
},
};
};
export default getImageLoader;

View File

@ -1,7 +1,7 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../types';
import { getFontLoader } from './font-loader';
import { getImageLoader } from './image-loader';
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),

View File

@ -1,30 +1,15 @@
import type { WebpackOptions } from 'types';
import type webpack from 'webpack';
import type { WebpackOptions } from '../types';
import { getAssetsLoaders } from './asset-loaders';
import getMainLoader from './main-loaders';
import { getStyleLoaders } from './style-loaders';
import { getEsbuildLoader } from './esbuild-loader';
import { getSvgLoader } from './svgr-loader';
import { getSwcLoader } from './swc-loader';
import getSvgLoader from './svg-loaders';
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(),
const createLoaders = (options: WebpackOptions): (webpack.RuleSetRule | null)[] => [
getMainLoader(options),
getSvgLoader(options),
...getStyleLoaders(options),
...getAssetsLoaders(options),
].filter(Boolean);
};
].filter(Boolean);
export default createLoaders;

View File

@ -0,0 +1,31 @@
import path from 'path';
import type webpack from 'webpack';
import type { WebpackOptions } from 'types';
const getBabelLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
loaders: {
babelLoader,
} = {},
} = options;
if (babelLoader === 'off') {
return null;
}
return {
test: /\.[j|t]sx?$/,
loader: 'babel-loader',
options: {
...babelLoader,
presets: [
['@babel/preset-env', { targets: 'defaults', ...babelLoader?.presets?.env }],
['@babel/preset-react', { runtime: 'automatic', ...babelLoader?.presets?.react }],
['@babel/preset-typescript', { ...babelLoader?.presets?.typescript }],
],
},
exclude: path.resolve('node_modules'),
};
};
export default getBabelLoader;

View File

@ -1,8 +1,8 @@
import path from 'path';
import type webpack from 'webpack';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
export const getEsbuildLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const getEsbuildLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
paths,
loaders: {
@ -19,8 +19,11 @@ export const getEsbuildLoader = (options: WebpackOptions): webpack.RuleSetRule |
loader: 'esbuild-loader',
options: {
loader: 'tsx',
jsx: 'automatic',
...esbuildLoader,
},
exclude: path.resolve(paths.root, 'node_modules'),
};
};
export default getEsbuildLoader;

View File

@ -0,0 +1,25 @@
import type { WebpackOptions } from 'types';
import getTsLoader from './ts-loader';
import getBabelLoader from './babel-loader';
import getSwcLoader from './swc-loader';
import getEsbuildLoader from './esbuild-loader';
const getMainLoader = (options: WebpackOptions) => {
const {
mainLoader = 'esbuild',
} = options;
switch (mainLoader) {
case 'babel':
return getBabelLoader(options);
case 'ts-loader':
return getTsLoader(options);
case 'swc':
return getSwcLoader(options);
case 'esbuild':
default:
return getEsbuildLoader(options);
}
};
export default getMainLoader;

View File

@ -0,0 +1,60 @@
import type webpack from 'webpack';
import type { WebpackOptions } from 'types';
const getSwcLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
mode,
loaders: {
swcLoader,
} = {},
plugins: {
reactRefreshPlugin,
} = {},
} = options;
const isDev = mode === 'development';
if (swcLoader === 'off') {
return null;
}
return {
test: /\.[j|t]sx?$/,
exclude: /(node_modules|bower_components)/,
loader: 'swc-loader',
options: {
...swcLoader,
sync: true,
jsc: {
loose: false,
externalHelpers: false,
keepClassNames: false,
...swcLoader?.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: {
...swcLoader?.jsc?.transform,
react: {
runtime: 'automatic',
development: isDev,
refresh: isDev && reactRefreshPlugin !== 'off',
...swcLoader?.jsc?.transform?.react,
},
},
},
},
};
};
export default getSwcLoader;

View File

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

View File

@ -2,9 +2,9 @@
/* 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';
import type { WebpackOptions } from 'types';
export const getCssLoader = (options: WebpackOptions): webpack.RuleSetRule => {
const getCssLoader = (options: WebpackOptions): webpack.RuleSetRule => {
const {
mode,
loaders: {
@ -39,6 +39,11 @@ export const getCssLoader = (options: WebpackOptions): webpack.RuleSetRule => {
options: {
sourceMap: isDev,
...cssLoader,
modules: {
localIdentName: '[local]--[hash:5]',
// @ts-ignore
...cssLoader?.modules,
},
},
},
postCssLoader === 'off'
@ -46,7 +51,9 @@ export const getCssLoader = (options: WebpackOptions): webpack.RuleSetRule => {
: {
loader: 'postcss-loader',
options: {
...postCssLoader,
postcssOptions: {
...postCssLoader?.postcssOptions,
plugins: [
[
'postcss-preset-env',
@ -57,11 +64,12 @@ export const getCssLoader = (options: WebpackOptions): webpack.RuleSetRule => {
],
...postCssLoader?.postcssOptions?.plugins ?? [],
].filter(Boolean),
...postCssLoader?.postcssOptions,
},
...postCssLoader,
sourceMap: isDev,
},
},
].filter(Boolean),
};
};
export default getCssLoader;

View File

@ -1,7 +1,7 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../types';
import { getCssLoader } from './css-loader';
import { getSassLoader } from './sass-loader';
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),

View File

@ -1,8 +1,8 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../../types';
import { getCssLoader } from '../css-loader';
import type { WebpackOptions } from 'types';
import getCssLoader from '../css-loader';
export const getSassLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const getSassLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
mode,
loaders: {
@ -30,3 +30,5 @@ export const getSassLoader = (options: WebpackOptions): webpack.RuleSetRule | nu
],
};
};
export default getSassLoader;

View File

@ -0,0 +1,20 @@
import getInlineSvgLoader from 'loaders/svg-loaders/inline-svg-loader';
import getSvgrLoader from 'loaders/svg-loaders/svgr-loader';
import type { WebpackOptions } from 'types';
const getSvgLoader = (options: WebpackOptions) => {
const {
svgLoader = 'svgr',
} = options;
switch (svgLoader) {
case 'inline':
return getInlineSvgLoader(options);
case 'svgr':
default:
return getSvgrLoader(options);
}
};
export default getSvgLoader;

View File

@ -0,0 +1,24 @@
import type { WebpackOptions } from 'types';
const getInlineSvgLoader = (options: WebpackOptions) => {
const {
loaders: {
inlineSvgLoader,
} = {},
} = options;
if (inlineSvgLoader === 'off') {
return null;
}
return {
test: /\.svg$/i,
type: 'asset/source',
generator: {
filename: 'static/icons/[hash][ext][query]',
...inlineSvgLoader,
},
};
};
export default getInlineSvgLoader;

View File

@ -1,7 +1,7 @@
import type webpack from 'webpack';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
export const getSvgLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const getSvgrLoader = (options: WebpackOptions): webpack.RuleSetRule | null => {
const {
loaders: {
svgrLoader,
@ -24,3 +24,5 @@ export const getSvgLoader = (options: WebpackOptions): webpack.RuleSetRule | nul
}],
};
};
export default getSvgrLoader;

View File

@ -1,58 +0,0 @@
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,
},
},
};
};

View File

@ -1,9 +1,9 @@
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import TerserPlugin from 'terser-webpack-plugin';
import type webpack from 'webpack';
import type { WebpackOptions } from '../types';
import type { WebpackOptions } from 'types';
export const getOptimization = (options: WebpackOptions): webpack.Configuration['optimization'] => {
const getOptimization = (options: WebpackOptions): webpack.Configuration['optimization'] => {
const {
mode,
} = options;
@ -24,3 +24,5 @@ export const getOptimization = (options: WebpackOptions): webpack.Configuration[
},
};
};
export default getOptimization;

View File

@ -1,8 +1,8 @@
import path from 'path';
import type webpack from 'webpack';
import type { WebpackOptions } from '../types';
import type { WebpackOptions } from 'types';
export const createOutput = (options: WebpackOptions): webpack.Configuration['output'] => {
const createOutput = (options: WebpackOptions): webpack.Configuration['output'] => {
const {
paths,
mode,
@ -27,3 +27,5 @@ export const createOutput = (options: WebpackOptions): webpack.Configuration['ou
return isDev ? devOutput : productionOutput;
};
export default createOutput;

View File

@ -1,7 +1,7 @@
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
export const getBundleAnalyzerPlugin = (options: WebpackOptions) => {
const getBundleAnalyzerPlugin = (options: WebpackOptions) => {
const {
analyze,
plugins: {
@ -15,3 +15,5 @@ export const getBundleAnalyzerPlugin = (options: WebpackOptions) => {
return new BundleAnalyzerPlugin(analyzerPlugin);
};
export default getBundleAnalyzerPlugin;

View File

@ -1,8 +1,8 @@
import CopyPlugin from 'copy-webpack-plugin';
import path from 'path';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
export const getCopyPlugin = (options: WebpackOptions) => {
const getCopyPlugin = (options: WebpackOptions) => {
const {
paths,
plugins: {
@ -25,10 +25,12 @@ export const getCopyPlugin = (options: WebpackOptions) => {
],
},
to: path.resolve(paths.root, paths.build),
noErrorOnMissing: true,
},
...copyPlugin?.patterns ?? [],
].filter(Boolean),
options: copyPlugin?.options,
});
};
export default getCopyPlugin;

View File

@ -1,7 +1,7 @@
import webpack from 'webpack';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
export const getDefinePlugin = (options: WebpackOptions) => {
const getDefinePlugin = (options: WebpackOptions) => {
const {
mode,
plugins: {
@ -20,3 +20,5 @@ export const getDefinePlugin = (options: WebpackOptions) => {
...definePlugin,
});
};
export default getDefinePlugin;

View File

@ -1,8 +1,9 @@
import ESLintPlugin from 'eslint-webpack-plugin';
import path from 'path';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
import { hasPackage } from 'utils/helpers';
export const getEslintPlugin = (options: WebpackOptions) => {
const getEslintPlugin = (options: WebpackOptions) => {
const {
paths,
plugins: {
@ -10,36 +11,21 @@ export const getEslintPlugin = (options: WebpackOptions) => {
} = {},
} = options;
const hasEslintPackage = Boolean(process.env.npm_package_dependencies_eslint
?? process.env.npm_package_devDependencies_eslint);
if (!hasEslintPackage || eslintPlugin === 'off') {
if (!hasPackage('eslint') || 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,
...eslintPlugin,
exclude: [
path.resolve(paths.root, 'node_modules'),
paths.build,
...getExcludeArray(),
...eslintPlugin?.exclude ?? [],
].filter(Boolean),
...eslintPlugin,
});
};
export default getEslintPlugin;

View File

@ -1,16 +1,24 @@
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
import { hasPackage } from 'utils/helpers';
export const getForkTsCheckerPlugin = (options: WebpackOptions) => {
const getForkTsCheckerPlugin = (options: WebpackOptions) => {
const {
plugins: {
forkTsCheckerPlugin,
} = {},
} = options;
if (forkTsCheckerPlugin === 'off') {
if (forkTsCheckerPlugin === 'off' || !hasPackage('typescript')) {
return null;
}
return new ForkTsCheckerWebpackPlugin(forkTsCheckerPlugin);
return new ForkTsCheckerWebpackPlugin({
...forkTsCheckerPlugin,
typescript: {
...forkTsCheckerPlugin?.typescript,
},
});
};
export default getForkTsCheckerPlugin;

View File

@ -1,8 +1,9 @@
import fs from 'fs';
import HTMLWebpackPlugin from 'html-webpack-plugin';
import path from 'path';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
export const getHtmlPlugin = (options: WebpackOptions) => {
const getHtmlPlugin = (options: WebpackOptions) => {
const {
paths,
plugins: {
@ -14,10 +15,18 @@ export const getHtmlPlugin = (options: WebpackOptions) => {
return null;
}
if (!fs.existsSync(paths.html)) {
throw new Error(`index.html file not found. Define path to index.html file or create one in "${paths.public}" directory`);
}
const isExistFavicon = fs.existsSync(path.resolve(paths.assets, 'favicon.ico'));
return new HTMLWebpackPlugin({
template: paths.html,
favicon: path.resolve(paths.assets, 'favicon.ico'),
favicon: isExistFavicon && path.resolve(paths.assets, 'favicon.ico'),
title: 'My App',
...htmlWebpackPlugin,
});
};
export default getHtmlPlugin;

View File

@ -1,7 +1,7 @@
import webpack from 'webpack';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
export const getIgnorePlugin = (options: WebpackOptions) => {
const getIgnorePlugin = (options: WebpackOptions) => {
const {
plugins: {
ignorePlugin,
@ -18,3 +18,5 @@ export const getIgnorePlugin = (options: WebpackOptions) => {
...ignorePlugin,
});
};
export default getIgnorePlugin;

View File

@ -1,17 +1,17 @@
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';
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 createPlugins = (options: WebpackOptions): webpack.Configuration['plugins'] => {
const {
mode,
} = options;
@ -39,5 +39,8 @@ export const createPlugins = (options: WebpackOptions): webpack.Configuration['p
const plugins = isDev ? developmentPlugins : productionPlugins;
// @ts-ignore
return plugins.filter(Boolean);
};
export default createPlugins;

View File

@ -1,7 +1,7 @@
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
export const getMiniCssPlugin = (options: WebpackOptions) => {
const getMiniCssPlugin = (options: WebpackOptions) => {
const {
plugins: {
miniCssPlugin,
@ -18,3 +18,5 @@ export const getMiniCssPlugin = (options: WebpackOptions) => {
...miniCssPlugin,
});
};
export default getMiniCssPlugin;

View File

@ -1,7 +1,7 @@
import webpack from 'webpack';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
export const getProgressPlugin = (options: WebpackOptions) => {
const getProgressPlugin = (options: WebpackOptions) => {
const {
plugins: {
progressPlugin,
@ -14,3 +14,5 @@ export const getProgressPlugin = (options: WebpackOptions) => {
return new webpack.ProgressPlugin(progressPlugin);
};
export default getProgressPlugin;

View File

@ -1,7 +1,7 @@
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import type { WebpackOptions } from '../../types';
import type { WebpackOptions } from 'types';
export const getReactRefreshPlugin = (options: WebpackOptions) => {
const getReactRefreshPlugin = (options: WebpackOptions) => {
const {
plugins: {
reactRefreshPlugin,
@ -17,3 +17,5 @@ export const getReactRefreshPlugin = (options: WebpackOptions) => {
...reactRefreshPlugin,
});
};
export default getReactRefreshPlugin;

View File

@ -1,9 +1,10 @@
import path from 'path';
import type webpack from 'webpack';
import type { WebpackOptions } from '../types';
import type { WebpackOptions } from 'types';
import getResolversPlugins from './plugins';
export const createResolvers = (options: WebpackOptions): webpack.ResolveOptions => {
const createResolvers = (options: WebpackOptions): webpack.ResolveOptions => {
const {
paths,
} = options;
@ -16,5 +17,8 @@ export const createResolvers = (options: WebpackOptions): webpack.ResolveOptions
path.resolve(paths.root),
path.resolve(paths.root, 'node_modules'),
],
plugins: getResolversPlugins(options),
};
};
export default createResolvers;

View File

@ -0,0 +1,9 @@
import type webpack from 'webpack';
import type { WebpackOptions } from 'types';
import getTsConfigPathsPlugin from './tsconfig-paths-webpack-plugin';
const getResolversPlugins = (options: WebpackOptions): webpack.ResolveOptions['plugins'] => [
getTsConfigPathsPlugin(options),
].filter(Boolean);
export default getResolversPlugins;

View File

@ -0,0 +1,25 @@
import process from 'process';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import type { WebpackOptions } from 'types';
import { hasPackage } from 'utils/helpers';
const getTsConfigPathsPlugin = (options: WebpackOptions) => {
const {
resolvers: {
plugins: {
tsConfigPathsPlugin = {},
} = {},
} = {},
} = options;
if (tsConfigPathsPlugin === 'off' || !hasPackage('typescript')) {
return null;
}
return new TsconfigPathsPlugin({
baseUrl: process.cwd(),
...tsConfigPathsPlugin,
});
};
export default getTsConfigPathsPlugin;

View File

@ -12,10 +12,17 @@ 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';
import type { Options as TsconfigPathsPluginOptions } from 'tsconfig-paths-webpack-plugin/lib/options';
import type { Options as TsLoaderOptions } from 'ts-loader';
type PackageJson = {
dependencies: Record<string, string>;
devDependencies: Record<string, string>;
};
type WebpackMode = 'development' | 'production';
type WebpackMainLoaders = 'esbuild' | 'swc';
type WebpackMainLoaders = 'babel' | 'esbuild' | 'swc' | 'ts-loader';
type WebpackPaths = {
entry: string[] | string;
@ -130,11 +137,89 @@ declare namespace CssLoader {
};
}
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace BabelLoader {
type CallerData = {
name: string;
supportsStaticESM?: boolean;
supportsDynamicImport?: boolean;
supportsTopLevelAwait?: boolean;
supportsExportNamespaceFrom?: boolean;
};
type MatchPatternFunc = (
filename: string,
context: { caller?: { name: string }; envName: string; dirname: string }
) => boolean;
type MatchPattern = MatchPatternFunc | RegExp | string;
type EntryTarget = () => Record<string, unknown> | string ;
type EntryOptions = Record<string, unknown> | false | undefined;
type PluginEntry = EntryTarget | [EntryTarget, EntryOptions, string] | [EntryTarget, EntryOptions];
type PresetEntry = EntryTarget | [EntryTarget, EntryOptions, string] | [EntryTarget, EntryOptions];
type Options = {
cwd?: string;
caller?: CallerData;
filename?: string;
filenameRelative?: string;
code?: boolean;
ast?: boolean;
cloneInputAst?: boolean;
root?: string;
rootMode?: 'root' | 'upward-optional' | 'upward';
envName?: string;
configFile?: boolean | string;
babelrc?: boolean;
babelrcRoots?: MatchPattern | MatchPattern[] | boolean;
plugins?: PluginEntry[];
presets?: {
env: EntryOptions;
react: EntryOptions;
typescript: EntryOptions;
};
passPerPreset?: boolean;
targets?: Record<string, string> | string[] | string;
browserslistConfigFile?: boolean;
browserslistEnv?: string;
extends?: string;
env?: Record<string, unknown>;
overrides?: unknown[];
test?: MatchPattern | MatchPattern[];
include?: MatchPattern | MatchPattern[];
exclude?: MatchPattern | MatchPattern[];
ignore?: MatchPattern[];
only?: MatchPattern[];
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
inputSourceMap?: webpack.Configuration['devtool'] | boolean; // todo
sourceMaps?: boolean | 'both' | 'inline';
sourceFileName?: string;
sourceRoot?: string;
sourceType?: 'module' | 'script' | 'unambiguous';
assumptions?: Record<string, boolean>;
highlightCode?: boolean;
wrapPluginVisitorMethod?: unknown;
parserOpts?: Record<string, unknown>;
generatorOpts?: Record<string, unknown>;
retainLines?: boolean;
compact?: boolean | 'auto';
minified?: boolean;
auxiliaryCommentBefore?: string;
auxiliaryCommentAfter?: string;
comments?: boolean;
shouldPrintComment?: (value: string) => boolean;
moduleIds?: boolean;
moduleId?: string;
getModuleId?: (name: string) => string;
moduleRoot?: string;
};
}
type WebpackOptions = {
mode: WebpackMode;
paths: WebpackPaths;
port: number;
mainLoader: WebpackMainLoaders;
mainLoader?: WebpackMainLoaders;
svgLoader?: 'inline' | 'svgr';
analyze: boolean;
devtool?: webpack.Configuration['devtool'];
plugins?: {
@ -153,6 +238,7 @@ type WebpackOptions = {
esbuildLoader?: EsbuildLoaderOptions | 'off';
swcLoader?: SwcLoaderOptions | 'off';
svgrLoader?: SvgrLoaderOptions | 'off';
inlineSvgLoader?: AssetsLoader.Options | 'off';
styleLoader?: StyleLoader.Options | 'off';
cssLoader?: CssLoader.Options | 'off';
miniCssLoader?: MiniCssLoaderOptions | 'off';
@ -161,7 +247,15 @@ type WebpackOptions = {
sassLoader?: SassLoader.Options | 'off';
imageLoader?: AssetsLoader.Options | 'off';
fontLoader?: AssetsLoader.Options | 'off';
babelLoader?: BabelLoader.Options | 'off';
tsLoader?: TsLoaderOptions | 'off';
};
resolvers?: {
plugins: {
tsConfigPathsPlugin?: TsconfigPathsPluginOptions | 'off';
};
};
cache?: webpack.Configuration['cache'] | 'off';
};
type WebpackEnv = {
@ -177,4 +271,5 @@ export type {
WebpackOptions,
WebpackEnv,
WebpackMainLoaders,
PackageJson,
};

13
src/utils/helpers.ts Normal file
View File

@ -0,0 +1,13 @@
import fs from 'fs';
import type { PackageJson } from 'types';
export const hasPackage = (packageName: string) => {
if (!packageName) {
return false;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-non-null-assertion
const packageJson: PackageJson = JSON.parse(fs.readFileSync(process.env.npm_package_json!, 'utf8'));
return Boolean(packageJson.dependencies[packageName] || packageJson.devDependencies[packageName]);
};

View File

@ -12,7 +12,10 @@
"target": "ESNext",
"lib": [
"DOM"
]
],
"paths": {
"*": ["./src/*"]
}
},
"exclude": ["node_modules"]
}

2888
yarn.lock

File diff suppressed because it is too large Load Diff