From b10f2442ba2e059b6abade2fb5d0a9091390b37d Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 19 Aug 2026 16:54:46 -0400 Subject: [PATCH 1/5] feat: add page_react() and template-mode page_react_html() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- pkg-py/README.md | 1 + pkg-py/src/shinyreact/__init__.py | 2 + pkg-py/src/shinyreact/_page.py | 180 ++++++++++++++---- .../tests/playwright/test_bookmark_restore.py | 12 +- pkg-py/tests/test_bookmark_restore.py | 8 +- pkg-py/tests/test_page.py | 99 ++++++++++ pkg-py/tests/test_set_react_page.py | 75 +++++++- pkg-r/NAMESPACE | 1 + pkg-r/R/dep.R | 15 ++ pkg-r/R/page.R | 93 ++++++++- pkg-r/_pkgdown.yml | 2 +- pkg-r/man/page_react.Rd | 48 +++++ pkg-r/man/page_react_html.Rd | 22 ++- pkg-r/tests/testthat/helper-render.R | 17 ++ .../testthat/test-bookmark-restore-context.R | 14 +- pkg-r/tests/testthat/test-page.R | 157 +++++++++++++-- 16 files changed, 655 insertions(+), 91 deletions(-) create mode 100644 pkg-r/man/page_react.Rd create mode 100644 pkg-r/tests/testthat/helper-render.R diff --git a/pkg-py/README.md b/pkg-py/README.md index 4653dae4..a0574d99 100644 --- a/pkg-py/README.md +++ b/pkg-py/README.md @@ -33,6 +33,7 @@ from shinyreact import reactive_output, set_react_page set_react_page() + @reactive_output def greeting(): return {"message": f"Hello, {input.name()}"} diff --git a/pkg-py/src/shinyreact/__init__.py b/pkg-py/src/shinyreact/__init__.py index 366fb0c3..de0fa8f5 100644 --- a/pkg-py/src/shinyreact/__init__.py +++ b/pkg-py/src/shinyreact/__init__.py @@ -3,6 +3,7 @@ ) from ._page import ( page_bare, + page_react, page_react_dep, page_react_html, set_react_page, @@ -12,6 +13,7 @@ __all__ = [ "page_bare", + "page_react", "page_react_dep", "page_react_html", "reactive_output", diff --git a/pkg-py/src/shinyreact/_page.py b/pkg-py/src/shinyreact/_page.py index fa7d5379..7afe2e46 100644 --- a/pkg-py/src/shinyreact/_page.py +++ b/pkg-py/src/shinyreact/_page.py @@ -45,6 +45,74 @@ def page_bare( ) +def _resolve_react_dirs( + src_dir: str | Path | None, caller_dir: Path +) -> tuple[Path, str]: + """Resolve ``page_react``'s asset dir and derive the app name. + + Returns ``(base_dir, app_name)``. ``app_name`` is the app folder's name: + when the asset dir is the conventional ``www/``, its parent (the app dir) + names the app; otherwise the asset dir itself does. + """ + if src_dir is None: + base_dir = caller_dir / "www" + else: + src_dir = Path(src_dir) + base_dir = src_dir if src_dir.is_absolute() else caller_dir / src_dir + app_name = base_dir.parent.name if base_dir.name == "www" else base_dir.name + return base_dir, app_name + + +def page_react( + *args: TagChild, + src_dir: str | Path | None = None, + js_file: str = "ui.js", + css_file: str = "ui.css", + title: str | None = None, + lang: str = "en", +) -> Tag: + """Create a React page from conventional assets — no HTML file required. + + The zero-configuration page for the ui.tsx pattern: the server emits no + body HTML at all. It attaches the shinyreact bundle plus your app's entry + assets, discovered at ``www/ui.js`` and ``www/ui.css`` (relative to the + calling module). Your JS owns the DOM — create and append your own mount + container:: + + const root = ReactDOM.createRoot( + document.body.appendChild(document.createElement("div")), + ); + + ``ui.js`` is required (a missing file warns, pointing at the resolved + path); ``ui.css`` is attached only when it exists. Both are served as an + :class:`~htmltools.HTMLDependency` versioned by ``ui.js``'s mtime, so the + browser re-fetches after every edit — unlike raw `` diff --git a/examples/04-shadcn/.gitignore b/examples/04-shadcn/.gitignore index 9558ca43..a790fdb1 100644 --- a/examples/04-shadcn/.gitignore +++ b/examples/04-shadcn/.gitignore @@ -1,4 +1,4 @@ node_modules/ package-lock.json -www/app.js -www/style.css +www/ui.js +www/ui.css diff --git a/examples/04-shadcn/README.md b/examples/04-shadcn/README.md index 9b24b6b3..563fe163 100644 --- a/examples/04-shadcn/README.md +++ b/examples/04-shadcn/README.md @@ -30,7 +30,7 @@ examples/04-shadcn/ ├── README.md ├── src/ │ ├── App.jsx # composes the cards -│ ├── main.jsx # mounts via window.shinyreact.React/ReactDOM +│ ├── ui.jsx # mounts via window.shinyreact.React/ReactDOM (appends its own container) │ ├── index.css # Tailwind v4 + shadcn theme tokens │ ├── lib/utils.js # cn() = clsx + tailwind-merge │ └── components/ @@ -46,16 +46,15 @@ examples/04-shadcn/ │ ├── input.jsx │ └── separator.jsx └── www/ - ├── index.html # 3 lines, committed - ├── app.js # built by Vite (gitignored) - └── style.css # built by Vite (gitignored) + ├── ui.js # built by Vite (gitignored) + └── ui.css # built by Vite (gitignored) ``` ## Build plumbing The non-obvious bit is how the bundle stays compatible with the page-level `window.shinyreact` runtime: -- `vite.config.js` is in **lib mode** with format `iife`, output filename `app.js`. +- `vite.config.js` is in **lib mode** with format `iife`, output filename `ui.js` — the name `set_react_page()` discovers, so the app file needs no arguments and no `index.html`. - `react`, `react-dom`, and `react-dom/client` are listed as `external` and mapped via `rollupOptions.output.globals` to `window.shinyreact.React` / `window.shinyreact.ReactDOM`. The IIFE bundle reuses the React instance that owns the shinyreact hooks (mixing two React copies would break the hooks). - `react`/`react-dom` are still listed as `devDependencies` so `react/jsx-runtime` resolves at build time when Vite's automatic JSX transform inlines it. - Tailwind v4 is wired in through `@tailwindcss/vite`; the shadcn design tokens live in `src/index.css`. diff --git a/examples/04-shadcn/src/main.jsx b/examples/04-shadcn/src/main.jsx deleted file mode 100644 index 7e1137b6..00000000 --- a/examples/04-shadcn/src/main.jsx +++ /dev/null @@ -1,8 +0,0 @@ -import "@/index.css"; - -import App from "@/App"; - -const { React, ReactDOM } = window.shinyreact; - -const root = ReactDOM.createRoot(document.getElementById("root")); -root.render(React.createElement(App)); diff --git a/examples/04-shadcn/src/ui.jsx b/examples/04-shadcn/src/ui.jsx new file mode 100644 index 00000000..e19e6126 --- /dev/null +++ b/examples/04-shadcn/src/ui.jsx @@ -0,0 +1,12 @@ +import "@/index.css"; + +import App from "@/App"; + +const { React, ReactDOM } = window.shinyreact; + +// No index.html in this example -- the server page is generated by +// set_react_page() discovery, so the app appends its own mount container. +const root = ReactDOM.createRoot( + document.body.appendChild(document.createElement("div")), +); +root.render(React.createElement(App)); diff --git a/examples/04-shadcn/vite.config.js b/examples/04-shadcn/vite.config.js index 0afcf25a..7962d843 100644 --- a/examples/04-shadcn/vite.config.js +++ b/examples/04-shadcn/vite.config.js @@ -22,14 +22,16 @@ export default defineConfig({ emptyOutDir: false, cssCodeSplit: false, lib: { - entry: path.resolve(__dirname, "src/main.jsx"), + entry: path.resolve(__dirname, "src/ui.jsx"), formats: ["iife"], name: "ColumnsShadcn", - fileName: () => "app.js", + fileName: () => "ui.js", }, rollupOptions: { external: ["react", "react-dom", "react-dom/client"], output: { + // Name the emitted CSS asset ui.css (Vite 5 lib mode defaults to style.css). + assetFileNames: "ui.[ext]", globals: { react: "window.shinyreact.React", "react-dom": "window.shinyreact.ReactDOM", diff --git a/examples/04-shadcn/www/index.html b/examples/04-shadcn/www/index.html deleted file mode 100644 index 11059352..00000000 --- a/examples/04-shadcn/www/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/examples/09-hmr/.gitignore b/examples/09-hmr/.gitignore index 9c039c55..b428e492 100644 --- a/examples/09-hmr/.gitignore +++ b/examples/09-hmr/.gitignore @@ -1,3 +1,3 @@ node_modules/ package-lock.json -www/app.js +www/ui.js diff --git a/examples/09-hmr/README.md b/examples/09-hmr/README.md index 2d000165..63bba406 100644 --- a/examples/09-hmr/README.md +++ b/examples/09-hmr/README.md @@ -6,11 +6,11 @@ state — no full page reload, no `vite build` wait. ## How it works -Shiny serves `www/index.html` (which loads `www/app.js` as a module) and the +Shiny serves a `set_react_page()`-generated page (which loads `www/ui.js` as a module) and the reactive WebSocket. A Vite dev server serves your React modules with Fast -Refresh. The dev server writes `www/app.js` as a tiny stub that pulls the HMR +Refresh. The dev server writes `www/ui.js` as a tiny stub that pulls the HMR client + your entry from the dev server; `npm run build` overwrites it with the -real bundle. `index.html` never changes between modes. +real bundle. The served page never changes between modes. Component code lives in `src/App.tsx` (the Fast Refresh boundary). The entry `src/ui.tsx` only mounts it — keep `createRoot()` there, never in a file that @@ -23,7 +23,7 @@ externalized to the shared `window.shinyreact`. ## Develop (two terminals) ```bash -# terminal 1 — Vite dev server (writes www/app.js as a dev stub, serves HMR) +# terminal 1 — Vite dev server (writes www/ui.js as a dev stub, serves HMR) npm install npm run dev @@ -44,7 +44,7 @@ Open the URL Shiny prints. Editing `app.py` reloads via Shiny; editing ## Build for production ```bash -npm run build # writes www/app.js (the real bundle) +npm run build # writes www/ui.js (the real bundle) uv run shiny run app.py ``` diff --git a/examples/09-hmr/src/ui.tsx b/examples/09-hmr/src/ui.tsx index bfef27f9..19a65ee2 100644 --- a/examples/09-hmr/src/ui.tsx +++ b/examples/09-hmr/src/ui.tsx @@ -6,4 +6,8 @@ import App from "./App"; // clean. Editing THIS file triggers a full reload (rare); editing App.tsx does // not. Calling createRoot in a file that also defines components would defeat // Fast Refresh. -ReactDOM.createRoot(document.getElementById("root")!).render(); +// No index.html in this example -- the server page is generated by +// set_react_page() discovery, so the app appends its own mount container. +ReactDOM.createRoot( + document.body.appendChild(document.createElement("div")), +).render(); diff --git a/examples/09-hmr/vite-dev-stub.js b/examples/09-hmr/vite-dev-stub.js index 10b481dc..2139b090 100644 --- a/examples/09-hmr/vite-dev-stub.js +++ b/examples/09-hmr/vite-dev-stub.js @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -// The contents of the dev-mode `www/app.js`. Shiny serves this file statically. +// The contents of the dev-mode `www/ui.js`. Shiny serves this file statically. // It (1) installs the React Fast Refresh preamble that @vitejs/plugin-react // normally injects into the HTML it serves — but here Shiny serves the page, so // we install it ourselves — then (2) dynamically imports Vite's HMR client and @@ -22,7 +22,7 @@ export function makeDevStub(origin, entry) { } // Vite plugin (serve only). On dev-server start it writes `outFile` (e.g. -// www/app.js) as the dev stub, and removes it on shutdown so a later plain +// www/ui.js) as the dev stub, and removes it on shutdown so a later plain // `shiny run` doesn't load a stub pointing at a dead dev server. `vite build` // (apply:"serve" excludes this plugin) overwrites `outFile` with the real bundle. export function shinyreactDevStub({ entry, outFile }) { diff --git a/examples/09-hmr/vite.config.js b/examples/09-hmr/vite.config.js index 0962756a..742ac41c 100644 --- a/examples/09-hmr/vite.config.js +++ b/examples/09-hmr/vite.config.js @@ -18,7 +18,7 @@ export default defineConfig(({ command }) => ({ plugins: [ react(), // serve only: writes www/app.js as the dev stub (apply:"serve" inside). - shinyreactDevStub({ entry: ENTRY, outFile: "www/app.js" }), + shinyreactDevStub({ entry: ENTRY, outFile: "www/ui.js" }), ], resolve: { // One React instance across App.tsx and the bundled shiny-react source. @@ -43,7 +43,7 @@ export default defineConfig(({ command }) => ({ entry: path.resolve(__dirname, ENTRY), formats: ["iife"], name: "HmrExample", - fileName: () => "app.js", + fileName: () => "ui.js", }, rollupOptions: { external: ["react", "react-dom", "react-dom/client"], diff --git a/examples/09-hmr/www/index.html b/examples/09-hmr/www/index.html deleted file mode 100644 index a80fc4b3..00000000 --- a/examples/09-hmr/www/index.html +++ /dev/null @@ -1,2 +0,0 @@ -
- From 45f3e91c927188e9597bce9c5327b4f10a687f97 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 20 Aug 2026 11:19:48 -0400 Subject: [PATCH 3/5] refactor: page_react_dep() defaults follow the ui.js/ui.css convention - main.js/main.css defaults were inconsistent with page_react()'s discovery names; one convention everywhere (#205) - Update default-exercising tests in both languages; fix a stale dev-stub comment in 09-hmr's vite config --- examples/09-hmr/vite.config.js | 2 +- pkg-py/src/shinyreact/_page.py | 10 +++---- pkg-py/tests/test_dep.py | 32 +++++++++++----------- pkg-py/tests/test_page.py | 2 +- pkg-r/R/page.R | 8 +++--- pkg-r/man/page_react_dep.Rd | 8 +++--- pkg-r/tests/testthat/test-page-react-dep.R | 28 +++++++++---------- 7 files changed, 45 insertions(+), 45 deletions(-) diff --git a/examples/09-hmr/vite.config.js b/examples/09-hmr/vite.config.js index 742ac41c..c0a1dae0 100644 --- a/examples/09-hmr/vite.config.js +++ b/examples/09-hmr/vite.config.js @@ -17,7 +17,7 @@ export default defineConfig(({ command }) => ({ }, plugins: [ react(), - // serve only: writes www/app.js as the dev stub (apply:"serve" inside). + // serve only: writes www/ui.js as the dev stub (apply:"serve" inside). shinyreactDevStub({ entry: ENTRY, outFile: "www/ui.js" }), ], resolve: { diff --git a/pkg-py/src/shinyreact/_page.py b/pkg-py/src/shinyreact/_page.py index 7afe2e46..7d89c609 100644 --- a/pkg-py/src/shinyreact/_page.py +++ b/pkg-py/src/shinyreact/_page.py @@ -67,7 +67,7 @@ def page_react( *args: TagChild, src_dir: str | Path | None = None, js_file: str = "ui.js", - css_file: str = "ui.css", + css_file: str | None = "ui.css", title: str | None = None, lang: str = "en", ) -> Tag: @@ -116,8 +116,8 @@ def page_react( def page_react_dep( *, src_dir: str | Path | None = None, - js_file: str = "main.js", - css_file: str | None = "main.css", + js_file: str = "ui.js", + css_file: str | None = "ui.css", name: str | None = None, ) -> HTMLDependency: """Build an HTMLDependency for a React app's JS and CSS entry points. @@ -172,9 +172,9 @@ def page_react_dep( src_dir: Directory containing the JS/CSS. Inferred from the calling frame when omitted (see above). js_file: Filename of the JS entry point, relative to ``src_dir`` - (default ``"main.js"``). Attached only if the file exists. + (default ``"ui.js"``). Attached only if the file exists. css_file: Filename of the CSS file, relative to ``src_dir`` (default - ``"main.css"``). Attached only if the file exists; ``None`` to skip. + ``"ui.css"``). Attached only if the file exists; ``None`` to skip. name: Dependency name. Defaults to ``src_dir``'s basename. """ if src_dir is not None: diff --git a/pkg-py/tests/test_dep.py b/pkg-py/tests/test_dep.py index 654fbbb1..ec45cbe9 100644 --- a/pkg-py/tests/test_dep.py +++ b/pkg-py/tests/test_dep.py @@ -35,8 +35,8 @@ def test_dep_script_has_defer(): def _run_page_react_dep( tmp_path: Path, *, - js_file: str = "main.js", - css_file: str = "main.css", + js_file: str = "ui.js", + css_file: str = "ui.css", ) -> HTMLDependency: """Call page_react_dep() from a script located in tmp_path.""" app_file = tmp_path / "app.py" @@ -50,8 +50,8 @@ def _run_page_react_dep( def test_page_react_dep_returns_htmldependency(tmp_path): - (tmp_path / "main.js").write_text("// app") - (tmp_path / "main.css").write_text("/* styles */") + (tmp_path / "ui.js").write_text("// app") + (tmp_path / "ui.css").write_text("/* styles */") dep = _run_page_react_dep(tmp_path) assert isinstance(dep, HTMLDependency) @@ -60,9 +60,9 @@ def test_page_react_dep_returns_htmldependency(tmp_path): def test_page_react_dep_uses_mtime_version(tmp_path): - js = tmp_path / "main.js" + js = tmp_path / "ui.js" js.write_text("// app") - (tmp_path / "main.css").write_text("/* styles */") + (tmp_path / "ui.css").write_text("/* styles */") dep = _run_page_react_dep(tmp_path) expected_version = str(int(js.stat().st_mtime)) @@ -85,11 +85,11 @@ def test_page_react_dep_omits_script_when_js_absent(tmp_path): def test_page_react_dep_attaches_script_when_js_present(tmp_path): - (tmp_path / "main.js").write_text("// app") + (tmp_path / "ui.js").write_text("// app") dep = page_react_dep(src_dir=tmp_path) script = dep.script if isinstance(dep.script, dict) else dep.script[0] - assert script["src"] == "main.js" + assert script["src"] == "ui.js" assert script.get("type") == "module" @@ -107,8 +107,8 @@ def test_page_react_dep_custom_filenames(tmp_path): def test_page_react_dep_script_type_module(tmp_path): - (tmp_path / "main.js").write_text("// app") - (tmp_path / "main.css").write_text("/* styles */") + (tmp_path / "ui.js").write_text("// app") + (tmp_path / "ui.css").write_text("/* styles */") dep = _run_page_react_dep(tmp_path) script = dep.script if isinstance(dep.script, dict) else dep.script[0] @@ -121,18 +121,18 @@ def test_page_react_dep_explicit_src_dir_and_name(tmp_path): Frame inspection reads the *immediate* caller, so wrapping page_react_dep() in a helper would otherwise resolve against the wrapper's directory (#184). """ - (tmp_path / "main.js").write_text("// app") + (tmp_path / "ui.js").write_text("// app") dep = page_react_dep(src_dir=tmp_path, name="my-app") assert dep.source is not None assert dep.source["subdir"] == str(tmp_path) assert dep.name == "my-app" - assert str(dep.version) == str(int((tmp_path / "main.js").stat().st_mtime)) + assert str(dep.version) == str(int((tmp_path / "ui.js").stat().st_mtime)) def test_page_react_dep_omits_stylesheet_when_css_absent(tmp_path): """A bundle with no CSS must not emit a 404-ing stylesheet link (#184).""" - (tmp_path / "main.js").write_text("// app") + (tmp_path / "ui.js").write_text("// app") # htmltools normalizes an absent stylesheet to an empty list. assert page_react_dep(src_dir=tmp_path).stylesheet == [] @@ -140,10 +140,10 @@ def test_page_react_dep_omits_stylesheet_when_css_absent(tmp_path): def test_page_react_dep_attaches_stylesheet_when_css_present(tmp_path): - (tmp_path / "main.js").write_text("// app") - (tmp_path / "main.css").write_text("/* styles */") + (tmp_path / "ui.js").write_text("// app") + (tmp_path / "ui.css").write_text("/* styles */") stylesheet = page_react_dep(src_dir=tmp_path).stylesheet assert stylesheet is not None entry = stylesheet if isinstance(stylesheet, dict) else stylesheet[0] - assert entry["href"] == "main.css" + assert entry["href"] == "ui.css" diff --git a/pkg-py/tests/test_page.py b/pkg-py/tests/test_page.py index aca22b67..a4a145fa 100644 --- a/pkg-py/tests/test_page.py +++ b/pkg-py/tests/test_page.py @@ -31,7 +31,7 @@ def test_page_bare_no_shinyreact_dep(): def test_page_react_dep_falls_back_to_cwd_without_file(tmp_path, monkeypatch): """page_react_dep() falls back to CWD when the caller has no __file__.""" - (tmp_path / "main.js").write_text("// ...") + (tmp_path / "ui.js").write_text("// ...") monkeypatch.chdir(tmp_path) captured: dict[str, HTMLDependency] = {} diff --git a/pkg-r/R/page.R b/pkg-r/R/page.R index c10a3cd9..ae85f804 100644 --- a/pkg-r/R/page.R +++ b/pkg-r/R/page.R @@ -138,17 +138,17 @@ page_react_html <- function(path = "www/index.html") { #' @param src_dir Directory containing the JS/CSS. Required; Python infers this #' from the calling module's `__file__` when omitted, which R has no #' equivalent of. -#' @param js_file JS filename within `src_dir`. Defaults to `"main.js"`, +#' @param js_file JS filename within `src_dir`. Defaults to `"ui.js"`, #' matching Python; attached only if the file exists. -#' @param css_file CSS filename within `src_dir`. Defaults to `"main.css"`, +#' @param css_file CSS filename within `src_dir`. Defaults to `"ui.css"`, #' matching Python; attached only if the file exists. `NULL` to skip. #' @param name Dependency name; defaults to `basename(src_dir)`. #' @return An [htmltools::htmlDependency]. #' @export page_react_dep <- function( src_dir, - js_file = "main.js", - css_file = "main.css", + js_file = "ui.js", + css_file = "ui.css", name = basename(src_dir) ) { js_path <- file.path(src_dir, js_file) diff --git a/pkg-r/man/page_react_dep.Rd b/pkg-r/man/page_react_dep.Rd index 121ded5b..f608f682 100644 --- a/pkg-r/man/page_react_dep.Rd +++ b/pkg-r/man/page_react_dep.Rd @@ -6,8 +6,8 @@ \usage{ page_react_dep( src_dir, - js_file = "main.js", - css_file = "main.css", + js_file = "ui.js", + css_file = "ui.css", name = basename(src_dir) ) } @@ -16,10 +16,10 @@ page_react_dep( from the calling module's \verb{__file__} when omitted, which R has no equivalent of.} -\item{js_file}{JS filename within \code{src_dir}. Defaults to \code{"main.js"}, +\item{js_file}{JS filename within \code{src_dir}. Defaults to \code{"ui.js"}, matching Python; attached only if the file exists.} -\item{css_file}{CSS filename within \code{src_dir}. Defaults to \code{"main.css"}, +\item{css_file}{CSS filename within \code{src_dir}. Defaults to \code{"ui.css"}, matching Python; attached only if the file exists. \code{NULL} to skip.} \item{name}{Dependency name; defaults to \code{basename(src_dir)}.} diff --git a/pkg-r/tests/testthat/test-page-react-dep.R b/pkg-r/tests/testthat/test-page-react-dep.R index b65eae0d..b5fca81c 100644 --- a/pkg-r/tests/testthat/test-page-react-dep.R +++ b/pkg-r/tests/testthat/test-page-react-dep.R @@ -1,8 +1,8 @@ test_that("page_react_dep() returns an html_dependency pointing at src_dir", { dir <- withr::local_tempdir() - writeLines("// app", file.path(dir, "main.js")) + writeLines("// app", file.path(dir, "ui.js")) - dep <- page_react_dep(dir, "main.js") + dep <- page_react_dep(dir, "ui.js") expect_s3_class(dep, "html_dependency") expect_identical(dep$src$file, dir) expect_identical(dep$name, basename(dir)) @@ -10,16 +10,16 @@ test_that("page_react_dep() returns an html_dependency pointing at src_dir", { test_that("page_react_dep() versions by the JS file's mtime", { dir <- withr::local_tempdir() - js <- file.path(dir, "main.js") + js <- file.path(dir, "ui.js") writeLines("// app", js) - dep <- page_react_dep(dir, "main.js") + dep <- page_react_dep(dir, "ui.js") expect_identical(dep$version, as.character(as.integer(file.mtime(js)))) }) test_that("page_react_dep() falls back to version \"0\" when the JS is missing", { dir <- withr::local_tempdir() - expect_warning(dep <- page_react_dep(dir, "main.js"), "JS entry point") + expect_warning(dep <- page_react_dep(dir, "ui.js"), "JS entry point") expect_identical(dep$version, "0") }) @@ -42,20 +42,20 @@ test_that("page_react_dep() honours custom filenames and the name override", { expect_identical(dep$stylesheet, "app.css") }) -test_that("page_react_dep() defaults to main.js / main.css like Python", { +test_that("page_react_dep() defaults to ui.js / ui.css like Python", { dir <- withr::local_tempdir() - writeLines("// app", file.path(dir, "main.js")) - writeLines("/* styles */", file.path(dir, "main.css")) + writeLines("// app", file.path(dir, "ui.js")) + writeLines("/* styles */", file.path(dir, "ui.css")) dep <- page_react_dep(dir) - expect_identical(dep$script[["src"]], "main.js") - expect_identical(dep$stylesheet, "main.css") + expect_identical(dep$script[["src"]], "ui.js") + expect_identical(dep$stylesheet, "ui.css") }) test_that("page_react_dep() omits the stylesheet when the CSS is absent", { - # A bundle that ships no CSS should not 404 on main.css (#184). + # A bundle that ships no CSS should not 404 on ui.css (#184). dir <- withr::local_tempdir() - writeLines("// app", file.path(dir, "main.js")) + writeLines("// app", file.path(dir, "ui.js")) expect_null(page_react_dep(dir)$stylesheet) expect_null(page_react_dep(dir, css_file = NULL)$stylesheet) @@ -65,9 +65,9 @@ test_that("page_react_dep() emits script type=\"module\" and no defer", { # Matches Python (`page_react_dep()` in _page.py); an ESM bundle throws on # its first `import` when served from a classic diff --git a/examples/01-hello/www/main.css b/examples/01-hello/www/ui.css similarity index 100% rename from examples/01-hello/www/main.css rename to examples/01-hello/www/ui.css diff --git a/examples/01-hello/www/app.js b/examples/01-hello/www/ui.js similarity index 98% rename from examples/01-hello/www/app.js rename to examples/01-hello/www/ui.js index b5a85903..65c170d9 100644 --- a/examples/01-hello/www/app.js +++ b/examples/01-hello/www/ui.js @@ -183,7 +183,7 @@ function App() { ); } -// No mount div in index.html — create the container and append it to . +// No mount div in the generated page — create the container and append it to . // The script is deferred, so document.body is parsed by the time this runs. const root = ReactDOM.createRoot( document.body.appendChild(document.createElement("div")), diff --git a/examples/02-columns/README.md b/examples/02-columns/README.md index dfcd901f..e727861d 100644 --- a/examples/02-columns/README.md +++ b/examples/02-columns/README.md @@ -21,9 +21,8 @@ The result: ~20 lines of server logic, versus ~80 lines wrestling with observers examples/02-columns/ ├── app.py # set_react_page() + 1 reactive_output (column_data) + 1 reactive.effect on input.move_item └── www/ - ├── index.html - ├── app.js # raw React.createElement (Column + ItemRow components) - └── main.css + ├── ui.js # raw React.createElement (Column + ItemRow components) + └── ui.css ``` Same no-build shape as [01-hello](../01-hello/). No `package.json`, no bundler. diff --git a/examples/02-columns/www/index.html b/examples/02-columns/www/index.html deleted file mode 100644 index 7a54efb1..00000000 --- a/examples/02-columns/www/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/examples/02-columns/www/main.css b/examples/02-columns/www/ui.css similarity index 100% rename from examples/02-columns/www/main.css rename to examples/02-columns/www/ui.css diff --git a/examples/02-columns/www/app.js b/examples/02-columns/www/ui.js similarity index 89% rename from examples/02-columns/www/app.js rename to examples/02-columns/www/ui.js index 6369a707..2130e1cd 100644 --- a/examples/02-columns/www/app.js +++ b/examples/02-columns/www/ui.js @@ -90,5 +90,9 @@ function App() { ); } -const root = ReactDOM.createRoot(document.getElementById("root")); +// No mount div in the generated page -- create the container and append it +// to . The script is deferred, so document.body is parsed by now. +const root = ReactDOM.createRoot( + document.body.appendChild(document.createElement("div")), +); root.render(h(App)); diff --git a/examples/05-temperature/www/index.html b/examples/05-temperature/www/index.html deleted file mode 100644 index 7a54efb1..00000000 --- a/examples/05-temperature/www/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/examples/05-temperature/www/main.css b/examples/05-temperature/www/ui.css similarity index 100% rename from examples/05-temperature/www/main.css rename to examples/05-temperature/www/ui.css diff --git a/examples/05-temperature/www/app.js b/examples/05-temperature/www/ui.js similarity index 91% rename from examples/05-temperature/www/app.js rename to examples/05-temperature/www/ui.js index 7272c23b..02bfe85d 100644 --- a/examples/05-temperature/www/app.js +++ b/examples/05-temperature/www/ui.js @@ -106,5 +106,9 @@ function App() { ); } -const root = ReactDOM.createRoot(document.getElementById("root")); +// No mount div in the generated page -- create the container and append it +// to . The script is deferred, so document.body is parsed by now. +const root = ReactDOM.createRoot( + document.body.appendChild(document.createElement("div")), +); root.render(h(App)); diff --git a/examples/06-data-frame/www/index.html b/examples/06-data-frame/www/index.html deleted file mode 100644 index 8145daad..00000000 --- a/examples/06-data-frame/www/index.html +++ /dev/null @@ -1,2 +0,0 @@ -
- diff --git a/examples/06-data-frame/www/app.js b/examples/06-data-frame/www/ui.js similarity index 81% rename from examples/06-data-frame/www/app.js rename to examples/06-data-frame/www/ui.js index a39cab45..0607b1fb 100644 --- a/examples/06-data-frame/www/app.js +++ b/examples/06-data-frame/www/ui.js @@ -34,5 +34,9 @@ function App() { ); } -const root = ReactDOM.createRoot(document.getElementById("root")); +// No mount div in the generated page -- create the container and append it +// to . The script is deferred, so document.body is parsed by now. +const root = ReactDOM.createRoot( + document.body.appendChild(document.createElement("div")), +); root.render(h(App)); diff --git a/examples/07-plotly/www/index.html b/examples/07-plotly/www/index.html deleted file mode 100644 index 8145daad..00000000 --- a/examples/07-plotly/www/index.html +++ /dev/null @@ -1,2 +0,0 @@ -
- diff --git a/examples/07-plotly/www/app.js b/examples/07-plotly/www/ui.js similarity index 83% rename from examples/07-plotly/www/app.js rename to examples/07-plotly/www/ui.js index 3de37fbe..55b687cf 100644 --- a/examples/07-plotly/www/app.js +++ b/examples/07-plotly/www/ui.js @@ -38,5 +38,9 @@ function App() { ); } -const root = ReactDOM.createRoot(document.getElementById("root")); +// No mount div in the generated page -- create the container and append it +// to . The script is deferred, so document.body is parsed by now. +const root = ReactDOM.createRoot( + document.body.appendChild(document.createElement("div")), +); root.render(h(App)); diff --git a/examples/08-input-handler/www/index.html b/examples/08-input-handler/www/index.html deleted file mode 100644 index 7a54efb1..00000000 --- a/examples/08-input-handler/www/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/examples/08-input-handler/www/main.css b/examples/08-input-handler/www/ui.css similarity index 100% rename from examples/08-input-handler/www/main.css rename to examples/08-input-handler/www/ui.css diff --git a/examples/08-input-handler/www/app.js b/examples/08-input-handler/www/ui.js similarity index 84% rename from examples/08-input-handler/www/app.js rename to examples/08-input-handler/www/ui.js index ac051262..459530d9 100644 --- a/examples/08-input-handler/www/app.js +++ b/examples/08-input-handler/www/ui.js @@ -53,5 +53,9 @@ function App() { ); } -const root = ReactDOM.createRoot(document.getElementById("root")); +// No mount div in the generated page -- create the container and append it +// to . The script is deferred, so document.body is parsed by now. +const root = ReactDOM.createRoot( + document.body.appendChild(document.createElement("div")), +); root.render(h(App)); diff --git a/examples/10-bookmarking/www/index.html b/examples/10-bookmarking/www/index.html deleted file mode 100644 index 37a81bc0..00000000 --- a/examples/10-bookmarking/www/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/examples/10-bookmarking/www/styles.css b/examples/10-bookmarking/www/ui.css similarity index 100% rename from examples/10-bookmarking/www/styles.css rename to examples/10-bookmarking/www/ui.css diff --git a/examples/10-bookmarking/www/app.js b/examples/10-bookmarking/www/ui.js similarity index 89% rename from examples/10-bookmarking/www/app.js rename to examples/10-bookmarking/www/ui.js index f8247baa..25240c7f 100644 --- a/examples/10-bookmarking/www/app.js +++ b/examples/10-bookmarking/www/ui.js @@ -81,5 +81,9 @@ ); } - ReactDOM.createRoot(document.getElementById("root")).render(h(App)); + // No mount div in the generated page -- create the container and append it + // to . The script is deferred, so document.body is parsed by now. + ReactDOM.createRoot( + document.body.appendChild(document.createElement("div")), + ).render(h(App)); })(); diff --git a/examples/README.md b/examples/README.md index 4c9e8e4d..f9d9b7bb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,8 +2,8 @@ Runnable example apps for the `ui.tsx` pattern: the server contains only reactive computation, and the UI is defined in a client-side React codebase -whose entry is conventionally `ui.tsx` (simpler variants like `www/app.js` -for no-build or `src/App.jsx` for Vite + JSX fill the same role). +whose entry is conventionally `ui.tsx` (simpler variants like `www/ui.js` +for no-build or `src/ui.jsx` for Vite + JSX fill the same role). Examples are Python unless noted; [01-hello](01-hello/) also ships an `app.R` showing the same app on the R package. @@ -18,5 +18,5 @@ showing the same app on the R package. | [06-data-frame](06-data-frame/) | Embeds `@render.data_frame` via `ShinyOutput` and `set_react_page()` | | [07-plotly](07-plotly/) | Embeds `@render_plotly` via `ShinyOutput` and `set_react_page()` | | [08-input-handler](08-input-handler/) | `useShinyInput` with `type="shiny.datetime"` — client sends unix seconds; server `input.when()` is a `datetime.datetime` via Shiny's built-in handler | -| [09-hmr](09-hmr/) | React Fast Refresh in dev (Vite dev server alongside Shiny); the `app.py` and no-build `www/app.js` paths reload too | +| [09-hmr](09-hmr/) | React Fast Refresh in dev (Vite dev server alongside Shiny); the `app.py` and no-build `www/ui.js` paths reload too | | [10-bookmarking](10-bookmarking/) | Bookmark restoration: URL query string (or server-stored state) hydrates `useShinyInput` initial values via a head `