create-react-adam 0.2.1 → 0.3.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 +72 -20
- package/bin/index.js +264 -94
- package/package.json +8 -3
- package/template/.github/workflows/check.yml +1 -1
- package/template/.github/workflows/e2e.yml +3 -3
- package/template/.github/workflows/lighthouse.yml +33 -0
- package/template/.nvmrc +1 -1
- package/template/README.md +133 -31
- package/template/e2e/gitignore +13 -0
- package/template/e2e/package.json +2 -3
- package/template/e2e/playwright.config.ts +0 -1
- package/template/eslint-rules/prefer-webp-images.js +66 -0
- package/template/eslint.config.js +3 -0
- package/template/eslint.config.no-webp.js +54 -0
- package/template/gitignore +26 -0
- package/template/index.html +19 -0
- package/template/lighthouserc.json +17 -0
- package/template/npmrc +2 -0
- package/template/package.json +18 -18
- package/template/public/fonts/InterVariable.woff2 +0 -0
- package/template/public/robots.txt +5 -0
- package/template/src/App.tsx +25 -11
- package/template/src/app.css +27 -9
- package/template/src/components/Button.tsx +27 -0
- package/template/src/pages/About/index.no-utils.tsx +5 -0
- package/template/src/pages/About/index.tsx +22 -12
- package/template/src/pages/Home/index.no-utils.tsx +37 -1
- package/template/src/pages/Home/index.tsx +22 -12
- package/template/src/pages/NotFound/index.tsx +25 -18
- package/template/src/types/ui.ts +1 -0
- package/template/src/utils/classNames.ts +23 -2
- package/template/src/utils/helpers.ts +22 -0
- package/template/src/utils/useUrlState.ts +17 -4
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 --
|
|
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
|
-
- `--
|
|
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 --
|
|
38
|
+
npm create react-adam@latest my-app --yes --no-e2e
|
|
25
39
|
```
|
|
26
40
|
|
|
27
|
-
- `--no-e2e` -
|
|
28
|
-
|
|
29
|
-
|
|
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,18 @@ 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** - pages load via `React.lazy` + `Suspense`
|
|
66
|
+
- **SEO-ready `index.html`** - meta description, Open Graph and Twitter card
|
|
67
|
+
tags, theme-color, plus a `public/robots.txt`
|
|
68
|
+
- **Accessible shell** - skip-to-content link, `<main>` landmark, visible
|
|
69
|
+
`:focus-visible` styles, and per-route document titles
|
|
70
|
+
|
|
42
71
|
### Code Quality Tools
|
|
43
72
|
|
|
44
73
|
#### ESLint
|
|
@@ -50,8 +79,9 @@ Configured with a modern flat config (`eslint.config.js`) that includes:
|
|
|
50
79
|
- **React Refresh** - Validates fast refresh compatibility
|
|
51
80
|
- **JSX Accessibility** - Enforces accessibility best practices (a11y)
|
|
52
81
|
- **Prettier integration** - Disables conflicting formatting rules
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
- **prefer-webp-images** (optional) - a local custom rule that errors on
|
|
83
|
+
references to `.png`/`.jpg`/`.jpeg`/`.gif`/`.bmp` images and suggests
|
|
84
|
+
converting them at [tools.sturge.dev/webp](https://tools.sturge.dev/webp)
|
|
55
85
|
|
|
56
86
|
#### Prettier
|
|
57
87
|
|
|
@@ -60,17 +90,24 @@ Configured with two powerful plugins:
|
|
|
60
90
|
- **prettier-plugin-organize-imports** - Automatically sorts and removes unused imports
|
|
61
91
|
- **prettier-plugin-tailwindcss** - Sorts Tailwind class names consistently
|
|
62
92
|
|
|
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
93
|
### Optional E2E Testing
|
|
66
94
|
|
|
67
|
-
When you include E2E testing (via
|
|
95
|
+
When you include E2E testing (via wizard or `--with-e2e` flag), you get:
|
|
68
96
|
|
|
69
97
|
- **Playwright** - Modern, reliable E2E testing framework
|
|
70
98
|
- **Allure Reports** - Beautiful, detailed test reports
|
|
71
99
|
- Pre-configured test setup with example tests
|
|
72
100
|
- HTML reports and Allure integration
|
|
73
101
|
|
|
102
|
+
### Git Integration
|
|
103
|
+
|
|
104
|
+
After scaffolding (when not already inside a git repository), the CLI:
|
|
105
|
+
|
|
106
|
+
1. Runs `git init`
|
|
107
|
+
2. Installs a pre-commit hook that formats and lints staged files
|
|
108
|
+
(the same hook as `npm run setFormatToPrecommitHook`)
|
|
109
|
+
3. Creates an initial commit
|
|
110
|
+
|
|
74
111
|
### Dependency Management
|
|
75
112
|
|
|
76
113
|
- **Exact version pinning** - All dependencies use exact versions (no `^` or `~`)
|
|
@@ -79,7 +116,7 @@ When you include E2E testing (via prompt or `--with-e2e` flag), you get:
|
|
|
79
116
|
|
|
80
117
|
### GitHub Actions Workflows
|
|
81
118
|
|
|
82
|
-
|
|
119
|
+
Up to three CI workflows are included in generated projects:
|
|
83
120
|
|
|
84
121
|
1. **Code Checks** (`.github/workflows/check.yml`)
|
|
85
122
|
|
|
@@ -89,10 +126,16 @@ Two CI workflows are included in the generated projects:
|
|
|
89
126
|
- Production build verification
|
|
90
127
|
|
|
91
128
|
2. **E2E Tests** (`.github/workflows/e2e.yml`) - if E2E is included
|
|
129
|
+
|
|
92
130
|
- Runs Playwright tests in CI
|
|
93
131
|
- Uploads test reports as artifacts
|
|
94
132
|
- Configures Allure results
|
|
95
133
|
|
|
134
|
+
3. **Lighthouse CI** (`.github/workflows/lighthouse.yml`) - if Lighthouse is included
|
|
135
|
+
- Builds the app and audits it with Lighthouse
|
|
136
|
+
- Fails CI below score budgets: accessibility ≥ 95, SEO ≥ 95, performance ≥ 85
|
|
137
|
+
- Budgets live in `lighthouserc.json`
|
|
138
|
+
|
|
96
139
|
## Available Scripts
|
|
97
140
|
|
|
98
141
|
### Development
|
|
@@ -155,14 +198,23 @@ Runs basic troubleshooting checks:
|
|
|
155
198
|
|
|
156
199
|
```
|
|
157
200
|
my-app/
|
|
201
|
+
├── public/
|
|
202
|
+
│ ├── fonts/ # Self-hosted Inter (variable woff2)
|
|
203
|
+
│ └── robots.txt
|
|
158
204
|
├── src/
|
|
159
|
-
│ ├── pages/ #
|
|
205
|
+
│ ├── pages/ # Route pages
|
|
160
206
|
│ │ ├── Home/
|
|
161
|
-
│ │
|
|
207
|
+
│ │ ├── About/
|
|
208
|
+
│ │ └── NotFound/
|
|
209
|
+
│ ├── components/ # Reusable components (Button example)
|
|
210
|
+
│ ├── types/ # Shared TypeScript types
|
|
211
|
+
│ ├── utils/ # Utility functions (optional)
|
|
162
212
|
│ ├── App.tsx # Main app with routing
|
|
163
213
|
│ ├── main.tsx # Entry point
|
|
164
|
-
│ └── app.css # Tailwind
|
|
214
|
+
│ └── app.css # Tailwind theme + base styles
|
|
165
215
|
├── e2e/ # E2E tests (optional)
|
|
216
|
+
├── eslint-rules/ # Local ESLint rules (optional)
|
|
217
|
+
├── lighthouserc.json # Lighthouse budgets (optional)
|
|
166
218
|
├── .github/ # GitHub Actions workflows
|
|
167
219
|
└── package.json
|
|
168
220
|
```
|
|
@@ -170,12 +222,12 @@ my-app/
|
|
|
170
222
|
## Why This Stack?
|
|
171
223
|
|
|
172
224
|
- **Modern**: Latest versions of React, Vite, and Tailwind
|
|
173
|
-
- **Lightweight**: Minimal dependencies,
|
|
225
|
+
- **Lightweight**: Minimal dependencies, fast installs and builds
|
|
174
226
|
- **Type-safe**: Full TypeScript support with strict checking
|
|
175
|
-
- **Fast**: Vite's instant HMR
|
|
176
|
-
- **Accessible**: Built-in a11y linting
|
|
227
|
+
- **Fast**: Vite's instant HMR, route-level code splitting, self-hosted fonts
|
|
228
|
+
- **Accessible**: Built-in a11y linting, skip links, focus styles
|
|
177
229
|
- **Maintainable**: Automated formatting and linting prevent technical debt
|
|
178
|
-
- **CI-ready**: Workflows for
|
|
230
|
+
- **CI-ready**: Workflows for checks, E2E tests, and Lighthouse budgets out of the box
|
|
179
231
|
|
|
180
232
|
## License
|
|
181
233
|
|
package/bin/index.js
CHANGED
|
@@ -1,17 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,73 +277,104 @@ async function main() {
|
|
|
137
277
|
process.exit(1);
|
|
138
278
|
}
|
|
139
279
|
|
|
140
|
-
|
|
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 (!
|
|
170
|
-
excludePaths.push('
|
|
293
|
+
if (!features.e2e) {
|
|
294
|
+
excludePaths.push('e2e', '.github/workflows/e2e.yml');
|
|
171
295
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (!
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
);
|
|
180
|
-
await copyFile(
|
|
181
|
-
join(projectPath, 'src', 'pages', 'About', 'index.no-utils.tsx'),
|
|
182
|
-
join(projectPath, 'src', 'pages', 'About', 'index.tsx')
|
|
183
|
-
);
|
|
184
|
-
await rm(join(projectPath, 'src', 'pages', 'Home', 'index.no-utils.tsx'));
|
|
185
|
-
await rm(join(projectPath, 'src', 'pages', 'About', 'index.no-utils.tsx'));
|
|
186
|
-
} else {
|
|
187
|
-
await rm(join(projectPath, 'src', 'pages', 'Home', 'index.no-utils.tsx'));
|
|
188
|
-
await rm(join(projectPath, 'src', 'pages', 'About', 'index.no-utils.tsx'));
|
|
296
|
+
if (!features.utils) {
|
|
297
|
+
excludePaths.push('src/utils');
|
|
298
|
+
}
|
|
299
|
+
if (!features.lighthouse) {
|
|
300
|
+
excludePaths.push('.github/workflows/lighthouse.yml', 'lighthouserc.json');
|
|
301
|
+
}
|
|
302
|
+
if (!features.webpLint) {
|
|
303
|
+
excludePaths.push('eslint-rules');
|
|
189
304
|
}
|
|
190
305
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
+
}
|
|
194
325
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}
|
|
198
334
|
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
}
|
|
201
360
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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;
|
|
205
374
|
}
|
|
206
375
|
|
|
376
|
+
await setupGit(projectPath);
|
|
377
|
+
|
|
207
378
|
console.log('\n✓ Success! Created', projectName, 'at', projectPath);
|
|
208
379
|
console.log('\nInside that directory, you can run several commands:\n');
|
|
209
380
|
console.log(' npm run dev');
|
|
@@ -214,8 +385,8 @@ async function main() {
|
|
|
214
385
|
console.log(' Previews the production build.\n');
|
|
215
386
|
console.log(' npm run lint');
|
|
216
387
|
console.log(' Runs the linter.\n');
|
|
217
|
-
|
|
218
|
-
if (
|
|
388
|
+
|
|
389
|
+
if (features.e2e) {
|
|
219
390
|
console.log(' npm run test:e2e');
|
|
220
391
|
console.log(' Runs E2E tests with Playwright.\n');
|
|
221
392
|
console.log(' npm run test:e2e:ui');
|
|
@@ -223,7 +394,7 @@ async function main() {
|
|
|
223
394
|
console.log(' npm run test:e2e:report');
|
|
224
395
|
console.log(' Generates and opens Allure report.\n');
|
|
225
396
|
}
|
|
226
|
-
|
|
397
|
+
|
|
227
398
|
console.log('We suggest that you begin by typing:\n');
|
|
228
399
|
console.log(` cd ${projectName}`);
|
|
229
400
|
console.log(' npm run dev\n');
|
|
@@ -233,4 +404,3 @@ main().catch((error) => {
|
|
|
233
404
|
console.error('Error:', error.message);
|
|
234
405
|
process.exit(1);
|
|
235
406
|
});
|
|
236
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-react-adam",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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
|
-
|