Skip to content

feat: add page_react() and template-mode page_react_html() - #208

Open
schloerke wants to merge 1 commit into
schloerke/npm-distributionfrom
schloerke/page-react-modes
Open

feat: add page_react() and template-mode page_react_html()#208
schloerke wants to merge 1 commit into
schloerke/npm-distributionfrom
schloerke/page-react-modes

Conversation

@schloerke

@schloerke schloerke commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Closes #205. Part of stack #209 (on top of #198#197). Follows the two-mode design discussed for replacing the halfway index.html fragment.

Summary

Mode 1 — page_react() (Python + R, new export). Zero-configuration server-composed page: no HTML file at all. Discovers www/ui.js and www/ui.css (named for the ui.tsx pattern per #205 — the source entry ui.tsx compiles to www/ui.js, paired with ui.css; the client entry is the ui, the server file is the app, and #204's rename discussion can flip the default later), serves them as an mtime-versioned HTMLDependency — so edits cache-bust, unlike raw <script src> tags in a hand-written HTML file. Title defaults to the app folder's name; extra HTMLDependency args pass through. The server emits no body HTML — the client appends its own mount container to <body>.

Express follows: set_react_page()'s path is now optional. With no args it uses www/index.html when present (existing behavior, so every current example keeps working), else falls back to page_react() discovery. Renderer-dependency harvesting works in both modes (extracted into _harvest_renderer_deps()).

Mode 2 — R page_react_html() accepts a real document. The file must be a complete HTML document containing a {{ headContent() }} marker (the kind a Vite build emits); it renders through htmltools::htmlTemplate(document_ = TRUE) with the shinyreact bundle and the #shinyreact-config tag injected at the marker via the new config_head_dep() (an href-less dependency carrying only head HTML). Fragment input now errors with a message pointing at the marker or at page_react(). Assets the document references live in www/ and are served statically.

Python page_react_html() is deliberately unchanged (fragment mode): shiny.App has no way to accept a pre-rendered document with extra dependencies — HTMLTextDocument is only used internally for ui=Path with no injection hook (verified empirically: App(ui=HTMLTextDocument(...)) fails on get_dependencies). The docstring documents the divergence and points users at page_react(). Closing this needs an upstream py-shiny change.

Examples intentionally untouched#207 rewrites example 01 against main; converting examples to page_react() should happen after it merges, avoiding conflicts.

Verification

  • Python: 84 tests (new: page_react defaults/title/css-skip/missing-js-warn/extra-deps/cwd-fallback; set_react_page index-precedence and discovery-fallback), pyright, ruff.
  • R: 140 tests (new: template rendering with single <html>, config payload extraction, marker-missing error, page_react suite mirroring Python), R CMD check 0 errors / 0 warnings / 0 notes. Tests render through a createWebDependency-mimicking helper (helper-render.R) since standalone renderDocument can't href file-based deps.

Two coherent page modes replacing the halfway index.html fragment:

- page_react() (Python + R): zero-config server-composed page. Discovers
  www/ui.js + www/ui.css, serves them as an mtime-versioned
  HTMLDependency (cache-busted, unlike raw script tags), title defaults
  to the app folder name, no body HTML — the client appends its own
  mount container
- set_react_page() (Express): path now optional; with no args it uses
  www/index.html when present, else falls back to page_react() discovery
  (renderer-dep harvesting preserved via extracted helper)
- R page_react_html(): accepts a complete HTML document with a
  {{ headContent() }} marker (htmltools::htmlTemplate); shinyreact deps
  render at the marker via the new config_head_dep(). Fragments now
  error with a pointer to page_react()
- Python page_react_html(): unchanged fragment behavior, documented as
  blocked on an upstream py-shiny gap (App cannot accept a pre-rendered
  document with extra dependencies)
@schloerke
schloerke marked this pull request as ready for review August 20, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Maybe have default app file produced be called ui.tsx?

1 participant