{"id":13718297,"url":"https://github.com/hrbrmstr/webr-app","last_synced_at":"2025-03-21T12:30:53.231Z","repository":{"id":143693060,"uuid":"612968021","full_name":"hrbrmstr/webr-app","owner":"hrbrmstr","description":"🧪 🕸️ A Way Better Structured WebR Demo App","archived":false,"fork":false,"pushed_at":"2023-03-19T19:52:55.000Z","size":113,"stargazers_count":27,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"batman","last_synced_at":"2025-03-01T06:09:47.721Z","etag":null,"topics":["javascript-modules","r","rstats","wasm","webr","webr-experiment"],"latest_commit_sha":null,"homepage":"https://rud.is/b/2023/03/12/almost-bare-bones-webr-starter-app/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hrbrmstr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-03-12T14:19:06.000Z","updated_at":"2024-10-02T18:29:56.000Z","dependencies_parsed_at":"2023-05-30T14:47:00.233Z","dependency_job_id":null,"html_url":"https://github.com/hrbrmstr/webr-app","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hrbrmstr%2Fwebr-app","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hrbrmstr%2Fwebr-app/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hrbrmstr%2Fwebr-app/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hrbrmstr%2Fwebr-app/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hrbrmstr","download_url":"https://codeload.github.com/hrbrmstr/webr-app/tar.gz/refs/heads/batman","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244135909,"owners_count":20403798,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript-modules","r","rstats","wasm","webr","webr-experiment"],"created_at":"2024-08-03T01:00:22.359Z","updated_at":"2025-03-21T12:30:52.273Z","avatar_url":"https://github.com/hrbrmstr.png","language":"JavaScript","funding_links":[],"categories":["Community Demos"],"sub_categories":["Miscellaneous"],"readme":"Almost Bare Bones WebR Starter App\n================\n\nLet’s walk through how to set up a [~minimal HTML/JS/CS + WebR-powered\n“app”](https://rud.is/webr-app/) on a server you own. This will be\nvanilla JS (i.e. no React/Vue/npm/bundler) you can hack on at-will.\n\nIn the `docs/` directory you’ll see an example of using this in GH\nPages.Here it is live: \u003chttps://hrbrmstr.github.io/webr-app/index.html\u003e.\nInfo on what you need to do for that is below.\n\nTL;DR: You can find the source to the app and track changes to it [over\non GitHub](https://github.com/hrbrmstr/webr-app/tree/batman) if you want\nto jump right in.\n\n## Getting Your Server Set Up\n\nI’ll try to keep updating this with newer WebR releases. Current version\nis 0.1.0 and you can grab that from:\n\u003chttps://github.com/r-wasm/webr/releases/download/v0.1.0/webr-0.1.0.tar.gz\u003e.\n\n### System-Wide WebR\n\n\u003e You should [read this\n\u003e section](https://docs.r-wasm.org/webr/v0.1.0/serving.html) in the\n\u003e official WebR documentation before continuing.\n\nI’m using a server-wide `/webr` directory on my `rud.is` domain so I can\nuse it on any page I serve.\n\nWebR performance will suffer if it can’t use `SharedArrayBuffer`s. So, I\nhave these headers enabled on my `/webr` directory:\n\n    Cross-Origin-Opener-Policy: same-origin\n    Cross-Origin-Embedder-Policy: require-corp\n\nI use nginx, so that looks like:\n\n    location ^~ /webr {\n      add_header \"Cross-Origin-Opener-Policy\" \"same-origin\";\n      add_header \"Cross-Origin-Embedder-Policy\" \"require-corp\";\n    }\n\nYMMV.\n\nFor good measure (and in case I move things around), I stick those\nheaders on my any app dir that will use WebR. I don’t use them\nserver-wide, though.\n\nAlso, this `Cache-Control` heading appears to help keep things under\n`/webr` in the browser cache longer, and will also let any ISP or\nenterprise proxies keep the files in their caches as well:\n\n    Cache-Control: public, max-age=604800\n\nand, in nginx:\n\n    location ^~ /webr {\n      add_header \"Cache-Control\" \"public, max-age=604800\";\n      add_header \"Cross-Origin-Opener-Policy\" \"same-origin\";\n      add_header \"Cross-Origin-Embedder-Policy\" \"require-corp\";\n    }\n\n### And They Call It a MIME. A MIME!\n\nWebR is a [JavaScript\nmodule](https://www.w3schools.com/js/js_modules.asp), and you need to\nmake sure that files with an `mjs` extension have a [MIME\ntype](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)\nof `text/javascript`, or some browsers won’t be happy.\n\nA typical way for webservers to know how to communicate this is via a\n`mime.types` file. That is not true for all webservers, and I’ll add\nsteps for ones that use a different way to configure this. The entry\nshould look like this:\n\n    text/javascript  mjs;\n\n### Testing The WebR Set Up\n\nYou should be able to hit that path on your webserver in your browser\nand see the WebR console app. If you do, you can continue. If not, leave\nan issue and I can try to help you debug it, but that’s on a best-effort\nbasis for me.\n\n## Installing The App\n\nWe’ll dig into the app in a bit, but you probably want to see it\nworking, so let’s install this ~minimal app.\n\nMy personal demo app is anchored off of `/webr-app` on my `rud.is` web\nserver. Here’s how to replicate it:\n\n    # Go someplace safe\n    $ cd $TMPDIR\n\n    # Get the app bundle\n    # You can also use the GH release version, just delete the README after installing it.\n    $ curl -o webr-app.tgz https://rud.is/dl/webr-app.tgz\n\n    # Expand it\n    $ tar -xvzf webr-app.tgz\n    x ./webr-app/\n    x ./webr-app/modules/\n    x ./webr-app/modules/webr-app.js\n    x ./webr-app/modules/webr-helpers.js\n    x ./webr-app/css/\n    x ./webr-app/css/simple.min.css\n    x ./webr-app/css/app.css\n    x ./webr-app/main.js\n    x ./webr-app/index.html\n\n    # 🚨 GO THROUGH EACH FILE\n    # 🚨 to make sure I'm not pwning you!\n    # 🚨 Don't trust anything or anyone.\n\n    # Go to the webserver root\n    $ cd $PATH_TO_WEBSERVER_DOC_ROOT_PATH\n\n    # Move the directory\n    $ mv $TMPDIR/webr-app .\n\n    # Delete the tarball (optional)\n    $ rm $TMPDIR/webr-app.tgz\n\nHit up that path on *your* web server and you should see what you saw on\nmine.\n\n## WebR-Powered App Structure\n\n    .\n    ├── css                  # CSS (obvsly)\n    │   ├── app.css          # app-specific ones\n    │   └── simple.min.css   # more on this in a bit\n    ├── index.html           # The main app page\n    ├── main.js              # The main app JS\n    └── modules              # We use ES6 JS modules\n        ├── webr-app.js      # Main app module\n        └── webr-helpers.js  # Some WebR JS Helpers I wrote\n\n### Simple CSS\n\nIf you sub to [my newsletter](https://dailyfinds.hrbrmstr.dev/), you\nknow I play with tons of tools and frameworks. Please use what you\nprefer.For folks who don’t normally do this type of stuff, I included a\ncopy of [Simple CSS](https://github.com/kevquirk/simple.css) b/c, well,\nit is *simple* to use. Please [use this\nresource](https://simplecss.org/demo) to get familiar with it if you do\ncontinue to use it.\n\n### JavaScript Modules\n\nWhen I’m in “hack” mode (like I was for the first few days after WebR’s\nlaunch), I revert to old, bad habits. We will not replicate those here.\n\nWe’re using [JavaScript\nModules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)\nas the project structure. We aren’t “bundling” (slurping up all app\nsupport files into a single, minified file) since not every R person is\na JS tooling expert. We’re also not using them as they really aren’t\nneeded, and I like to keep things simple and as dependency-free as\npossible.\n\nIn `index.html` you’ll see this line:\n\n    \u003cscript type=\"module\" src=\"./main.js\"\u003e\u003c/script\u003e \n\nThis tells the browser to load that JS file as if it were a module. As\nyou read (you *did* read the MDN link, above, *right*?), modules give us\nlocally-scoped names/objects/features and protection from clobbering\nimported names.\n\nOur main module contains all the crunchy goodness core functionality of\nour app, which does nothing more than:\n\n- loads WebR\n- Tells you how fast it loaded + instantiated\n- Yanks `mtcars` from the instantiated R session (`mtcars` was the third\n  “thing” I typed into R, ever, so my brain defaults to it).\n- Makes an HTML table from it using D3.\n\nIt’s small enough to include here:\n\n    import { format } from \"https://cdn.skypack.dev/d3-format@3\";\n    import * as HelpR from './modules/webr-helpers.js'; // WebR-specific helpers\n    // import * as App from './modules/webr-app.js'; // our app's functions, if it had some\n\n    console.time('Execution Time'); // keeps on tickin'\n    const timerStart = performance.now();\n\n    import { WebR } from '/webr/webr.mjs'; // service workers == full path starting with /\n\n    globalThis.webR = new WebR({\n        WEBR_URL: \"/webr/\", # our system-wide WebR\n        SW_URL: \"/webr/\"    # what ^^ said\n    }); \n    await globalThis.webR.init(); \n\n    // WebR is ready to use. So, brag about it!\n\n    const timerEnd = performance.now();\n    console.timeEnd('Execution Time');\n\n    document.getElementById('loading').innerText = `WebR Loaded! (${format(\",.2r\")((timerEnd - timerStart) / 1000)} seconds)`;\n\n    const mtcars = await HelpR.getDataFrame(globalThis.webR, \"mtcars\");\n    console.table(mtcars);\n    HelpR.simpleDataFrameTable(\"#tbl\", mtcars);\n\n[`globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis)\nis a special JS object that lets you shove stuff into the global JS\nenvironment. Not 100% needed, but if you want to use the same WebR\ncontext in in other app module blocks, this is how you’d do it.\n\nLet’s focus on the last three lines.\n\n    const mtcars = await HelpR.getDataFrame(globalThis.webR, \"mtcars\");\n\nThis uses a helper function I made to get a data frame object from R in\na way more compatible for most JS and JS libraries than [the default JS\nobject WebR’s `toJs()` function converts all R objects\nto](https://docs.r-wasm.org/webr/v0.1.0/convert-r-to-js.html).\n\n    console.table(mtcars);\n\nThis makes a nice table in the browser’s Developer Tools console. I did\nthis so I could have you open up the console to see it, but I also want\nyou to inspect the contents of the object (just type `mtcars` and hit\nenter/return) to see this nice format.\n\nWe pass in a WebR context we know will work, and then *any* R code that\nwill evaluate and return a data frame. It is all on you (for the moment)\nto ensure the code runs and that it returns a data frame.\n\nThe last line:\n\n    HelpR.simpleDataFrameTable(\"#tbl\", mtcars);\n\ncalls another helper function to make the table.\n\n### HelpR\n\nI may eventually blather eloquently and completely about what’s in\n`modules/webr-helpers.js`. For now, let me focus on just a couple\nthings, especially since it’s got some *sweet* [JSDoc\ncomments](https://jsdoc.app/).\n\nFirst off, let’s talk more about those comments.\n\nI use VS Code for ~60% of my daily ops, and used it for this project. If\nyou open up the project root in VS Code and select/hover over\n`simpleDataFrameTable` in that last line, you’ll get some sweet\nlookin’formatted help. VS Code is wired up for this (other editors/IDEs\nare too), so I encourage you to make liberal use of JSDoc comments in\nyour own functions/modules.\n\nNow, let’s peek behind the curtain of `getDataFrame`:\n\n    export async function getDataFrame(ctx, rEvalCode) {\n        let result = await ctx.evalR(`${rEvalCode}`);\n        let output = await result.toJs();\n        return (Promise.resolve(webRDataFrameToJS(output)));\n    }\n\nThe `export` tells the JS environment that that function is available if\nimported properly. Without the `export` the function is local to the\nmodule.\n\n    let result = await ctx.evalR(`${rEvalCode}`);\n\nA proper app would use JS `try`/`catch` potential errors. There’s an\nexample of that in the fancy React app code [over at WebR’s\nsite](https://docs.r-wasm.org/webr/v0.1.0/examples.html#fully-worked-examples).\nWe just throw caution to the wind and evaluate whatever we’re given. In\ntheory, we should have R ensure it’s a data frame which we kind of can’t\ndo on the JS side since the next line:\n\n    let output = await result.toJs();\n\nwill show the type as a `list` (b/c `data.frame`s are `list`s).\n\nI’ll likely add some more helpers to a more standalone helper module,\nbut I suspect that corporate R will beat me to that, so I will likely\nalso not invest too much time on it, at least externally.\n\n#### Await! Await! Do Tell Me (about `await`)!\n\nBefore we can talk about the last line:\n\n    return (Promise.resolve(webRDataFrameToJS(output)));\n\nlet’s briefly talk about\n[async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)\nops in JS.\n\nThe JavaScript environment in your browser is single-threaded.\n`async`-hronous ops let pass of code to threads to avoid blocking page\noperations. These get executed “whenever”, so all you get is a vapid and\nshallow promise to of code execution and potentially giving you\nsomething back.\n\nWe explicitly use `await` for when we *really* need the code to run and,\nin this case, give us something back. We can keep chaining async\nfunction calls, but — if we need to make sure the code runs and/or we\nget data back — we will eventually need to keep our promise to do so;\nhence, `Promise.resolve`.\n\n## Serving WebR From GitHub Pages\n\nThe `docs/` directory in the repo shows a working version on GH pages.\n\n`main.js` needs a few tweaks:\n\n``` javascript\n// This will use Posit's CDN\n\nimport('https://webr.r-wasm.org/latest/webr.mjs').then( // this wraps the main app code\n    async ({ WebR }) =\u003e {\n        \n        globalThis.webR = new WebR({\n            WEBR_URL: \"https://webr.r-wasm.org/latest/\",\n            SW_URL: \"/webr-app/\"            // 👈🏼 needs to be your GHP main path\n        });\n        await globalThis.webR.init();\n\n        const timerEnd = performance.now();\n        console.timeEnd('Execution Time');\n\n        document.getElementById('loading').innerText = `WebR Loaded! (${format(\",.2r\")((timerEnd - timerStart) / 1000)} seconds)`;\n\n        const mtcars = await HelpR.getDataFrame(globalThis.webR, \"mtcars\");\n        console.table(mtcars);\n        HelpR.simpleDataFrameTable(\"#tbl\", mtcars);\n        \n  }\n);\n```\n\n## Moar To Come\n\nPlease hit up [this terribly coded dashboard\napp](https://rud.is/webr-dash/no-dplyr.html) to see some fancier use.\nI’ll be converting that to modules and expanding git a bit.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhrbrmstr%2Fwebr-app","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhrbrmstr%2Fwebr-app","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhrbrmstr%2Fwebr-app/lists"}