77import types
88from importlib .machinery import ModuleSpec
99from pathlib import Path
10- from typing import Mapping , Sequence , cast
10+ from typing import Literal , Mapping , Sequence , cast
1111
1212from htmltools import Tag , TagList
13+ from starlette .requests import Request
1314
1415from .._app import App
1516from .._docstring import no_example
1617from .._typing_extensions import NotRequired , TypedDict
1718from .._utils import import_module_from_path
19+ from ..bookmark ._types import BookmarkStore
1820from ..session import Inputs , Outputs , Session , get_current_session , session_context
1921from ..types import MISSING , MISSING_TYPE
2022from ._is_express import find_magic_comment_mode
@@ -115,13 +117,13 @@ def create_express_app(file: Path, package_name: str) -> App:
115117
116118 file = file .resolve ()
117119
120+ stub_session = ExpressStubSession ()
118121 try :
119122 globals_file = file .parent / "globals.py"
120123 if globals_file .is_file ():
121124 with session_context (None ):
122125 import_module_from_path ("globals" , globals_file )
123126
124- stub_session = ExpressStubSession ()
125127 with session_context (stub_session ):
126128 # We tagify here, instead of waiting for the App object to do it when it wraps
127129 # the UI in a HTMLDocument and calls render() on it. This is because
@@ -134,6 +136,17 @@ def create_express_app(file: Path, package_name: str) -> App:
134136 except AttributeError as e :
135137 raise RuntimeError (e ) from e
136138
139+ express_bookmark_store = stub_session .app_opts .get ("bookmark_store" , "disable" )
140+ if express_bookmark_store != "disable" :
141+ # If bookmarking is enabled, wrap UI in function to automatically leverage UI
142+ # functions to restore their values
143+ def app_ui_wrapper (request : Request ):
144+ # Stub session used to pass `app_opts()` checks.
145+ with session_context (ExpressStubSession ()):
146+ return run_express (file , package_name ).tagify ()
147+
148+ app_ui = app_ui_wrapper
149+
137150 def express_server (input : Inputs , output : Outputs , session : Session ):
138151 try :
139152 run_express (file , package_name )
@@ -290,12 +303,15 @@ def __getattr__(self, name: str):
290303
291304class AppOpts (TypedDict ):
292305 static_assets : NotRequired [dict [str , Path ]]
306+ bookmark_store : NotRequired [BookmarkStore ]
293307 debug : NotRequired [bool ]
294308
295309
296310@no_example ()
297311def app_opts (
312+ * ,
298313 static_assets : str | Path | Mapping [str , str | Path ] | MISSING_TYPE = MISSING ,
314+ bookmark_store : Literal ["url" , "server" , "disable" ] | MISSING_TYPE = MISSING ,
299315 debug : bool | MISSING_TYPE = MISSING ,
300316):
301317 """
@@ -313,6 +329,12 @@ def app_opts(
313329 that mount point. In Shiny Express, if there is a `www` subdirectory of the
314330 directory containing the app file, it will automatically be mounted at `/`, even
315331 without needing to set the option here.
332+ bookmark_store
333+ Where to store the bookmark state.
334+
335+ * `"url"`: Encode the bookmark state in the URL.
336+ * `"server"`: Store the bookmark state on the server.
337+ * `"disable"`: Disable bookmarking.
316338 debug
317339 Whether to enable debug mode.
318340 """
@@ -339,6 +361,9 @@ def app_opts(
339361
340362 stub_session .app_opts ["static_assets" ] = static_assets_paths
341363
364+ if not isinstance (bookmark_store , MISSING_TYPE ):
365+ stub_session .app_opts ["bookmark_store" ] = bookmark_store
366+
342367 if not isinstance (debug , MISSING_TYPE ):
343368 stub_session .app_opts ["debug" ] = debug
344369
@@ -357,6 +382,9 @@ def _merge_app_opts(app_opts: AppOpts, app_opts_new: AppOpts) -> AppOpts:
357382 elif "static_assets" in app_opts_new :
358383 app_opts ["static_assets" ] = app_opts_new ["static_assets" ].copy ()
359384
385+ if "bookmark_store" in app_opts_new :
386+ app_opts ["bookmark_store" ] = app_opts_new ["bookmark_store" ]
387+
360388 if "debug" in app_opts_new :
361389 app_opts ["debug" ] = app_opts_new ["debug" ]
362390
0 commit comments