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.
@@ -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
+ [![CI](https://github.com/Sanju2911/belazywithreact/actions/workflows/tests.yml/badge.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ belazywithreact = belazywithreact.cli:app
@@ -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.