create-react-adam 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,11 +5,23 @@ Create opinionated React apps with TypeScript, Vite, Wouter, and Tailwind CSS.
5
5
  ## Quick Start
6
6
 
7
7
  ```bash
8
- npx create-react-adam@latest my-app --with-e2e --with-utils
8
+ npx create-react-adam@latest my-app --yes
9
9
  cd my-app
10
10
  npm run dev
11
11
  ```
12
12
 
13
+ Running without flags opens an interactive wizard where you pick features
14
+ (all pre-selected by default):
15
+
16
+ ```
17
+ ◆ Select features
18
+ │ ◼ E2E testing (Playwright + Allure)
19
+ │ ◼ Utility functions (classNames, Storage, useUrlState, safeTimeout)
20
+ │ ◼ Lighthouse CI workflow (a11y/SEO/performance budgets)
21
+ │ ◼ ESLint rule: prefer WebP images
22
+
23
+ ```
24
+
13
25
  ## CLI Options
14
26
 
15
27
  - `--dir <path>` - Create the project in a specific directory
@@ -18,16 +30,21 @@ npm run dev
18
30
  npm create react-adam@latest my-app --dir ~/projects
19
31
  ```
20
32
 
21
- - `--with-e2e` - Include E2E testing setup without prompting
33
+ - `--yes` - Skip the wizard and include every feature not explicitly disabled
34
+ by a flag. This is also the behavior when there is no TTY (CI, scripts, AI
35
+ agents), so the CLI never hangs waiting for input.
22
36
 
23
37
  ```bash
24
- npm create react-adam@latest my-app --with-e2e
38
+ npm create react-adam@latest my-app --yes --no-e2e
25
39
  ```
26
40
 
27
- - `--no-e2e` - Skip E2E testing setup without prompting
28
- ```bash
29
- npm create react-adam@latest my-app --no-e2e
30
- ```
41
+ - `--with-e2e` / `--no-e2e` - Include or skip E2E testing (Playwright + Allure)
42
+ - `--with-utils` / `--no-utils` - Include or skip utility functions
43
+ - `--with-lighthouse` / `--no-lighthouse` - Include or skip the Lighthouse CI workflow
44
+ - `--with-webp-lint` / `--no-webp-lint` - Include or skip the prefer-webp-images ESLint rule
45
+
46
+ Any feature answered by a flag is removed from the wizard; if all four are
47
+ answered, the wizard is skipped entirely.
31
48
 
32
49
  ## What's Included
33
50
 
@@ -39,6 +56,21 @@ npm run dev
39
56
  - **Tailwind CSS 4** - Utility-first CSS
40
57
  - **React Icons** - Popular icon library
41
58
 
59
+ ### Performance, SEO, and Accessibility Baseline
60
+
61
+ Every generated project starts with:
62
+
63
+ - **Self-hosted Inter font** - variable `woff2` served from `public/fonts/`
64
+ with `font-display: swap` and a preload hint; no third-party font requests
65
+ - **Route-level code splitting with preloading** - pages load via
66
+ `React.lazy` + `Suspense`; chunks preload on link hover/focus/touch and an
67
+ idle prefetcher warms the rest after first paint (documented in the
68
+ generated README, easy to remove)
69
+ - **SEO-ready `index.html`** - meta description, Open Graph and Twitter card
70
+ tags, theme-color, plus a `public/robots.txt`
71
+ - **Accessible shell** - skip-to-content link, `<main>` landmark, visible
72
+ `:focus-visible` styles, and per-route document titles
73
+
42
74
  ### Code Quality Tools
43
75
 
44
76
  #### ESLint
@@ -50,8 +82,9 @@ Configured with a modern flat config (`eslint.config.js`) that includes:
50
82
  - **React Refresh** - Validates fast refresh compatibility
51
83
  - **JSX Accessibility** - Enforces accessibility best practices (a11y)
52
84
  - **Prettier integration** - Disables conflicting formatting rules
53
-
54
- Why ESLint? It catches bugs early, enforces consistent code patterns, and integrates seamlessly with TypeScript. The strict preset helps maintain high code quality standards.
85
+ - **prefer-webp-images** (optional) - a local custom rule that errors on
86
+ references to `.png`/`.jpg`/`.jpeg`/`.gif`/`.bmp` images and suggests
87
+ converting them at [tools.sturge.dev/webp](https://tools.sturge.dev/webp)
55
88
 
56
89
  #### Prettier
57
90
 
@@ -60,17 +93,24 @@ Configured with two powerful plugins:
60
93
  - **prettier-plugin-organize-imports** - Automatically sorts and removes unused imports
61
94
  - **prettier-plugin-tailwindcss** - Sorts Tailwind class names consistently
62
95
 
63
- Why Prettier? It eliminates debates about code style by automatically formatting code. The organize-imports plugin keeps imports clean, and the Tailwind plugin ensures class names follow the recommended ordering.
64
-
65
96
  ### Optional E2E Testing
66
97
 
67
- When you include E2E testing (via prompt or `--with-e2e` flag), you get:
98
+ When you include E2E testing (via wizard or `--with-e2e` flag), you get:
68
99
 
69
100
  - **Playwright** - Modern, reliable E2E testing framework
70
101
  - **Allure Reports** - Beautiful, detailed test reports
71
102
  - Pre-configured test setup with example tests
72
103
  - HTML reports and Allure integration
73
104
 
105
+ ### Git Integration
106
+
107
+ After scaffolding (when not already inside a git repository), the CLI:
108
+
109
+ 1. Runs `git init`
110
+ 2. Installs a pre-commit hook that formats and lints staged files
111
+ (the same hook as `npm run setFormatToPrecommitHook`)
112
+ 3. Creates an initial commit
113
+
74
114
  ### Dependency Management
75
115
 
76
116
  - **Exact version pinning** - All dependencies use exact versions (no `^` or `~`)
@@ -79,7 +119,7 @@ When you include E2E testing (via prompt or `--with-e2e` flag), you get:
79
119
 
80
120
  ### GitHub Actions Workflows
81
121
 
82
- Two CI workflows are included in the generated projects:
122
+ Up to three CI workflows are included in generated projects:
83
123
 
84
124
  1. **Code Checks** (`.github/workflows/check.yml`)
85
125
 
@@ -89,10 +129,16 @@ Two CI workflows are included in the generated projects:
89
129
  - Production build verification
90
130
 
91
131
  2. **E2E Tests** (`.github/workflows/e2e.yml`) - if E2E is included
132
+
92
133
  - Runs Playwright tests in CI
93
134
  - Uploads test reports as artifacts
94
135
  - Configures Allure results
95
136
 
137
+ 3. **Lighthouse CI** (`.github/workflows/lighthouse.yml`) - if Lighthouse is included
138
+ - Builds the app and audits it with Lighthouse
139
+ - Fails CI below score budgets: accessibility ≥ 95, SEO ≥ 95, performance ≥ 85
140
+ - Budgets live in `lighthouserc.json`
141
+
96
142
  ## Available Scripts
97
143
 
98
144
  ### Development
@@ -155,14 +201,23 @@ Runs basic troubleshooting checks:
155
201
 
156
202
  ```
157
203
  my-app/
204
+ ├── public/
205
+ │ ├── fonts/ # Self-hosted Inter (variable woff2)
206
+ │ └── robots.txt
158
207
  ├── src/
159
- │ ├── pages/ # Page components
208
+ │ ├── pages/ # Route pages
160
209
  │ │ ├── Home/
161
- │ │ └── About/
210
+ │ │ ├── About/
211
+ │ │ └── NotFound/
212
+ │ ├── components/ # Reusable components (Button example)
213
+ │ ├── types/ # Shared TypeScript types
214
+ │ ├── utils/ # Utility functions (optional)
162
215
  │ ├── App.tsx # Main app with routing
163
216
  │ ├── main.tsx # Entry point
164
- │ └── app.css # Tailwind directives
217
+ │ └── app.css # Tailwind theme + base styles
165
218
  ├── e2e/ # E2E tests (optional)
219
+ ├── eslint-rules/ # Local ESLint rules (optional)
220
+ ├── lighthouserc.json # Lighthouse budgets (optional)
166
221
  ├── .github/ # GitHub Actions workflows
167
222
  └── package.json
168
223
  ```
@@ -170,12 +225,12 @@ my-app/
170
225
  ## Why This Stack?
171
226
 
172
227
  - **Modern**: Latest versions of React, Vite, and Tailwind
173
- - **Lightweight**: Minimal dependencies, under 1MB node_modules (before dev deps)
228
+ - **Lightweight**: Minimal dependencies, fast installs and builds
174
229
  - **Type-safe**: Full TypeScript support with strict checking
175
- - **Fast**: Vite's instant HMR and optimized builds
176
- - **Accessible**: Built-in a11y linting ensures inclusive UIs
230
+ - **Fast**: Vite's instant HMR, route-level code splitting, self-hosted fonts
231
+ - **Accessible**: Built-in a11y linting, skip links, focus styles
177
232
  - **Maintainable**: Automated formatting and linting prevent technical debt
178
- - **CI-ready**: Workflows for testing and validation out of the box
233
+ - **CI-ready**: Workflows for checks, E2E tests, and Lighthouse budgets out of the box
179
234
 
180
235
  ## License
181
236
 
package/bin/index.js CHANGED
@@ -1,17 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { fileURLToPath } from 'url';
4
- import { dirname, join } from 'path';
5
- import { readdir, mkdir, readFile, writeFile, copyFile, stat, rm } from 'fs/promises';
6
- import { existsSync } from 'fs';
3
+ import * as p from '@clack/prompts';
7
4
  import { spawn } from 'child_process';
8
- import * as readline from 'readline';
5
+ import { existsSync } from 'fs';
6
+ import { chmod, copyFile, mkdir, readdir, readFile, rm, writeFile } from 'fs/promises';
7
+ import { dirname, join, relative, sep } from 'path';
8
+ import { fileURLToPath } from 'url';
9
9
 
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = dirname(__filename);
12
12
 
13
13
  const args = process.argv.slice(2);
14
14
 
15
+ const FEATURES = [
16
+ {
17
+ id: 'e2e',
18
+ flag: 'e2e',
19
+ label: 'E2E testing (Playwright + Allure)'
20
+ },
21
+ {
22
+ id: 'utils',
23
+ flag: 'utils',
24
+ label: 'Utility functions (classNames, Storage, useUrlState, safeTimeout)'
25
+ },
26
+ {
27
+ id: 'lighthouse',
28
+ flag: 'lighthouse',
29
+ label: 'Lighthouse CI workflow (a11y/SEO/performance budgets)'
30
+ },
31
+ {
32
+ id: 'webpLint',
33
+ flag: 'webp-lint',
34
+ label: 'ESLint rule: prefer WebP images'
35
+ }
36
+ ];
37
+
38
+ // Flags that consume the next argument as their value; all others are boolean.
39
+ const VALUE_FLAGS = new Set(['dir', 'directory']);
40
+
41
+ const PRE_COMMIT_HOOK = `#!/bin/bash
42
+
43
+ STAGED_JS_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(tsx?|js)$' || true)
44
+
45
+ if [ -n "$STAGED_JS_FILES" ]; then
46
+ echo "$STAGED_JS_FILES" | xargs npx prettier --write --ignore-unknown || exit 1
47
+ echo "$STAGED_JS_FILES" | xargs npx eslint --fix || exit 1
48
+ echo "$STAGED_JS_FILES" | xargs git add
49
+ fi
50
+ `;
51
+
15
52
  function parseArgs(args) {
16
53
  const parsed = {
17
54
  projectName: null,
@@ -23,7 +60,7 @@ function parseArgs(args) {
23
60
  if (arg.startsWith('--')) {
24
61
  const key = arg.slice(2);
25
62
  const nextArg = args[i + 1];
26
- if (nextArg && !nextArg.startsWith('--')) {
63
+ if (VALUE_FLAGS.has(key) && nextArg && !nextArg.startsWith('--')) {
27
64
  parsed.flags[key] = nextArg;
28
65
  i++;
29
66
  } else {
@@ -47,47 +84,103 @@ function validateProjectName(name) {
47
84
  return null;
48
85
  }
49
86
 
50
- async function copyDirectory(src, dest, excludePaths = []) {
87
+ function printUsage() {
88
+ console.log('\nUsage: npm create react-adam@latest <project-name> [options]');
89
+ console.log('\nOptions:');
90
+ console.log(' --dir <path> Create the project in a specific directory');
91
+ console.log(' --yes Skip the wizard; include every feature not disabled by a flag');
92
+ console.log(' --with-e2e Include E2E testing setup (Playwright + Allure)');
93
+ console.log(' --no-e2e Skip E2E testing setup');
94
+ console.log(' --with-utils Include utility functions (classNames, Storage, useUrlState)');
95
+ console.log(' --no-utils Skip utility functions');
96
+ console.log(' --with-lighthouse Include Lighthouse CI workflow');
97
+ console.log(' --no-lighthouse Skip Lighthouse CI workflow');
98
+ console.log(' --with-webp-lint Include the prefer-webp-images ESLint rule');
99
+ console.log(' --no-webp-lint Skip the prefer-webp-images ESLint rule');
100
+ }
101
+
102
+ async function resolveFeatures(flags) {
103
+ const resolved = {};
104
+
105
+ for (const feature of FEATURES) {
106
+ if (flags[`with-${feature.flag}`]) {
107
+ resolved[feature.id] = true;
108
+ } else if (flags[`no-${feature.flag}`]) {
109
+ resolved[feature.id] = false;
110
+ }
111
+ }
112
+
113
+ const unresolved = FEATURES.filter((feature) => resolved[feature.id] === undefined);
114
+ if (unresolved.length === 0) {
115
+ return resolved;
116
+ }
117
+
118
+ const isNonInteractive = flags.yes || !process.stdin.isTTY || !process.stdout.isTTY;
119
+ if (isNonInteractive) {
120
+ for (const feature of unresolved) {
121
+ resolved[feature.id] = true;
122
+ }
123
+ return resolved;
124
+ }
125
+
126
+ p.intro('create-react-adam');
127
+ const selected = await p.multiselect({
128
+ message: 'Select features',
129
+ options: unresolved.map((feature) => ({
130
+ value: feature.id,
131
+ label: feature.label
132
+ })),
133
+ initialValues: unresolved.map((feature) => feature.id),
134
+ required: false
135
+ });
136
+
137
+ if (p.isCancel(selected)) {
138
+ p.cancel('Scaffold cancelled.');
139
+ process.exit(1);
140
+ }
141
+
142
+ for (const feature of unresolved) {
143
+ resolved[feature.id] = selected.includes(feature.id);
144
+ }
145
+ return resolved;
146
+ }
147
+
148
+ // Local dev/build artifacts that must never be copied into a scaffold
149
+ // (npm excludes most of these from the published package anyway).
150
+ const ALWAYS_SKIP = new Set([
151
+ 'node_modules',
152
+ 'dist',
153
+ '.DS_Store',
154
+ 'package-lock.json',
155
+ 'playwright-report',
156
+ 'test-results',
157
+ 'allure-results'
158
+ ]);
159
+
160
+ async function copyDirectory(src, dest, excludePaths = [], root = src) {
51
161
  await mkdir(dest, { recursive: true });
52
162
  const entries = await readdir(src, { withFileTypes: true });
53
163
 
54
164
  for (const entry of entries) {
165
+ if (ALWAYS_SKIP.has(entry.name)) {
166
+ continue;
167
+ }
55
168
  const srcPath = join(src, entry.name);
56
169
  const destPath = join(dest, entry.name);
170
+ const relPath = relative(root, srcPath).split(sep).join('/');
57
171
 
58
- const shouldExclude = excludePaths.some(excludePath => {
59
- if (entry.isDirectory() && entry.name === excludePath) {
60
- return true;
61
- }
62
- return srcPath.endsWith(`/${excludePath}`) || srcPath.endsWith(`\\${excludePath}`);
63
- });
64
-
65
- if (shouldExclude) {
172
+ if (excludePaths.includes(relPath)) {
66
173
  continue;
67
174
  }
68
175
 
69
176
  if (entry.isDirectory()) {
70
- await copyDirectory(srcPath, destPath, excludePaths);
177
+ await copyDirectory(srcPath, destPath, excludePaths, root);
71
178
  } else {
72
179
  await copyFile(srcPath, destPath);
73
180
  }
74
181
  }
75
182
  }
76
183
 
77
- function askQuestion(query) {
78
- const rl = readline.createInterface({
79
- input: process.stdin,
80
- output: process.stdout
81
- });
82
-
83
- return new Promise((resolve) => {
84
- rl.question(query, (answer) => {
85
- rl.close();
86
- resolve(answer);
87
- });
88
- });
89
- }
90
-
91
184
  async function replaceInFile(filePath, replacements) {
92
185
  let content = await readFile(filePath, 'utf-8');
93
186
  for (const [key, value] of Object.entries(replacements)) {
@@ -103,6 +196,7 @@ function runCommand(command, args, cwd) {
103
196
  stdio: 'inherit'
104
197
  });
105
198
 
199
+ child.on('error', reject);
106
200
  child.on('close', (code) => {
107
201
  if (code !== 0) {
108
202
  reject(new Error(`Command failed with exit code ${code}`));
@@ -113,19 +207,65 @@ function runCommand(command, args, cwd) {
113
207
  });
114
208
  }
115
209
 
210
+ function runCommandQuiet(command, args, cwd) {
211
+ return new Promise((resolve) => {
212
+ const child = spawn(command, args, {
213
+ cwd,
214
+ stdio: 'ignore'
215
+ });
216
+
217
+ child.on('error', () => resolve(-1));
218
+ child.on('close', (code) => resolve(code));
219
+ });
220
+ }
221
+
222
+ async function setupGit(projectPath) {
223
+ const gitAvailable = (await runCommandQuiet('git', ['--version'])) === 0;
224
+ if (!gitAvailable) {
225
+ console.log('\nGit not found; skipping repository setup.');
226
+ return;
227
+ }
228
+
229
+ const insideRepo =
230
+ (await runCommandQuiet('git', ['rev-parse', '--is-inside-work-tree'], projectPath)) === 0;
231
+ if (insideRepo) {
232
+ console.log(
233
+ '\nAlready inside a git repository; skipping git init.' +
234
+ '\nAfter setting up your own repo, run: npm run setFormatToPrecommitHook'
235
+ );
236
+ return;
237
+ }
238
+
239
+ try {
240
+ await runCommand('git', ['init', '--quiet'], projectPath);
241
+
242
+ const hookPath = join(projectPath, '.git', 'hooks', 'pre-commit');
243
+ await writeFile(hookPath, PRE_COMMIT_HOOK, 'utf-8');
244
+ await chmod(hookPath, 0o755);
245
+
246
+ await runCommand('git', ['add', '-A'], projectPath);
247
+ await runCommand(
248
+ 'git',
249
+ ['commit', '--quiet', '-m', 'Initial commit from create-react-adam'],
250
+ projectPath
251
+ );
252
+ console.log('\nInitialized a git repository with a formatting pre-commit hook.');
253
+ } catch (error) {
254
+ console.warn(
255
+ `\nWarning: git setup did not complete (${error.message}).` +
256
+ '\nYou can finish it manually with: git init && git add -A && git commit' +
257
+ '\nThen run: npm run setFormatToPrecommitHook'
258
+ );
259
+ }
260
+ }
261
+
116
262
  async function main() {
117
263
  const { projectName, flags } = parseArgs(args);
118
264
 
119
265
  const validationError = validateProjectName(projectName);
120
266
  if (validationError) {
121
267
  console.error(`Error: ${validationError}`);
122
- console.log('\nUsage: npm create react-adam@latest <project-name> [options]');
123
- console.log('\nOptions:');
124
- console.log(' --dir <path> Create the project in a specific directory');
125
- console.log(' --with-e2e Include E2E testing setup (Playwright + Allure)');
126
- console.log(' --no-e2e Skip E2E testing setup');
127
- console.log(' --with-utils Include utility functions (classNames, Storage, useUrlState)');
128
- console.log(' --no-utils Skip utility functions');
268
+ printUsage();
129
269
  process.exit(1);
130
270
  }
131
271
 
@@ -137,80 +277,104 @@ async function main() {
137
277
  process.exit(1);
138
278
  }
139
279
 
140
- let includeE2E = false;
141
- if (flags['with-e2e']) {
142
- includeE2E = true;
143
- } else if (flags['no-e2e']) {
144
- includeE2E = false;
145
- } else {
146
- const answer = await askQuestion('Include E2E testing setup (Playwright + Allure)? (y/N): ');
147
- includeE2E = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
148
- }
149
-
150
- let includeUtils = false;
151
- if (flags['with-utils']) {
152
- includeUtils = true;
153
- } else if (flags['no-utils']) {
154
- includeUtils = false;
155
- } else {
156
- const answer = await askQuestion('Include utility functions (classNames, Storage, useUrlState)? (y/N): ');
157
- includeUtils = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
158
- }
280
+ const features = await resolveFeatures(flags);
159
281
 
160
282
  console.log(`Creating a new Adam React app in ${projectPath}...`);
283
+ const enabled = FEATURES.filter((feature) => features[feature.id]);
284
+ console.log(
285
+ enabled.length > 0
286
+ ? `Features: ${enabled.map((feature) => feature.label).join(', ')}`
287
+ : 'Features: none (core stack only)'
288
+ );
161
289
 
162
290
  const templatePath = join(__dirname, '..', 'template');
163
291
  const excludePaths = [];
164
-
165
- if (!includeE2E) {
166
- excludePaths.push('e2e');
167
- }
168
292
 
169
- if (!includeUtils) {
170
- excludePaths.push('utils');
293
+ if (!features.e2e) {
294
+ excludePaths.push('e2e', '.github/workflows/e2e.yml');
171
295
  }
172
-
173
- await copyDirectory(templatePath, projectPath, excludePaths);
174
-
175
- // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
176
- const gitignorePath = join(projectPath, 'gitignore');
177
- if (existsSync(gitignorePath)) {
178
- await copyFile(gitignorePath, join(projectPath, '.gitignore'));
179
- await rm(gitignorePath);
296
+ if (!features.utils) {
297
+ excludePaths.push('src/utils');
180
298
  }
181
-
182
- if (!includeUtils) {
183
- await copyFile(
184
- join(projectPath, 'src', 'pages', 'Home', 'index.no-utils.tsx'),
185
- join(projectPath, 'src', 'pages', 'Home', 'index.tsx')
186
- );
187
- await copyFile(
188
- join(projectPath, 'src', 'pages', 'About', 'index.no-utils.tsx'),
189
- join(projectPath, 'src', 'pages', 'About', 'index.tsx')
190
- );
191
- await rm(join(projectPath, 'src', 'pages', 'Home', 'index.no-utils.tsx'));
192
- await rm(join(projectPath, 'src', 'pages', 'About', 'index.no-utils.tsx'));
193
- } else {
194
- await rm(join(projectPath, 'src', 'pages', 'Home', 'index.no-utils.tsx'));
195
- await rm(join(projectPath, 'src', 'pages', 'About', 'index.no-utils.tsx'));
299
+ if (!features.lighthouse) {
300
+ excludePaths.push('.github/workflows/lighthouse.yml', 'lighthouserc.json');
301
+ }
302
+ if (!features.webpLint) {
303
+ excludePaths.push('eslint-rules');
196
304
  }
197
305
 
198
- const replacements = {
199
- '__PROJECT_NAME__': projectName
200
- };
306
+ let createdProjectDir = false;
307
+ try {
308
+ createdProjectDir = true;
309
+ await copyDirectory(templatePath, projectPath, excludePaths);
310
+
311
+ // npm never publishes .gitignore/.npmrc files, so the template ships them
312
+ // without the leading dot and we restore the real names here.
313
+ const dotfileRenames = [
314
+ ['gitignore', '.gitignore'],
315
+ ['npmrc', '.npmrc'],
316
+ [join('e2e', 'gitignore'), join('e2e', '.gitignore')]
317
+ ];
318
+ for (const [from, to] of dotfileRenames) {
319
+ const fromPath = join(projectPath, from);
320
+ if (existsSync(fromPath)) {
321
+ await copyFile(fromPath, join(projectPath, to));
322
+ await rm(fromPath);
323
+ }
324
+ }
201
325
 
202
- await replaceInFile(join(projectPath, 'package.json'), replacements);
203
- await replaceInFile(join(projectPath, 'index.html'), replacements);
204
- await replaceInFile(join(projectPath, 'README.md'), replacements);
326
+ // Resolve .no-utils page variants
327
+ for (const page of ['Home', 'About']) {
328
+ const variantPath = join(projectPath, 'src', 'pages', page, 'index.no-utils.tsx');
329
+ if (!features.utils) {
330
+ await copyFile(variantPath, join(projectPath, 'src', 'pages', page, 'index.tsx'));
331
+ }
332
+ await rm(variantPath);
333
+ }
205
334
 
206
- console.log('\nInstalling dependencies...');
207
- await runCommand('npm', ['install'], projectPath);
335
+ // Resolve the eslint config variant (with or without the WebP rule)
336
+ const noWebpConfigPath = join(projectPath, 'eslint.config.no-webp.js');
337
+ if (!features.webpLint) {
338
+ await copyFile(noWebpConfigPath, join(projectPath, 'eslint.config.js'));
339
+ }
340
+ await rm(noWebpConfigPath);
341
+
342
+ // README.md uses {{PROJECT_NAME}} because Prettier's markdown formatter
343
+ // rewrites __PROJECT_NAME__ (underscore emphasis) to **PROJECT_NAME**.
344
+ const replacements = {
345
+ __PROJECT_NAME__: projectName,
346
+ '{{PROJECT_NAME}}': projectName
347
+ };
348
+
349
+ const filesToReplace = [
350
+ 'package.json',
351
+ 'index.html',
352
+ 'README.md',
353
+ join('src', 'pages', 'Home', 'index.tsx'),
354
+ join('src', 'pages', 'About', 'index.tsx'),
355
+ join('src', 'pages', 'NotFound', 'index.tsx')
356
+ ];
357
+ for (const file of filesToReplace) {
358
+ await replaceInFile(join(projectPath, file), replacements);
359
+ }
208
360
 
209
- if (includeE2E) {
210
- console.log('\nInstalling E2E dependencies...');
211
- await runCommand('npm', ['install'], join(projectPath, 'e2e'));
361
+ console.log('\nInstalling dependencies...');
362
+ await runCommand('npm', ['install'], projectPath);
363
+
364
+ if (features.e2e) {
365
+ console.log('\nInstalling E2E dependencies...');
366
+ await runCommand('npm', ['install'], join(projectPath, 'e2e'));
367
+ }
368
+ } catch (error) {
369
+ if (createdProjectDir) {
370
+ await rm(projectPath, { recursive: true, force: true });
371
+ console.error(`\nScaffolding failed; removed ${projectPath}`);
372
+ }
373
+ throw error;
212
374
  }
213
375
 
376
+ await setupGit(projectPath);
377
+
214
378
  console.log('\n✓ Success! Created', projectName, 'at', projectPath);
215
379
  console.log('\nInside that directory, you can run several commands:\n');
216
380
  console.log(' npm run dev');
@@ -221,8 +385,8 @@ async function main() {
221
385
  console.log(' Previews the production build.\n');
222
386
  console.log(' npm run lint');
223
387
  console.log(' Runs the linter.\n');
224
-
225
- if (includeE2E) {
388
+
389
+ if (features.e2e) {
226
390
  console.log(' npm run test:e2e');
227
391
  console.log(' Runs E2E tests with Playwright.\n');
228
392
  console.log(' npm run test:e2e:ui');
@@ -230,7 +394,7 @@ async function main() {
230
394
  console.log(' npm run test:e2e:report');
231
395
  console.log(' Generates and opens Allure report.\n');
232
396
  }
233
-
397
+
234
398
  console.log('We suggest that you begin by typing:\n');
235
399
  console.log(` cd ${projectName}`);
236
400
  console.log(' npm run dev\n');
@@ -240,4 +404,3 @@ main().catch((error) => {
240
404
  console.error('Error:', error.message);
241
405
  process.exit(1);
242
406
  });
243
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-react-adam",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "Create opinionated React apps with TypeScript, Vite, Wouter, and Tailwind CSS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,10 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
- "template"
11
+ "template",
12
+ "!template/package-lock.json",
13
+ "!template/node_modules",
14
+ "!template/dist"
12
15
  ],
13
16
  "keywords": [
14
17
  "react",
@@ -38,6 +41,8 @@
38
41
  "itDoesNotWork": "echo 'Wrong Directory'",
39
42
  "cleanup": "rm -rf ./tmp/* && rm -rf ./template/node_modules && rm -rf ./template/dist && rm -rf ./template/package-lock.json && echo 'Template Dev Files Deleted, you must run npm install in the template directory for those features'",
40
43
  "dev": "npm run cleanup && TIMESTAMP=$(date +%Y%m%d-%H%M%S) && npm link && cd ./tmp && npx create-react-adam test-app-$TIMESTAMP --with-e2e --with-utils && cd test-app-$TIMESTAMP && npm run dev"
44
+ },
45
+ "dependencies": {
46
+ "@clack/prompts": "1.7.0"
41
47
  }
42
48
  }
43
-
@@ -17,7 +17,7 @@ jobs:
17
17
  - name: Setup Node.js
18
18
  uses: actions/setup-node@v6
19
19
  with:
20
- node-version: "24.11.1"
20
+ node-version: "24.18.0"
21
21
  cache: "npm"
22
22
 
23
23
  - name: Install CLI dependencies