create-alistt69-kit 0.1.22 → 0.2.3

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 CHANGED
@@ -53,15 +53,44 @@ Setting up a frontend project from scratch usually means repeating the same stuf
53
53
 
54
54
  This starter removes that boilerplate so you can get straight to building.
55
55
 
56
- ---
56
+ ## ⚡ Generated project helpers
57
+
58
+ Some optional features also add local project generators to the scaffolded app.
59
+
60
+ ### Page generator
61
+
62
+ When `React Router DOM` is enabled, the project includes a built-in page generator.
63
+
64
+ ```bash
65
+ npm run generate:page about
66
+ ```
67
+ This will create:
68
+
69
+ * `src/pages/about/index.ts`
70
+ * `src/pages/about/lazy.ts`
71
+ * `src/pages/about/page.tsx`
72
+
73
+ If the standard router files are present, the generator will also register the page automatically in:
74
+
75
+ * `src/app/providers/router/types/index.ts`
76
+ * `src/app/providers/router/model/config/index.ts`
77
+ * `src/app/providers/router/model/router/index.tsx`
78
+
79
+ You can also configure custom paths in `scripts/generate/page.mjs`:
80
+ * `ROUTER_TYPES_PATH`
81
+ * `ROUTER_CONFIG_PATH`
82
+ * `ROUTER_FILE_PATH`
83
+
84
+ #### Important
85
+
86
+ Route auto-registration relies on special marker comments `@route-...` inside the router files.
87
+ Do not remove these markers unless you also want to disable automatic updates.
57
88
 
58
89
  ## 📦 Requirements
59
90
 
60
91
  - **Node.js** `18.18` or higher
61
92
  - **npm**, **pnpm**, or **yarn**
62
93
 
63
- ---
64
-
65
94
  ## 🔥 Quick start
66
95
 
67
96
  Create a new app interactively:
@@ -121,3 +150,5 @@ npm create alistt69-kit@latest my-app -- --defaults --overwrite
121
150
  ## 📄 License
122
151
 
123
152
  MIT — free and open for everyone.
153
+
154
+ _See [LICENSE](./LICENSE)._
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-alistt69-kit",
3
- "version": "0.1.22",
3
+ "version": "0.2.3",
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(
@@ -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
+ });
@@ -5,4 +5,5 @@ export const routesConfig: Record<Exclude<ERoutePath, ERoutePath.ERROR>, IRouteC
5
5
  path: ERoutePath.MAIN,
6
6
  title: 'Main',
7
7
  },
8
+ // @route-config
8
9
  };
@@ -1,7 +1,9 @@
1
1
  import { ComponentType, ReactNode } from 'react';
2
2
  import { createBrowserRouter, createRoutesFromElements, Route } from 'react-router-dom';
3
+ import { ERoutePath } from '../../types';
3
4
  import { Error } from '../../../../../pages/error';
4
5
  import { Main } from '../../../../../pages/main';
6
+ /* @route-imports */
5
7
  import AppLayout from '../../ui/app';
6
8
 
7
9
  export const getRouter = (ErrorBoundary: ComponentType<{ children: ReactNode }>) => createBrowserRouter(
@@ -15,7 +17,8 @@ export const getRouter = (ErrorBoundary: ComponentType<{ children: ReactNode }>)
15
17
  )}
16
18
  >
17
19
  <Route index element={<Main />} />
18
- <Route path="*" element={<Error />} />
20
+ {/* @route-routes */}
21
+ <Route path={ERoutePath.ERROR} element={<Error />} />
19
22
  </Route>,
20
23
  ),
21
24
  );
@@ -1,5 +1,6 @@
1
1
  export enum ERoutePath {
2
2
  MAIN = '',
3
+ // @route-enum
3
4
  ERROR = '*',
4
5
  }
5
6
 
@@ -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
  });