create-alistt69-kit 0.1.20 → 0.2.2
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/README.md +35 -0
- package/package.json +1 -1
- package/src/core/parse-cli-args.js +1 -1
- package/src/core/render-project-readme.js +56 -14
- package/src/features/eslint/files/eslint.config.mjs +4 -3
- package/src/features/react-router/files/scripts/generate/page.mjs +341 -0
- package/src/features/react-router/files/src/app/App.tsx +3 -3
- package/src/features/react-router/files/src/app/providers/error-boundary/lib/provider/index.tsx +4 -4
- package/src/features/react-router/files/src/app/providers/error-boundary/ui/error-screen/index.tsx +3 -3
- package/src/features/react-router/files/src/app/providers/index.ts +14 -4
- package/src/features/react-router/files/src/app/providers/router/lib/hooks/useGetCurrentRouteConfig.ts +13 -0
- package/src/features/react-router/files/src/app/providers/router/lib/provider/index.tsx +16 -5
- package/src/features/react-router/files/src/app/providers/router/model/config/index.ts +9 -0
- package/src/features/react-router/files/src/app/providers/router/model/router/index.tsx +24 -0
- package/src/features/react-router/files/src/app/providers/router/types/index.ts +10 -0
- package/src/features/react-router/files/src/app/providers/router/ui/app/index.tsx +36 -0
- package/src/features/react-router/files/src/index.tsx +8 -4
- package/src/features/react-router/index.js +3 -0
- package/src/templates/base/config/build/buildPlugins.ts +1 -1
- package/src/templates/base/src/styles/index.scss +1 -1
- package/src/templates/base/src/styles/normalize.scss +36 -0
- package/src/features/react-router/files/src/app/layouts/app/index.tsx +0 -39
- package/src/features/react-router/files/src/app/providers/router/lib/router/index.tsx +0 -13
- /package/src/features/react-router/files/src/app/{layouts → providers/router/ui}/app/styles.module.scss +0 -0
package/README.md
CHANGED
|
@@ -55,6 +55,41 @@ This starter removes that boilerplate so you can get straight to building.
|
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
58
|
+
## ⚡ Generated project helpers
|
|
59
|
+
|
|
60
|
+
Some optional features also add local project generators to the scaffolded app.
|
|
61
|
+
|
|
62
|
+
### Page generator
|
|
63
|
+
|
|
64
|
+
When `React Router DOM` is enabled, the project includes a built-in page generator.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm run generate:page about
|
|
68
|
+
```
|
|
69
|
+
This will create:
|
|
70
|
+
|
|
71
|
+
* `src/pages/about/index.ts`
|
|
72
|
+
* `src/pages/about/lazy.ts`
|
|
73
|
+
* `src/pages/about/page.tsx`
|
|
74
|
+
|
|
75
|
+
If the standard router files are present, the generator will also register the page automatically in:
|
|
76
|
+
|
|
77
|
+
* `src/app/providers/router/types/index.ts`
|
|
78
|
+
* `src/app/providers/router/model/config/index.ts`
|
|
79
|
+
* `src/app/providers/router/model/router/index.tsx`
|
|
80
|
+
|
|
81
|
+
You can also configure custom paths in `scripts/generate/page.mjs`:
|
|
82
|
+
* `ROUTER_TYPES_PATH`
|
|
83
|
+
* `ROUTER_CONFIG_PATH`
|
|
84
|
+
* `ROUTER_FILE_PATH`
|
|
85
|
+
|
|
86
|
+
#### Important
|
|
87
|
+
|
|
88
|
+
Route auto-registration relies on special marker comments `@route-...` inside the router files.
|
|
89
|
+
Do not remove these markers unless you also want to disable automatic updates.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
58
93
|
## 📦 Requirements
|
|
59
94
|
|
|
60
95
|
- **Node.js** `18.18` or higher
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@ export function formatHelpMessage() {
|
|
|
28
28
|
' create-alistt69-kit <project-name> [options]',
|
|
29
29
|
'',
|
|
30
30
|
'Options:',
|
|
31
|
-
' -def, --defaults
|
|
31
|
+
' -def, --defaults Skip prompts and use defaults',
|
|
32
32
|
' --overwrite Overwrite target directory if it exists',
|
|
33
33
|
' --no-install Do not install dependencies',
|
|
34
34
|
' --features <comma-list> Example: eslint,stylelint,react-router',
|
|
@@ -49,6 +49,7 @@ const scriptDescriptions = {
|
|
|
49
49
|
'lint:fix': 'Run ESLint with autofix',
|
|
50
50
|
'lint:styles': 'Run Stylelint',
|
|
51
51
|
'lint:styles:fix': 'Run Stylelint with autofix',
|
|
52
|
+
'generate:page': 'Generate a new page and auto-register it in the router when available',
|
|
52
53
|
};
|
|
53
54
|
|
|
54
55
|
function formatMarkdownList(items, fallback = '- None') {
|
|
@@ -105,10 +106,10 @@ function formatScripts(packageManager, scripts) {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
function formatQuickStart({
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
projectName,
|
|
110
|
+
packageManager,
|
|
111
|
+
shouldInstallDependencies,
|
|
112
|
+
}) {
|
|
112
113
|
const steps = [`cd ${projectName}`];
|
|
113
114
|
|
|
114
115
|
if (!shouldInstallDependencies) {
|
|
@@ -128,24 +129,64 @@ function formatQuickStart({
|
|
|
128
129
|
].join('\n');
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
function formatProjectStructure() {
|
|
132
|
-
|
|
132
|
+
function formatProjectStructure(selectedFeatureIds) {
|
|
133
|
+
const items = [
|
|
133
134
|
'- `public/` — static assets and HTML template',
|
|
134
135
|
'- `src/` — application source code',
|
|
135
136
|
'- `src/app/` — app bootstrap, providers, entry-level setup',
|
|
136
137
|
'- `src/styles/` — global styles and shared styling layer',
|
|
137
138
|
'- `config/build/` — split webpack configuration',
|
|
138
|
-
]
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
if (selectedFeatureIds.includes('react-router')) {
|
|
142
|
+
items.splice(3, 0, '- `src/pages/` — route-level pages');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return items.join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function formatFeatureSpecificSections({ selectedFeatureIds, packageManager }) {
|
|
149
|
+
const sections = [];
|
|
150
|
+
|
|
151
|
+
if (selectedFeatureIds.includes('react-router')) {
|
|
152
|
+
sections.push([
|
|
153
|
+
'## Page generator',
|
|
154
|
+
'',
|
|
155
|
+
'When `React Router DOM` is enabled, you can scaffold a new page with one command.',
|
|
156
|
+
'',
|
|
157
|
+
'```bash',
|
|
158
|
+
`${getRunScriptCommand(packageManager, 'generate:page')} about`,
|
|
159
|
+
'```',
|
|
160
|
+
'',
|
|
161
|
+
'This creates:',
|
|
162
|
+
'',
|
|
163
|
+
'- `src/pages/about/index.ts`',
|
|
164
|
+
'- `src/pages/about/lazy.ts`',
|
|
165
|
+
'- `src/pages/about/page.tsx`',
|
|
166
|
+
'',
|
|
167
|
+
'If the standard router files are still present, the generator also updates:',
|
|
168
|
+
'',
|
|
169
|
+
'- `src/app/providers/router/types/index.ts`',
|
|
170
|
+
'- `src/app/providers/router/model/config/index.ts`',
|
|
171
|
+
'- `src/app/providers/router/model/router/index.tsx`',
|
|
172
|
+
].join('\n'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return sections.join('\n\n');
|
|
139
176
|
}
|
|
140
177
|
|
|
141
178
|
export async function renderProjectReadme({
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
179
|
+
projectPath,
|
|
180
|
+
projectName,
|
|
181
|
+
selectedFeatureIds,
|
|
182
|
+
packageManager,
|
|
183
|
+
shouldInstallDependencies,
|
|
184
|
+
}) {
|
|
148
185
|
const packageJson = await readPackageJson(projectPath);
|
|
186
|
+
const featureSpecificSections = formatFeatureSpecificSections({
|
|
187
|
+
selectedFeatureIds,
|
|
188
|
+
packageManager,
|
|
189
|
+
});
|
|
149
190
|
|
|
150
191
|
const readmeContent = [
|
|
151
192
|
`# ${projectName}`,
|
|
@@ -174,11 +215,12 @@ export async function renderProjectReadme({
|
|
|
174
215
|
'',
|
|
175
216
|
'## Project structure',
|
|
176
217
|
'',
|
|
177
|
-
formatProjectStructure(),
|
|
218
|
+
formatProjectStructure(selectedFeatureIds),
|
|
178
219
|
'',
|
|
179
220
|
'## Available scripts',
|
|
180
221
|
'',
|
|
181
222
|
formatScripts(packageManager, packageJson.scripts ?? {}),
|
|
223
|
+
...(featureSpecificSections ? ['', featureSpecificSections] : []),
|
|
182
224
|
].join('\n');
|
|
183
225
|
|
|
184
226
|
await writeFile(
|
|
@@ -102,9 +102,10 @@ export default [
|
|
|
102
102
|
// === React ===
|
|
103
103
|
...react.configs.recommended.rules,
|
|
104
104
|
...reactHooks.configs.recommended.rules,
|
|
105
|
-
'react/react-in-jsx-scope': 'off',
|
|
106
|
-
'react/jsx-uses-react': 'off',
|
|
107
|
-
'react/prop-types': 'off',
|
|
105
|
+
'react/react-in-jsx-scope': 'off', // Not needed in React 17+
|
|
106
|
+
'react/jsx-uses-react': 'off', // Not needed in React 17+
|
|
107
|
+
'react/prop-types': 'off', // TypeScript handles this
|
|
108
|
+
'react-hooks/set-state-in-effect': 'warn',
|
|
108
109
|
|
|
109
110
|
// === JSX formatting ===
|
|
110
111
|
'react/jsx-indent': ['error', 4],
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const ROUTER_TYPES_PATH = path.join(
|
|
5
|
+
'src',
|
|
6
|
+
'app',
|
|
7
|
+
'providers',
|
|
8
|
+
'router',
|
|
9
|
+
'types',
|
|
10
|
+
'index.ts',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const ROUTER_CONFIG_PATH = path.join(
|
|
14
|
+
'src',
|
|
15
|
+
'app',
|
|
16
|
+
'providers',
|
|
17
|
+
'router',
|
|
18
|
+
'model',
|
|
19
|
+
'config',
|
|
20
|
+
'index.ts',
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const ROUTER_FILE_PATH = path.join(
|
|
24
|
+
'src',
|
|
25
|
+
'app',
|
|
26
|
+
'providers',
|
|
27
|
+
'router',
|
|
28
|
+
'model',
|
|
29
|
+
'router',
|
|
30
|
+
'index.tsx',
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
async function main() {
|
|
34
|
+
const rawPageName = process.argv[2];
|
|
35
|
+
|
|
36
|
+
if (!rawPageName) {
|
|
37
|
+
printUsageAndExit();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const pageSlug = normalizePageSlug(rawPageName);
|
|
41
|
+
|
|
42
|
+
if (!pageSlug) {
|
|
43
|
+
fail('Page name cannot be empty.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const pageDirPath = path.join(process.cwd(), 'src', 'pages', pageSlug);
|
|
47
|
+
|
|
48
|
+
if (await pathExists(pageDirPath)) {
|
|
49
|
+
fail(`Page "${pageSlug}" already exists: ${toRelativeProjectPath(pageDirPath)}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pageMeta = buildPageMeta(pageSlug);
|
|
53
|
+
|
|
54
|
+
await createPageFiles(pageDirPath, pageMeta);
|
|
55
|
+
|
|
56
|
+
const autoRegisterResult = await autoRegisterPage(pageMeta);
|
|
57
|
+
|
|
58
|
+
console.log(`✔ Page "${pageSlug}" created.`);
|
|
59
|
+
|
|
60
|
+
if (autoRegisterResult.status === 'registered') {
|
|
61
|
+
console.log('✔ Route registration updated.');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`ℹ ${autoRegisterResult.message}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function printUsageAndExit() {
|
|
69
|
+
console.error('Usage: npm run generate:page -- <page-name>');
|
|
70
|
+
console.error('Example: npm run generate:page -- about');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fail(message) {
|
|
75
|
+
console.error(`✖ ${message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizePageSlug(value) {
|
|
80
|
+
return value
|
|
81
|
+
.trim()
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
.replace(/[_\s]+/g, '-')
|
|
84
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
85
|
+
.replace(/-+/g, '-')
|
|
86
|
+
.replace(/^-+|-+$/g, '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildPageMeta(pageSlug) {
|
|
90
|
+
return {
|
|
91
|
+
pageSlug,
|
|
92
|
+
pageComponentName: toPascalCase(pageSlug),
|
|
93
|
+
pageTitle: toTitleCase(pageSlug),
|
|
94
|
+
routeEnumKey: toScreamingSnake(pageSlug),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toPascalCase(value) {
|
|
99
|
+
return value
|
|
100
|
+
.split('-')
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
103
|
+
.join('');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toScreamingSnake(value) {
|
|
107
|
+
return value
|
|
108
|
+
.split('-')
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.join('_')
|
|
111
|
+
.toUpperCase();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toTitleCase(value) {
|
|
115
|
+
return value
|
|
116
|
+
.split('-')
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
119
|
+
.join(' ');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function createPageFiles(pageDirPath, pageMeta) {
|
|
123
|
+
const { pageComponentName, pageTitle } = pageMeta;
|
|
124
|
+
|
|
125
|
+
await mkdir(pageDirPath, {
|
|
126
|
+
recursive: true,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await writeFile(
|
|
130
|
+
path.join(pageDirPath, 'index.ts'),
|
|
131
|
+
`export { Lazy${pageComponentName} as ${pageComponentName} } from './lazy';\n`,
|
|
132
|
+
'utf8',
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
await writeFile(
|
|
136
|
+
path.join(pageDirPath, 'lazy.ts'),
|
|
137
|
+
[
|
|
138
|
+
"import { lazy } from 'react';",
|
|
139
|
+
'',
|
|
140
|
+
`export const Lazy${pageComponentName} = lazy(() => import('./page'));`,
|
|
141
|
+
'',
|
|
142
|
+
].join('\n'),
|
|
143
|
+
'utf8',
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
await writeFile(
|
|
147
|
+
path.join(pageDirPath, 'page.tsx'),
|
|
148
|
+
[
|
|
149
|
+
`function ${pageComponentName}Page() {`,
|
|
150
|
+
' return (',
|
|
151
|
+
` <h2>${pageTitle} page</h2>`,
|
|
152
|
+
' );',
|
|
153
|
+
'}',
|
|
154
|
+
'',
|
|
155
|
+
`export default ${pageComponentName}Page;`,
|
|
156
|
+
'',
|
|
157
|
+
].join('\n'),
|
|
158
|
+
'utf8',
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function autoRegisterPage(pageMeta) {
|
|
163
|
+
const projectRoot = process.cwd();
|
|
164
|
+
|
|
165
|
+
const typesFilePath = path.join(projectRoot, ROUTER_TYPES_PATH);
|
|
166
|
+
const configFilePath = path.join(projectRoot, ROUTER_CONFIG_PATH);
|
|
167
|
+
const routerFilePath = path.join(projectRoot, ROUTER_FILE_PATH);
|
|
168
|
+
|
|
169
|
+
const [hasTypesFile, hasConfigFile, hasRouterFile] = await Promise.all([
|
|
170
|
+
pathExists(typesFilePath),
|
|
171
|
+
pathExists(configFilePath),
|
|
172
|
+
pathExists(routerFilePath),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
if (!hasTypesFile || !hasConfigFile || !hasRouterFile) {
|
|
176
|
+
const missingFiles = [
|
|
177
|
+
!hasTypesFile ? ROUTER_TYPES_PATH : null,
|
|
178
|
+
!hasConfigFile ? ROUTER_CONFIG_PATH : null,
|
|
179
|
+
!hasRouterFile ? ROUTER_FILE_PATH : null,
|
|
180
|
+
].filter(Boolean);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
status: 'skipped',
|
|
184
|
+
message: `Route auto-registration skipped. Missing files: ${missingFiles.join(', ')}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await insertRouteEnum(typesFilePath, pageMeta);
|
|
189
|
+
await insertRouteConfig(configFilePath, pageMeta);
|
|
190
|
+
await insertRouteImport(routerFilePath, pageMeta);
|
|
191
|
+
await insertRouteDefinition(routerFilePath, pageMeta);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
status: 'registered',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function insertRouteEnum(filepath, pageMeta) {
|
|
199
|
+
await insertBeforeMarkerLine({
|
|
200
|
+
filepath,
|
|
201
|
+
marker: '// @route-enum',
|
|
202
|
+
block: `${pageMeta.routeEnumKey} = '${pageMeta.pageSlug}',`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function insertRouteConfig(filepath, pageMeta) {
|
|
207
|
+
await insertBeforeMarkerLine({
|
|
208
|
+
filepath,
|
|
209
|
+
marker: '// @route-config',
|
|
210
|
+
block: [
|
|
211
|
+
`[ERoutePath.${pageMeta.routeEnumKey}]: {`,
|
|
212
|
+
` path: ERoutePath.${pageMeta.routeEnumKey},`,
|
|
213
|
+
` title: '${pageMeta.pageTitle}',`,
|
|
214
|
+
'},',
|
|
215
|
+
].join('\n'),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function insertRouteImport(filepath, pageMeta) {
|
|
220
|
+
await insertBeforeMarkerLine({
|
|
221
|
+
filepath,
|
|
222
|
+
marker: '/* @route-imports */',
|
|
223
|
+
block: `import { ${pageMeta.pageComponentName} } from '../../../../../pages/${pageMeta.pageSlug}';`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function insertRouteDefinition(filepath, pageMeta) {
|
|
228
|
+
await insertBeforeMarkerLine({
|
|
229
|
+
filepath,
|
|
230
|
+
marker: '{/* @route-routes */}',
|
|
231
|
+
block: `<Route path={ERoutePath.${pageMeta.routeEnumKey}} element={<${pageMeta.pageComponentName} />} />`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function insertBeforeMarkerLine({ filepath, marker, block }) {
|
|
236
|
+
const fileContent = await readFile(filepath, 'utf8');
|
|
237
|
+
const eol = detectEol(fileContent);
|
|
238
|
+
const hadTrailingNewline = /\r?\n$/.test(fileContent);
|
|
239
|
+
|
|
240
|
+
const lines = fileContent.split(/\r?\n/);
|
|
241
|
+
|
|
242
|
+
if (hadTrailingNewline && lines.at(-1) === '') {
|
|
243
|
+
lines.pop();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const markerLineIndex = lines.findIndex((line) => line.includes(marker));
|
|
247
|
+
|
|
248
|
+
if (markerLineIndex === -1) {
|
|
249
|
+
fail(`Marker "${marker}" not found in ${toRelativeProjectPath(filepath)}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const markerLine = lines[markerLineIndex];
|
|
253
|
+
const baseIndent = getLineIndent(markerLine);
|
|
254
|
+
const normalizedBlock = indentBlock(block, baseIndent);
|
|
255
|
+
const normalizedBlockLines = normalizedBlock.split('\n');
|
|
256
|
+
|
|
257
|
+
if (blockAlreadyInserted(lines, normalizedBlockLines)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
lines.splice(markerLineIndex, 0, ...normalizedBlockLines);
|
|
262
|
+
|
|
263
|
+
let nextContent = lines.join(eol);
|
|
264
|
+
|
|
265
|
+
if (hadTrailingNewline) {
|
|
266
|
+
nextContent += eol;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
await writeFile(filepath, nextContent, 'utf8');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function detectEol(content) {
|
|
273
|
+
return content.includes('\r\n') ? '\r\n' : '\n';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function getLineIndent(line) {
|
|
277
|
+
const match = line.match(/^\s*/);
|
|
278
|
+
|
|
279
|
+
return match ? match[0] : '';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function indentBlock(block, baseIndent) {
|
|
283
|
+
const rawLines = block.split('\n');
|
|
284
|
+
|
|
285
|
+
return rawLines
|
|
286
|
+
.map((line) => {
|
|
287
|
+
if (!line.trim()) {
|
|
288
|
+
return '';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const lineIndentSize = countLeadingSpaces(line);
|
|
292
|
+
const relativeIndentLevel = Math.floor(lineIndentSize / 4);
|
|
293
|
+
const relativeIndent = ' '.repeat(relativeIndentLevel * 4);
|
|
294
|
+
|
|
295
|
+
return `${baseIndent}${relativeIndent}${line.trimStart()}`;
|
|
296
|
+
})
|
|
297
|
+
.join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function countLeadingSpaces(line) {
|
|
301
|
+
const match = line.match(/^ */);
|
|
302
|
+
|
|
303
|
+
return match ? match[0].length : 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function blockAlreadyInserted(lines, normalizedBlockLines) {
|
|
307
|
+
const comparableLines = normalizedBlockLines.filter((line) => line.trim() !== '');
|
|
308
|
+
|
|
309
|
+
if (comparableLines.length === 0) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (let index = 0; index <= lines.length - comparableLines.length; index += 1) {
|
|
314
|
+
const currentSlice = lines.slice(index, index + comparableLines.length);
|
|
315
|
+
|
|
316
|
+
if (currentSlice.join('\n') === comparableLines.join('\n')) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function pathExists(filepath) {
|
|
325
|
+
try {
|
|
326
|
+
await access(filepath);
|
|
327
|
+
return true;
|
|
328
|
+
} catch (_) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function toRelativeProjectPath(filepath) {
|
|
334
|
+
return path.relative(process.cwd(), filepath) || '.';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
main().catch((error) => {
|
|
338
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
339
|
+
|
|
340
|
+
fail(message);
|
|
341
|
+
});
|
|
@@ -3,10 +3,10 @@ import Logo from '../../public/create-alistt69-kit-logo.svg';
|
|
|
3
3
|
import styles from './styles.module.scss';
|
|
4
4
|
|
|
5
5
|
interface AppProps {
|
|
6
|
-
|
|
6
|
+
router: ReactNode;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
function App({
|
|
9
|
+
function App({ router }: AppProps) {
|
|
10
10
|
return (
|
|
11
11
|
<div className={styles.app_wrapper}>
|
|
12
12
|
<div className={styles.created_by_section}>
|
|
@@ -15,7 +15,7 @@ function App({ children }: AppProps) {
|
|
|
15
15
|
created by create-alistt69-kit
|
|
16
16
|
</p>
|
|
17
17
|
</div>
|
|
18
|
-
{
|
|
18
|
+
{router}
|
|
19
19
|
</div>
|
|
20
20
|
);
|
|
21
21
|
}
|
package/src/features/react-router/files/src/app/providers/error-boundary/lib/provider/index.tsx
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Component, ErrorInfo, ReactNode } from 'react';
|
|
2
2
|
import ErrorScreen from '../../ui/error-screen';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
interface ErrorBoundaryProps {
|
|
5
5
|
children: ReactNode;
|
|
6
|
-
}
|
|
6
|
+
}
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface ErrorBoundaryState {
|
|
9
9
|
hasError: boolean;
|
|
10
10
|
error: Error | null;
|
|
11
|
-
}
|
|
11
|
+
}
|
|
12
12
|
|
|
13
13
|
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
14
14
|
constructor(props: ErrorBoundaryProps) {
|
package/src/features/react-router/files/src/app/providers/error-boundary/ui/error-screen/index.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
interface ErrorScreenProps {
|
|
2
2
|
title?: string;
|
|
3
3
|
description?: string;
|
|
4
4
|
errorMessage?: string;
|
|
5
5
|
onRetry?: () => void;
|
|
6
|
-
}
|
|
6
|
+
}
|
|
7
7
|
|
|
8
8
|
function ErrorScreen({
|
|
9
9
|
title = 'Something went wrong',
|
|
@@ -148,4 +148,4 @@ const buttonSecondaryStyle: React.CSSProperties = {
|
|
|
148
148
|
border: '1px solid rgba(255,255,255,0.1)',
|
|
149
149
|
};
|
|
150
150
|
|
|
151
|
-
export default ErrorScreen;
|
|
151
|
+
export default ErrorScreen;
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import ErrorBoundary from './error-boundary/lib/provider'
|
|
2
|
-
import
|
|
1
|
+
import ErrorBoundary from './error-boundary/lib/provider';
|
|
2
|
+
import { useGetCurrentRouteConfig } from './router/lib/hooks/useGetCurrentRouteConfig';
|
|
3
|
+
import RouterProvider from './router/lib/provider';
|
|
4
|
+
import { routesConfig } from './router/model/config';
|
|
5
|
+
import { ERoutePath, IRouteConfig } from './router/types';
|
|
3
6
|
|
|
4
7
|
export {
|
|
5
|
-
Router
|
|
6
|
-
|
|
8
|
+
// Router
|
|
9
|
+
ERoutePath,
|
|
10
|
+
IRouteConfig,
|
|
11
|
+
routesConfig,
|
|
12
|
+
RouterProvider,
|
|
13
|
+
useGetCurrentRouteConfig,
|
|
14
|
+
|
|
15
|
+
// Error Boundary
|
|
16
|
+
ErrorBoundary,
|
|
7
17
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useLocation } from 'react-router-dom';
|
|
2
|
+
import { routesConfig } from '../../model/config';
|
|
3
|
+
import { IRouteConfig, ERoutePath } from '../../types';
|
|
4
|
+
|
|
5
|
+
export const useGetCurrentRouteConfig = (): IRouteConfig => {
|
|
6
|
+
const location = useLocation();
|
|
7
|
+
|
|
8
|
+
const firstPathSegment = location.pathname.split('/')[1] || '';
|
|
9
|
+
|
|
10
|
+
const matchedRoute = Object.values(routesConfig).find((route) => route.path === firstPathSegment);
|
|
11
|
+
|
|
12
|
+
return matchedRoute || routesConfig[ERoutePath.MAIN];
|
|
13
|
+
};
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ComponentType, ReactNode, useMemo } from 'react';
|
|
2
|
+
import { RouterProvider as RouterDomProvider } from 'react-router-dom';
|
|
3
|
+
import { getRouter } from '../../model/router';
|
|
4
|
+
|
|
5
|
+
interface RouterProviderProps {
|
|
6
|
+
errorBoundary: ComponentType<{ children: ReactNode }>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function RouterProvider({ errorBoundary }: RouterProviderProps) {
|
|
10
|
+
const router = useMemo(() => (
|
|
11
|
+
getRouter(errorBoundary)
|
|
12
|
+
), [errorBoundary]);
|
|
3
13
|
|
|
4
|
-
function Router() {
|
|
5
14
|
return (
|
|
6
|
-
<
|
|
15
|
+
<RouterDomProvider
|
|
16
|
+
router={router}
|
|
17
|
+
/>
|
|
7
18
|
);
|
|
8
19
|
}
|
|
9
20
|
|
|
10
|
-
export default
|
|
21
|
+
export default RouterProvider;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from 'react';
|
|
2
|
+
import { createBrowserRouter, createRoutesFromElements, Route } from 'react-router-dom';
|
|
3
|
+
import { ERoutePath } from '../../types';
|
|
4
|
+
import { Error } from '../../../../../pages/error';
|
|
5
|
+
import { Main } from '../../../../../pages/main';
|
|
6
|
+
/* @route-imports */
|
|
7
|
+
import AppLayout from '../../ui/app';
|
|
8
|
+
|
|
9
|
+
export const getRouter = (ErrorBoundary: ComponentType<{ children: ReactNode }>) => createBrowserRouter(
|
|
10
|
+
createRoutesFromElements(
|
|
11
|
+
<Route
|
|
12
|
+
path="/"
|
|
13
|
+
element={(
|
|
14
|
+
<ErrorBoundary>
|
|
15
|
+
<AppLayout />
|
|
16
|
+
</ErrorBoundary>
|
|
17
|
+
)}
|
|
18
|
+
>
|
|
19
|
+
<Route index element={<Main />} />
|
|
20
|
+
{/* @route-routes */}
|
|
21
|
+
<Route path={ERoutePath.ERROR} element={<Error />} />
|
|
22
|
+
</Route>,
|
|
23
|
+
),
|
|
24
|
+
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { NavLink, Outlet } from 'react-router-dom';
|
|
3
|
+
import styles from './styles.module.scss';
|
|
4
|
+
|
|
5
|
+
export default function AppLayout() {
|
|
6
|
+
return (
|
|
7
|
+
<div className={styles.layout_wrapper}>
|
|
8
|
+
<aside className={styles.sidebar}>
|
|
9
|
+
<nav>
|
|
10
|
+
<NavLink
|
|
11
|
+
className={({ isActive }) => clsx({
|
|
12
|
+
[styles.active]: isActive,
|
|
13
|
+
})}
|
|
14
|
+
to="/"
|
|
15
|
+
>
|
|
16
|
+
Main
|
|
17
|
+
</NavLink>
|
|
18
|
+
</nav>
|
|
19
|
+
<nav>
|
|
20
|
+
<NavLink
|
|
21
|
+
className={({ isActive }) => clsx({
|
|
22
|
+
[styles.active]: isActive,
|
|
23
|
+
})}
|
|
24
|
+
to="/error-route"
|
|
25
|
+
>
|
|
26
|
+
Error
|
|
27
|
+
</NavLink>
|
|
28
|
+
</nav>
|
|
29
|
+
</aside>
|
|
30
|
+
|
|
31
|
+
<main>
|
|
32
|
+
<Outlet />
|
|
33
|
+
</main>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StrictMode } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
3
|
import App from '@/app/App';
|
|
4
|
-
import {
|
|
4
|
+
import { RouterProvider, ErrorBoundary } from '@/app/providers';
|
|
5
5
|
import './styles/index.scss';
|
|
6
6
|
|
|
7
7
|
const container = document.getElementById('root');
|
|
@@ -12,8 +12,12 @@ if (!container) {
|
|
|
12
12
|
|
|
13
13
|
createRoot(container).render(
|
|
14
14
|
<StrictMode>
|
|
15
|
-
<App
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
<App
|
|
16
|
+
router={(
|
|
17
|
+
<RouterProvider
|
|
18
|
+
errorBoundary={ErrorBoundary}
|
|
19
|
+
/>
|
|
20
|
+
)}
|
|
21
|
+
/>
|
|
18
22
|
</StrictMode>,
|
|
19
23
|
);
|
|
@@ -6,7 +6,7 @@ import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
|
|
6
6
|
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
|
|
7
7
|
|
|
8
8
|
export function buildPlugins({ paths, isDev, apiUrl }: BuildOptions): webpack.WebpackPluginInstance[] {
|
|
9
|
-
const plugins = [
|
|
9
|
+
const plugins: webpack.WebpackPluginInstance[] = [
|
|
10
10
|
new HTMLWebpackPlugin({
|
|
11
11
|
template: paths.html,
|
|
12
12
|
}),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
*,
|
|
2
|
+
*::before,
|
|
3
|
+
*::after {
|
|
4
|
+
margin: 0;
|
|
5
|
+
padding: 0;
|
|
6
|
+
border: none;
|
|
7
|
+
list-style: none;
|
|
8
|
+
text-decoration: none;
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
color: inherit;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
body {
|
|
14
|
+
-webkit-font-smoothing: antialiased;
|
|
15
|
+
text-rendering: optimizelegibility;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
input,
|
|
19
|
+
button,
|
|
20
|
+
textarea {
|
|
21
|
+
font-size: inherit;
|
|
22
|
+
font-family: inherit;
|
|
23
|
+
|
|
24
|
+
&::placeholder {
|
|
25
|
+
user-select: none;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
button {
|
|
30
|
+
user-select: none;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
img {
|
|
35
|
+
user-select: none;
|
|
36
|
+
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import clsx from 'clsx';
|
|
2
|
-
import { NavLink, Outlet } from 'react-router-dom';
|
|
3
|
-
import { AppErrorBoundary } from '@/app/providers';
|
|
4
|
-
import styles from './styles.module.scss';
|
|
5
|
-
|
|
6
|
-
export default function AppLayout() {
|
|
7
|
-
return (
|
|
8
|
-
<AppErrorBoundary>
|
|
9
|
-
<div className={styles.layout_wrapper}>
|
|
10
|
-
<aside className={styles.sidebar}>
|
|
11
|
-
<nav>
|
|
12
|
-
<NavLink
|
|
13
|
-
className={({ isActive }) => clsx({
|
|
14
|
-
[styles.active]: isActive,
|
|
15
|
-
})}
|
|
16
|
-
to="/"
|
|
17
|
-
>
|
|
18
|
-
Main
|
|
19
|
-
</NavLink>
|
|
20
|
-
</nav>
|
|
21
|
-
<nav>
|
|
22
|
-
<NavLink
|
|
23
|
-
className={({ isActive }) => clsx({
|
|
24
|
-
[styles.active]: isActive,
|
|
25
|
-
})}
|
|
26
|
-
to="/error-route"
|
|
27
|
-
>
|
|
28
|
-
Error
|
|
29
|
-
</NavLink>
|
|
30
|
-
</nav>
|
|
31
|
-
</aside>
|
|
32
|
-
|
|
33
|
-
<main>
|
|
34
|
-
<Outlet />
|
|
35
|
-
</main>
|
|
36
|
-
</div>
|
|
37
|
-
</AppErrorBoundary>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { createBrowserRouter, createRoutesFromElements, Route } from 'react-router-dom';
|
|
2
|
-
import { Error } from '../../../../../pages/error';
|
|
3
|
-
import { Main } from '../../../../../pages/main';
|
|
4
|
-
import AppLayout from '../../../../layouts/app';
|
|
5
|
-
|
|
6
|
-
export const router = createBrowserRouter(
|
|
7
|
-
createRoutesFromElements(
|
|
8
|
-
<Route path="/" element={<AppLayout />}>
|
|
9
|
-
<Route index element={<Main />} />
|
|
10
|
-
<Route path="*" element={<Error />} />
|
|
11
|
-
</Route>,
|
|
12
|
-
),
|
|
13
|
-
);
|
|
File without changes
|