Thank you for helping. This guide says how to report a problem, how to propose a change, and how the package fits together.
Reporting a problem
Open an issue at https://github.com/coatless-wasm/webrarian/issues and include:
- the output of
webrarian::diagnose_collection()run in your collection - your
_webrarian.yml, and the call that failed with its full message - for a problem in the browser, the browser and its version, where the site is served from (the preview, GitHub Pages, Netlify, …) and the errors in the browser’s console
Report a security problem privately instead, as described in the security policy.
Proposing a change
Follow these steps to propose a change:
- For anything larger than a typo, open an issue first so we can agree on the approach.
- Fork the repository and branch from
main. - Write a test that fails without your change (testthat 3, in
tests/testthat/), then make it pass. - Run the checks under “Testing” and make sure they pass.
- Add a bullet for your change to the top section of NEWS.md.
- Open a pull request against
main.
Commit messages are one sentence-case imperative line with no feat: or fix: prefix, for example “Guard the output directory before cleaning”. Stage the files you changed by name, and never commit build output (_site/, *.Rcheck/, tarballs).
The R code is formatted with air, using the settings in air.toml. Run air format . before you commit, because CI fails on code air would reformat.
The browser workspace in inst/viewer/ is built from exlibris. Change it there, then re-vendor it with tools/vendor-exlibris.sh. On every run, CI checks the vendored files against the hashes in inst/viewer/PROVENANCE.json. Where the EXLIBRIS_TOKEN secret is set (exlibris is private for now), it also rebuilds the pinned exlibris commit and fails if the vendored files differ. Without the secret, that rebuild is skipped.
Architecture
webrarian builds static sites where R runs in the browser through webR. It has two halves:
- The R package (this repository) holds the collection model (
_webrarian.yml), package resolution and download, Docker compilation of local and GitHub packages, file selection, the webR engine cache, the site build, previews, deployment helpers and mirrors. - The browser workspace is the prebuilt exlibris bundle vendored in
inst/viewer/(exlibris-r.js,exlibris-r.css,index.html, notices andPROVENANCE.json) plusinst/viewer-config.schema.json. webrarian never builds it.tools/vendor-exlibris.shcopies it from an exlibris checkout at a pinned commit and regenerates the notices fromPROVENANCE.json.
The two meet at one object. bind() writes window.__VIEWER_CONFIG__ inline into the page (R/viewer-config.R, build_viewer_config()), with kebab-case keys, and the tests validate real output against the vendored schema.
Package sources
Packages come from two kinds of source:
- Prebuilt packages are WebAssembly binaries from repo.r-wasm.org, then each repository in
packages.reposin order. The first repository with the package wins (R/repo.R). - Local and GitHub packages are compiled in
ghcr.io/r-wasm/webr:v<webR version>with Docker, then indexed into the site’srepo/.
rwasm runs only inside the Docker container. Never load rwasm locally. The library(rwasm) in build_packages_rwasm_docker() is part of a script written for the container.
The engine, package sources and offline sites
build.bundle-engine: true (the default) copies the webR engine into webr/v<version>/, and false loads it from webr.r-wasm.org. A site that bundles packages has them in repo/. At runtime the page installs from ./repo when the site bundles packages, then, unless the site is offline, from repo.r-wasm.org and each repository in packages.repos (viewer_package_repos()). exlibris’s webR driver sets the webr_pkg_repos option to the same list, so install.packages() in the console searches the same places the boot installs do. getOption("repos") is not touched. An inline script after the config makes every relative URL absolute against the page, so sites work from a sub-path.
build.offline is strict and false by default, as in pyodidarian. true means the deployed site makes no request to another origin. It implies a bundled engine and sets the wire key offline, which exlibris honors. A share link’s ?packages=, the Packages tab or install.packages() can add only bundled packages, and anything else ends with a ✗ and a reason, never a request. collection_mirror() output is always offline.
When repo/ holds every package the site installs, bind() also writes them, installed, as one gzipped tar image, library/library-<hash>.tgz (build_library_image()), named in the wire key packages.library-images. exlibris mounts it at /exlibris/library/<n> and installs nothing it holds, falling back to repo/ when it cannot mount it. build.library-image: true is the default and false turns it off. The packages stay in repo/ either way, so runtime installs keep working.
Key files
These files hold the main parts of the package:
-
R/build.R:bind(), the staged build, engine install, viewer copy, cache headers -
R/output.R: output-directory guard, build marker, staging and swap -
R/config.R,R/config-spec.R: settings, the key table, validation -
R/utils.R: YAML read/write (kebab <-> snake keys), Docker probe, versions -
R/project.R:catalog(), its templates and dependency detection -
R/files.R: file selection (gitignore-style),acquire_file() -
R/packages.R,R/repo.R: package lists, repository indexes, verified downloads -
R/library-image.R:build_library_image()andwrite_library_image(), the mountable package library image (library/library-<hash>.tgz) -
R/drift.R: WebAssembly versions against CRAN and the local library (package_drift(),report_package_drift()writingpackages.json, and thewebrarian.cran_repooption) -
R/viewer-config.R,R/html.R,R/css.R,R/brand.R: the page, its config and branding (the same brand rule as pyodidarian: accent and fonts on:root, surface and text colors scoped to light mode unless the palette is dark) -
R/preview.R,R/watch.R:reading_room()on httpuv static paths, watch mode -
R/mirror.R,R/licenses.R: mirrors and theLICENSES/directory (the same file names as pyodidarian’s) -
R/deploy.R: GitHub Pages and Netlify workflows (templates ininst/templates/). A generatednetlify.tomlholds only[build] publish, because_headerscarries every header. -
R/diagnostics.R:check_inventory()(with the drift columns),diagnose_collection() -
R/assets.R: the webR engine cache, which keeps the three most recently used versions -
R/examples.R,inst/examples/:collection_example() -
R/webrarian-package.R: the package help page and the package options -
tools/vendor-exlibris.sh,tools/notices/: vendoring and notices -
tools/config-reference.R: regeneratesvignettes/config-reference.qmdandinst/templates/_webrarian.ymlfromconfig_spec() -
tools/measure-sizes.R: the size table invignettes/getting-started.qmd -
tools/ci-test.R: runs a slice of the test suite the way CI does
Build flow
A build runs these steps in order:
-
bind()reads and validates_webrarian.ymland resolves the output directory. - It builds into
.webrarian/staging-*. - Engine: cached download, verified, copied to
webr/v<version>/(unlessbuild.bundle-engineis false). - Packages: prebuilt downloads go through the checksum-verified cache, local and GitHub packages are compiled in Docker, and the
repo/index is written. - Library image (when
repo/holds every package the site installs, unlessbuild.library-imageis false):build_library_image()unpacks the binariesrepo/’s index names and writeslibrary/library-<hash>.tgz. - Files: selected and copied to
vfs-files/. - Page:
index.htmlwith the inline config, content-hashed viewer bundle, branding,_headers(the only source of the COOP/COEP and cache headers, with/library/*immutable),sw.js(a kill switch that retires any earlier worker unlessbuild.service-workeris true) andLICENSES/. Thenreport_package_drift()writespackages.jsonand names bundled packages older than on CRAN (it asks CRAN at most once a day and stays silent when it cannot reach it). - The staging directory replaces the output, and
bind()returns awebrarian_bind_resultwith sizes by part (library/counts as packages).
Testing
NOT_CRAN=true Rscript -e 'devtools::test()' # everything
NOT_CRAN=true Rscript -e 'devtools::test(filter = "docs")' # documentation checks
NOT_CRAN=true Rscript tools/ci-test.R --filter 'browser|examples' --no-skips # as CI runs the browser suite
R CMD build . && R CMD check --as-cran webrarian_*.tar.gzTests follow these rules:
- Point
R_USER_CACHE_DIRat a scratch directory for anything that downloads.WEBRARIAN_PACKAGE_CACHEis an on/off switch, not a path. -
tests/testthat/setup.Rsetswebrarian.cran_repo = FALSE, so no test asks CRAN. A test that needs CRAN versions mockscran_package_versions(). - Docker paths use the fake
dockerintests/testthat/fixtures/fake-docker.sh(local_fake_docker()). Real Docker runs only withWEBRARIAN_TEST_DOCKER=true, as CI’sdockerjob does. - Browser tests use chromote through
local_browser_page().
Documentation
The documentation follows these rules:
-
README.mdis rendered fromREADME.qmd(quarto render README.qmd). - The vignettes run no code except the provenance chunk in
ecosystem.qmd. Vignettes must build without a browser, so never add a{mermaid}block. - The figures in
vignettes/images/and the README’sman/figures/hero-*.svgare SVG files written by hand, so edit them directly. Each one carries a<title>and a<desc>, draws its own background so it reads in both site themes, and uses only system fonts or outlined lettering. The two hero files differ only in their<style>line. - After changing
config_spec(), runRscript tools/config-reference.R. - Every YAML block in the docs starts with a
# <file name>comment, and thetest-docs-*.Rtests validate the blocks, the R code and the quoted messages. -
test-vignettes-persistence.Rpins the sections on library images, the drift report and Shiny (packages.qmd) and on kept edits and embedding (customization.qmd). Move them freely, but keep their headings. - The pkgdown site is published only by
.github/workflows/pkgdown.yml. To preview it, build into a scratch directory:pkgdown::build_site(override = list(destination = tempfile("site"))).