belazywithreact 0.1.0__py3-none-any.whl
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.
- belazywithreact/__init__.py +1 -0
- belazywithreact/cli.py +102 -0
- belazywithreact/scaffold.py +367 -0
- belazywithreact/validators.py +60 -0
- belazywithreact-0.1.0.dist-info/METADATA +205 -0
- belazywithreact-0.1.0.dist-info/RECORD +9 -0
- belazywithreact-0.1.0.dist-info/WHEEL +4 -0
- belazywithreact-0.1.0.dist-info/entry_points.txt +2 -0
- belazywithreact-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
belazywithreact/cli.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
|
|
7
|
+
from .scaffold import ProjectOptions, create_project
|
|
8
|
+
from .validators import check_node_tools, resolve_destination
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="belazywithreact",
|
|
12
|
+
help="Scaffold a React + TypeScript project with your preferred starter tools.",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
add_completion=False,
|
|
15
|
+
)
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.callback()
|
|
20
|
+
def main() -> None:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command()
|
|
25
|
+
def create(
|
|
26
|
+
name: str = typer.Argument(..., help="Name of the project directory to create."),
|
|
27
|
+
styling: str = typer.Option("both", "--styling", help="Styling setup: css, tailwind, or both."),
|
|
28
|
+
router: bool = typer.Option(True, "--router/--no-router", help="Configure React Router."),
|
|
29
|
+
axios: bool = typer.Option(True, "--axios/--no-axios", help="Install Axios and add an API client."),
|
|
30
|
+
zustand: bool = typer.Option(True, "--zustand/--no-zustand", help="Install Zustand and add an example store."),
|
|
31
|
+
eslint: bool = typer.Option(True, "--eslint/--no-eslint", help="Configure ESLint for TypeScript and React."),
|
|
32
|
+
prettier: bool = typer.Option(True, "--prettier/--no-prettier", help="Install Prettier and add a format script."),
|
|
33
|
+
vitest: bool = typer.Option(True, "--vitest/--no-vitest", help="Install Vitest and add a test script."),
|
|
34
|
+
package_manager: str = typer.Option("npm", "--package-manager", help="Package manager (npm only in v0.1.0)."),
|
|
35
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation; never overwrite existing paths."),
|
|
36
|
+
) -> None:
|
|
37
|
+
styling = styling.lower().strip()
|
|
38
|
+
if styling not in {"css", "tailwind", "both"}:
|
|
39
|
+
console.print("[red]Invalid --styling value.[/red] Choose css, tailwind, or both.")
|
|
40
|
+
raise typer.Exit(code=2)
|
|
41
|
+
package_manager = package_manager.lower().strip()
|
|
42
|
+
if package_manager != "npm":
|
|
43
|
+
console.print("[red]Only npm is supported in version 0.1.0.[/red]")
|
|
44
|
+
raise typer.Exit(code=2)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
destination = resolve_destination(name, Path.cwd())
|
|
48
|
+
if destination.exists() or destination.is_symlink():
|
|
49
|
+
console.print(f"[red]Destination already exists:[/red] {destination}")
|
|
50
|
+
console.print("Choose a different project name. Existing files will not be overwritten.")
|
|
51
|
+
raise typer.Exit(code=2)
|
|
52
|
+
check_node_tools()
|
|
53
|
+
except (ValueError, RuntimeError) as exc:
|
|
54
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
55
|
+
raise typer.Exit(code=2) from exc
|
|
56
|
+
|
|
57
|
+
options = ProjectOptions(
|
|
58
|
+
styling=styling, router=router, axios=axios, zustand=zustand,
|
|
59
|
+
eslint=eslint, prettier=prettier, vitest=vitest, package_manager=package_manager,
|
|
60
|
+
)
|
|
61
|
+
if not yes:
|
|
62
|
+
console.print(Panel.fit(
|
|
63
|
+
f"[bold]Project:[/bold] {name}\n"
|
|
64
|
+
f"[bold]Styling:[/bold] {styling}\n"
|
|
65
|
+
f"[bold]Router:[/bold] {'yes' if router else 'no'}\n"
|
|
66
|
+
f"[bold]Axios:[/bold] {'yes' if axios else 'no'}\n"
|
|
67
|
+
f"[bold]Zustand:[/bold] {'yes' if zustand else 'no'}\n"
|
|
68
|
+
f"[bold]ESLint:[/bold] {'yes' if eslint else 'no'}\n"
|
|
69
|
+
f"[bold]Prettier:[/bold] {'yes' if prettier else 'no'}\n"
|
|
70
|
+
f"[bold]Vitest:[/bold] {'yes' if vitest else 'no'}\n"
|
|
71
|
+
"[bold]Package manager:[/bold] npm",
|
|
72
|
+
title="belazywithreact setup",
|
|
73
|
+
))
|
|
74
|
+
if not typer.confirm("Create this project?"):
|
|
75
|
+
console.print("Cancelled.")
|
|
76
|
+
raise typer.Exit()
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
with console.status("[bold green]Creating your React project...[/bold green]"):
|
|
80
|
+
create_project(name, destination, options)
|
|
81
|
+
except KeyboardInterrupt as exc:
|
|
82
|
+
console.print("[yellow]Setup interrupted.[/yellow] Any partial project is preserved; inspect it before retrying.")
|
|
83
|
+
raise typer.Exit(code=130) from exc
|
|
84
|
+
except FileExistsError as exc:
|
|
85
|
+
console.print(f"[red]{exc}[/red]")
|
|
86
|
+
raise typer.Exit(code=2) from exc
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
console.print(f"[red]Setup failed:[/red] {exc}")
|
|
89
|
+
console.print(
|
|
90
|
+
"The project directory may have been partially created. Inspect it before retrying; "
|
|
91
|
+
"belazywithreact does not delete it automatically."
|
|
92
|
+
)
|
|
93
|
+
raise typer.Exit(code=1) from exc
|
|
94
|
+
|
|
95
|
+
console.print(f"\n[bold green]Done![/bold green] Created [cyan]{name}[/cyan].")
|
|
96
|
+
console.print(f"Next:\n cd {name}\n npm run dev")
|
|
97
|
+
if vitest:
|
|
98
|
+
console.print("Run tests with: npm test")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
app()
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from .validators import resolve_destination
|
|
11
|
+
|
|
12
|
+
CREATE_VITE_VERSION = "9.2.1"
|
|
13
|
+
_IS_WINDOWS = os.name == "nt"
|
|
14
|
+
|
|
15
|
+
Runner = Callable[..., subprocess.CompletedProcess]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ProjectOptions:
|
|
20
|
+
styling: str = "both"
|
|
21
|
+
router: bool = True
|
|
22
|
+
axios: bool = True
|
|
23
|
+
zustand: bool = True
|
|
24
|
+
eslint: bool = True
|
|
25
|
+
prettier: bool = True
|
|
26
|
+
vitest: bool = True
|
|
27
|
+
package_manager: str = "npm"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_command(
|
|
31
|
+
command: list[str],
|
|
32
|
+
*,
|
|
33
|
+
cwd: Path | None = None,
|
|
34
|
+
runner: Runner = subprocess.run,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Run commands cross-platform, including Windows npm .cmd launchers."""
|
|
37
|
+
working_directory = str(cwd) if cwd else None
|
|
38
|
+
|
|
39
|
+
if _IS_WINDOWS and command and command[0].lower() == "npm":
|
|
40
|
+
npm_cmd = shutil.which("npm.cmd") or shutil.which("npm")
|
|
41
|
+
|
|
42
|
+
if not npm_cmd:
|
|
43
|
+
raise FileNotFoundError(
|
|
44
|
+
"Cannot find npm.cmd. Install Node.js LTS "
|
|
45
|
+
"and reopen PowerShell."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# list2cmdline quotes spaces but does not escape cmd.exe operators.
|
|
49
|
+
# Keep the working CALL/npm.cmd invocation; reject shell syntax first.
|
|
50
|
+
if any(re.search(r'[&|<>^%!"\r\n]', arg) for arg in command[1:]):
|
|
51
|
+
raise ValueError("Unsafe shell characters in npm arguments.")
|
|
52
|
+
args = subprocess.list2cmdline(command[1:])
|
|
53
|
+
command_line = f'call "{npm_cmd}" {args}'
|
|
54
|
+
|
|
55
|
+
runner(
|
|
56
|
+
command_line,
|
|
57
|
+
cwd=working_directory,
|
|
58
|
+
check=True,
|
|
59
|
+
shell=True,
|
|
60
|
+
)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
runner(
|
|
64
|
+
command,
|
|
65
|
+
cwd=working_directory,
|
|
66
|
+
check=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _write(path: Path, content: str) -> None:
|
|
71
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
path.write_text(content.rstrip() + "\n", encoding="utf-8")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _add_gitkeep(path: Path) -> None:
|
|
76
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
if not any(path.iterdir()):
|
|
78
|
+
(path / ".gitkeep").write_text("", encoding="utf-8")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def configure_project(destination: Path, options: ProjectOptions) -> None:
|
|
82
|
+
src = destination / "src"
|
|
83
|
+
gitignore = destination / ".gitignore"
|
|
84
|
+
existing_ignore = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
|
|
85
|
+
_write(gitignore, existing_ignore.rstrip() + "\n\n# Local environment values\n.env\n.env.*\n!.env.example\n")
|
|
86
|
+
for folder in ("assets", "components", "hooks", "layouts", "pages", "services", "store", "types", "utils"):
|
|
87
|
+
_add_gitkeep(src / folder)
|
|
88
|
+
|
|
89
|
+
_write(src / "components" / "Button.tsx", '''import type { ButtonHTMLAttributes, PropsWithChildren } from "react";
|
|
90
|
+
|
|
91
|
+
type ButtonProps = PropsWithChildren<ButtonHTMLAttributes<HTMLButtonElement>>;
|
|
92
|
+
|
|
93
|
+
export function Button({ children, className = "", ...props }: ButtonProps) {
|
|
94
|
+
return (
|
|
95
|
+
<button className={`app-button ${className}`.trim()} {...props}>
|
|
96
|
+
{children}
|
|
97
|
+
</button>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
''')
|
|
101
|
+
_write(src / "pages" / "HomePage.tsx", '''import { Button } from "../components/Button";
|
|
102
|
+
|
|
103
|
+
export function HomePage() {
|
|
104
|
+
return (
|
|
105
|
+
<main className="page">
|
|
106
|
+
<p className="eyebrow">Your project is ready</p>
|
|
107
|
+
<h1>Build something great.</h1>
|
|
108
|
+
<p className="intro">
|
|
109
|
+
This React + TypeScript starter was created with belazywithreact.
|
|
110
|
+
</p>
|
|
111
|
+
<Button onClick={() => window.alert("Your setup is working!")}>
|
|
112
|
+
Test the starter
|
|
113
|
+
</Button>
|
|
114
|
+
</main>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
''')
|
|
118
|
+
|
|
119
|
+
css = '''@import "./tailwind.css";
|
|
120
|
+
|
|
121
|
+
:root {
|
|
122
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
123
|
+
color: #172033;
|
|
124
|
+
background: #f6f8fc;
|
|
125
|
+
font-synthesis: none;
|
|
126
|
+
text-rendering: optimizeLegibility;
|
|
127
|
+
-webkit-font-smoothing: antialiased;
|
|
128
|
+
}
|
|
129
|
+
* { box-sizing: border-box; }
|
|
130
|
+
body { margin: 0; min-width: 320px; min-height: 100vh; }
|
|
131
|
+
button { font: inherit; }
|
|
132
|
+
.page {
|
|
133
|
+
width: min(760px, calc(100% - 40px));
|
|
134
|
+
margin: 12vh auto;
|
|
135
|
+
padding: clamp(24px, 5vw, 56px);
|
|
136
|
+
border: 1px solid #e1e7f0;
|
|
137
|
+
border-radius: 28px;
|
|
138
|
+
background: white;
|
|
139
|
+
box-shadow: 0 24px 70px rgb(31 49 82 / 9%);
|
|
140
|
+
}
|
|
141
|
+
.eyebrow { color: #6558d3; font-size: .8rem; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
|
|
142
|
+
h1 { margin: 12px 0; font-size: clamp(2.2rem, 6vw, 4rem); letter-spacing: -.055em; line-height: 1.05; }
|
|
143
|
+
.intro { max-width: 560px; color: #59657a; font-size: 1.1rem; line-height: 1.7; }
|
|
144
|
+
.app-button { margin-top: 18px; padding: 12px 18px; color: white; background: #5145cd; border: 0; border-radius: 12px; cursor: pointer; font-weight: 700; }
|
|
145
|
+
.app-button:hover { background: #4035b2; }
|
|
146
|
+
'''
|
|
147
|
+
if options.styling in {"tailwind", "both"}:
|
|
148
|
+
_write(src / "styles" / "tailwind.css", '@import "tailwindcss";')
|
|
149
|
+
_write(destination / "vite.config.ts", '''import react from "@vitejs/plugin-react";
|
|
150
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
151
|
+
import { defineConfig } from "vite";
|
|
152
|
+
|
|
153
|
+
export default defineConfig({ plugins: [react(), tailwindcss()] });
|
|
154
|
+
''')
|
|
155
|
+
else:
|
|
156
|
+
_write(src / "styles" / "tailwind.css", "/* Tailwind is disabled for this project. */")
|
|
157
|
+
|
|
158
|
+
_write(src / "styles" / "index.css", css)
|
|
159
|
+
app_content = '''import "./styles/index.css";
|
|
160
|
+
import { HomePage } from "./pages/HomePage";
|
|
161
|
+
|
|
162
|
+
export default function App() {
|
|
163
|
+
return <HomePage />;
|
|
164
|
+
}
|
|
165
|
+
'''
|
|
166
|
+
if options.router:
|
|
167
|
+
app_content = '''import { BrowserRouter, Route, Routes } from "react-router-dom";
|
|
168
|
+
import "./styles/index.css";
|
|
169
|
+
import { HomePage } from "./pages/HomePage";
|
|
170
|
+
|
|
171
|
+
function NotFoundPage() {
|
|
172
|
+
return <main className="page"><h1>Page not found</h1><p>Check the URL and try again.</p></main>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default function App() {
|
|
176
|
+
return (
|
|
177
|
+
<BrowserRouter>
|
|
178
|
+
<Routes>
|
|
179
|
+
<Route path="/" element={<HomePage />} />
|
|
180
|
+
<Route path="*" element={<NotFoundPage />} />
|
|
181
|
+
</Routes>
|
|
182
|
+
</BrowserRouter>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
'''
|
|
186
|
+
_write(src / "App.tsx", app_content)
|
|
187
|
+
_write(src / "main.tsx", '''import { StrictMode } from "react";
|
|
188
|
+
import { createRoot } from "react-dom/client";
|
|
189
|
+
import App from "./App";
|
|
190
|
+
|
|
191
|
+
createRoot(document.getElementById("root")!).render(
|
|
192
|
+
<StrictMode><App /></StrictMode>,
|
|
193
|
+
);
|
|
194
|
+
''')
|
|
195
|
+
if options.eslint:
|
|
196
|
+
_write(destination / "eslint.config.js", '''import js from "@eslint/js";
|
|
197
|
+
import globals from "globals";
|
|
198
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
199
|
+
import reactRefresh from "eslint-plugin-react-refresh";
|
|
200
|
+
import tseslint from "typescript-eslint";
|
|
201
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
202
|
+
|
|
203
|
+
export default defineConfig([
|
|
204
|
+
globalIgnores(["dist", "coverage"]),
|
|
205
|
+
{
|
|
206
|
+
files: ["**/*.{ts,tsx}"],
|
|
207
|
+
extends: [js.configs.recommended, tseslint.configs.recommended,
|
|
208
|
+
reactHooks.configs.flat.recommended, reactRefresh.configs.vite],
|
|
209
|
+
languageOptions: { globals: globals.browser },
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
''')
|
|
213
|
+
|
|
214
|
+
if options.axios:
|
|
215
|
+
_write(src / "services" / "api.ts", '''import axios from "axios";
|
|
216
|
+
|
|
217
|
+
export const api = axios.create({
|
|
218
|
+
baseURL: import.meta.env.VITE_API_BASE_URL ?? "",
|
|
219
|
+
timeout: 15_000,
|
|
220
|
+
headers: { "Content-Type": "application/json" },
|
|
221
|
+
});
|
|
222
|
+
''')
|
|
223
|
+
_write(destination / ".env.example", "VITE_API_BASE_URL=https://api.example.com")
|
|
224
|
+
else:
|
|
225
|
+
_write(destination / ".env.example", "# Add VITE_ prefixed public environment variables here.")
|
|
226
|
+
|
|
227
|
+
if options.zustand:
|
|
228
|
+
_write(src / "store" / "useAppStore.ts", '''import { create } from "zustand";
|
|
229
|
+
|
|
230
|
+
type AppState = {
|
|
231
|
+
count: number;
|
|
232
|
+
increment: () => void;
|
|
233
|
+
reset: () => void;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export const useAppStore = create<AppState>((set) => ({
|
|
237
|
+
count: 0,
|
|
238
|
+
increment: () => set((state) => ({ count: state.count + 1 })),
|
|
239
|
+
reset: () => set({ count: 0 }),
|
|
240
|
+
}));
|
|
241
|
+
''')
|
|
242
|
+
|
|
243
|
+
if options.vitest:
|
|
244
|
+
_write(src / "utils" / "example.test.ts", '''import { describe, expect, it } from "vitest";
|
|
245
|
+
|
|
246
|
+
describe("starter test", () => {
|
|
247
|
+
it("runs Vitest successfully", () => {
|
|
248
|
+
expect(1 + 1).toBe(2);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
''')
|
|
252
|
+
|
|
253
|
+
test_script = '\n- `npm test` — run Vitest' if options.vitest else ""
|
|
254
|
+
format_script = '\n- `npm run format` — format files with Prettier' if options.prettier else ""
|
|
255
|
+
lint_script = '\n- `npm run lint` — run ESLint' if options.eslint else ""
|
|
256
|
+
readme = f'''# {destination.name}
|
|
257
|
+
|
|
258
|
+
React + TypeScript starter generated by **belazywithreact**.
|
|
259
|
+
|
|
260
|
+
## Start development
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
npm run dev
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Available scripts
|
|
267
|
+
|
|
268
|
+
- `npm run dev` — start the Vite development server
|
|
269
|
+
- `npm run build` — type-check and build for production{lint_script}{test_script}{format_script}
|
|
270
|
+
|
|
271
|
+
## Project structure
|
|
272
|
+
|
|
273
|
+
- `src/components` — reusable UI components
|
|
274
|
+
- `src/pages` — route-level pages
|
|
275
|
+
- `src/hooks` — reusable React hooks
|
|
276
|
+
- `src/services` — API clients and service functions
|
|
277
|
+
- `src/store` — client-side state
|
|
278
|
+
- `src/styles` — global CSS and Tailwind entry point
|
|
279
|
+
- `src/utils` — shared utilities
|
|
280
|
+
'''
|
|
281
|
+
_write(destination / "README.md", readme)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def create_project(name: str, destination: Path, options: ProjectOptions, *, runner: Runner = subprocess.run) -> None:
|
|
285
|
+
if options.package_manager != "npm":
|
|
286
|
+
raise ValueError("Only npm is supported in belazywithreact 0.1.0.")
|
|
287
|
+
if options.styling not in {"css", "tailwind", "both"}:
|
|
288
|
+
raise ValueError("Styling must be css, tailwind, or both.")
|
|
289
|
+
# Validate library callers too, before any shell command or file write.
|
|
290
|
+
if destination.is_symlink() or destination.exists():
|
|
291
|
+
raise FileExistsError(
|
|
292
|
+
f"Destination already exists: {destination}. Choose another name; existing files are never overwritten."
|
|
293
|
+
)
|
|
294
|
+
if resolve_destination(name, destination.parent) != destination.resolve():
|
|
295
|
+
raise ValueError("Project name must match the destination directory.")
|
|
296
|
+
|
|
297
|
+
run_command(
|
|
298
|
+
["npm", "create", f"vite@{CREATE_VITE_VERSION}", name, "--", "--template", "react-ts", "--no-interactive"],
|
|
299
|
+
cwd=destination.parent,
|
|
300
|
+
runner=runner,
|
|
301
|
+
)
|
|
302
|
+
if not destination.is_dir():
|
|
303
|
+
raise RuntimeError("Vite completed without creating the expected directory. Check npm output.")
|
|
304
|
+
|
|
305
|
+
configure_project(destination, options)
|
|
306
|
+
|
|
307
|
+
dependencies: list[str] = []
|
|
308
|
+
dev_dependencies: list[str] = []
|
|
309
|
+
if options.router:
|
|
310
|
+
dependencies.append("react-router-dom@7.18.4")
|
|
311
|
+
if options.axios:
|
|
312
|
+
dependencies.append("axios@1.20.0")
|
|
313
|
+
if options.zustand:
|
|
314
|
+
dependencies.append("zustand@5.0.15")
|
|
315
|
+
if options.styling in {"tailwind", "both"}:
|
|
316
|
+
dev_dependencies.extend(["tailwindcss@4.3.3", "@tailwindcss/vite@4.3.3"])
|
|
317
|
+
if options.eslint:
|
|
318
|
+
dev_dependencies.extend(["eslint@10.12.0", "@eslint/js@10.0.1", "typescript-eslint@8.71.2",
|
|
319
|
+
"globals@17.13.0", "eslint-plugin-react-hooks@7.1.1",
|
|
320
|
+
"eslint-plugin-react-refresh@0.5.7"])
|
|
321
|
+
if options.prettier:
|
|
322
|
+
dev_dependencies.append("prettier@3.9.10")
|
|
323
|
+
if options.vitest:
|
|
324
|
+
dev_dependencies.append("vitest@5.0.4")
|
|
325
|
+
|
|
326
|
+
if dependencies:
|
|
327
|
+
run_command(["npm", "install", *dependencies], cwd=destination, runner=runner)
|
|
328
|
+
if dev_dependencies:
|
|
329
|
+
run_command(["npm", "install", "--save-dev", *dev_dependencies], cwd=destination, runner=runner)
|
|
330
|
+
|
|
331
|
+
package_json_path = destination / "package.json"
|
|
332
|
+
package_data = json.loads(package_json_path.read_text(encoding="utf-8"))
|
|
333
|
+
scripts = package_data.setdefault("scripts", {})
|
|
334
|
+
# Current Vite templates use Oxlint. Explicitly honor the ESLint option.
|
|
335
|
+
for section in ("dependencies", "devDependencies"):
|
|
336
|
+
package_data.get(section, {}).pop("oxlint", None)
|
|
337
|
+
package_data.get(section, {}).pop("oxlint-tsgolint", None)
|
|
338
|
+
for config_name in (".oxlintrc.json", "oxlint.config.ts"):
|
|
339
|
+
(destination / config_name).unlink(missing_ok=True)
|
|
340
|
+
if options.eslint:
|
|
341
|
+
scripts["lint"] = "eslint ."
|
|
342
|
+
if options.vitest:
|
|
343
|
+
scripts["test"] = "vitest run"
|
|
344
|
+
scripts["test:watch"] = "vitest"
|
|
345
|
+
if options.prettier:
|
|
346
|
+
scripts["format"] = "prettier --write ."
|
|
347
|
+
_write(destination / ".prettierignore", "node_modules\ndist\ncoverage\npackage-lock.json\n")
|
|
348
|
+
|
|
349
|
+
if not options.eslint:
|
|
350
|
+
scripts.pop("lint", None)
|
|
351
|
+
for config_name in ("eslint.config.js", "eslint.config.mjs", ".eslintrc", ".eslintrc.json"):
|
|
352
|
+
config_path = destination / config_name
|
|
353
|
+
if config_path.exists():
|
|
354
|
+
config_path.unlink()
|
|
355
|
+
eslint_packages = {
|
|
356
|
+
"eslint", "@eslint/js", "eslint-plugin-react-hooks",
|
|
357
|
+
"eslint-plugin-react-refresh", "typescript-eslint", "globals",
|
|
358
|
+
}
|
|
359
|
+
for section in ("dependencies", "devDependencies"):
|
|
360
|
+
packages = package_data.get(section, {})
|
|
361
|
+
for package_name in eslint_packages:
|
|
362
|
+
packages.pop(package_name, None)
|
|
363
|
+
|
|
364
|
+
package_json_path.write_text(json.dumps(package_data, indent=2) + "\n", encoding="utf-8")
|
|
365
|
+
# Always install: minimal projects have no optional installation commands.
|
|
366
|
+
# This also synchronizes the lockfile after removing optional tooling.
|
|
367
|
+
run_command(["npm", "install"], cwd=destination, runner=runner)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
_PROJECT_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
7
|
+
_WINDOWS_RESERVED = {"con", "prn", "aux", "nul", "conin$", "conout$"} | {
|
|
8
|
+
f"{prefix}{number}" for prefix in ("com", "lpt") for number in range(1, 10)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_project_name(name: str) -> str:
|
|
13
|
+
if not name or name in {".", ".."}:
|
|
14
|
+
raise ValueError("Project name must be a non-empty directory name.")
|
|
15
|
+
if not _PROJECT_NAME.fullmatch(name):
|
|
16
|
+
raise ValueError(
|
|
17
|
+
"Project name may contain only letters, numbers, dots, underscores, and hyphens, "
|
|
18
|
+
"and must start with a letter or number."
|
|
19
|
+
)
|
|
20
|
+
if name.endswith(".") or name.endswith(" "):
|
|
21
|
+
raise ValueError("Project name cannot end with a dot or space.")
|
|
22
|
+
if name.split(".", 1)[0].lower() in _WINDOWS_RESERVED:
|
|
23
|
+
raise ValueError("Project name is reserved on Windows. Choose another name.")
|
|
24
|
+
if len(name) > 214 or name.lower() in {"node_modules", "favicon.ico"}:
|
|
25
|
+
raise ValueError("Project name is reserved by npm or exceeds 214 characters.")
|
|
26
|
+
return name
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_destination(name: str, cwd: Path | None = None) -> Path:
|
|
30
|
+
validate_project_name(name)
|
|
31
|
+
base = (cwd or Path.cwd()).resolve()
|
|
32
|
+
destination = (base / name).resolve()
|
|
33
|
+
if destination == base or base not in destination.parents:
|
|
34
|
+
raise ValueError("Destination must be a child directory of the current directory.")
|
|
35
|
+
if (base / name).is_symlink():
|
|
36
|
+
raise ValueError("Destination is a symbolic link. Choose another project name.")
|
|
37
|
+
return destination
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def check_node_tools() -> tuple[str, str]:
|
|
41
|
+
node = shutil.which("node")
|
|
42
|
+
npm = shutil.which("npm.cmd") or shutil.which("npm")
|
|
43
|
+
missing = []
|
|
44
|
+
if not node:
|
|
45
|
+
missing.append("Node.js (node)")
|
|
46
|
+
if not npm:
|
|
47
|
+
missing.append("npm")
|
|
48
|
+
if missing:
|
|
49
|
+
raise RuntimeError(
|
|
50
|
+
"Missing required tool(s): " + ", ".join(missing) +
|
|
51
|
+
". Install a supported Node.js LTS release, then reopen your terminal."
|
|
52
|
+
)
|
|
53
|
+
try:
|
|
54
|
+
result = subprocess.run([node, "--version"], check=True, capture_output=True, text=True, timeout=10)
|
|
55
|
+
version = tuple(int(part) for part in result.stdout.strip().lstrip("v").split("."))
|
|
56
|
+
except (OSError, subprocess.SubprocessError, ValueError) as exc:
|
|
57
|
+
raise RuntimeError("Cannot run Node.js. Check the installation and PATH, then reopen your terminal.") from exc
|
|
58
|
+
if version < (22, 13, 0) or version[0] % 2:
|
|
59
|
+
raise RuntimeError("Use Node.js 22.13+ on an even-numbered release; Node.js 24 LTS is recommended.")
|
|
60
|
+
return node, npm
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: belazywithreact
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python CLI that scaffolds ready-to-use React projects.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sanju2911/belazywithreact
|
|
6
|
+
Project-URL: Repository, https://github.com/Sanju2911/belazywithreact
|
|
7
|
+
Project-URL: Issues, https://github.com/Sanju2911/belazywithreact/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cli,react,scaffold,typescript,vite
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: rich<15.0,>=13.7
|
|
23
|
+
Requires-Dist: typer<1.0,>=0.12
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: twine>=6.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# belazywithreact
|
|
32
|
+
|
|
33
|
+
[](https://github.com/Sanju2911/belazywithreact/actions/workflows/tests.yml)
|
|
34
|
+
|
|
35
|
+
A Python CLI that creates a React + TypeScript app with Vite and a useful starter
|
|
36
|
+
structure. Version **0.1.0** is the initial release. npm is the supported package
|
|
37
|
+
manager.
|
|
38
|
+
|
|
39
|
+
## Prerequisites
|
|
40
|
+
|
|
41
|
+
- Python 3.10 or newer.
|
|
42
|
+
- An even-numbered Node.js release >=22.13; Node.js 24 LTS is recommended.
|
|
43
|
+
- npm on PATH and internet access to download packages from the npm registry.
|
|
44
|
+
|
|
45
|
+
Check `python --version`, `node --version`, and `npm --version`.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
Install a published release from PyPI:
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
python -m pip install belazywithreact
|
|
53
|
+
belazywithreact --help
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Install from a local checkout:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
python -m pip install .
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For development on Windows, a virtual environment avoids global changes and
|
|
63
|
+
does not require changing PowerShell's execution policy:
|
|
64
|
+
|
|
65
|
+
```powershell
|
|
66
|
+
py -m venv .venv
|
|
67
|
+
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
|
|
68
|
+
.\.venv\Scripts\belazywithreact.exe --help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
On macOS/Linux use `python3 -m venv .venv`, then `.venv/bin/python` and
|
|
72
|
+
`.venv/bin/belazywithreact` instead.
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
belazywithreact create my-app
|
|
78
|
+
cd my-app
|
|
79
|
+
npm run dev
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Creation asks for confirmation. `--yes` skips that question and still refuses
|
|
83
|
+
to overwrite an existing destination, including an empty directory.
|
|
84
|
+
|
|
85
|
+
Defaults include CSS + Tailwind CSS 4, React Router, Axios, Zustand, ESLint,
|
|
86
|
+
Prettier, and Vitest. Tailwind uses its official Vite plugin and CSS import;
|
|
87
|
+
customize its theme in CSS. ESLint is explicitly configured for TypeScript and
|
|
88
|
+
React. `npm test` runs once, and `npm run test:watch` starts watch mode.
|
|
89
|
+
|
|
90
|
+
A minimal CSS app:
|
|
91
|
+
|
|
92
|
+
```powershell
|
|
93
|
+
belazywithreact create my-app --styling css --no-router --no-axios --no-zustand --no-eslint --no-prettier --no-vitest --yes
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| Option | Default | Behavior |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `--styling css\|tailwind\|both` | `both` | Enables Tailwind in tailwind/both; custom starter CSS is always included |
|
|
99
|
+
| `--router` / `--no-router` | enabled | Home and fallback routes with BrowserRouter |
|
|
100
|
+
| `--axios` / `--no-axios` | enabled | Axios API client and public API base URL example |
|
|
101
|
+
| `--zustand` / `--no-zustand` | enabled | Example counter store |
|
|
102
|
+
| `--eslint` / `--no-eslint` | enabled | ESLint config, dependencies, and lint script |
|
|
103
|
+
| `--prettier` / `--no-prettier` | enabled | Format script and ignore file |
|
|
104
|
+
| `--vitest` / `--no-vitest` | enabled | Test scripts and example unit test |
|
|
105
|
+
| `--package-manager npm` | `npm` | Only npm is supported in 0.1.0 |
|
|
106
|
+
| `--yes`, `-y` | disabled | Skip confirmation |
|
|
107
|
+
|
|
108
|
+
Use `belazywithreact create --help` for all options.
|
|
109
|
+
|
|
110
|
+
## Generated structure
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
my-app/
|
|
114
|
+
src/
|
|
115
|
+
assets/
|
|
116
|
+
components/Button.tsx
|
|
117
|
+
hooks/
|
|
118
|
+
layouts/
|
|
119
|
+
pages/HomePage.tsx
|
|
120
|
+
services/api.ts # when Axios is enabled
|
|
121
|
+
store/useAppStore.ts # when Zustand is enabled
|
|
122
|
+
styles/index.css
|
|
123
|
+
styles/tailwind.css
|
|
124
|
+
types/
|
|
125
|
+
utils/example.test.ts # when Vitest is enabled
|
|
126
|
+
App.tsx
|
|
127
|
+
main.tsx
|
|
128
|
+
.env.example
|
|
129
|
+
eslint.config.js # when ESLint is enabled
|
|
130
|
+
vite.config.ts
|
|
131
|
+
package.json
|
|
132
|
+
package-lock.json
|
|
133
|
+
README.md
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The Button supports native button props and children. Customize the generated
|
|
137
|
+
components, folders, API client, and store after creation. Empty starter folders
|
|
138
|
+
contain .gitkeep files. The scaffold does not provide authentication, backend
|
|
139
|
+
services, deployment, or arbitrary folder/component generation flags.
|
|
140
|
+
|
|
141
|
+
Validate your generated app:
|
|
142
|
+
|
|
143
|
+
```powershell
|
|
144
|
+
npm ci
|
|
145
|
+
npm test
|
|
146
|
+
npm run lint
|
|
147
|
+
npm run build
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Test and lint commands require their respective enabled options.
|
|
151
|
+
BrowserRouter deployments need a server fallback to index.html for route URLs.
|
|
152
|
+
All `VITE_` environment variables are public browser values, never secrets.
|
|
153
|
+
|
|
154
|
+
## Troubleshooting
|
|
155
|
+
|
|
156
|
+
- **Missing Node/npm or WinError 2:** install Node.js 24 LTS and reopen the
|
|
157
|
+
terminal so PATH updates. On Windows, check `where.exe node` and
|
|
158
|
+
`where.exe npm`; `npm.cmd --version` should work. The CLI preserves its
|
|
159
|
+
tested `call "<resolved npm.cmd>"` launch method for paths containing spaces.
|
|
160
|
+
- **Unsupported Node version:** upgrade to an even-numbered release >=22.13.
|
|
161
|
+
The generated tooling needs a newer Node version than some older Vite releases.
|
|
162
|
+
- **Invalid name:** use a directory name starting with a letter or number and
|
|
163
|
+
containing only letters, numbers, dots, underscores, or hyphens. Names cannot
|
|
164
|
+
end in a dot, exceed 214 characters, use Windows device names like CON/LPT1,
|
|
165
|
+
or use npm-reserved names. Path separators and shell operators are rejected.
|
|
166
|
+
Vite normalizes uppercase directory names to valid npm package names.
|
|
167
|
+
- **Destination exists:** choose another name; existing files are preserved.
|
|
168
|
+
- **npm failure or interruption:** read npm's output and check network/PATH.
|
|
169
|
+
Partial projects are preserved. Inspect them before manually removing or
|
|
170
|
+
repairing them; a retry with the same name will refuse to overwrite them.
|
|
171
|
+
- **Dependency advisory:** run `npm audit` and review compatible upgrades;
|
|
172
|
+
do not blindly force a major upgrade.
|
|
173
|
+
|
|
174
|
+
## Architecture and development
|
|
175
|
+
|
|
176
|
+
`cli.py` parses options with Typer and prints progress/errors with Rich.
|
|
177
|
+
`validators.py` validates names, destination boundaries, and Node prerequisites.
|
|
178
|
+
`scaffold.py` invokes create-vite **9.2.1**, writes the starter files, installs
|
|
179
|
+
selected tools, updates package.json, and synchronizes package-lock.json. npm
|
|
180
|
+
output remains visible, and failures return nonzero status. Interruption during
|
|
181
|
+
creation returns 130. The CLI never deletes a partial project automatically.
|
|
182
|
+
|
|
183
|
+
The Vite creator and optional-tool install requests specify reviewed versions;
|
|
184
|
+
Vite's base template and npm semver ranges can still resolve newer compatible
|
|
185
|
+
dependencies. Commit the generated lockfile and use `npm ci` for reproducible
|
|
186
|
+
app installations. Revalidate dependencies before a future CLI release.
|
|
187
|
+
|
|
188
|
+
```powershell
|
|
189
|
+
python -m pytest
|
|
190
|
+
python -m build
|
|
191
|
+
python -m twine check --strict dist/*
|
|
192
|
+
python scripts/check_dist.py
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Unit tests mock npm and run offline. CI checks Python 3.10, 3.12, and 3.14 on
|
|
196
|
+
Windows, macOS, and Linux, validates distributions, and builds real default and
|
|
197
|
+
minimal React apps. See [CONTRIBUTING.md](CONTRIBUTING.md) for contributions,
|
|
198
|
+
[SECURITY.md](SECURITY.md) for security reporting, and
|
|
199
|
+
[RELEASING.md](RELEASING.md) for TestPyPI-first Trusted Publishing and explicit
|
|
200
|
+
production-release approval. GitHub and both package-index accounts must be
|
|
201
|
+
configured separately before publication.
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
belazywithreact/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
belazywithreact/cli.py,sha256=-q89q0FCwCujxwB1cjJjTXjiQXFWnS--R51VQeOu9XM,4524
|
|
3
|
+
belazywithreact/scaffold.py,sha256=wGweDMUbadOrY_s5U2roh1kJQ0fJVJT7WSsY8s0RkiU,13024
|
|
4
|
+
belazywithreact/validators.py,sha256=KA5aeh4lLirko4GUSKpccjeGmR0BdZx0v61RwC3YeN8,2641
|
|
5
|
+
belazywithreact-0.1.0.dist-info/METADATA,sha256=fzkXGxlG7lxWa_wiNh0mahoMeXK6LaS3pzc8weCrqpk,7842
|
|
6
|
+
belazywithreact-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
belazywithreact-0.1.0.dist-info/entry_points.txt,sha256=HaDMASqg7QfnwAXp-m4S2khdLpg7NQRnDZuTT159c4E,60
|
|
8
|
+
belazywithreact-0.1.0.dist-info/licenses/LICENSE,sha256=pQ--5-WB6kwYHgoSVgVOr5xEdyQzfy2g-fh8xBSL5so,1085
|
|
9
|
+
belazywithreact-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 belazywithreact contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
18
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
19
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
20
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
21
|
+
DEALINGS IN THE SOFTWARE.
|