This post is the fifth one of a series of post about webR:
Note: the first post of this series explaining roughly what webR is, I won’t introduce it again here.
When I wrote my blogpost about Preloading your R packages in webR in an Express JS API, I mentioned that there was no native way to preload things in the webR filesystem — meaning that you had to reinstall all the R packages whenever the app was launched (and reported it in a Github issue). This also meant that I couldn’t easily take my own functions and run them in the webR environment, as described in Using my own R functions in webR in an Express JS API, and thoughts on building web apps with Node & webR. These needs made me write the webrtools NodeJS package, to do just that:
The last webR version now exposes Emscripten’s FS.mount
, so this makes the process easier, as there is now no need to read the file tree and recreate it: the folder from the server can be mounted into webR filesystem.
That implied some rework (now integrated to webrtools
) :
webR.FS.mkdir
to create a local lib: the module can’t mount the package library into the libPath as is, because it would overwrite the already bundled library (in other words, if you mount into a folder that is not empty, the content from the server overwrites the content from webR).webR.FS.mount
to mount the local directory into the newly created libraryHere is the new code for loadPackages
, bundled into webrtools
:
async function loadPackages(webR, dirPath, libName = "webr_packages") { // Create a custom lib so that we don't have to worry about // overwriting any packages that are already installed. await webR.FS.mkdir(`/usr/lib/R/${libName}`) // Mount the custom lib await webR.FS.mount("NODEFS", { root: dirPath }, `/usr/lib/R/${libName}`); // Add the custom lib to the R search path await webR.evalR(`.libPaths(c('/usr/lib/R/${libName}', .libPaths()))`); }
I’ve also decided to deprecate the loadFolder
function, as it is now native with webR.FS.mkdir
+ webR.FS.mount
.
So here is a rewrite of the app from Using my own R functions in webR in an Express JS API, and thoughts on building web apps with Node & webR.
const app = require('express')() const path = require('path'); const { loadPackages } = require('webrtools'); const { WebR } = require('webr'); (async () => { globalThis.webR = new WebR(); await globalThis.webR.init(); console.log("🚀 webR is ready 🚀"); await loadPackages( globalThis.webR, path.join(__dirname, 'webr_packages') ) await globalThis.webR.FS.mkdir("/home/rfuns") await globalThis.webR.FS.mount( "NODEFS", { root: path.join(__dirname, 'rfuns') }, "/home/rfuns" ) console.log("📦 Packages written to webR 📦"); // see https://github.com/r-wasm/webr/issues/292 await globalThis.webR.evalR("options(expressions=1000)") await globalThis.webR.evalR("pkgload::load_all('/home/rfuns')"); app.listen(3000, '0.0.0.0', () => { console.log('http://localhost:3000') }) })(); app.get('/', async (req, res) => { let result = await globalThis.webR.evalR( 'unique_species()' ); try { let js_res = await result.toJs() res.send(js_res.values) } finally { webR.destroy(result); } }) app.get('/:n', async (req, res) => { let result = await globalThis.webR.evalR( 'star_wars_by_species(n)', { env: { n: req.params.n } } ); try { const result_js = await result.toJs(); res.send(result_js) } finally { webR.destroy(result); } });
Full code is at ColinFay/webr-examples/.
Further exploration to be done: webR now bundles a way to package a file system in a file, which can then be downloaded and mounted into the runtime, as described here. This might come handy for our current structure, but I’ll have to explore it a bit more.