fastreact 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.
- fastreact/__init__.py +15 -0
- fastreact/cli.py +557 -0
- fastreact/core.py +256 -0
- fastreact/flask_core.py +188 -0
- fastreact/utils.py +233 -0
- fastreact-0.1.0.dist-info/METADATA +211 -0
- fastreact-0.1.0.dist-info/RECORD +10 -0
- fastreact-0.1.0.dist-info/WHEEL +5 -0
- fastreact-0.1.0.dist-info/entry_points.txt +2 -0
- fastreact-0.1.0.dist-info/top_level.txt +1 -0
fastreact/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .core import FastReact
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
__all__ = ["FastReact", "FlaskReact"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def __getattr__(name):
|
|
8
|
+
"""
|
|
9
|
+
Lazy import FlaskReact only when explicitly requested.
|
|
10
|
+
This prevents Flask import errors for users who only use FastReact.
|
|
11
|
+
"""
|
|
12
|
+
if name == "FlaskReact":
|
|
13
|
+
from .flask_core import FlaskReact
|
|
14
|
+
return FlaskReact
|
|
15
|
+
raise AttributeError(f"module 'fastreact' has no attribute {name!r}")
|
fastreact/cli.py
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import signal
|
|
6
|
+
import threading
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
BANNER = """
|
|
12
|
+
███████╗ █████╗ ███████╗████████╗██████╗ ███████╗ █████╗ ██████╗████████╗
|
|
13
|
+
██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔════╝██╔══██╗██╔════╝╚══██╔══╝
|
|
14
|
+
█████╗ ███████║███████╗ ██║ ██████╔╝█████╗ ███████║██║ ██║
|
|
15
|
+
██╔══╝ ██╔══██║╚════██║ ██║ ██╔══██╗██╔══╝ ██╔══██║██║ ██║
|
|
16
|
+
██║ ██║ ██║███████║ ██║ ██║ ██║███████╗██║ ██║╚██████╗ ██║
|
|
17
|
+
╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝
|
|
18
|
+
|
|
19
|
+
FastAPI + React = One Unified Stack 🚀
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ── Node helpers ─────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
def get_version(cmd):
|
|
26
|
+
for flag in ["--version", "-v", "-V"]:
|
|
27
|
+
try:
|
|
28
|
+
out = subprocess.check_output(
|
|
29
|
+
[cmd, flag],
|
|
30
|
+
stderr=subprocess.DEVNULL,
|
|
31
|
+
shell=(sys.platform == "win32")
|
|
32
|
+
).decode().strip()
|
|
33
|
+
if out:
|
|
34
|
+
return out
|
|
35
|
+
except Exception:
|
|
36
|
+
continue
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def auto_install_node():
|
|
41
|
+
platform = sys.platform
|
|
42
|
+
print("\n 🔧 Attempting to auto-install Node.js...\n")
|
|
43
|
+
try:
|
|
44
|
+
if platform == "win32":
|
|
45
|
+
print(" 📦 Using winget...")
|
|
46
|
+
result = subprocess.run(
|
|
47
|
+
["winget", "install", "OpenJS.NodeJS.LTS",
|
|
48
|
+
"--accept-source-agreements", "--accept-package-agreements"],
|
|
49
|
+
shell=True
|
|
50
|
+
)
|
|
51
|
+
if result.returncode == 0:
|
|
52
|
+
print("\n ✅ Node.js installed! Please reopen your terminal and run again.")
|
|
53
|
+
return True
|
|
54
|
+
raise Exception("winget failed")
|
|
55
|
+
elif platform == "darwin":
|
|
56
|
+
result = subprocess.run(["brew", "install", "node"])
|
|
57
|
+
if result.returncode == 0:
|
|
58
|
+
return True
|
|
59
|
+
raise Exception("brew failed")
|
|
60
|
+
elif platform.startswith("linux"):
|
|
61
|
+
subprocess.run(["sudo", "apt", "update"], check=True)
|
|
62
|
+
result = subprocess.run(["sudo", "apt", "install", "-y", "nodejs", "npm"])
|
|
63
|
+
if result.returncode == 0:
|
|
64
|
+
return True
|
|
65
|
+
raise Exception("apt failed")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f"\n ❌ Auto-install failed: {e}")
|
|
68
|
+
print(" 👉 Install manually from: https://nodejs.org")
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def check_node(auto_install=True):
|
|
73
|
+
node_version = get_version("node")
|
|
74
|
+
npm_version = get_version("npm")
|
|
75
|
+
if node_version and npm_version:
|
|
76
|
+
print(f" ✅ Node.js {node_version}")
|
|
77
|
+
print(f" ✅ npm v{npm_version}")
|
|
78
|
+
return True
|
|
79
|
+
print(" ❌ Node.js is not installed.")
|
|
80
|
+
if auto_install:
|
|
81
|
+
print(" 💡 Attempting auto-install...\n")
|
|
82
|
+
if auto_install_node():
|
|
83
|
+
node_version = get_version("node")
|
|
84
|
+
npm_version = get_version("npm")
|
|
85
|
+
if node_version and npm_version:
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
print(" 👉 Install from: https://nodejs.org")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ── Scaffold ─────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
def scaffold_react(project_name: str, target_dir: Path):
|
|
95
|
+
print(BANNER)
|
|
96
|
+
print(f"🛠️ Scaffolding React project: '{project_name}'")
|
|
97
|
+
print(f"📁 Location: {target_dir / project_name}\n")
|
|
98
|
+
print("🔍 Checking dependencies...")
|
|
99
|
+
|
|
100
|
+
if not check_node(auto_install=True):
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
react_path = target_dir / project_name
|
|
104
|
+
is_windows = sys.platform == "win32"
|
|
105
|
+
|
|
106
|
+
print(f"\n⚡ Running: npm create vite@latest {project_name} -- --template react\n")
|
|
107
|
+
result = subprocess.run(
|
|
108
|
+
["npm", "create", "vite@latest", project_name, "--", "--template", "react"],
|
|
109
|
+
cwd=str(target_dir),
|
|
110
|
+
input=b"\n",
|
|
111
|
+
shell=is_windows,
|
|
112
|
+
)
|
|
113
|
+
if result.returncode != 0:
|
|
114
|
+
print("\n❌ Failed to scaffold React project.")
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
|
|
117
|
+
print(f"\n📦 Installing npm dependencies...\n")
|
|
118
|
+
result = subprocess.run(
|
|
119
|
+
["npm", "install"],
|
|
120
|
+
cwd=str(react_path),
|
|
121
|
+
shell=is_windows,
|
|
122
|
+
)
|
|
123
|
+
if result.returncode != 0:
|
|
124
|
+
print("\n❌ Failed to install npm dependencies.")
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
inject_vite_config(react_path)
|
|
128
|
+
print_success(project_name, react_path)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def inject_vite_config(react_path: Path, extra_proxies: list[str] = None):
|
|
132
|
+
"""
|
|
133
|
+
Write vite.config.js with proxy config.
|
|
134
|
+
extra_proxies: list of path prefixes to proxy to FastAPI
|
|
135
|
+
e.g. ['/data', '/ui']
|
|
136
|
+
"""
|
|
137
|
+
# Default proxies always included
|
|
138
|
+
proxy_prefixes = ['/data', '/api', '/ui']
|
|
139
|
+
|
|
140
|
+
if extra_proxies:
|
|
141
|
+
for p in extra_proxies:
|
|
142
|
+
if p not in proxy_prefixes:
|
|
143
|
+
proxy_prefixes.append(p)
|
|
144
|
+
|
|
145
|
+
# Build proxy entries
|
|
146
|
+
proxy_entries = ""
|
|
147
|
+
for prefix in proxy_prefixes:
|
|
148
|
+
proxy_entries += f""" '{prefix}': {{
|
|
149
|
+
target: 'http://127.0.0.1:8000',
|
|
150
|
+
changeOrigin: true,
|
|
151
|
+
secure: false,
|
|
152
|
+
}},
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
config_content = f"""import {{ defineConfig }} from 'vite'
|
|
156
|
+
import react from '@vitejs/plugin-react'
|
|
157
|
+
|
|
158
|
+
// FastReact: Tunnels API/page calls from React dev server to FastAPI
|
|
159
|
+
export default defineConfig({{
|
|
160
|
+
plugins: [react()],
|
|
161
|
+
server: {{
|
|
162
|
+
port: 5173,
|
|
163
|
+
proxy: {{
|
|
164
|
+
{proxy_entries} }}
|
|
165
|
+
}},
|
|
166
|
+
build: {{
|
|
167
|
+
outDir: '../frontend_build',
|
|
168
|
+
emptyOutDir: true,
|
|
169
|
+
}}
|
|
170
|
+
}})
|
|
171
|
+
"""
|
|
172
|
+
vite_config = react_path / "vite.config.js"
|
|
173
|
+
with open(vite_config, "w") as f:
|
|
174
|
+
f.write(config_content)
|
|
175
|
+
print(" ✅ Injected FastReact proxy config into vite.config.js")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ── Dev mode ─────────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
VALID_UVICORN_OPTIONS = {
|
|
181
|
+
"--host", "--port", "--reload", "--reload-delay", "--reload-dir",
|
|
182
|
+
"--workers", "--log-level", "--access-log", "--no-access-log",
|
|
183
|
+
"--use-colors", "--no-use-colors", "--proxy-headers",
|
|
184
|
+
"--forwarded-allow-ips", "--root-path", "--limit-concurrency",
|
|
185
|
+
"--limit-max-requests", "--timeout-keep-alive", "--ssl-keyfile",
|
|
186
|
+
"--ssl-certfile", "--ssl-version", "--ssl-cert-reqs",
|
|
187
|
+
"--ssl-ca-certs", "--ssl-ciphers", "--h11-max-incomplete-event-size",
|
|
188
|
+
"--interface", "--http", "--ws", "--ws-max-size", "--ws-ping-interval",
|
|
189
|
+
"--ws-ping-timeout", "--lifespan", "--env-file", "--app-dir",
|
|
190
|
+
"--factory", "--loop", "--backlog",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
def _validate_uvicorn_args(args: list[str]):
|
|
194
|
+
"""
|
|
195
|
+
Check uvicorn args for typos before launching.
|
|
196
|
+
Exits with a clear error message if invalid option found.
|
|
197
|
+
"""
|
|
198
|
+
for arg in args:
|
|
199
|
+
if arg.startswith("--"):
|
|
200
|
+
# Strip value part e.g. --port=8000 → --port
|
|
201
|
+
flag = arg.split("=")[0]
|
|
202
|
+
if flag not in VALID_UVICORN_OPTIONS:
|
|
203
|
+
# Find closest match
|
|
204
|
+
import difflib
|
|
205
|
+
close = difflib.get_close_matches(flag, VALID_UVICORN_OPTIONS, n=1, cutoff=0.6)
|
|
206
|
+
suggestion = f" Did you mean: {close[0]}" if close else ""
|
|
207
|
+
print(f"""
|
|
208
|
+
╔══════════════════════════════════════════════╗
|
|
209
|
+
║ ❌ FastReact — Invalid Option ║
|
|
210
|
+
╠══════════════════════════════════════════════╣
|
|
211
|
+
║ ║
|
|
212
|
+
║ Unknown option: {flag:<28}║
|
|
213
|
+
║ {suggestion:<46}║
|
|
214
|
+
║ ║
|
|
215
|
+
║ Run: fastreact dev --help ║
|
|
216
|
+
║ ║
|
|
217
|
+
╚══════════════════════════════════════════════╝
|
|
218
|
+
""")
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
|
|
221
|
+
def detect_react_dir(cwd: Path) -> Path | None:
|
|
222
|
+
"""Find the React project folder (contains package.json + vite.config.js)."""
|
|
223
|
+
for candidate in ["frontend", "client", "web", "react-app"]:
|
|
224
|
+
p = cwd / candidate
|
|
225
|
+
if (p / "package.json").exists() and (p / "vite.config.js").exists():
|
|
226
|
+
return p
|
|
227
|
+
# fallback: any subfolder with vite.config.js
|
|
228
|
+
for item in cwd.iterdir():
|
|
229
|
+
if item.is_dir() and (item / "vite.config.js").exists():
|
|
230
|
+
return item
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def extract_prefixes_from_main(main_file: Path) -> list[str]:
|
|
235
|
+
"""
|
|
236
|
+
Parse main.py to extract react_prefix and all route paths.
|
|
237
|
+
Returns list of unique path prefixes to proxy.
|
|
238
|
+
e.g. ['/data', '/ui', '/items']
|
|
239
|
+
"""
|
|
240
|
+
prefixes = set()
|
|
241
|
+
try:
|
|
242
|
+
content = main_file.read_text()
|
|
243
|
+
|
|
244
|
+
# Extract react_prefix value e.g. react_prefix="ui" or react_prefix="/api/"
|
|
245
|
+
match = re.search(r'react_prefix\s*=\s*["\']([^"\']+)["\']', content)
|
|
246
|
+
if match:
|
|
247
|
+
raw = match.group(1).strip("/")
|
|
248
|
+
prefixes.add("/" + raw)
|
|
249
|
+
|
|
250
|
+
# Extract all @app.get("/something/...") route prefixes
|
|
251
|
+
for m in re.finditer(r'@app\.\w+\(["\'](/[^/"\']*)', content):
|
|
252
|
+
segment = m.group(1) # e.g. /data or /ui
|
|
253
|
+
if segment and segment != "/":
|
|
254
|
+
prefixes.add(segment)
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
print(f" ⚠️ Could not parse {main_file.name}: {e}")
|
|
258
|
+
|
|
259
|
+
return list(prefixes)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def run_dev(app_string: str, uvicorn_args: list[str], cwd: Path, show_calls: bool = False):
|
|
263
|
+
"""
|
|
264
|
+
Start both Uvicorn and Vite dev server together.
|
|
265
|
+
app_string: e.g. "main:app"
|
|
266
|
+
uvicorn_args: extra args passed through e.g. ["--reload", "--port", "8000"]
|
|
267
|
+
"""
|
|
268
|
+
print(BANNER)
|
|
269
|
+
is_windows = sys.platform == "win32"
|
|
270
|
+
|
|
271
|
+
# Parse port from uvicorn args
|
|
272
|
+
port = "8000"
|
|
273
|
+
if "--port" in uvicorn_args:
|
|
274
|
+
port = uvicorn_args[uvicorn_args.index("--port") + 1]
|
|
275
|
+
|
|
276
|
+
# Find React project dir
|
|
277
|
+
react_dir = detect_react_dir(cwd)
|
|
278
|
+
if not react_dir:
|
|
279
|
+
print("❌ Could not find React project folder.")
|
|
280
|
+
print(" Make sure you ran: fastreact create frontend")
|
|
281
|
+
sys.exit(1)
|
|
282
|
+
|
|
283
|
+
print(f"📁 React project: {react_dir.name}/")
|
|
284
|
+
|
|
285
|
+
# Extract prefixes from main file and silently update vite.config.js
|
|
286
|
+
main_file_name = app_string.split(":")[0] + ".py"
|
|
287
|
+
main_file = cwd / main_file_name
|
|
288
|
+
prefixes = extract_prefixes_from_main(main_file)
|
|
289
|
+
|
|
290
|
+
if prefixes:
|
|
291
|
+
print(f" 🔍 Detected route prefixes: {prefixes}")
|
|
292
|
+
inject_vite_config(react_dir, extra_proxies=prefixes)
|
|
293
|
+
else:
|
|
294
|
+
inject_vite_config(react_dir)
|
|
295
|
+
|
|
296
|
+
call_line = "║ 📡 Call monitor → watching all requests ║" if show_calls else "║ 💡 Tip: add --call to monitor all requests ║"
|
|
297
|
+
|
|
298
|
+
print(f"""
|
|
299
|
+
╔══════════════════════════════════════════════════════════╗
|
|
300
|
+
║ ⚡ FastReact Dev Mode ║
|
|
301
|
+
╠══════════════════════════════════════════════════════════╣
|
|
302
|
+
║ ║
|
|
303
|
+
║ 🌐 Open → http://localhost:5173 ║
|
|
304
|
+
║ ║
|
|
305
|
+
║ 🐍 FastAPI → http://127.0.0.1:{port} ║
|
|
306
|
+
║ ⚛️ Vite → http://localhost:5173 (HMR on) ║
|
|
307
|
+
║ 🔀 Proxy → all routes tunneled ║
|
|
308
|
+
║ ║
|
|
309
|
+
{call_line}
|
|
310
|
+
║ ║
|
|
311
|
+
║ 🛑 Ctrl+C to stop everything ║
|
|
312
|
+
║ ║
|
|
313
|
+
╚══════════════════════════════════════════════════════════╝
|
|
314
|
+
""")
|
|
315
|
+
|
|
316
|
+
# Strip out fastreact-owned flags before validating uvicorn args
|
|
317
|
+
# --call is ours, not uvicorn's
|
|
318
|
+
FASTREACT_FLAGS = {"--call"}
|
|
319
|
+
uvicorn_only_args = [a for a in uvicorn_args if a not in FASTREACT_FLAGS]
|
|
320
|
+
|
|
321
|
+
# Validate uvicorn args before starting anything
|
|
322
|
+
_validate_uvicorn_args(uvicorn_only_args)
|
|
323
|
+
uvicorn_args = uvicorn_only_args
|
|
324
|
+
|
|
325
|
+
# Always add --reload if not present
|
|
326
|
+
uvicorn_cmd = ["uvicorn", app_string] + uvicorn_args
|
|
327
|
+
if "--reload" not in uvicorn_args:
|
|
328
|
+
uvicorn_cmd.append("--reload")
|
|
329
|
+
|
|
330
|
+
# Filter output — only show errors + reload notices from uvicorn
|
|
331
|
+
uvicorn_proc = subprocess.Popen(
|
|
332
|
+
uvicorn_cmd,
|
|
333
|
+
cwd=str(cwd),
|
|
334
|
+
shell=is_windows,
|
|
335
|
+
stdout=subprocess.PIPE,
|
|
336
|
+
stderr=subprocess.STDOUT,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Vite output fully suppressed — we show our own banner
|
|
340
|
+
vite_proc = subprocess.Popen(
|
|
341
|
+
["npm", "run", "dev"],
|
|
342
|
+
cwd=str(react_dir),
|
|
343
|
+
shell=is_windows,
|
|
344
|
+
stdout=subprocess.DEVNULL,
|
|
345
|
+
stderr=subprocess.DEVNULL,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Handle Ctrl+C — kill both cleanly
|
|
349
|
+
def shutdown(sig=None, frame=None):
|
|
350
|
+
print("\n\n🛑 Shutting down FastReact...")
|
|
351
|
+
print(" ✅ Uvicorn stopped")
|
|
352
|
+
print(" ✅ Vite stopped")
|
|
353
|
+
try:
|
|
354
|
+
uvicorn_proc.terminate()
|
|
355
|
+
vite_proc.terminate()
|
|
356
|
+
except Exception:
|
|
357
|
+
pass
|
|
358
|
+
sys.exit(0)
|
|
359
|
+
|
|
360
|
+
signal.signal(signal.SIGINT, shutdown)
|
|
361
|
+
if sys.platform != "win32":
|
|
362
|
+
signal.signal(signal.SIGTERM, shutdown)
|
|
363
|
+
|
|
364
|
+
# Stream uvicorn output — filter based on --call flag
|
|
365
|
+
def stream_uvicorn():
|
|
366
|
+
SHOW_ALWAYS = (
|
|
367
|
+
"error", "Error", "ERROR",
|
|
368
|
+
"Reloading", "reloading",
|
|
369
|
+
"Application startup complete",
|
|
370
|
+
"Traceback",
|
|
371
|
+
)
|
|
372
|
+
SKIP_PATTERNS = (
|
|
373
|
+
"Will watch for changes",
|
|
374
|
+
"Started reloader",
|
|
375
|
+
"Started server process",
|
|
376
|
+
"Waiting for application",
|
|
377
|
+
"Uvicorn running on",
|
|
378
|
+
)
|
|
379
|
+
# HTTP method colors
|
|
380
|
+
METHOD_COLORS = {
|
|
381
|
+
"GET": "\033[32m", # green
|
|
382
|
+
"POST": "\033[34m", # blue
|
|
383
|
+
"PUT": "\033[33m", # yellow
|
|
384
|
+
"DELETE": "\033[31m", # red
|
|
385
|
+
"PATCH": "\033[35m", # magenta
|
|
386
|
+
}
|
|
387
|
+
STATUS_COLORS = {
|
|
388
|
+
"2": "\033[32m", # 2xx green
|
|
389
|
+
"3": "\033[36m", # 3xx cyan
|
|
390
|
+
"4": "\033[33m", # 4xx yellow
|
|
391
|
+
"5": "\033[31m", # 5xx red
|
|
392
|
+
}
|
|
393
|
+
RESET = "\033[0m"
|
|
394
|
+
BOLD = "\033[1m"
|
|
395
|
+
DIM = "\033[2m"
|
|
396
|
+
|
|
397
|
+
for line in uvicorn_proc.stdout:
|
|
398
|
+
text = line.decode(errors="replace").rstrip()
|
|
399
|
+
if not text:
|
|
400
|
+
continue
|
|
401
|
+
if any(skip in text for skip in SKIP_PATTERNS):
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
# Always show errors/reloads
|
|
405
|
+
if any(show in text for show in SHOW_ALWAYS):
|
|
406
|
+
print(f" {text}")
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
# HTTP request lines e.g:
|
|
410
|
+
# 127.0.0.1:PORT - "GET /api/users HTTP/1.1" 200 OK
|
|
411
|
+
if show_calls and '" ' in text and "HTTP/" in text:
|
|
412
|
+
try:
|
|
413
|
+
# Parse: IP - "METHOD PATH HTTP" STATUS
|
|
414
|
+
import re
|
|
415
|
+
m = re.search(r'"(\w+) ([^ ]+) HTTP/[\d.]+" (\d+)', text)
|
|
416
|
+
if m:
|
|
417
|
+
method = m.group(1)
|
|
418
|
+
path = m.group(2)
|
|
419
|
+
status = m.group(3)
|
|
420
|
+
|
|
421
|
+
method_color = METHOD_COLORS.get(method, "\033[37m")
|
|
422
|
+
status_color = STATUS_COLORS.get(status[0], "\033[37m")
|
|
423
|
+
|
|
424
|
+
import datetime
|
|
425
|
+
ts = datetime.datetime.now().strftime("%H:%M:%S")
|
|
426
|
+
|
|
427
|
+
print(
|
|
428
|
+
f" {DIM}{ts}{RESET} "
|
|
429
|
+
f"{method_color}{BOLD}{method:<7}{RESET} "
|
|
430
|
+
f"{path:<40} "
|
|
431
|
+
f"{status_color}{BOLD}{status}{RESET}"
|
|
432
|
+
)
|
|
433
|
+
else:
|
|
434
|
+
print(f" {text}")
|
|
435
|
+
except Exception:
|
|
436
|
+
print(f" {text}")
|
|
437
|
+
|
|
438
|
+
print("\n⚠️ FastAPI server stopped. Press Ctrl+C to exit.")
|
|
439
|
+
|
|
440
|
+
def watch_vite():
|
|
441
|
+
vite_proc.wait()
|
|
442
|
+
print("\n⚠️ Vite server stopped. Press Ctrl+C to exit.")
|
|
443
|
+
|
|
444
|
+
t1 = threading.Thread(target=stream_uvicorn, daemon=True)
|
|
445
|
+
t2 = threading.Thread(target=watch_vite, daemon=True)
|
|
446
|
+
t1.start()
|
|
447
|
+
t2.start()
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
t1.join()
|
|
451
|
+
t2.join()
|
|
452
|
+
except KeyboardInterrupt:
|
|
453
|
+
shutdown()
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ── Print success ─────────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
def print_success(project_name: str, react_path: Path):
|
|
459
|
+
print(f"""
|
|
460
|
+
╔══════════════════════════════════════════════════════╗
|
|
461
|
+
║ ✅ FastReact scaffold complete! ║
|
|
462
|
+
╚══════════════════════════════════════════════════════╝
|
|
463
|
+
|
|
464
|
+
📁 React project created at:
|
|
465
|
+
{react_path}
|
|
466
|
+
|
|
467
|
+
🚀 Next steps:
|
|
468
|
+
|
|
469
|
+
Dev mode (both servers at once):
|
|
470
|
+
fastreact dev main:app --reload
|
|
471
|
+
|
|
472
|
+
Or manually:
|
|
473
|
+
uvicorn main:app --reload
|
|
474
|
+
cd {project_name} && npm run dev
|
|
475
|
+
|
|
476
|
+
Production build:
|
|
477
|
+
cd {project_name} && npm run build
|
|
478
|
+
|
|
479
|
+
⚡ Powered by FastReact
|
|
480
|
+
""")
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# ── CLI entry ─────────────────────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
HELP_TEXT = (
|
|
488
|
+
"\033[1m\033[35m\n"
|
|
489
|
+
"███████╗ █████╗ ███████╗████████╗██████╗ ███████╗ █████╗ ██████╗████████╗\n"
|
|
490
|
+
"██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔════╝██╔══██╗██╔════╝╚══██╔══╝\n"
|
|
491
|
+
"█████╗ ███████║███████╗ ██║ ██████╔╝█████╗ ███████║██║ ██║ \n"
|
|
492
|
+
"██╔══╝ ██╔══██║╚════██║ ██║ ██╔══██╗██╔══╝ ██╔══██║██║ ██║ \n"
|
|
493
|
+
"██║ ██║ ██║███████║ ██║ ██║ ██║███████╗██║ ██║╚██████╗ ██║ \n"
|
|
494
|
+
"\033[0m\n"
|
|
495
|
+
"\033[2m FastAPI + React = One Unified Stack\033[0m\n\n"
|
|
496
|
+
"\033[1m\033[36m COMMANDS\033[0m\n\n"
|
|
497
|
+
" \033[1m\033[32mfastreact create\033[0m \033[33m<name>\033[0m\n"
|
|
498
|
+
" \033[2m Scaffold a Vite React app wired into your FastAPI project\033[0m\n\n"
|
|
499
|
+
" $ fastreact create frontend\n\n"
|
|
500
|
+
" \033[1m\033[32mfastreact dev\033[0m \033[33m<file:app>\033[0m \033[2m[options]\033[0m\n"
|
|
501
|
+
" \033[2m Start FastAPI + Vite together. Same syntax as uvicorn.\033[0m\n\n"
|
|
502
|
+
" $ fastreact dev main:app --reload\n"
|
|
503
|
+
" $ fastreact dev main:app --reload --port 8000\n"
|
|
504
|
+
" $ fastreact dev main:app --reload --host 0.0.0.0\n"
|
|
505
|
+
" $ fastreact dev main:app --reload --call\n\n"
|
|
506
|
+
"\033[1m\033[36m UVICORN OPTIONS\033[0m\n\n"
|
|
507
|
+
" \033[33m--reload\033[0m Auto-restart on file save\n"
|
|
508
|
+
" \033[33m--port\033[0m \033[35m<number>\033[0m Port number \033[2mdefault: 8000\033[0m\n"
|
|
509
|
+
" \033[33m--host\033[0m \033[35m<address>\033[0m Host address \033[2mdefault: 127.0.0.1\033[0m\n"
|
|
510
|
+
" \033[33m--workers\033[0m \033[35m<number>\033[0m Worker processes \033[2mdefault: 1\033[0m\n\n"
|
|
511
|
+
"\033[1m\033[36m FASTREACT FLAGS\033[0m\n\n"
|
|
512
|
+
" \033[33m--call\033[0m Live monitor - every request colored by method and status\n\n"
|
|
513
|
+
"\033[1m\033[36m HOW ROUTING WORKS\033[0m\n\n"
|
|
514
|
+
" \033[32m@app.get(\"/ui/users\")\033[0m React page - browser gets React, Postman gets \033[31m405\033[0m\n"
|
|
515
|
+
" \033[32m@app.get(\"/data/users\")\033[0m Normal route - everyone gets JSON \033[32m200\033[0m\n\n"
|
|
516
|
+
"\033[1m\033[36m QUICK START\033[0m\n\n"
|
|
517
|
+
" fastreact create frontend\n"
|
|
518
|
+
" fastreact dev main:app --reload --call\n\n"
|
|
519
|
+
" \033[2m# build for prod\033[0m\n"
|
|
520
|
+
" cd frontend && npm run build\n"
|
|
521
|
+
" uvicorn main:app --host 0.0.0.0 --port 8000\n\n"
|
|
522
|
+
" \033[36m pip install fastreact\033[0m\n"
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def main():
|
|
527
|
+
if len(sys.argv) == 1 or (len(sys.argv) == 2 and sys.argv[1] in ("--help", "-h", "help")):
|
|
528
|
+
print(HELP_TEXT)
|
|
529
|
+
sys.exit(0)
|
|
530
|
+
|
|
531
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
532
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
533
|
+
|
|
534
|
+
create_parser = subparsers.add_parser("create", add_help=False)
|
|
535
|
+
create_parser.add_argument("name")
|
|
536
|
+
|
|
537
|
+
dev_parser = subparsers.add_parser("dev", add_help=False)
|
|
538
|
+
dev_parser.add_argument("app")
|
|
539
|
+
dev_parser.add_argument("uvicorn_args", nargs=argparse.REMAINDER)
|
|
540
|
+
|
|
541
|
+
args = parser.parse_args()
|
|
542
|
+
cwd = Path(os.getcwd())
|
|
543
|
+
|
|
544
|
+
if args.command == "create":
|
|
545
|
+
scaffold_react(args.name, cwd)
|
|
546
|
+
|
|
547
|
+
elif args.command == "dev":
|
|
548
|
+
show_calls = "--call" in args.uvicorn_args
|
|
549
|
+
uvicorn_args = [a for a in args.uvicorn_args if a != "--call"]
|
|
550
|
+
run_dev(args.app, uvicorn_args, cwd, show_calls=show_calls)
|
|
551
|
+
|
|
552
|
+
else:
|
|
553
|
+
print(HELP_TEXT)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
if __name__ == "__main__":
|
|
557
|
+
main()
|