2121from reactpy .backend .hooks import ConnectionContext
2222from reactpy .backend .types import Connection , Location
2323from reactpy .config import REACTPY_WEB_MODULES_DIR
24+ from reactpy .core .component import Component
2425from reactpy .core .layout import Layout
2526from reactpy .core .serve import serve_layout
26- from reactpy .core .types import ComponentConstructor , VdomDict
27+ from reactpy .core .types import ComponentType , VdomDict
2728
2829_logger = logging .getLogger (__name__ )
2930_backhaul_loop = asyncio .new_event_loop ()
@@ -42,20 +43,24 @@ def start_backhaul_loop():
4243class ReactPy :
4344 def __init__ (
4445 self ,
45- app_or_component : ComponentConstructor | Callable [..., Coroutine ],
46+ app_or_component : ComponentType | Callable [..., Coroutine ],
4647 * ,
47- dispatcher_path : str = "^reactpy/([^/]+)/?" ,
48- js_modules_path : str | None = "^reactpy/modules/([^/]+)/?" ,
49- static_path : str | None = "^reactpy/static/([^/]+)/?" ,
48+ dispatcher_path : str = "reactpy/" ,
49+ web_modules_path : str = "reactpy/modules/" ,
50+ web_modules_dir : Path | str | None = REACTPY_WEB_MODULES_DIR .current ,
51+ static_path : str = "reactpy/static/" ,
5052 static_dir : Path | str | None = None ,
5153 head : Sequence [VdomDict ] | VdomDict | str = "" ,
5254 backhaul_thread : bool = True ,
5355 block_size : int = 8192 ,
5456 ) -> None :
57+ """Anything initialized in this method will be shared across all
58+ requests."""
5559 # Convert kwargs to class attributes
56- self .dispatch_path = re .compile (dispatcher_path )
57- self .js_modules_path = re .compile (js_modules_path ) if js_modules_path else None
58- self .static_path = re .compile (static_path ) if static_path else None
60+ self .dispatch_path = re .compile (f"^{ dispatcher_path } (?P<dotted_path>[^/]+)/?" )
61+ self .js_modules_path = re .compile (f"^{ web_modules_path } " )
62+ self .web_modules_dir = web_modules_dir
63+ self .static_path = re .compile (f"^{ static_path } " )
5964 self .static_dir = static_dir
6065 self .head = vdom_head_elements_to_html (head )
6166 self .backhaul_thread = backhaul_thread
@@ -67,30 +72,39 @@ def __init__(
6772 if asyncio .iscoroutinefunction (app_or_component )
6873 else None
6974 )
70- self .component : ComponentConstructor | None = (
71- None if self .user_app else app_or_component
75+ self .component : ComponentType | None = (
76+ None if self .user_app else app_or_component # type: ignore
7277 )
7378 self .all_paths : re .Pattern = re .compile (
7479 "|" .join (
75- path for path in [dispatcher_path , js_modules_path , static_path ] if path
80+ path
81+ for path in [dispatcher_path , web_modules_path , static_path ]
82+ if path
7683 )
7784 )
7885 self .dispatcher : Future | asyncio .Task | None = None
7986 self ._cached_index_html : str = ""
8087 self ._static_file_server : StaticFiles | None = None
81- self ._js_module_server : StaticFiles | None = None
82- self .connected : bool = False
83- # TODO: Remove this setting from ReactPy config
84- self .js_modules_dir : Path | None = REACTPY_WEB_MODULES_DIR .current
88+ self ._web_module_server : StaticFiles | None = None
89+
90+ # Startup tasks
91+ if self .backhaul_thread and not _backhaul_thread .is_alive ():
92+ _backhaul_thread .start ()
93+ if self .web_modules_dir != REACTPY_WEB_MODULES_DIR .current :
94+ REACTPY_WEB_MODULES_DIR .set_current (self .web_modules_dir )
8595
8696 # Validate the arguments
8797 if not self .component and not self .user_app :
8898 raise TypeError (
8999 "The first argument to ReactPy(...) must be a component or an "
90100 "ASGI application."
91101 )
92- if self .backhaul_thread and not _backhaul_thread .is_alive ():
93- _backhaul_thread .start ()
102+ if check_path (dispatcher_path ):
103+ raise ValueError ("Invalid `dispatcher_path`." )
104+ if check_path (web_modules_path ):
105+ raise ValueError ("Invalid `web_modules_path`." )
106+ if check_path (static_path ):
107+ raise ValueError ("Invalid `static_path`." )
94108
95109 async def __call__ (
96110 self ,
@@ -131,12 +145,12 @@ async def reactpy_app(
131145 return
132146
133147 # JS modules app
134- if self . js_modules_path and re .match (self .js_modules_path , scope ["path" ]):
135- await self .js_module_app (scope , receive , send )
148+ if re .match (self .js_modules_path , scope ["path" ]):
149+ await self .web_module_app (scope , receive , send )
136150 return
137151
138152 # Static file app
139- if self . static_path and re .match (self .static_path , scope ["path" ]):
153+ if re .match (self .static_path , scope ["path" ]):
140154 await self .static_file_app (scope , receive , send )
141155 return
142156
@@ -181,23 +195,25 @@ async def component_dispatch_app(
181195 else :
182196 await recv_queue_put
183197
184- async def js_module_app (
198+ async def web_module_app (
185199 self ,
186200 scope : dict [str , Any ],
187201 receive : Callable [..., Coroutine ],
188202 send : Callable [..., Coroutine ],
189203 ) -> None :
190204 """ASGI app for ReactPy web modules."""
191- if not self .js_modules_dir :
192- raise RuntimeError ("No web modules directory configured." )
193- if not self .js_modules_path :
194- raise RuntimeError (
195- "Web modules cannot be served without defining `js_module_path`."
205+ if not self .web_modules_dir :
206+ await asyncio .to_thread (
207+ _logger .info ,
208+ "Tried to serve web module without a configured directory." ,
196209 )
197- if not self ._js_module_server :
198- self ._js_module_server = StaticFiles (directory = self .js_modules_dir )
210+ if self .user_app :
211+ await self .user_app (scope , receive , send )
212+ return
199213
200- await self ._js_module_server (scope , receive , send )
214+ if not self ._web_module_server :
215+ self ._web_module_server = StaticFiles (directory = self .web_modules_dir )
216+ await self ._web_module_server (scope , receive , send )
201217
202218 async def static_file_app (
203219 self ,
@@ -206,17 +222,18 @@ async def static_file_app(
206222 send : Callable [..., Coroutine ],
207223 ) -> None :
208224 """ASGI app for ReactPy static files."""
225+ # If no static directory is configured, serve the user's application
209226 if not self .static_dir :
210- raise RuntimeError (
211- "Static files cannot be served without defining `static_dir`."
212- )
213- if not self .static_path :
214- raise RuntimeError (
215- "Static files cannot be served without defining `static_path`."
227+ await asyncio .to_thread (
228+ _logger .info ,
229+ "Tried to serve static file without a configured directory." ,
216230 )
231+ if self .user_app :
232+ await self .user_app (scope , receive , send )
233+ return
234+
217235 if not self ._static_file_server :
218236 self ._static_file_server = StaticFiles (directory = self .static_dir )
219-
220237 await self ._static_file_server (scope , receive , send )
221238
222239 async def standalone_app (
@@ -317,3 +334,13 @@ async def http_response(
317334 # Head requests don't need a body
318335 if scope ["method" ] != "HEAD" :
319336 await send ({"type" : "http.response.body" , "body" : message .encode ()})
337+
338+
339+ def check_path (url_path : str ) -> bool :
340+ """Check that a path is valid URL path."""
341+ return (
342+ not url_path
343+ or not isinstance (url_path , str )
344+ or not url_path [0 ].isalnum ()
345+ or not url_path .endswith ("/" )
346+ )
0 commit comments