create-alistt69-kit 0.3.6 → 0.3.7

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 (62) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +189 -189
  3. package/bin/index.js +24 -24
  4. package/package.json +44 -44
  5. package/src/core/apply-features.js +14 -14
  6. package/src/core/collect-project-info.js +170 -170
  7. package/src/core/copy-base-template.js +11 -11
  8. package/src/core/create-project.js +99 -99
  9. package/src/core/install-dependencies.js +27 -27
  10. package/src/core/parse-cli-args.js +122 -122
  11. package/src/core/prepare-target-directory.js +69 -69
  12. package/src/core/render-project-readme.js +278 -278
  13. package/src/core/replace-tokens.js +45 -45
  14. package/src/core/restore-special-files.js +18 -18
  15. package/src/features/agents-md/files/AGENTS.md +36 -36
  16. package/src/features/agents-md/index.js +13 -13
  17. package/src/features/autoprefixer/files/postcss.config.cjs +4 -4
  18. package/src/features/autoprefixer/index.js +31 -31
  19. package/src/features/define-feature.js +32 -32
  20. package/src/features/eslint/files/eslint.config.mjs +135 -135
  21. package/src/features/eslint/index.js +43 -43
  22. package/src/features/index.js +28 -28
  23. package/src/features/prerender/files/prerender.routes.mjs +5 -5
  24. package/src/features/prerender/files/scripts/prerender.mjs +114 -114
  25. package/src/features/prerender/index.js +40 -40
  26. package/src/features/react-router/files/scripts/generate/page.mjs +366 -366
  27. package/src/features/react-router/files/src/app/App.tsx +18 -18
  28. package/src/features/react-router/files/src/app/providers/error-boundary/lib/provider/index.tsx +44 -44
  29. package/src/features/react-router/files/src/app/providers/error-boundary/ui/error-screen/index.tsx +151 -151
  30. package/src/features/react-router/files/src/app/providers/index.ts +17 -17
  31. package/src/features/react-router/files/src/app/providers/router/lib/provider/index.tsx +21 -21
  32. package/src/features/react-router/files/src/app/providers/router/model/router/index.tsx +24 -24
  33. package/src/features/react-router/files/src/app/providers/router/types/index.ts +10 -10
  34. package/src/features/react-router/files/src/app/providers/router/ui/app/index.tsx +36 -36
  35. package/src/features/react-router/files/src/index.tsx +23 -23
  36. package/src/features/react-router/files/src/pages/error/index.ts +1 -1
  37. package/src/features/react-router/files/src/pages/error/lazy.ts +3 -3
  38. package/src/features/react-router/files/src/pages/error/page.tsx +7 -7
  39. package/src/features/react-router/files/src/pages/main/index.ts +1 -1
  40. package/src/features/react-router/files/src/pages/main/lazy.ts +3 -3
  41. package/src/features/react-router/files/src/pages/main/page.tsx +7 -7
  42. package/src/features/react-router/index.js +36 -36
  43. package/src/features/stylelint/files/stylelint.config.mjs +13 -13
  44. package/src/features/stylelint/index.js +37 -37
  45. package/src/templates/base/.editorconfig +11 -11
  46. package/src/templates/base/README.md +2 -2
  47. package/src/templates/base/babel.config.json +12 -12
  48. package/src/templates/base/gitignore +27 -27
  49. package/src/templates/base/package.json +48 -48
  50. package/src/templates/base/public/index.html +12 -12
  51. package/src/templates/base/src/app/App.tsx +12 -12
  52. package/src/templates/base/src/app/providers/error-boundary/lib/provider/index.tsx +44 -44
  53. package/src/templates/base/src/app/providers/error-boundary/ui/error-screen/index.tsx +150 -150
  54. package/src/templates/base/src/app/providers/index.ts +5 -5
  55. package/src/templates/base/src/index.tsx +19 -19
  56. package/src/templates/base/src/styles/index.scss +13 -13
  57. package/src/templates/base/src/styles/normalize.scss +36 -36
  58. package/src/templates/base/tsconfig.json +25 -25
  59. package/src/utils/agents-md.js +52 -52
  60. package/src/utils/console-format.js +11 -11
  61. package/src/utils/package-json.js +96 -96
  62. package/src/utils/package-manager.js +22 -22
@@ -1,367 +1,367 @@
1
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- // todo: eslint ignore
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
- const PRERENDER_ROUTES_PATH = 'prerender.routes.mjs';
34
-
35
- async function main() {
36
- const rawPageName = process.argv[2];
37
-
38
- if (!rawPageName) {
39
- printUsageAndExit();
40
- }
41
-
42
- const pageSlug = normalizePageSlug(rawPageName);
43
-
44
- if (!pageSlug) {
45
- fail('Page name cannot be empty.');
46
- }
47
-
48
- const pageDirPath = path.join(process.cwd(), 'src', 'pages', pageSlug);
49
-
50
- if (await pathExists(pageDirPath)) {
51
- fail(`Page "${pageSlug}" already exists: ${toRelativeProjectPath(pageDirPath)}`);
52
- }
53
-
54
- const pageMeta = buildPageMeta(pageSlug);
55
-
56
- await createPageFiles(pageDirPath, pageMeta);
57
-
58
- const autoRegisterResult = await autoRegisterPage(pageMeta);
59
-
60
- console.log(`✔ Page "${pageSlug}" created.`);
61
-
62
- if (autoRegisterResult.status === 'registered') {
63
- console.log('✔ Route registration updated.');
64
- return;
65
- }
66
-
67
- console.log(`ℹ ${autoRegisterResult.message}`);
68
- }
69
-
70
- function printUsageAndExit() {
71
- console.error('Usage: npm run generate:page -- <page-name>');
72
- console.error('Example: npm run generate:page -- about');
73
- process.exit(1);
74
- }
75
-
76
- function fail(message) {
77
- console.error(`✖ ${message}`);
78
- process.exit(1);
79
- }
80
-
81
- function normalizePageSlug(value) {
82
- return value
83
- .trim()
84
- .toLowerCase()
85
- .replace(/[_\s]+/g, '-')
86
- .replace(/[^a-z0-9-]/g, '')
87
- .replace(/-+/g, '-')
88
- .replace(/^-+|-+$/g, '');
89
- }
90
-
91
- function buildPageMeta(pageSlug) {
92
- return {
93
- pageSlug,
94
- pageComponentName: toPascalCase(pageSlug),
95
- pageTitle: toTitleCase(pageSlug),
96
- routeEnumKey: toScreamingSnake(pageSlug),
97
- };
98
- }
99
-
100
- function toPascalCase(value) {
101
- return value
102
- .split('-')
103
- .filter(Boolean)
104
- .map((part) => part[0].toUpperCase() + part.slice(1))
105
- .join('');
106
- }
107
-
108
- function toScreamingSnake(value) {
109
- return value
110
- .split('-')
111
- .filter(Boolean)
112
- .join('_')
113
- .toUpperCase();
114
- }
115
-
116
- function toTitleCase(value) {
117
- return value
118
- .split('-')
119
- .filter(Boolean)
120
- .map((part) => part[0].toUpperCase() + part.slice(1))
121
- .join(' ');
122
- }
123
-
124
- async function createPageFiles(pageDirPath, pageMeta) {
125
- const { pageComponentName, pageTitle } = pageMeta;
126
-
127
- await mkdir(pageDirPath, {
128
- recursive: true,
129
- });
130
-
131
- await writeFile(
132
- path.join(pageDirPath, 'index.ts'),
133
- `export { Lazy${pageComponentName} as ${pageComponentName} } from './lazy';\n`,
134
- 'utf8',
135
- );
136
-
137
- await writeFile(
138
- path.join(pageDirPath, 'lazy.ts'),
139
- [
140
- "import { lazy } from 'react';",
141
- '',
142
- `export const Lazy${pageComponentName} = lazy(() => import('./page'));`,
143
- '',
144
- ].join('\n'),
145
- 'utf8',
146
- );
147
-
148
- await writeFile(
149
- path.join(pageDirPath, 'page.tsx'),
150
- [
151
- `function ${pageComponentName}Page() {`,
152
- ' return (',
153
- ` <h2>${pageTitle} page</h2>`,
154
- ' );',
155
- '}',
156
- '',
157
- `export default ${pageComponentName}Page;`,
158
- '',
159
- ].join('\n'),
160
- 'utf8',
161
- );
162
- }
163
-
164
- async function autoRegisterPage(pageMeta) {
165
- const projectRoot = process.cwd();
166
-
167
- const typesFilePath = path.join(projectRoot, ROUTER_TYPES_PATH);
168
- const configFilePath = path.join(projectRoot, ROUTER_CONFIG_PATH);
169
- const routerFilePath = path.join(projectRoot, ROUTER_FILE_PATH);
170
- const prerenderRoutesFilePath = path.join(projectRoot, PRERENDER_ROUTES_PATH);
171
-
172
- const [hasTypesFile, hasConfigFile, hasRouterFile, hasPrerenderRoutesFile] = await Promise.all([
173
- pathExists(typesFilePath),
174
- pathExists(configFilePath),
175
- pathExists(routerFilePath),
176
- pathExists(prerenderRoutesFilePath),
177
- ]);
178
-
179
- const updates = [];
180
-
181
- if (hasTypesFile && hasConfigFile && hasRouterFile) {
182
- await insertRouteEnum(typesFilePath, pageMeta);
183
- await insertRouteConfig(configFilePath, pageMeta);
184
- await insertRouteImport(routerFilePath, pageMeta);
185
- await insertRouteDefinition(routerFilePath, pageMeta);
186
-
187
- updates.push('router');
188
- }
189
-
190
- if (hasPrerenderRoutesFile) {
191
- await insertPrerenderRoute(prerenderRoutesFilePath, pageMeta);
192
- updates.push('prerender');
193
- }
194
-
195
- if (updates.length === 0) {
196
- return {
197
- status: 'skipped',
198
- message: [
199
- 'Route auto-registration skipped.',
200
- `Missing files: ${[
201
- ROUTER_TYPES_PATH,
202
- ROUTER_CONFIG_PATH,
203
- ROUTER_FILE_PATH,
204
- PRERENDER_ROUTES_PATH,
205
- ].join(', ')}`,
206
- ].join(' '),
207
- };
208
- }
209
-
210
- return {
211
- status: 'registered',
212
- message: `Updated: ${updates.join(', ')}`,
213
- };
214
- }
215
-
216
- async function insertRouteEnum(filepath, pageMeta) {
217
- await insertBeforeMarkerLine({
218
- filepath,
219
- marker: '// @route-enum',
220
- block: `${pageMeta.routeEnumKey} = '${pageMeta.pageSlug}',`,
221
- });
222
- }
223
-
224
- async function insertRouteConfig(filepath, pageMeta) {
225
- await insertBeforeMarkerLine({
226
- filepath,
227
- marker: '// @route-config',
228
- block: [
229
- `[ERoutePath.${pageMeta.routeEnumKey}]: {`,
230
- ` path: ERoutePath.${pageMeta.routeEnumKey},`,
231
- ` title: '${pageMeta.pageTitle}',`,
232
- '},',
233
- ].join('\n'),
234
- });
235
- }
236
-
237
- async function insertRouteImport(filepath, pageMeta) {
238
- await insertBeforeMarkerLine({
239
- filepath,
240
- marker: '/* @route-imports */',
241
- block: `import { ${pageMeta.pageComponentName} } from '../../../../../pages/${pageMeta.pageSlug}';`,
242
- });
243
- }
244
-
245
- async function insertRouteDefinition(filepath, pageMeta) {
246
- await insertBeforeMarkerLine({
247
- filepath,
248
- marker: '{/* @route-routes */}',
249
- block: `<Route path={ERoutePath.${pageMeta.routeEnumKey}} element={<${pageMeta.pageComponentName} />} />`,
250
- });
251
- }
252
-
253
- async function insertPrerenderRoute(filepath, pageMeta) {
254
- await insertBeforeMarkerLine({
255
- filepath,
256
- marker: '// @prerender-routes',
257
- block: `'/${pageMeta.pageSlug}',`,
258
- });
259
- }
260
-
261
- async function insertBeforeMarkerLine({ filepath, marker, block }) {
262
- const fileContent = await readFile(filepath, 'utf8');
263
- const eol = detectEol(fileContent);
264
- const hadTrailingNewline = /\r?\n$/.test(fileContent);
265
-
266
- const lines = fileContent.split(/\r?\n/);
267
-
268
- if (hadTrailingNewline && lines.at(-1) === '') {
269
- lines.pop();
270
- }
271
-
272
- const markerLineIndex = lines.findIndex((line) => line.includes(marker));
273
-
274
- if (markerLineIndex === -1) {
275
- fail(`Marker "${marker}" not found in ${toRelativeProjectPath(filepath)}`);
276
- }
277
-
278
- const markerLine = lines[markerLineIndex];
279
- const baseIndent = getLineIndent(markerLine);
280
- const normalizedBlock = indentBlock(block, baseIndent);
281
- const normalizedBlockLines = normalizedBlock.split('\n');
282
-
283
- if (blockAlreadyInserted(lines, normalizedBlockLines)) {
284
- return;
285
- }
286
-
287
- lines.splice(markerLineIndex, 0, ...normalizedBlockLines);
288
-
289
- let nextContent = lines.join(eol);
290
-
291
- if (hadTrailingNewline) {
292
- nextContent += eol;
293
- }
294
-
295
- await writeFile(filepath, nextContent, 'utf8');
296
- }
297
-
298
- function detectEol(content) {
299
- return content.includes('\r\n') ? '\r\n' : '\n';
300
- }
301
-
302
- function getLineIndent(line) {
303
- const match = line.match(/^\s*/);
304
-
305
- return match ? match[0] : '';
306
- }
307
-
308
- function indentBlock(block, baseIndent) {
309
- const rawLines = block.split('\n');
310
-
311
- return rawLines
312
- .map((line) => {
313
- if (!line.trim()) {
314
- return '';
315
- }
316
-
317
- const lineIndentSize = countLeadingSpaces(line);
318
- const relativeIndentLevel = Math.floor(lineIndentSize / 4);
319
- const relativeIndent = ' '.repeat(relativeIndentLevel * 4);
320
-
321
- return `${baseIndent}${relativeIndent}${line.trimStart()}`;
322
- })
323
- .join('\n');
324
- }
325
-
326
- function countLeadingSpaces(line) {
327
- const match = line.match(/^ */);
328
-
329
- return match ? match[0].length : 0;
330
- }
331
-
332
- function blockAlreadyInserted(lines, normalizedBlockLines) {
333
- const comparableLines = normalizedBlockLines.filter((line) => line.trim() !== '');
334
-
335
- if (comparableLines.length === 0) {
336
- return false;
337
- }
338
-
339
- for (let index = 0; index <= lines.length - comparableLines.length; index += 1) {
340
- const currentSlice = lines.slice(index, index + comparableLines.length);
341
-
342
- if (currentSlice.join('\n') === comparableLines.join('\n')) {
343
- return true;
344
- }
345
- }
346
-
347
- return false;
348
- }
349
-
350
- async function pathExists(filepath) {
351
- try {
352
- await access(filepath);
353
- return true;
354
- } catch (_) {
355
- return false;
356
- }
357
- }
358
-
359
- function toRelativeProjectPath(filepath) {
360
- return path.relative(process.cwd(), filepath) || '.';
361
- }
362
-
363
- main().catch((error) => {
364
- const message = error instanceof Error ? error.message : String(error);
365
-
366
- fail(message);
1
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ // todo: eslint ignore
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
+ const PRERENDER_ROUTES_PATH = 'prerender.routes.mjs';
34
+
35
+ async function main() {
36
+ const rawPageName = process.argv[2];
37
+
38
+ if (!rawPageName) {
39
+ printUsageAndExit();
40
+ }
41
+
42
+ const pageSlug = normalizePageSlug(rawPageName);
43
+
44
+ if (!pageSlug) {
45
+ fail('Page name cannot be empty.');
46
+ }
47
+
48
+ const pageDirPath = path.join(process.cwd(), 'src', 'pages', pageSlug);
49
+
50
+ if (await pathExists(pageDirPath)) {
51
+ fail(`Page "${pageSlug}" already exists: ${toRelativeProjectPath(pageDirPath)}`);
52
+ }
53
+
54
+ const pageMeta = buildPageMeta(pageSlug);
55
+
56
+ await createPageFiles(pageDirPath, pageMeta);
57
+
58
+ const autoRegisterResult = await autoRegisterPage(pageMeta);
59
+
60
+ console.log(`✔ Page "${pageSlug}" created.`);
61
+
62
+ if (autoRegisterResult.status === 'registered') {
63
+ console.log('✔ Route registration updated.');
64
+ return;
65
+ }
66
+
67
+ console.log(`ℹ ${autoRegisterResult.message}`);
68
+ }
69
+
70
+ function printUsageAndExit() {
71
+ console.error('Usage: npm run generate:page -- <page-name>');
72
+ console.error('Example: npm run generate:page -- about');
73
+ process.exit(1);
74
+ }
75
+
76
+ function fail(message) {
77
+ console.error(`✖ ${message}`);
78
+ process.exit(1);
79
+ }
80
+
81
+ function normalizePageSlug(value) {
82
+ return value
83
+ .trim()
84
+ .toLowerCase()
85
+ .replace(/[_\s]+/g, '-')
86
+ .replace(/[^a-z0-9-]/g, '')
87
+ .replace(/-+/g, '-')
88
+ .replace(/^-+|-+$/g, '');
89
+ }
90
+
91
+ function buildPageMeta(pageSlug) {
92
+ return {
93
+ pageSlug,
94
+ pageComponentName: toPascalCase(pageSlug),
95
+ pageTitle: toTitleCase(pageSlug),
96
+ routeEnumKey: toScreamingSnake(pageSlug),
97
+ };
98
+ }
99
+
100
+ function toPascalCase(value) {
101
+ return value
102
+ .split('-')
103
+ .filter(Boolean)
104
+ .map((part) => part[0].toUpperCase() + part.slice(1))
105
+ .join('');
106
+ }
107
+
108
+ function toScreamingSnake(value) {
109
+ return value
110
+ .split('-')
111
+ .filter(Boolean)
112
+ .join('_')
113
+ .toUpperCase();
114
+ }
115
+
116
+ function toTitleCase(value) {
117
+ return value
118
+ .split('-')
119
+ .filter(Boolean)
120
+ .map((part) => part[0].toUpperCase() + part.slice(1))
121
+ .join(' ');
122
+ }
123
+
124
+ async function createPageFiles(pageDirPath, pageMeta) {
125
+ const { pageComponentName, pageTitle } = pageMeta;
126
+
127
+ await mkdir(pageDirPath, {
128
+ recursive: true,
129
+ });
130
+
131
+ await writeFile(
132
+ path.join(pageDirPath, 'index.ts'),
133
+ `export { Lazy${pageComponentName} as ${pageComponentName} } from './lazy';\n`,
134
+ 'utf8',
135
+ );
136
+
137
+ await writeFile(
138
+ path.join(pageDirPath, 'lazy.ts'),
139
+ [
140
+ "import { lazy } from 'react';",
141
+ '',
142
+ `export const Lazy${pageComponentName} = lazy(() => import('./page'));`,
143
+ '',
144
+ ].join('\n'),
145
+ 'utf8',
146
+ );
147
+
148
+ await writeFile(
149
+ path.join(pageDirPath, 'page.tsx'),
150
+ [
151
+ `function ${pageComponentName}Page() {`,
152
+ ' return (',
153
+ ` <h2>${pageTitle} page</h2>`,
154
+ ' );',
155
+ '}',
156
+ '',
157
+ `export default ${pageComponentName}Page;`,
158
+ '',
159
+ ].join('\n'),
160
+ 'utf8',
161
+ );
162
+ }
163
+
164
+ async function autoRegisterPage(pageMeta) {
165
+ const projectRoot = process.cwd();
166
+
167
+ const typesFilePath = path.join(projectRoot, ROUTER_TYPES_PATH);
168
+ const configFilePath = path.join(projectRoot, ROUTER_CONFIG_PATH);
169
+ const routerFilePath = path.join(projectRoot, ROUTER_FILE_PATH);
170
+ const prerenderRoutesFilePath = path.join(projectRoot, PRERENDER_ROUTES_PATH);
171
+
172
+ const [hasTypesFile, hasConfigFile, hasRouterFile, hasPrerenderRoutesFile] = await Promise.all([
173
+ pathExists(typesFilePath),
174
+ pathExists(configFilePath),
175
+ pathExists(routerFilePath),
176
+ pathExists(prerenderRoutesFilePath),
177
+ ]);
178
+
179
+ const updates = [];
180
+
181
+ if (hasTypesFile && hasConfigFile && hasRouterFile) {
182
+ await insertRouteEnum(typesFilePath, pageMeta);
183
+ await insertRouteConfig(configFilePath, pageMeta);
184
+ await insertRouteImport(routerFilePath, pageMeta);
185
+ await insertRouteDefinition(routerFilePath, pageMeta);
186
+
187
+ updates.push('router');
188
+ }
189
+
190
+ if (hasPrerenderRoutesFile) {
191
+ await insertPrerenderRoute(prerenderRoutesFilePath, pageMeta);
192
+ updates.push('prerender');
193
+ }
194
+
195
+ if (updates.length === 0) {
196
+ return {
197
+ status: 'skipped',
198
+ message: [
199
+ 'Route auto-registration skipped.',
200
+ `Missing files: ${[
201
+ ROUTER_TYPES_PATH,
202
+ ROUTER_CONFIG_PATH,
203
+ ROUTER_FILE_PATH,
204
+ PRERENDER_ROUTES_PATH,
205
+ ].join(', ')}`,
206
+ ].join(' '),
207
+ };
208
+ }
209
+
210
+ return {
211
+ status: 'registered',
212
+ message: `Updated: ${updates.join(', ')}`,
213
+ };
214
+ }
215
+
216
+ async function insertRouteEnum(filepath, pageMeta) {
217
+ await insertBeforeMarkerLine({
218
+ filepath,
219
+ marker: '// @route-enum',
220
+ block: `${pageMeta.routeEnumKey} = '${pageMeta.pageSlug}',`,
221
+ });
222
+ }
223
+
224
+ async function insertRouteConfig(filepath, pageMeta) {
225
+ await insertBeforeMarkerLine({
226
+ filepath,
227
+ marker: '// @route-config',
228
+ block: [
229
+ `[ERoutePath.${pageMeta.routeEnumKey}]: {`,
230
+ ` path: ERoutePath.${pageMeta.routeEnumKey},`,
231
+ ` title: '${pageMeta.pageTitle}',`,
232
+ '},',
233
+ ].join('\n'),
234
+ });
235
+ }
236
+
237
+ async function insertRouteImport(filepath, pageMeta) {
238
+ await insertBeforeMarkerLine({
239
+ filepath,
240
+ marker: '/* @route-imports */',
241
+ block: `import { ${pageMeta.pageComponentName} } from '../../../../../pages/${pageMeta.pageSlug}';`,
242
+ });
243
+ }
244
+
245
+ async function insertRouteDefinition(filepath, pageMeta) {
246
+ await insertBeforeMarkerLine({
247
+ filepath,
248
+ marker: '{/* @route-routes */}',
249
+ block: `<Route path={ERoutePath.${pageMeta.routeEnumKey}} element={<${pageMeta.pageComponentName} />} />`,
250
+ });
251
+ }
252
+
253
+ async function insertPrerenderRoute(filepath, pageMeta) {
254
+ await insertBeforeMarkerLine({
255
+ filepath,
256
+ marker: '// @prerender-routes',
257
+ block: `'/${pageMeta.pageSlug}',`,
258
+ });
259
+ }
260
+
261
+ async function insertBeforeMarkerLine({ filepath, marker, block }) {
262
+ const fileContent = await readFile(filepath, 'utf8');
263
+ const eol = detectEol(fileContent);
264
+ const hadTrailingNewline = /\r?\n$/.test(fileContent);
265
+
266
+ const lines = fileContent.split(/\r?\n/);
267
+
268
+ if (hadTrailingNewline && lines.at(-1) === '') {
269
+ lines.pop();
270
+ }
271
+
272
+ const markerLineIndex = lines.findIndex((line) => line.includes(marker));
273
+
274
+ if (markerLineIndex === -1) {
275
+ fail(`Marker "${marker}" not found in ${toRelativeProjectPath(filepath)}`);
276
+ }
277
+
278
+ const markerLine = lines[markerLineIndex];
279
+ const baseIndent = getLineIndent(markerLine);
280
+ const normalizedBlock = indentBlock(block, baseIndent);
281
+ const normalizedBlockLines = normalizedBlock.split('\n');
282
+
283
+ if (blockAlreadyInserted(lines, normalizedBlockLines)) {
284
+ return;
285
+ }
286
+
287
+ lines.splice(markerLineIndex, 0, ...normalizedBlockLines);
288
+
289
+ let nextContent = lines.join(eol);
290
+
291
+ if (hadTrailingNewline) {
292
+ nextContent += eol;
293
+ }
294
+
295
+ await writeFile(filepath, nextContent, 'utf8');
296
+ }
297
+
298
+ function detectEol(content) {
299
+ return content.includes('\r\n') ? '\r\n' : '\n';
300
+ }
301
+
302
+ function getLineIndent(line) {
303
+ const match = line.match(/^\s*/);
304
+
305
+ return match ? match[0] : '';
306
+ }
307
+
308
+ function indentBlock(block, baseIndent) {
309
+ const rawLines = block.split('\n');
310
+
311
+ return rawLines
312
+ .map((line) => {
313
+ if (!line.trim()) {
314
+ return '';
315
+ }
316
+
317
+ const lineIndentSize = countLeadingSpaces(line);
318
+ const relativeIndentLevel = Math.floor(lineIndentSize / 4);
319
+ const relativeIndent = ' '.repeat(relativeIndentLevel * 4);
320
+
321
+ return `${baseIndent}${relativeIndent}${line.trimStart()}`;
322
+ })
323
+ .join('\n');
324
+ }
325
+
326
+ function countLeadingSpaces(line) {
327
+ const match = line.match(/^ */);
328
+
329
+ return match ? match[0].length : 0;
330
+ }
331
+
332
+ function blockAlreadyInserted(lines, normalizedBlockLines) {
333
+ const comparableLines = normalizedBlockLines.filter((line) => line.trim() !== '');
334
+
335
+ if (comparableLines.length === 0) {
336
+ return false;
337
+ }
338
+
339
+ for (let index = 0; index <= lines.length - comparableLines.length; index += 1) {
340
+ const currentSlice = lines.slice(index, index + comparableLines.length);
341
+
342
+ if (currentSlice.join('\n') === comparableLines.join('\n')) {
343
+ return true;
344
+ }
345
+ }
346
+
347
+ return false;
348
+ }
349
+
350
+ async function pathExists(filepath) {
351
+ try {
352
+ await access(filepath);
353
+ return true;
354
+ } catch (_) {
355
+ return false;
356
+ }
357
+ }
358
+
359
+ function toRelativeProjectPath(filepath) {
360
+ return path.relative(process.cwd(), filepath) || '.';
361
+ }
362
+
363
+ main().catch((error) => {
364
+ const message = error instanceof Error ? error.message : String(error);
365
+
366
+ fail(message);
367
367
  });