Use npm and webpack to build a new shiny widget (cytoscpe.js)

5:36 PM (less than a minute ago)

We have a bioconductor package, RCyjs, whose functionality we wish to port to a shiny widget. RCyjs uses httpuv websockets to communicate between an R session and javascript in the browser - rather like shiny, but it lacks easy integration into a shiny app.

Is there an established protocol, maybe an example shiny widget, which uses npm and webpack to assemble a multitude of javascript libraries? This approach to webapp assembly has many merits (up-to-date versioning, dependency resolution, unused code elimination). I use this with RCyjs. It would be great to use it with this the shiny version as well. Any suggestions?

Thanks!

  • Paul

Hi Paul,

Small world! I'm currently building a stand alone webpage that uses cytoscape.js to display shiny reactivity dependencies. Cytoscape.js is a very smooth network library!

Repo: https://github.com/rstudio/shinyreactlog

shinyreactlog is not a shiny app, but it does provide as an example on how to assemble the javascript. I've tried to make the package have file structures that work both with R packages and npm-like packages (I use yarn). I do not enjoy keeping one set of code in a non standard location, but rather both sets of code in the root repo directory. The yarn package.json file keeps the versions and dependencies listed and gives me a single location to update dependency versions.

shinyreactlog contains:


Building:

While maybe overkill, I use grunt to call webpack to bundle the app. The grunt workflow makes more sense to me over a raw webpack config and is nice to run multiple / different tasks if I have more tasks to be run. For now, it is a single build task and a single watch task. Inside my build task, I call webpack on my ./src/index.js file that imports the local flowtype es6 javascript files. Webpack can automatically read from the ./node_modules directory where all of the external dependencies are stored and automatically performs light weight dead code tree shaking to provide the smallest bundle file. By adding in the commented BundleAnalyzerPlugin plugin in the webpack config, I can inspect where the bulk of my javascript file is coming from. Babel (called from webpack) is happen when the source files are located in the src folder. I store the output bundle in the ./inst folder to be able to be found as an external file in the R package bundle (system.file("reactlogAsset/reactlog.js", package = "shinyreactlog") ). Any javascript configuration file is placed in the top level folder and ignored in .Rbuildignore.

./package.json:

I have many scripts that I have found to be useful inside my ./package.json file. This allows me to type yarn watch or yarn lint-all without having to populate my $PATH for a local project. For more complicated scripts, I've made executable files in the ./bin folder. I have the package.json field "private": true, as there is no intention of releasing it as a yarn/npm package.

Given that all dependencies are in the package.json (paired with a yarn.lock file), anyone who downloads the repo and calls yarn install in the base folder will be able to yarn build the javascirpt bundle or yarn watch for changes during development.

Always commit linted code:

By installing the dev-dependencies husky and lint-staged, scripts execute (that must succeed) for matching files being committed before any git commit is allowed. In shinyreactlog's pre-commit script, it makes sure all javascript files pass the lint check, pass a prettier code formatting check, and do not break flowtype. If all three pass, the matching files are added to git (may have been altered in the linting process) and the commit is accepted.


Non-standard dependencies:

Sometimes extra libraries do not have a local package.json file. To keep these non-standard dependencies, the napa package can be used to install git repos using a commitish tag. leaflet.extras does this with three packages. I used full commit tags rather than the branch name only to enforce explicit version control. You can install an unpublished, standard package (such as a fork with extra changes) using a github commitish tag instead of a version in the dependency. leaflet.extras does this with seven packages.

To require these dependencies, you can import the javascript file directly. For example to import jquery like normal import $ from "jquery", but to explicitly import the jquery.slim.js file in the ./dist folder of jquery, import $ from "jquery/dist/jquery.slim".


htmlwidget:

Down the road, I imagine that shinyreactlog could be an htmlwidget. If so, the bundled code could be placed in the (standard practice) folder of inst/htmlwidgets along with a shinyreactlog.yaml file describing the single javascript bundle dependency. This would be similar to how leaflet produces a htmlwidget, except the folder would only contain the yaml, a single bundled javascript file, and a single css file (with no extra libraries).

shiny application:

Once the javascript is bundled, it should be placed in the www folder of your shiny application

├── server.R
├── ui.R
└── www
    └── myJsBundle.js

Inside your ui.R file, include the bundle file with: tags$head(tags$script(src="myJsBundle.js")) . For more communication between shiny and javascript, I defer to Joe's article: https://shiny.rstudio.com/articles/communicating-with-js.html


I have briefly touched on MANY difficult topics. If you would like more information on any of them, I'd be happy to expand on any of them! :slight_smile:

Please let me know if you have any more questions!

Best,
Barret

5 Likes

(Reply from Paul on different forum. - Barret)

Hi Barret,

Thanks for your reply, and all the information you provided. I may use a -slightly- different mix of tools in my first draft, but I am glad to see that there is lots of overlap between my strategy and the more fully developed strategy you use. webpack, npm + package.json, adding lint and flowtype - that is where I will start. Maybe I can your comments and suggestions as I go.

Thanks again!

- Paul

2 Likes