Runs a learnr2 tutorial bundled with an installed package: renders it
into a per-user cache – or reuses that render, if nothing has changed –
and, when open is TRUE, serves it to a browser. learnr2 runs only its
own Quarto (.qmd) tutorials and does not depend on 'learnr'. Asked for
a classic 'learnr' tutorial (an .Rmd), it stops and says to run that
one with learnr::run_tutorial().
Usage
run_tutorial(
name = NULL,
package = "learnr2",
output_dir = tools::R_user_dir("learnr2", "cache"),
open = interactive(),
refresh = FALSE
)Arguments
- name
Name of the tutorial to run. See
available_tutorials(). IfNULL, the available tutorials inpackageare listed.- package
Name of the package the tutorial is bundled with. Defaults to
"learnr2"; set this to run a tutorial from another installed package (e.g. a 'primer.tutorials'-style content package), or from one loaded withpkgload::load_all()(seeavailable_tutorials()).- output_dir
Root of the render cache. Each tutorial is rendered into
output_dir/<package>/<name>/. Defaults to a persistent per-user directory (seetools::R_user_dir()), nottempfile(): a persistent location is what makes the render cache (below) work at all, and R deletes its session temp directory as soon as the R process exits, which races with the browser actually loading the page whenopen = TRUEis used fromRscript.- open
Whether to serve the tutorial and open it in a browser. Defaults to
TRUEwhen interactive. WhenTRUE, this call blocks (likehttpuv::runStaticServer()orshiny::runApp()) until you interrupt it (Ctrl+C, or the console's Stop button) – see the sections below for why. WhenFALSE, the tutorial is rendered (or found in the cache) and its path returned without serving or blocking. Note that underRscriptthe default isFALSE, so passopen = TRUEexplicitly there.- refresh
Re-render the tutorial even if the cached render is current. Defaults to
FALSE.
Render cache
Rendering a Quarto tutorial takes several seconds on a laptop and well
over ten on a small cloud machine, and nothing about an installed
tutorial changes between one launch and the next. So the render is
cached: alongside the HTML in output_dir/<package>/<name>/, a stamp
file records the learnr2 version and a fingerprint of every file in the
installed tutorial directory. On the next launch, if both still match,
the cached HTML is served at once. Reinstalling the tutorial's package
or upgrading learnr2 invalidates the cache; so does refresh = TRUE.
Before any re-render the old copy is deleted, so files a previous
version of the tutorial had and the current one does not cannot linger.
prerender_tutorials() fills the cache for every installed Quarto
tutorial ahead of time – for instance while building a container image
– so that even a student's first launch is instant.
Serving, the fixed port, and saved answers
A reader's answers and typed exercise code are saved in the browser's
localStorage, keyed by the page URL (see "Progress persistence" in
question()). For them to be found again tomorrow, the URL has to be
the same tomorrow. So learnr2 always serves on one fixed port, 7446
(getOption("learnr2.port") overrides it), and never falls back to a
random one; and it serves the whole cache root rather than a single
tutorial, so every tutorial has its own stable address,
http://127.0.0.1:7446/<package>/<name>/, and no two tutorials share
saved state or get wiped together by one tutorial's "Start Over".
Because the server serves the whole cache, one server is enough for any number of tutorials. If a learnr2 server is already running on the port (say, in another terminal), this function renders into the cache if needed and simply opens the tutorial's address in the browser, without starting a second server or blocking. If the port is held by something other than a learnr2 server, it stops with an error rather than silently serving somewhere the browser has no saved answers for.
The cache root itself (http://127.0.0.1:7446/) is a small page that
forwards to the tutorial launched most recently and lists every other
rendered tutorial in the cache. That root is what a tool that only knows
the port opens – notably the "open in browser" notification VS Code and
GitHub Codespaces show when they detect a new local port – so landing
on it has to reach the tutorial rather than a 404.
Running in GitHub Codespaces (or another remote container)
The server runs inside the container, so http://127.0.0.1:7446/...
is only reachable from inside it; in the browser on your own machine
that address refuses to connect. Codespaces forwards the port to a
public address, https://<codespace>-7446.app.github.dev/, and this
function detects a codespace (the CODESPACE_NAME and
GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN environment variables) and
prints the tutorial's forwarded address. The browser is still opened on
the local address, through the helper VS Code puts in BROWSER, which
forwards the port as part of opening it, just as clicking a localhost
link in the terminal does; opening the forwarded address directly can
race the port forwarding and show an empty 404 until reloaded. The
forwarded root page forwards to the tutorial too, so the "Open in
Browser" button on the port notification lands in the right place.
Saved answers are keyed by page URL, so they live under the forwarded
address and are found again as long as the codespace keeps its name.
Why this blocks and serves over local HTTP instead of opening the file directly
Every {webr} exercise compiles down to Observable JS (OJS), which
Quarto's runtime loads via ES modules – and browsers refuse to load ES
modules from a file:// URL. Opening the rendered HTML directly (e.g.
utils::browseURL() on the local path, or double-clicking the file) hits
this and shows an "OJS runtime" error, even though plain
question()/student_info() widgets (not OJS-based) work fine over
file://.
An earlier version of this function used quarto::quarto_preview() to
both render and serve the tutorial via a background daemon process, on
the theory that it would keep running after run_tutorial() returned.
In practice that daemon did not reliably stay alive (confirmed: it could
exit within seconds, even with the calling R session still running and
pumping its event loop), silently leaving you back at a file:// URL
with no server behind it. This function now renders with the same
one-shot quarto::quarto_render() call the package's own pkgdown
publishing script uses, and serves the result with httpuv's static
server – an in-process server with no separate daemon to lose track
of. Its trade-off is that it blocks the caller while serving, matching
how the original 'learnr' package's run_tutorial() (built on a
blocking Shiny app) behaved – stop the server to get your prompt back.
See also
prerender_tutorials() to fill the render cache in advance.
Examples
# With no `name`, just lists the tutorials that can be run.
run_tutorial()
#> Available tutorials in learnr2:
#> - hello-learnr2
# Render without serving (open = FALSE), into a temporary directory
# rather than the user cache. Calling it again would reuse this render;
# pass refresh = TRUE to force a new one. Runs only interactively, with the
# Quarto command line tool installed: a full tutorial takes several
# seconds to render.
if (interactive() && !is.null(quarto::quarto_path())) {
out <- tempfile()
html <- run_tutorial("hello-learnr2", output_dir = out, open = FALSE)
file.exists(html)
unlink(out, recursive = TRUE)
}
# Not run: with open = TRUE, starts a local web server that blocks the
# session until interrupted.
if (FALSE) { # \dontrun{
run_tutorial("hello-learnr2")
} # }