22
33from __future__ import annotations
44
5- import atexit
65from importlib .util import find_spec
76from pathlib import Path
87from typing import TYPE_CHECKING
@@ -132,8 +131,11 @@ def _run(
132131 frontend_port : int | None = None ,
133132 backend_port : int | None = None ,
134133 backend_host : str | None = None ,
134+ single_port : bool = False ,
135135):
136136 """Run the app in the given directory."""
137+ import atexit
138+
137139 from reflex .utils import build , exec , prerequisites , processes
138140
139141 config = get_config ()
@@ -173,7 +175,9 @@ def _run(
173175 auto_increment = auto_increment_frontend ,
174176 )
175177
176- if backend :
178+ if single_port :
179+ backend_port = frontend_port
180+ elif backend :
177181 auto_increment_backend = not bool (backend_port or config .backend_port )
178182
179183 backend_port = processes .handle_port (
@@ -223,23 +227,23 @@ def _run(
223227 if not return_result :
224228 raise SystemExit (1 )
225229
230+ if env != constants .Env .PROD and env != constants .Env .DEV :
231+ msg = f"Invalid env: { env } . Must be DEV or PROD."
232+ raise ValueError (msg )
233+
226234 # Get the frontend and backend commands, based on the environment.
227- setup_frontend = frontend_cmd = backend_cmd = None
228235 if env == constants .Env .DEV :
229236 setup_frontend , frontend_cmd , backend_cmd = (
230237 build .setup_frontend ,
231238 exec .run_frontend ,
232239 exec .run_backend ,
233240 )
234- if env == constants .Env .PROD :
241+ elif env == constants .Env .PROD :
235242 setup_frontend , frontend_cmd , backend_cmd = (
236243 build .setup_frontend_prod ,
237244 exec .run_frontend_prod ,
238245 exec .run_backend_prod ,
239246 )
240- if not setup_frontend or not frontend_cmd or not backend_cmd :
241- msg = f"Invalid env: { env } . Must be DEV or PROD."
242- raise ValueError (msg )
243247
244248 # Post a telemetry event.
245249 telemetry .send (f"run-{ env .value } " )
@@ -251,7 +255,7 @@ def _run(
251255 commands = []
252256
253257 # Run the frontend on a separate thread.
254- if frontend :
258+ if frontend and not single_port :
255259 setup_frontend (Path .cwd ())
256260 commands .append ((frontend_cmd , Path .cwd (), frontend_port , backend ))
257261
@@ -267,21 +271,30 @@ def _run(
267271 )
268272 )
269273
270- # Start the frontend and backend.
271- with processes .run_concurrently_context (* commands ):
272- # In dev mode, run the backend on the main thread.
273- if backend and backend_port and env == constants .Env .DEV :
274- backend_cmd (
275- backend_host ,
276- int (backend_port ),
277- config .loglevel .subprocess_level (),
278- frontend ,
279- )
280- # The windows uvicorn bug workaround
281- # https://github.com/reflex-dev/reflex/issues/2335
282- if constants .IS_WINDOWS and exec .frontend_process :
283- # Sends SIGTERM in windows
284- exec .kill (exec .frontend_process .pid )
274+ if single_port :
275+ setup_frontend (Path .cwd ())
276+ backend_function , * args = commands [0 ]
277+ exec .notify_app_running ()
278+ exec .notify_frontend (
279+ f"http://0.0.0.0:{ get_config ().frontend_port } " , backend_present = True
280+ )
281+ backend_function (* args , mount_frontend_compiled_app = True )
282+ else :
283+ # Start the frontend and backend.
284+ with processes .run_concurrently_context (* commands ):
285+ # In dev mode, run the backend on the main thread.
286+ if backend and backend_port and env == constants .Env .DEV :
287+ backend_cmd (
288+ backend_host ,
289+ int (backend_port ),
290+ config .loglevel .subprocess_level (),
291+ frontend ,
292+ )
293+ # The windows uvicorn bug workaround
294+ # https://github.com/reflex-dev/reflex/issues/2335
295+ if constants .IS_WINDOWS and exec .frontend_process :
296+ # Sends SIGTERM in windows
297+ exec .kill (exec .frontend_process .pid )
285298
286299
287300@cli .command ()
@@ -322,19 +335,43 @@ def _run(
322335 "--backend-host" ,
323336 help = "Specify the backend host." ,
324337)
338+ @click .option (
339+ "--single-port" ,
340+ is_flag = True ,
341+ help = "Run both frontend and backend on the same port." ,
342+ default = False ,
343+ )
325344def run (
326345 env : LITERAL_ENV ,
327346 frontend_only : bool ,
328347 backend_only : bool ,
329348 frontend_port : int | None ,
330349 backend_port : int | None ,
331350 backend_host : str | None ,
351+ single_port : bool ,
332352):
333353 """Run the app in the current directory."""
334354 if frontend_only and backend_only :
335355 console .error ("Cannot use both --frontend-only and --backend-only options." )
336356 raise SystemExit (1 )
337357
358+ if single_port :
359+ if env != constants .Env .PROD .value :
360+ console .error ("--single-port can only be used with --env=PROD." )
361+ raise click .exceptions .Exit (1 )
362+ if frontend_only or backend_only :
363+ console .error (
364+ "Cannot use --single-port with --frontend-only or --backend-only options."
365+ )
366+ raise click .exceptions .Exit (1 )
367+ if backend_port and frontend_port and backend_port != frontend_port :
368+ console .error (
369+ "When using --single-port, --backend-port and --frontend-port must be the same."
370+ )
371+ raise click .exceptions .Exit (1 )
372+ elif frontend_port and backend_port and frontend_port == backend_port :
373+ single_port = True
374+
338375 config = get_config ()
339376
340377 frontend_port = frontend_port or config .frontend_port
@@ -352,6 +389,7 @@ def run(
352389 frontend_port ,
353390 backend_port ,
354391 backend_host ,
392+ single_port ,
355393 )
356394
357395
0 commit comments