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.
Files changed (24) hide show
  1. package/README.md +35 -0
  2. package/package.json +1 -1
  3. package/src/core/parse-cli-args.js +1 -1
  4. package/src/core/render-project-readme.js +56 -14
  5. package/src/features/eslint/files/eslint.config.mjs +4 -3
  6. package/src/features/react-router/files/scripts/generate/page.mjs +341 -0
  7. package/src/features/react-router/files/src/app/App.tsx +3 -3
  8. package/src/features/react-router/files/src/app/providers/error-boundary/lib/provider/index.tsx +4 -4
  9. package/src/features/react-router/files/src/app/providers/error-boundary/ui/error-screen/index.tsx +3 -3
  10. package/src/features/react-router/files/src/app/providers/index.ts +14 -4
  11. package/src/features/react-router/files/src/app/providers/router/lib/hooks/useGetCurrentRouteConfig.ts +13 -0
  12. package/src/features/react-router/files/src/app/providers/router/lib/provider/index.tsx +16 -5
  13. package/src/features/react-router/files/src/app/providers/router/model/config/index.ts +9 -0
  14. package/src/features/react-router/files/src/app/providers/router/model/router/index.tsx +24 -0
  15. package/src/features/react-router/files/src/app/providers/router/types/index.ts +10 -0
  16. package/src/features/react-router/files/src/app/providers/router/ui/app/index.tsx +36 -0
  17. package/src/features/react-router/files/src/index.tsx +8 -4
  18. package/src/features/react-router/index.js +3 -0
  19. package/src/templates/base/config/build/buildPlugins.ts +1 -1
  20. package/src/templates/base/src/styles/index.scss +1 -1
  21. package/src/templates/base/src/styles/normalize.scss +36 -0
  22. package/src/features/react-router/files/src/app/layouts/app/index.tsx +0 -39
  23. package/src/features/react-router/files/src/app/providers/router/lib/router/index.tsx +0 -13
  24. /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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-alistt69-kit",
3
- "version": "0.1.20",
3
+ "version": "0.2.2",
4
4
  "description": "Opinionated React + TypeScript + Webpack project generator by alistt69",
5
5
  "keywords": [
6
6
  "create",
@@ -28,7 +28,7 @@ export function formatHelpMessage() {
28
28
  ' create-alistt69-kit <project-name> [options]',
29
29
  '',
30
30
  'Options:',
31
- ' -def, --defaults Skip prompts and use 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
- projectName,
109
- packageManager,
110
- shouldInstallDependencies,
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
- return [
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
- ].join('\n');
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
- projectPath,
143
- projectName,
144
- selectedFeatureIds,
145
- packageManager,
146
- shouldInstallDependencies,
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', // Not needed in React 17+
106
- 'react/jsx-uses-react': 'off', // Not needed in React 17+
107
- 'react/prop-types': 'off', // TypeScript handles this
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
- children: ReactNode;
6
+ router: ReactNode;
7
7
  }
8
8
 
9
- function App({ children }: AppProps) {
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
- {children}
18
+ {router}
19
19
  </div>
20
20
  );
21
21
  }
@@ -1,14 +1,14 @@
1
1
  import { Component, ErrorInfo, ReactNode } from 'react';
2
2
  import ErrorScreen from '../../ui/error-screen';
3
3
 
4
- type ErrorBoundaryProps = {
4
+ interface ErrorBoundaryProps {
5
5
  children: ReactNode;
6
- };
6
+ }
7
7
 
8
- type ErrorBoundaryState = {
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) {
@@ -1,9 +1,9 @@
1
- type ErrorScreenProps = {
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 Router from './router/lib/provider'
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 as AppRouter,
6
- ErrorBoundary as AppErrorBoundary,
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 { RouterProvider } from 'react-router-dom';
2
- import { router } from '../router';
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
- <RouterProvider router={router} />
15
+ <RouterDomProvider
16
+ router={router}
17
+ />
7
18
  );
8
19
  }
9
20
 
10
- export default Router;
21
+ export default RouterProvider;
@@ -0,0 +1,9 @@
1
+ import { ERoutePath, IRouteConfig } from '../../types';
2
+
3
+ export const routesConfig: Record<Exclude<ERoutePath, ERoutePath.ERROR>, IRouteConfig> = {
4
+ [ERoutePath.MAIN]: {
5
+ path: ERoutePath.MAIN,
6
+ title: 'Main',
7
+ },
8
+ // @route-config
9
+ };
@@ -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,10 @@
1
+ export enum ERoutePath {
2
+ MAIN = '',
3
+ // @route-enum
4
+ ERROR = '*',
5
+ }
6
+
7
+ export interface IRouteConfig {
8
+ path: ERoutePath;
9
+ title: string;
10
+ }
@@ -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 { AppRouter } from '@/app/providers';
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
- <AppRouter />
17
- </App>
15
+ <App
16
+ router={(
17
+ <RouterProvider
18
+ errorBoundary={ErrorBoundary}
19
+ />
20
+ )}
21
+ />
18
22
  </StrictMode>,
19
23
  );
@@ -14,6 +14,9 @@ export const reactRouterFeature = defineFeature({
14
14
  dependencies: {
15
15
  'react-router-dom': '^7.13.2',
16
16
  },
17
+ scripts: {
18
+ 'generate:page': 'node ./scripts/generate/page.mjs',
19
+ },
17
20
  },
18
21
  copyFiles: resolve(currentDirPath, 'files'),
19
22
  });
@@ -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
  }),
@@ -7,7 +7,7 @@ body,
7
7
  }
8
8
 
9
9
  body {
10
- font-family: Bahnschrift;
10
+ font-family: Bahnschrift, serif;
11
11
  background: #141617;
12
12
  color: #A6A8AAFF;
13
13
  }
@@ -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
- );