pulse-framework 0.1.38a2__tar.gz → 0.1.38a4__tar.gz
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.
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/PKG-INFO +1 -1
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/pyproject.toml +1 -1
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/app.py +36 -128
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/cli/cmd.py +79 -31
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/env.py +9 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/proxy.py +13 -10
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/README.md +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/__init__.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/channel.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/cli/__init__.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/cli/dependencies.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/cli/folder_lock.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/cli/helpers.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/cli/models.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/cli/packages.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/cli/processes.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/cli/secrets.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/cli/uvicorn_log_config.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/codegen/__init__.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/codegen/codegen.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/codegen/imports.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/codegen/js.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/codegen/templates/__init__.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/codegen/templates/layout.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/codegen/templates/route.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/codegen/templates/routes_ts.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/codegen/utils.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/components/__init__.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/components/for_.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/components/if_.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/components/react_router.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/context.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/cookies.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/css.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/decorators.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/form.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/helpers.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/hooks/__init__.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/hooks/core.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/hooks/effects.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/hooks/runtime.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/hooks/setup.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/hooks/stable.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/hooks/states.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/html/__init__.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/html/elements.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/html/events.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/html/props.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/html/svg.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/html/tags.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/html/tags.pyi +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/messages.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/middleware.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/plugin.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/py.typed +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/query.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/react_component.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/reactive.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/reactive_extensions.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/render_session.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/renderer.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/request.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/routing.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/serializer.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/state.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/types/__init__.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/types/event_handler.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/user_session.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/vdom.py +0 -0
- {pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/version.py +0 -0
|
@@ -5,15 +5,12 @@ This module provides the main App class that users instantiate in their main.py
|
|
|
5
5
|
to define routes and configure their Pulse application.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import asyncio
|
|
9
8
|
import logging
|
|
10
9
|
import os
|
|
11
|
-
import subprocess
|
|
12
10
|
from collections import defaultdict
|
|
13
11
|
from collections.abc import Awaitable, Sequence
|
|
14
12
|
from contextlib import asynccontextmanager
|
|
15
13
|
from enum import IntEnum
|
|
16
|
-
from pathlib import Path
|
|
17
14
|
from typing import Any, Callable, Literal, NotRequired, TypedDict, TypeVar, cast
|
|
18
15
|
|
|
19
16
|
import socketio
|
|
@@ -38,7 +35,11 @@ from pulse.css import (
|
|
|
38
35
|
registered_css_imports,
|
|
39
36
|
registered_css_modules,
|
|
40
37
|
)
|
|
41
|
-
from pulse.env import
|
|
38
|
+
from pulse.env import (
|
|
39
|
+
ENV_PULSE_HOST,
|
|
40
|
+
ENV_PULSE_PORT,
|
|
41
|
+
PulseEnv,
|
|
42
|
+
)
|
|
42
43
|
from pulse.env import env as envvars
|
|
43
44
|
from pulse.helpers import (
|
|
44
45
|
create_task,
|
|
@@ -225,10 +226,6 @@ class App:
|
|
|
225
226
|
# Map websocket sid -> renderId for message routing
|
|
226
227
|
self._socket_to_render = {}
|
|
227
228
|
|
|
228
|
-
# Subprocess management for single-server mode
|
|
229
|
-
self.web_server_proc: subprocess.Popen[str] | None = None
|
|
230
|
-
self.web_server_port: int | None = None
|
|
231
|
-
|
|
232
229
|
self.codegen = Codegen(
|
|
233
230
|
self.routes,
|
|
234
231
|
config=codegen or CodegenConfig(),
|
|
@@ -266,116 +263,26 @@ class App:
|
|
|
266
263
|
for plugin in self.plugins:
|
|
267
264
|
plugin.on_startup(self)
|
|
268
265
|
|
|
269
|
-
# Start React Router server in single-server mode
|
|
270
|
-
logger.info(f"Deployment mode: {self.mode}")
|
|
271
266
|
if self.mode == "single-server":
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
web_root = self.codegen.cfg.web_root
|
|
275
|
-
logger.info(f"Web root: {web_root}")
|
|
276
|
-
port = await self.start_web_server(web_root, self.env)
|
|
267
|
+
react_server_address = envvars.react_server_address
|
|
268
|
+
if react_server_address:
|
|
277
269
|
logger.info(
|
|
278
|
-
f"Single-server mode: React Router running
|
|
270
|
+
f"Single-server mode: React Router running at {react_server_address}"
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
logger.warning(
|
|
274
|
+
"Single-server mode: PULSE_REACT_SERVER_ADDRESS not set."
|
|
279
275
|
)
|
|
280
|
-
except Exception:
|
|
281
|
-
logger.exception("Failed to start React Router server")
|
|
282
|
-
raise
|
|
283
276
|
|
|
284
277
|
try:
|
|
285
278
|
yield
|
|
286
279
|
finally:
|
|
287
|
-
# Stop React Router server in single-server mode
|
|
288
|
-
if self.mode == "single-server":
|
|
289
|
-
try:
|
|
290
|
-
self.stop_web_server()
|
|
291
|
-
except Exception:
|
|
292
|
-
logger.exception("Error stopping React Router server")
|
|
293
|
-
|
|
294
280
|
try:
|
|
295
281
|
if isinstance(self.session_store, SessionStore):
|
|
296
282
|
await self.session_store.close()
|
|
297
283
|
except Exception:
|
|
298
284
|
logger.exception("Error during SessionStore.close()")
|
|
299
285
|
|
|
300
|
-
async def start_web_server(self, web_root: Path, mode: str) -> int:
|
|
301
|
-
"""Start React Router server as subprocess and return its port."""
|
|
302
|
-
|
|
303
|
-
# Find available port
|
|
304
|
-
port = find_available_port(5173)
|
|
305
|
-
|
|
306
|
-
# Build command based on mode
|
|
307
|
-
if mode == "prod":
|
|
308
|
-
# Check if build exists
|
|
309
|
-
build_server = web_root / "build" / "server" / "index.js"
|
|
310
|
-
if not build_server.exists():
|
|
311
|
-
raise RuntimeError(
|
|
312
|
-
f"Production build not found at {build_server}. Run 'bun run build' in the web directory first."
|
|
313
|
-
)
|
|
314
|
-
cli_path = (
|
|
315
|
-
web_root
|
|
316
|
-
/ "node_modules"
|
|
317
|
-
/ "@react-router"
|
|
318
|
-
/ "serve"
|
|
319
|
-
/ "dist"
|
|
320
|
-
/ "cli.js"
|
|
321
|
-
)
|
|
322
|
-
if not cli_path.exists():
|
|
323
|
-
raise RuntimeError(
|
|
324
|
-
f"React Router CLI not found at {cli_path}. Did you install web dependencies?"
|
|
325
|
-
)
|
|
326
|
-
# Production: use Node to serve the built bundle (Bun lacks renderToPipeableStream)
|
|
327
|
-
cmd = [
|
|
328
|
-
"bun",
|
|
329
|
-
"run",
|
|
330
|
-
"start",
|
|
331
|
-
"--port",
|
|
332
|
-
str(port),
|
|
333
|
-
]
|
|
334
|
-
else:
|
|
335
|
-
# Development: use dev server
|
|
336
|
-
cmd = ["bun", "run", "dev", "--port", str(port)]
|
|
337
|
-
|
|
338
|
-
logger.info(f"Starting React Router server: {' '.join(cmd)}")
|
|
339
|
-
|
|
340
|
-
proc = subprocess.Popen(
|
|
341
|
-
cmd,
|
|
342
|
-
cwd=web_root,
|
|
343
|
-
stdout=subprocess.PIPE,
|
|
344
|
-
stderr=subprocess.STDOUT,
|
|
345
|
-
env=os.environ.copy(),
|
|
346
|
-
text=True,
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
# Wait for server to be ready
|
|
350
|
-
await asyncio.sleep(2.0 if mode == "prod" else 1.0)
|
|
351
|
-
|
|
352
|
-
# Check if process is still running
|
|
353
|
-
if proc.poll() is not None:
|
|
354
|
-
output = proc.stdout.read() if proc.stdout else ""
|
|
355
|
-
raise RuntimeError(f"React Router server failed to start: {output}")
|
|
356
|
-
|
|
357
|
-
self.web_server_proc = proc
|
|
358
|
-
self.web_server_port = port
|
|
359
|
-
|
|
360
|
-
logger.info(f"React Router server started on port {port}")
|
|
361
|
-
return port
|
|
362
|
-
|
|
363
|
-
def stop_web_server(self):
|
|
364
|
-
"""Stop the React Router subprocess."""
|
|
365
|
-
if self.web_server_proc:
|
|
366
|
-
logger.info("Stopping React Router server...")
|
|
367
|
-
self.web_server_proc.terminate()
|
|
368
|
-
try:
|
|
369
|
-
self.web_server_proc.wait(timeout=5)
|
|
370
|
-
except subprocess.TimeoutExpired:
|
|
371
|
-
logger.warning(
|
|
372
|
-
"React Router server did not stop gracefully, killing..."
|
|
373
|
-
)
|
|
374
|
-
self.web_server_proc.kill()
|
|
375
|
-
self.web_server_proc.wait()
|
|
376
|
-
self.web_server_proc = None
|
|
377
|
-
self.web_server_port = None
|
|
378
|
-
|
|
379
286
|
def run_codegen(
|
|
380
287
|
self, address: str | None = None, internal_address: str | None = None
|
|
381
288
|
):
|
|
@@ -426,10 +333,11 @@ class App:
|
|
|
426
333
|
self.setup(server_address)
|
|
427
334
|
self.status = AppStatus.running
|
|
428
335
|
|
|
429
|
-
#
|
|
430
|
-
# __init__ to allow the CLI to override the mode.
|
|
336
|
+
# In single-server mode, the Pulse server acts as reverse proxy to the React server
|
|
431
337
|
if self.mode == "single-server":
|
|
432
|
-
return PulseProxy(
|
|
338
|
+
return PulseProxy(
|
|
339
|
+
self.asgi, lambda: envvars.react_server_address, self.api_prefix
|
|
340
|
+
)
|
|
433
341
|
return self.asgi
|
|
434
342
|
|
|
435
343
|
def run(
|
|
@@ -471,26 +379,26 @@ class App:
|
|
|
471
379
|
)
|
|
472
380
|
|
|
473
381
|
# Debug middleware to log CORS-related request details
|
|
474
|
-
@self.fastapi.middleware("http")
|
|
475
|
-
async def cors_debug_middleware( # pyright: ignore[reportUnusedFunction]
|
|
476
|
-
|
|
477
|
-
):
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
382
|
+
# @self.fastapi.middleware("http")
|
|
383
|
+
# async def cors_debug_middleware( # pyright: ignore[reportUnusedFunction]
|
|
384
|
+
# request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
|
385
|
+
# ):
|
|
386
|
+
# origin = request.headers.get("origin")
|
|
387
|
+
# method = request.method
|
|
388
|
+
# path = request.url.path
|
|
389
|
+
# print(
|
|
390
|
+
# f"[CORS Debug] {method} {path} | Origin: {origin} | "
|
|
391
|
+
# + f"Mode: {self.mode} | Server: {self.server_address}"
|
|
392
|
+
# )
|
|
393
|
+
# response = await call_next(request)
|
|
394
|
+
# allow_origin = response.headers.get("access-control-allow-origin")
|
|
395
|
+
# if allow_origin:
|
|
396
|
+
# print(f"[CORS Debug] Response allows origin: {allow_origin}")
|
|
397
|
+
# elif origin:
|
|
398
|
+
# logger.warning(
|
|
399
|
+
# f"[CORS Debug] Origin {origin} present but no Access-Control-Allow-Origin header set"
|
|
400
|
+
# )
|
|
401
|
+
# return response
|
|
494
402
|
|
|
495
403
|
# Mount PulseContext for all FastAPI routes (no route info). Other API
|
|
496
404
|
# routes / middleware should be added at the module-level, which means
|
|
@@ -34,6 +34,7 @@ from pulse.env import (
|
|
|
34
34
|
ENV_PULSE_DISABLE_CODEGEN,
|
|
35
35
|
ENV_PULSE_HOST,
|
|
36
36
|
ENV_PULSE_PORT,
|
|
37
|
+
ENV_PULSE_REACT_SERVER_ADDRESS,
|
|
37
38
|
ENV_PULSE_SECRET,
|
|
38
39
|
PulseEnv,
|
|
39
40
|
env,
|
|
@@ -69,8 +70,16 @@ def run(
|
|
|
69
70
|
prod: bool = typer.Option(False, "--prod", help="Run in production env"),
|
|
70
71
|
server_only: bool = typer.Option(False, "--server-only", "--backend-only"),
|
|
71
72
|
web_only: bool = typer.Option(False, "--web-only"),
|
|
73
|
+
react_server_address: str | None = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--react-server-address",
|
|
76
|
+
help="Full URL of React server (required for single-server + --server-only)",
|
|
77
|
+
),
|
|
72
78
|
reload: bool | None = typer.Option(None, "--reload/--no-reload"),
|
|
73
79
|
find_port: bool = typer.Option(True, "--find-port/--no-find-port"),
|
|
80
|
+
verbose: bool = typer.Option(
|
|
81
|
+
False, "--verbose", help="Show all logs without filtering"
|
|
82
|
+
),
|
|
74
83
|
):
|
|
75
84
|
"""Run the Pulse server and web development server together."""
|
|
76
85
|
extra_flags = list(ctx.args)
|
|
@@ -106,14 +115,19 @@ def run(
|
|
|
106
115
|
_apply_app_context_to_env(app_ctx)
|
|
107
116
|
app_instance = app_ctx.app
|
|
108
117
|
|
|
109
|
-
# Override mode to subdomains if server-only (single-server + server-only makes no sense)
|
|
110
|
-
if server_only and app_instance.mode == "single-server":
|
|
111
|
-
app_instance.mode = "subdomains"
|
|
112
|
-
|
|
113
118
|
is_single_server = app_instance.mode == "single-server"
|
|
114
119
|
if is_single_server:
|
|
115
120
|
console.log("🔧 [cyan]Single-server mode[/cyan]")
|
|
116
121
|
|
|
122
|
+
# In single-server + server-only mode, require explicit React server address
|
|
123
|
+
if is_single_server and server_only:
|
|
124
|
+
if not react_server_address:
|
|
125
|
+
typer.echo(
|
|
126
|
+
"❌ --react-server-address is required when using single-server mode with --server-only."
|
|
127
|
+
)
|
|
128
|
+
raise typer.Exit(1)
|
|
129
|
+
os.environ[ENV_PULSE_REACT_SERVER_ADDRESS] = react_server_address
|
|
130
|
+
|
|
117
131
|
web_root = app_instance.codegen.cfg.web_root
|
|
118
132
|
if not web_root.exists() and not server_only:
|
|
119
133
|
console.log(f"❌ Directory not found: {web_root.absolute()}")
|
|
@@ -125,37 +139,54 @@ def run(
|
|
|
125
139
|
web_root if web_root.exists() else app_ctx.app_file
|
|
126
140
|
)
|
|
127
141
|
|
|
128
|
-
if not server_only:
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
if env.pulse_env == "dev" and not server_only:
|
|
143
|
+
try:
|
|
144
|
+
to_add = check_web_dependencies(
|
|
145
|
+
web_root,
|
|
146
|
+
pulse_version=PULSE_PY_VERSION,
|
|
147
|
+
)
|
|
148
|
+
except DependencyResolutionError as exc:
|
|
149
|
+
console.log(f"❌ {exc}")
|
|
150
|
+
raise typer.Exit(1) from None
|
|
151
|
+
except DependencyError as exc:
|
|
152
|
+
console.log(f"❌ {exc}")
|
|
153
|
+
raise typer.Exit(1) from None
|
|
154
|
+
|
|
155
|
+
if to_add:
|
|
131
156
|
try:
|
|
132
|
-
|
|
157
|
+
dep_plan = prepare_web_dependencies(
|
|
133
158
|
web_root,
|
|
134
159
|
pulse_version=PULSE_PY_VERSION,
|
|
135
160
|
)
|
|
136
|
-
|
|
137
|
-
|
|
161
|
+
if dep_plan:
|
|
162
|
+
_run_dependency_plan(console, web_root, dep_plan)
|
|
163
|
+
except subprocess.CalledProcessError:
|
|
164
|
+
console.log("❌ Failed to install web dependencies with Bun.")
|
|
138
165
|
raise typer.Exit(1) from None
|
|
139
|
-
except DependencyError as exc:
|
|
140
|
-
console.log(f"❌ {exc}")
|
|
141
|
-
raise typer.Exit(1) from None
|
|
142
|
-
|
|
143
|
-
if to_add:
|
|
144
|
-
try:
|
|
145
|
-
dep_plan = prepare_web_dependencies(
|
|
146
|
-
web_root,
|
|
147
|
-
pulse_version=PULSE_PY_VERSION,
|
|
148
|
-
)
|
|
149
|
-
if dep_plan:
|
|
150
|
-
_run_dependency_plan(console, web_root, dep_plan)
|
|
151
|
-
except subprocess.CalledProcessError:
|
|
152
|
-
console.log("❌ Failed to install web dependencies with Bun.")
|
|
153
|
-
raise typer.Exit(1) from None
|
|
154
166
|
|
|
155
167
|
server_args = extra_flags if not web_only else []
|
|
156
168
|
web_args = extra_flags if web_only else []
|
|
157
169
|
|
|
158
170
|
commands: list[CommandSpec] = []
|
|
171
|
+
# Build web command first (when needed) so we can set PULSE_REACT_SERVER_ADDRESS
|
|
172
|
+
# before building the uvicorn command, which needs that env var
|
|
173
|
+
if not server_only:
|
|
174
|
+
if is_single_server:
|
|
175
|
+
web_port = find_available_port(5173)
|
|
176
|
+
commands.append(
|
|
177
|
+
build_web_command(
|
|
178
|
+
web_root=web_root,
|
|
179
|
+
extra_args=web_args,
|
|
180
|
+
port=web_port,
|
|
181
|
+
mode=app_instance.env,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
# Set env var so app can read the React server address
|
|
185
|
+
react_server_address = f"http://localhost:{web_port}"
|
|
186
|
+
os.environ[ENV_PULSE_REACT_SERVER_ADDRESS] = react_server_address
|
|
187
|
+
else:
|
|
188
|
+
commands.append(build_web_command(web_root=web_root, extra_args=web_args))
|
|
189
|
+
|
|
159
190
|
if not web_only:
|
|
160
191
|
commands.append(
|
|
161
192
|
build_uvicorn_command(
|
|
@@ -169,12 +200,10 @@ def run(
|
|
|
169
200
|
console=console,
|
|
170
201
|
web_root=web_root,
|
|
171
202
|
announce_url=is_single_server,
|
|
203
|
+
verbose=verbose,
|
|
172
204
|
)
|
|
173
205
|
)
|
|
174
206
|
|
|
175
|
-
if not is_single_server and not server_only:
|
|
176
|
-
commands.append(build_web_command(web_root=web_root, extra_args=web_args))
|
|
177
|
-
|
|
178
207
|
tag_colors = {"server": "cyan", "web": "orange1"}
|
|
179
208
|
|
|
180
209
|
with FolderLock(web_root):
|
|
@@ -301,6 +330,7 @@ def build_uvicorn_command(
|
|
|
301
330
|
console: Console,
|
|
302
331
|
web_root: Path,
|
|
303
332
|
announce_url: bool,
|
|
333
|
+
verbose: bool = False,
|
|
304
334
|
) -> CommandSpec:
|
|
305
335
|
app_import = f"{app_ctx.module_name}:{app_ctx.app_var}.asgi_factory"
|
|
306
336
|
args: list[str] = [
|
|
@@ -338,6 +368,11 @@ def build_uvicorn_command(
|
|
|
338
368
|
ENV_PULSE_PORT: str(port),
|
|
339
369
|
}
|
|
340
370
|
)
|
|
371
|
+
# Pass React server address to uvicorn process if set
|
|
372
|
+
if ENV_PULSE_REACT_SERVER_ADDRESS in os.environ:
|
|
373
|
+
command_env[ENV_PULSE_REACT_SERVER_ADDRESS] = os.environ[
|
|
374
|
+
ENV_PULSE_REACT_SERVER_ADDRESS
|
|
375
|
+
]
|
|
341
376
|
if app_ctx.app.env == "prod" and server_only:
|
|
342
377
|
command_env[ENV_PULSE_DISABLE_CODEGEN] = "1"
|
|
343
378
|
if dev_secret:
|
|
@@ -346,7 +381,7 @@ def build_uvicorn_command(
|
|
|
346
381
|
cwd = app_ctx.server_cwd or app_ctx.app_dir or Path.cwd()
|
|
347
382
|
|
|
348
383
|
# Apply custom log config to filter noisy requests (dev/ci only)
|
|
349
|
-
if app_ctx.app.env != "prod":
|
|
384
|
+
if app_ctx.app.env != "prod" and not verbose:
|
|
350
385
|
import json
|
|
351
386
|
import tempfile
|
|
352
387
|
|
|
@@ -374,8 +409,21 @@ def build_uvicorn_command(
|
|
|
374
409
|
)
|
|
375
410
|
|
|
376
411
|
|
|
377
|
-
def build_web_command(
|
|
378
|
-
|
|
412
|
+
def build_web_command(
|
|
413
|
+
*,
|
|
414
|
+
web_root: Path,
|
|
415
|
+
extra_args: Sequence[str],
|
|
416
|
+
port: int | None = None,
|
|
417
|
+
mode: PulseEnv = "dev",
|
|
418
|
+
) -> CommandSpec:
|
|
419
|
+
if mode == "prod":
|
|
420
|
+
# Production: use built server
|
|
421
|
+
args = ["bun", "run", "start"]
|
|
422
|
+
else:
|
|
423
|
+
# Development: use dev server
|
|
424
|
+
args = ["bun", "run", "dev"]
|
|
425
|
+
if port is not None:
|
|
426
|
+
args.extend(["--port", str(port)])
|
|
379
427
|
if extra_args:
|
|
380
428
|
args.extend(extra_args)
|
|
381
429
|
|
|
@@ -25,6 +25,7 @@ ENV_PULSE_APP_FILE = "PULSE_APP_FILE"
|
|
|
25
25
|
ENV_PULSE_APP_DIR = "PULSE_APP_DIR"
|
|
26
26
|
ENV_PULSE_HOST = "PULSE_HOST"
|
|
27
27
|
ENV_PULSE_PORT = "PULSE_PORT"
|
|
28
|
+
ENV_PULSE_REACT_SERVER_ADDRESS = "PULSE_REACT_SERVER_ADDRESS"
|
|
28
29
|
ENV_PULSE_SECRET = "PULSE_SECRET"
|
|
29
30
|
ENV_PULSE_DISABLE_CODEGEN = "PULSE_DISABLE_CODEGEN"
|
|
30
31
|
|
|
@@ -87,6 +88,14 @@ class EnvVars:
|
|
|
87
88
|
def pulse_port(self, value: int) -> None:
|
|
88
89
|
self._set(ENV_PULSE_PORT, str(value))
|
|
89
90
|
|
|
91
|
+
@property
|
|
92
|
+
def react_server_address(self) -> str | None:
|
|
93
|
+
return self._get(ENV_PULSE_REACT_SERVER_ADDRESS)
|
|
94
|
+
|
|
95
|
+
@react_server_address.setter
|
|
96
|
+
def react_server_address(self, value: str | None) -> None:
|
|
97
|
+
self._set(ENV_PULSE_REACT_SERVER_ADDRESS, value)
|
|
98
|
+
|
|
90
99
|
# Secrets
|
|
91
100
|
@property
|
|
92
101
|
def pulse_secret(self) -> str | None:
|
|
@@ -24,7 +24,7 @@ class PulseProxy:
|
|
|
24
24
|
def __init__(
|
|
25
25
|
self,
|
|
26
26
|
app: ASGIApp,
|
|
27
|
-
|
|
27
|
+
get_react_server_address: Callable[[], str | None],
|
|
28
28
|
api_prefix: str = "/_pulse",
|
|
29
29
|
):
|
|
30
30
|
"""
|
|
@@ -32,11 +32,13 @@ class PulseProxy:
|
|
|
32
32
|
|
|
33
33
|
Args:
|
|
34
34
|
app: The ASGI application to wrap (socketio.ASGIApp)
|
|
35
|
-
|
|
35
|
+
get_react_server_address: Callable that returns the React Router server full URL (or None if not started)
|
|
36
36
|
api_prefix: Prefix for API routes that should NOT be proxied (default: "/_pulse")
|
|
37
37
|
"""
|
|
38
38
|
self.app: ASGIApp = app
|
|
39
|
-
self.
|
|
39
|
+
self.get_react_server_address: Callable[[], str | None] = (
|
|
40
|
+
get_react_server_address
|
|
41
|
+
)
|
|
40
42
|
self.api_prefix: str = api_prefix
|
|
41
43
|
self._client: httpx.AsyncClient | None = None
|
|
42
44
|
|
|
@@ -84,10 +86,10 @@ class PulseProxy:
|
|
|
84
86
|
"""
|
|
85
87
|
Forward HTTP request to React Router server and stream response back.
|
|
86
88
|
"""
|
|
87
|
-
# Get the
|
|
88
|
-
|
|
89
|
-
if
|
|
90
|
-
#
|
|
89
|
+
# Get the React server address
|
|
90
|
+
react_server_address = self.get_react_server_address()
|
|
91
|
+
if react_server_address is None:
|
|
92
|
+
# React server not started yet, return error
|
|
91
93
|
await send(
|
|
92
94
|
{
|
|
93
95
|
"type": "http.response.start",
|
|
@@ -98,7 +100,7 @@ class PulseProxy:
|
|
|
98
100
|
await send(
|
|
99
101
|
{
|
|
100
102
|
"type": "http.response.body",
|
|
101
|
-
"body": b"Service Unavailable:
|
|
103
|
+
"body": b"Service Unavailable: React server not ready",
|
|
102
104
|
}
|
|
103
105
|
)
|
|
104
106
|
return
|
|
@@ -106,8 +108,9 @@ class PulseProxy:
|
|
|
106
108
|
# Build target URL
|
|
107
109
|
path = scope["path"]
|
|
108
110
|
query_string = scope.get("query_string", b"").decode("utf-8")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
# Ensure react_server_address doesn't end with /
|
|
112
|
+
base_url = react_server_address.rstrip("/")
|
|
113
|
+
target_path = f"{base_url}{path}"
|
|
111
114
|
if query_string:
|
|
112
115
|
target_path += f"?{query_string}"
|
|
113
116
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/codegen/templates/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulse_framework-0.1.38a2 → pulse_framework-0.1.38a4}/src/pulse/codegen/templates/routes_ts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|