Skip to content

Commit a7a6592

Browse files
authored
Fix the npm issue when running a fullstack Python app (#471)
1 parent af21426 commit a7a6592

File tree

2 files changed

+70
-28
lines changed

2 files changed

+70
-28
lines changed
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-llama": patch
3+
---
4+
5+
Fix the npm issue on the full-stack Python template

templates/types/streaming/fastapi/run.py

+65-28
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,33 @@
1414

1515

1616
FRONTEND_DIR = Path(os.getenv("FRONTEND_DIR", ".frontend"))
17-
DEFAULT_FRONTEND_PORT = 3000
17+
APP_HOST = os.getenv("APP_HOST", "localhost")
18+
APP_PORT = int(
19+
os.getenv("APP_PORT", 8000)
20+
) # Allocated to backend but also for access to the app, please change it in .env
21+
DEFAULT_FRONTEND_PORT = (
22+
3000 # Not for access directly, but for proxying to the backend in development
23+
)
1824
STATIC_DIR = Path(os.getenv("STATIC_DIR", "static"))
1925

2026

27+
class NodePackageManager(str):
28+
def __new__(cls, value: str) -> "NodePackageManager":
29+
return super().__new__(cls, value)
30+
31+
@property
32+
def name(self) -> str:
33+
return Path(self).stem
34+
35+
@property
36+
def is_pnpm(self) -> bool:
37+
return self.name == "pnpm"
38+
39+
@property
40+
def is_npm(self) -> bool:
41+
return self.name == "npm"
42+
43+
2144
def build():
2245
"""
2346
Build the frontend and copy the static files to the backend.
@@ -146,22 +169,20 @@ async def _run_frontend(
146169
package_manager,
147170
"run",
148171
"dev",
172+
"--" if package_manager.is_npm else "",
149173
"-p",
150174
str(port),
151175
cwd=FRONTEND_DIR,
152176
)
153-
rich.print(
154-
f"\n[bold]Waiting for frontend to start, port: {port}, process id: {frontend_process.pid}[/bold]"
155-
)
177+
rich.print("\n[bold]Waiting for frontend to start...")
156178
# Block until the frontend is accessible
157179
for _ in range(timeout):
158180
await asyncio.sleep(1)
159-
# Check if the frontend is accessible (port is open) or frontend_process is running
160181
if frontend_process.returncode is not None:
161182
raise RuntimeError("Could not start frontend dev server")
162-
if not _is_bindable_port(port):
183+
if _is_server_running(port):
163184
rich.print(
164-
f"\n[bold green]Frontend dev server is running on port {port}[/bold green]"
185+
"\n[bold]Frontend dev server is running. Please wait a while for the app to be ready...[/bold]"
165186
)
166187
return frontend_process, port
167188
raise TimeoutError(f"Frontend dev server failed to start within {timeout} seconds")
@@ -173,33 +194,50 @@ async def _run_backend(
173194
"""
174195
Start the backend development server.
175196
176-
Args:
177-
frontend_port: The port number the frontend is running on
178197
Returns:
179198
Process: The backend process
180199
"""
181200
# Merge environment variables
182201
envs = {**os.environ, **(envs or {})}
183-
rich.print("\n[bold]Starting backend FastAPI server...[/bold]")
202+
# Check if the port is free
203+
if not _is_port_available(APP_PORT):
204+
raise SystemError(
205+
f"Port {APP_PORT} is not available! Please change the port in .env file or kill the process running on this port."
206+
)
207+
rich.print(f"\n[bold]Starting app on port {APP_PORT}...[/bold]")
184208
poetry_executable = _get_poetry_executable()
185-
return await asyncio.create_subprocess_exec(
209+
process = await asyncio.create_subprocess_exec(
186210
poetry_executable,
187211
"run",
188212
"python",
189213
"main.py",
190214
env=envs,
191215
)
216+
# Wait for port is started
217+
timeout = 30
218+
for _ in range(timeout):
219+
await asyncio.sleep(1)
220+
if process.returncode is not None:
221+
raise RuntimeError("Could not start backend dev server")
222+
if _is_server_running(APP_PORT):
223+
rich.print(
224+
f"\n[bold green]App is running. You now can access it at http://{APP_HOST}:{APP_PORT}[/bold green]"
225+
)
226+
return process
227+
# Timeout, kill the process
228+
process.terminate()
229+
raise TimeoutError(f"Backend dev server failed to start within {timeout} seconds")
192230

193231

194232
def _install_frontend_dependencies():
195233
package_manager = _get_node_package_manager()
196234
rich.print(
197-
f"\n[bold]Installing frontend dependencies using {Path(package_manager).name}. It might take a while...[/bold]"
235+
f"\n[bold]Installing frontend dependencies using {package_manager.name}. It might take a while...[/bold]"
198236
)
199237
run([package_manager, "install"], cwd=".frontend", check=True)
200238

201239

202-
def _get_node_package_manager() -> str:
240+
def _get_node_package_manager() -> NodePackageManager:
203241
"""
204242
Check for available package managers and return the preferred one.
205243
Returns 'pnpm' if installed, falls back to 'npm'.
@@ -215,12 +253,12 @@ def _get_node_package_manager() -> str:
215253
for cmd in pnpm_cmds:
216254
cmd_path = which(cmd)
217255
if cmd_path is not None:
218-
return cmd_path
256+
return NodePackageManager(cmd_path)
219257

220258
for cmd in npm_cmds:
221259
cmd_path = which(cmd)
222260
if cmd_path is not None:
223-
return cmd_path
261+
return NodePackageManager(cmd_path)
224262

225263
raise SystemError(
226264
"Neither pnpm nor npm is installed. Please install Node.js and a package manager first."
@@ -244,28 +282,27 @@ def _get_poetry_executable() -> str:
244282
raise SystemError("Poetry is not installed. Please install Poetry first.")
245283

246284

247-
def _is_bindable_port(port: int) -> bool:
248-
"""Check if a port is available by attempting to connect to it."""
285+
def _is_port_available(port: int) -> bool:
286+
"""Check if a port is available for binding."""
249287
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
250288
try:
251-
# Try to connect to the port
252289
s.connect(("localhost", port))
253-
# If we can connect, port is in use
254-
return False
290+
return False # Port is in use, so not available
255291
except ConnectionRefusedError:
256-
# Connection refused means port is available
257-
return True
292+
return True # Port is available
258293
except socket.error:
259-
# Other socket errors also likely mean port is available
260-
return True
294+
return True # Other socket errors likely mean port is available
295+
296+
297+
def _is_server_running(port: int) -> bool:
298+
"""Check if a server is running on the specified port."""
299+
return not _is_port_available(port)
261300

262301

263302
def _find_free_port(start_port: int) -> int:
264-
"""
265-
Find a free port starting from the given port number.
266-
"""
303+
"""Find a free port starting from the given port number."""
267304
for port in range(start_port, 65535):
268-
if _is_bindable_port(port):
305+
if _is_port_available(port):
269306
return port
270307
raise SystemError("No free port found")
271308

0 commit comments

Comments
 (0)