create-alistt69-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/bin/index.js +25 -0
- package/package.json +44 -0
- package/src/core/apply-features.js +15 -0
- package/src/core/collect-project-info.js +195 -0
- package/src/core/copy-base-template.js +12 -0
- package/src/core/create-project.js +90 -0
- package/src/core/install-dependencies.js +28 -0
- package/src/core/parse-cli-args.js +123 -0
- package/src/core/prepare-target-directory.js +70 -0
- package/src/core/replace-tokens.js +46 -0
- package/src/core/restore-special-files.js +19 -0
- package/src/features/autoprefixer/files/postcss.config.cjs +5 -0
- package/src/features/autoprefixer/index.js +23 -0
- package/src/features/eslint/files/eslint.config.mjs +128 -0
- package/src/features/eslint/index.js +35 -0
- package/src/features/index.js +15 -0
- package/src/features/react-router/files/src/app/App.tsx +10 -0
- package/src/features/react-router/files/src/app/layouts/app/index.tsx +17 -0
- package/src/features/react-router/files/src/app/providers/router/config/router.tsx +13 -0
- package/src/features/react-router/files/src/pages/error/index.ts +1 -0
- package/src/features/react-router/files/src/pages/error/lazy.ts +3 -0
- package/src/features/react-router/files/src/pages/error/page.tsx +7 -0
- package/src/features/react-router/files/src/pages/main/index.ts +1 -0
- package/src/features/react-router/files/src/pages/main/lazy.ts +3 -0
- package/src/features/react-router/files/src/pages/main/page.tsx +7 -0
- package/src/features/react-router/index.js +23 -0
- package/src/features/stylelint/files/stylelint.config.mjs +14 -0
- package/src/features/stylelint/index.js +29 -0
- package/src/templates/base/.editorconfig +12 -0
- package/src/templates/base/README.md +3 -0
- package/src/templates/base/babel.config.json +12 -0
- package/src/templates/base/config/build/buildDevServer.ts +11 -0
- package/src/templates/base/config/build/buildLoaders.ts +39 -0
- package/src/templates/base/config/build/buildPlugins.ts +34 -0
- package/src/templates/base/config/build/buildResolvers.ts +14 -0
- package/src/templates/base/config/build/buildWebpackConfig.ts +27 -0
- package/src/templates/base/config/build/loaders/buildCssLoader.ts +21 -0
- package/src/templates/base/config/build/types/config.ts +22 -0
- package/src/templates/base/gitignore +27 -0
- package/src/templates/base/package.json +48 -0
- package/src/templates/base/public/index.html +11 -0
- package/src/templates/base/src/app/App.tsx +7 -0
- package/src/templates/base/src/index.tsx +16 -0
- package/src/templates/base/src/styles/index.scss +11 -0
- package/src/templates/base/tsconfig.json +25 -0
- package/src/templates/base/webpack.config.ts +27 -0
- package/src/utils/console-format.js +12 -0
- package/src/utils/package-json.js +73 -0
- package/src/utils/package-manager.js +23 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { confirm, isCancel } from '@clack/prompts';
|
|
2
|
+
import { access, readdir, rm } from 'node:fs/promises';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
async function pathExists(targetDirPath) {
|
|
7
|
+
try {
|
|
8
|
+
await access(targetDirPath);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function prepareTargetDirectory({
|
|
16
|
+
projectName,
|
|
17
|
+
force = false,
|
|
18
|
+
yes = false,
|
|
19
|
+
}) {
|
|
20
|
+
const targetDirPath = resolve(process.cwd(), projectName);
|
|
21
|
+
const exists = await pathExists(targetDirPath);
|
|
22
|
+
|
|
23
|
+
if (!exists) {
|
|
24
|
+
return {
|
|
25
|
+
targetDirPath,
|
|
26
|
+
wasOverwritten: false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const directoryEntries = await readdir(targetDirPath);
|
|
31
|
+
const isEmpty = directoryEntries.length === 0;
|
|
32
|
+
|
|
33
|
+
if (isEmpty) {
|
|
34
|
+
return {
|
|
35
|
+
targetDirPath,
|
|
36
|
+
wasOverwritten: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (force) {
|
|
41
|
+
await rm(targetDirPath, { recursive: true, force: true });
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
targetDirPath,
|
|
45
|
+
wasOverwritten: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (yes) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Directory already exists and is not empty: ${targetDirPath}. Use --force to overwrite it.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const overwriteAnswer = await confirm({
|
|
56
|
+
message: `Directory "${projectName}" already exists and will be overwritten. Continue?`,
|
|
57
|
+
initialValue: false,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (isCancel(overwriteAnswer) || !overwriteAnswer) {
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await rm(targetDirPath, { recursive: true, force: true });
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
targetDirPath,
|
|
68
|
+
wasOverwritten: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const replaceTokensInFile = async (filePath, tokens) => {
|
|
5
|
+
const fileContent = await readFile(filePath, 'utf8');
|
|
6
|
+
|
|
7
|
+
let updatedContent = fileContent;
|
|
8
|
+
|
|
9
|
+
for (const [token, value] of Object.entries(tokens)) {
|
|
10
|
+
updatedContent = updatedContent.replaceAll(token, value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
await writeFile(filePath, updatedContent, 'utf8');
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const replaceTokensRecursively = async (targetPath, tokens) => {
|
|
17
|
+
const targetStat = await stat(targetPath);
|
|
18
|
+
|
|
19
|
+
if (targetStat.isFile()) {
|
|
20
|
+
await replaceTokensInFile(targetPath, tokens);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!targetStat.isDirectory()) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const entries = await readdir(targetPath, { withFileTypes: true });
|
|
29
|
+
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const entryPath = resolve(targetPath, entry.name);
|
|
32
|
+
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
await replaceTokensRecursively(entryPath, tokens);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (entry.isFile()) {
|
|
39
|
+
await replaceTokensInFile(entryPath, tokens);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export async function replaceTokens(targetDirPath, tokens) {
|
|
45
|
+
await replaceTokensRecursively(targetDirPath, tokens);
|
|
46
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { access, rename } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
async function renameIfExists(fromPath, toPath) {
|
|
5
|
+
try {
|
|
6
|
+
await access(fromPath);
|
|
7
|
+
} catch {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
await rename(fromPath, toPath);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function restoreSpecialFiles(projectPath) {
|
|
15
|
+
await renameIfExists(
|
|
16
|
+
resolve(projectPath, 'gitignore'),
|
|
17
|
+
resolve(projectPath, '.gitignore'),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { cp } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { addDevDependencies } from '../../utils/package-json.js';
|
|
5
|
+
|
|
6
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
7
|
+
const currentDirPath = dirname(currentFilePath);
|
|
8
|
+
|
|
9
|
+
export const autoprefixerFeature = {
|
|
10
|
+
id: 'autoprefixer',
|
|
11
|
+
title: 'Autoprefixer',
|
|
12
|
+
apply: async ({ projectPath }) => {
|
|
13
|
+
await addDevDependencies(projectPath, {
|
|
14
|
+
autoprefixer: '^10.4.21',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const filesDirPath = resolve(currentDirPath, 'files');
|
|
18
|
+
|
|
19
|
+
await cp(filesDirPath, projectPath, {
|
|
20
|
+
recursive: true,
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import stylistic from '@stylistic/eslint-plugin';
|
|
3
|
+
import importPlugin from 'eslint-plugin-import';
|
|
4
|
+
import react from 'eslint-plugin-react';
|
|
5
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
6
|
+
import unused from 'eslint-plugin-unused-imports';
|
|
7
|
+
import tseslint from 'typescript-eslint';
|
|
8
|
+
|
|
9
|
+
export default [
|
|
10
|
+
js.configs.recommended,
|
|
11
|
+
{
|
|
12
|
+
ignores: ['**/dist', '**/node_modules', '**/build', 'webpack.config.ts'],
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
// TS base + parser
|
|
16
|
+
...tseslint.configs.recommended,
|
|
17
|
+
...tseslint.configs.stylistic, // TS-стилистика
|
|
18
|
+
{
|
|
19
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
20
|
+
languageOptions: {
|
|
21
|
+
parser: tseslint.parser,
|
|
22
|
+
parserOptions: { sourceType: 'module' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Глобальные стилевые правила (замена prettier)
|
|
27
|
+
stylistic.configs['recommended'],
|
|
28
|
+
|
|
29
|
+
// Реакт / импорты / утиль
|
|
30
|
+
{
|
|
31
|
+
plugins: {
|
|
32
|
+
react,
|
|
33
|
+
'react-hooks': reactHooks,
|
|
34
|
+
'@stylistic': stylistic,
|
|
35
|
+
import: importPlugin,
|
|
36
|
+
'unused-imports': unused,
|
|
37
|
+
},
|
|
38
|
+
settings: { react: { version: 'detect' } },
|
|
39
|
+
rules: {
|
|
40
|
+
// ==== БАЗОВОЕ ФОРМАТИРОВАНИЕ (как prettier) ====
|
|
41
|
+
'@stylistic/indent': ['error', 4, { SwitchCase: 1 }],
|
|
42
|
+
'@stylistic/semi': ['error', 'always'],
|
|
43
|
+
'@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
|
|
44
|
+
'@stylistic/comma-dangle': ['error', 'always-multiline'],
|
|
45
|
+
'@stylistic/object-curly-spacing': ['error', 'always'],
|
|
46
|
+
'@stylistic/array-bracket-spacing': ['error', 'never'],
|
|
47
|
+
'@stylistic/eol-last': ['error', 'always'],
|
|
48
|
+
'@stylistic/arrow-parens': ['error', 'always'],
|
|
49
|
+
'@stylistic/quote-props': ['error', 'as-needed', { keywords: false }],
|
|
50
|
+
'@stylistic/member-delimiter-style': ['error', {
|
|
51
|
+
multiline: { delimiter: 'semi', requireLast: true },
|
|
52
|
+
singleline: { delimiter: 'semi', requireLast: false },
|
|
53
|
+
}],
|
|
54
|
+
// запрет однострочного return (<JSX/>)
|
|
55
|
+
// (через строгую трактовку лишних скобок вокруг однострочного выражения)
|
|
56
|
+
'@stylistic/no-extra-parens': 'off',
|
|
57
|
+
'@stylistic/jsx-indent-props': 'off',
|
|
58
|
+
'@stylistic/multiline-ternary': 'off',
|
|
59
|
+
|
|
60
|
+
// Пустые строки/пробелы
|
|
61
|
+
'no-trailing-spaces': ['warn', { skipBlankLines: false }],
|
|
62
|
+
'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 1 }],
|
|
63
|
+
'comma-style': ['error', 'last'],
|
|
64
|
+
'max-len': ['error', {
|
|
65
|
+
code: 120,
|
|
66
|
+
tabWidth: 4,
|
|
67
|
+
ignoreUrls: true,
|
|
68
|
+
ignoreStrings: true,
|
|
69
|
+
ignoreComments: false,
|
|
70
|
+
ignoreTemplateLiterals: true,
|
|
71
|
+
}],
|
|
72
|
+
|
|
73
|
+
// Импорты
|
|
74
|
+
'unused-imports/no-unused-imports': 'warn',
|
|
75
|
+
'unused-imports/no-unused-vars': ['warn', {
|
|
76
|
+
args: 'after-used',
|
|
77
|
+
argsIgnorePattern: '^_',
|
|
78
|
+
varsIgnorePattern: '^_',
|
|
79
|
+
caughtErrorsIgnorePattern: '^_',
|
|
80
|
+
}],
|
|
81
|
+
'import/order': ['error', {
|
|
82
|
+
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
|
83
|
+
pathGroups: [
|
|
84
|
+
{ pattern: '@/**', group: 'internal', position: 'after' },
|
|
85
|
+
{ pattern: '@app/**', group: 'internal', position: 'after' },
|
|
86
|
+
{ pattern: '@pages/**', group: 'internal', position: 'after' },
|
|
87
|
+
{ pattern: '@widgets/**', group: 'internal', position: 'after' },
|
|
88
|
+
{ pattern: '@features/**', group: 'internal', position: 'after' },
|
|
89
|
+
{ pattern: '@entities/**', group: 'internal', position: 'after' },
|
|
90
|
+
{ pattern: '@shared/**', group: 'internal', position: 'after' },
|
|
91
|
+
],
|
|
92
|
+
pathGroupsExcludedImportTypes: ['builtin', 'external'],
|
|
93
|
+
'newlines-between': 'never',
|
|
94
|
+
alphabetize: { order: 'asc', caseInsensitive: true },
|
|
95
|
+
}],
|
|
96
|
+
|
|
97
|
+
// React / JSX
|
|
98
|
+
...react.configs.recommended.rules,
|
|
99
|
+
...reactHooks.configs.recommended.rules,
|
|
100
|
+
'react/react-in-jsx-scope': 'off',
|
|
101
|
+
'react/jsx-uses-react': 'off',
|
|
102
|
+
'react/prop-types': 'off',
|
|
103
|
+
|
|
104
|
+
'react/jsx-indent': ['error', 4],
|
|
105
|
+
'react/jsx-indent-props': ['error', 4],
|
|
106
|
+
'react/jsx-curly-spacing': ['error', { when: 'never', children: true }],
|
|
107
|
+
'react/jsx-equals-spacing': ['error', 'never'],
|
|
108
|
+
'react/jsx-tag-spacing': ['error', {
|
|
109
|
+
closingSlash: 'never',
|
|
110
|
+
beforeSelfClosing: 'always',
|
|
111
|
+
afterOpening: 'never',
|
|
112
|
+
beforeClosing: 'never',
|
|
113
|
+
}],
|
|
114
|
+
'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'],
|
|
115
|
+
'react/jsx-max-props-per-line': ['error', { maximum: 1, when: 'multiline' }],
|
|
116
|
+
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
|
|
117
|
+
|
|
118
|
+
// Прочее
|
|
119
|
+
'@typescript-eslint/no-unused-vars': 'off', // перекрываем unused-imports
|
|
120
|
+
'no-console': ['warn', { allow: ['error'] }],
|
|
121
|
+
'no-restricted-imports': ['error', {
|
|
122
|
+
name: 'react',
|
|
123
|
+
importNames: ['default'],
|
|
124
|
+
message: 'React импортировать не надо (v17+). Удалите import React from "react".',
|
|
125
|
+
}],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cp } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { addDevDependencies, addScripts } from '../../utils/package-json.js';
|
|
5
|
+
|
|
6
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
7
|
+
const currentDirPath = dirname(currentFilePath);
|
|
8
|
+
|
|
9
|
+
export const eslintFeature = {
|
|
10
|
+
id: 'eslint',
|
|
11
|
+
title: 'ESLint + Stylistic',
|
|
12
|
+
apply: async ({ projectPath }) => {
|
|
13
|
+
await addDevDependencies(projectPath, {
|
|
14
|
+
"eslint": "^9.0.0",
|
|
15
|
+
"@eslint/js": "^9.0.0",
|
|
16
|
+
"@stylistic/eslint-plugin": "^5.0.0",
|
|
17
|
+
"typescript-eslint": "^8.0.0",
|
|
18
|
+
"eslint-plugin-import": "^2.0.0",
|
|
19
|
+
"eslint-plugin-react": "^7.0.0",
|
|
20
|
+
"eslint-plugin-react-hooks": "^7.0.0",
|
|
21
|
+
"eslint-plugin-unused-imports": "^4.0.0"
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await addScripts(projectPath, {
|
|
25
|
+
lint: 'eslint .',
|
|
26
|
+
'lint:fix': 'eslint . --fix',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const filesDirPath = resolve(currentDirPath, 'files');
|
|
30
|
+
|
|
31
|
+
await cp(filesDirPath, projectPath, {
|
|
32
|
+
recursive: true,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { autoprefixerFeature } from './autoprefixer/index.js';
|
|
2
|
+
import { eslintFeature } from './eslint/index.js';
|
|
3
|
+
import { reactRouterFeature } from './react-router/index.js';
|
|
4
|
+
import { stylelintFeature } from './stylelint/index.js';
|
|
5
|
+
|
|
6
|
+
export const features = [
|
|
7
|
+
autoprefixerFeature,
|
|
8
|
+
eslintFeature,
|
|
9
|
+
reactRouterFeature,
|
|
10
|
+
stylelintFeature,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const featuresById = new Map(
|
|
14
|
+
features.map((feature) => [feature.id, feature]),
|
|
15
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createBrowserRouter, createRoutesFromElements, Route } from 'react-router-dom';
|
|
2
|
+
import AppLayout from '../../../layouts/app';
|
|
3
|
+
import { Main } from '../../../../pages/main';
|
|
4
|
+
import { Error } from '../../../../pages/error';
|
|
5
|
+
|
|
6
|
+
export const appRouter = createBrowserRouter(
|
|
7
|
+
createRoutesFromElements(
|
|
8
|
+
<Route path="/" element={<AppLayout />}>
|
|
9
|
+
<Route index element={<Main />} />
|
|
10
|
+
<Route path="*" element={<Error />} />
|
|
11
|
+
</Route>,
|
|
12
|
+
),
|
|
13
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LazyError as Error } from './lazy';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LazyMain as Main } from './lazy';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { cp } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { addDependencies } from '../../utils/package-json.js';
|
|
5
|
+
|
|
6
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
7
|
+
const currentDirPath = dirname(currentFilePath);
|
|
8
|
+
|
|
9
|
+
export const reactRouterFeature = {
|
|
10
|
+
id: 'react-router',
|
|
11
|
+
title: 'React Router DOM',
|
|
12
|
+
apply: async ({ projectPath }) => {
|
|
13
|
+
await addDependencies(projectPath, {
|
|
14
|
+
'react-router-dom': '^7.13.2',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const filesDirPath = resolve(currentDirPath, 'files');
|
|
18
|
+
|
|
19
|
+
await cp(filesDirPath, projectPath, {
|
|
20
|
+
recursive: true,
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** @type {import('stylelint').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
extends: ['stylelint-config-standard-scss'],
|
|
4
|
+
rules: {
|
|
5
|
+
'scss/no-global-function-names': null,
|
|
6
|
+
'selector-class-pattern': [
|
|
7
|
+
'^[a-z][a-z0-9-_]*$',
|
|
8
|
+
{
|
|
9
|
+
message: 'Class names should be lowercase and can include numbers, hyphens, and underscores.',
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
'at-rule-empty-line-before': null,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cp } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { addDevDependencies, addScripts } from '../../utils/package-json.js';
|
|
5
|
+
|
|
6
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
7
|
+
const currentDirPath = dirname(currentFilePath);
|
|
8
|
+
|
|
9
|
+
export const stylelintFeature = {
|
|
10
|
+
id: 'stylelint',
|
|
11
|
+
title: 'Stylelint',
|
|
12
|
+
apply: async ({ projectPath }) => {
|
|
13
|
+
await addDevDependencies(projectPath, {
|
|
14
|
+
stylelint: '^17.6.0',
|
|
15
|
+
'stylelint-config-standard-scss': '^17.0.0',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
await addScripts(projectPath, {
|
|
19
|
+
'lint:styles': 'stylelint "src/**/*.{scss,css}"',
|
|
20
|
+
'lint:styles:fix': 'stylelint "src/**/*.{scss,css}" --fix',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const filesDirPath = resolve(currentDirPath, 'files');
|
|
24
|
+
|
|
25
|
+
await cp(filesDirPath, projectPath, {
|
|
26
|
+
recursive: true,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
|
|
2
|
+
import { BuildOptions } from './types/config';
|
|
3
|
+
|
|
4
|
+
export function buildDevServer(options: BuildOptions): DevServerConfiguration {
|
|
5
|
+
return {
|
|
6
|
+
port: options.port,
|
|
7
|
+
open: true,
|
|
8
|
+
historyApiFallback: true,
|
|
9
|
+
hot: true,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import webpack from 'webpack';
|
|
2
|
+
import { BuildOptions } from './types/config';
|
|
3
|
+
import { buildCssLoader } from './loaders/buildCssLoader';
|
|
4
|
+
|
|
5
|
+
export function buildLoaders({ isDev }: BuildOptions): webpack.RuleSetRule[] {
|
|
6
|
+
const svgLoader = {
|
|
7
|
+
test: /\.svg$/,
|
|
8
|
+
use: ['@svgr/webpack'],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const babelLoader = {
|
|
12
|
+
test: /\.(tsx|js|jsx)$/,
|
|
13
|
+
exclude: /node_modules/,
|
|
14
|
+
use: {
|
|
15
|
+
loader: 'babel-loader',
|
|
16
|
+
options: {
|
|
17
|
+
presets: ['@babel/preset-env'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const cssLoader = buildCssLoader(isDev);
|
|
23
|
+
|
|
24
|
+
const typescriptLoader = {
|
|
25
|
+
test: /\.tsx?$/,
|
|
26
|
+
use: 'ts-loader',
|
|
27
|
+
exclude: /node_modules/,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const fileLoader = {
|
|
31
|
+
test: /\.(png|jpg|jpeg|gif|woff2|woff)$/i,
|
|
32
|
+
type: 'asset/resource',
|
|
33
|
+
generator: {
|
|
34
|
+
filename: 'static/media/[name].[hash][ext]',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return [fileLoader, svgLoader, babelLoader, typescriptLoader, cssLoader];
|
|
39
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import HTMLWebpackPlugin from 'html-webpack-plugin';
|
|
2
|
+
import webpack from 'webpack';
|
|
3
|
+
import { BuildOptions } from './types/config';
|
|
4
|
+
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
|
5
|
+
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
|
6
|
+
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
|
|
7
|
+
|
|
8
|
+
export function buildPlugins({ paths, isDev, apiUrl }: BuildOptions): webpack.WebpackPluginInstance[] {
|
|
9
|
+
const plugins = [
|
|
10
|
+
new HTMLWebpackPlugin({
|
|
11
|
+
template: paths.html,
|
|
12
|
+
}),
|
|
13
|
+
new webpack.ProgressPlugin(),
|
|
14
|
+
new MiniCssExtractPlugin({
|
|
15
|
+
filename: 'css/[name].[contenthash:8].css',
|
|
16
|
+
chunkFilename: 'css/[name].[contenthash:8].css',
|
|
17
|
+
}),
|
|
18
|
+
new webpack.DefinePlugin({
|
|
19
|
+
__IS_DEV__: JSON.stringify(isDev),
|
|
20
|
+
__API__: JSON.stringify(apiUrl),
|
|
21
|
+
}),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
if (isDev) {
|
|
25
|
+
plugins.push(
|
|
26
|
+
new ReactRefreshWebpackPlugin(),
|
|
27
|
+
new BundleAnalyzerPlugin({
|
|
28
|
+
openAnalyzer: false,
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return plugins;
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ResolveOptions } from 'webpack';
|
|
2
|
+
import { BuildOptions } from './types/config';
|
|
3
|
+
|
|
4
|
+
export function buildResolvers(options: BuildOptions): ResolveOptions {
|
|
5
|
+
return {
|
|
6
|
+
extensions: ['.tsx', '.ts', '.js'],
|
|
7
|
+
preferAbsolute: true,
|
|
8
|
+
modules: [options.paths.src, 'node_modules'],
|
|
9
|
+
mainFiles: ['index'],
|
|
10
|
+
alias: {
|
|
11
|
+
'@': options.paths.src,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BuildOptions } from './types/config';
|
|
2
|
+
import webpack from 'webpack';
|
|
3
|
+
import { buildPlugins } from './buildPlugins';
|
|
4
|
+
import { buildLoaders } from './buildLoaders';
|
|
5
|
+
import { buildResolvers } from './buildResolvers';
|
|
6
|
+
import { buildDevServer } from './buildDevServer';
|
|
7
|
+
|
|
8
|
+
export function buildWebpackConfig(options: BuildOptions): webpack.Configuration {
|
|
9
|
+
const { paths, mode, isDev } = options;
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
mode: mode,
|
|
13
|
+
entry: paths.entry,
|
|
14
|
+
output: {
|
|
15
|
+
filename: '[name].[contenthash].js',
|
|
16
|
+
path: paths.build,
|
|
17
|
+
clean: true,
|
|
18
|
+
},
|
|
19
|
+
plugins: buildPlugins(options),
|
|
20
|
+
module: {
|
|
21
|
+
rules: buildLoaders(options),
|
|
22
|
+
},
|
|
23
|
+
resolve: buildResolvers(options),
|
|
24
|
+
devtool: isDev ? 'inline-source-map' : undefined,
|
|
25
|
+
devServer: isDev ? buildDevServer(options) : undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|