Skip to content

Rethink local installs: serve the npm client from .venv / renv instead of the registry #261

Description

@schloerke

Deferred question from #217, which used a repo-relative file:../../pkg-js as a
placeholder until the first npm publish. The placeholder works better than
expected, and that is worth taking seriously before we commit to the registry as
the only distribution channel.

Proposal

Ship the npm-installable client (dist-npm/) inside the pip wheel and the R
inst/ directory
, and let an app install it from its own project environment:

// Python
{ "dependencies": { "@posit/shinyreact": "file:.venv/lib/python3.14/site-packages/shinyreact/npm" } }
// R (renv)
{ "dependencies": { "@posit/shinyreact": "file:renv/library/4.5/aarch64-apple-darwin20/shinyreact/npm" } }

pip install shinyreact / install.packages("shinyreact") would then control
both halves — the server and the client the browser runs — from one
dependency declaration.

Why this isn't just Option C again

decisions/2026-08-17-js-distribution.md investigated and rejected Option C
("npm installs from the server package's assets"). Its disqualifier was DX:

file: deps bake machine-specific absolute paths into package.json/lockfiles,
breaking every collaborator, CI runner, and venv/renv rebuild

That is true of the shape it considered — npm install "$(python -m shinyreact npm-path)", which yields an absolute path. The reframing is that modern
project environments are in-project
: uv puts the venv at .venv/, renv puts
the library at renv/library/. Both are relative to the app directory, so
package.json and the lockfile stay portable.

#217 verified this property empirically for the repo-relative case:

node_modules/@posit/shinyreact -> ../../../../pkg-js     # relative symlink
package-lock.json:  "resolved": "../../pkg-js", "link": true

Nothing machine-specific in either file.

What it buys

What has to be answered

This is the actual work of the issue; none of these are obviously fatal, but
none are free either.

  1. Path stability. .venv/lib/python3.14/site-packages/ embeds the Python
    version; renv's library path embeds R version and platform. A lockfile
    pinning either breaks on an upgrade. Options: a generated stable symlink, a
    setup command that rewrites the path, or a small resolver plugin.
  2. Environments that aren't in-project. conda, system Python, Docker layer
    caching, UV_PROJECT_ENVIRONMENT, R_LIBS_USER — all put the library
    somewhere no relative path reaches. Supported-when-in-project, documented
    otherwise? Or fall back to the registry?
  3. Tooling opacity. A file:/link: dep is invisible to Renovate, npm audit, and npm's cache. Arguably fine — the thing under version management
    is then the pip/CRAN package — but it should be a decision, not a surprise.
  4. Ordering. npm install has to run after uv sync / renv::restore().
    A documented step, or a generated script?
  5. Package size. Shipping dist-npm/ (ESM + .d.ts + sourcemap) on top of
    today's www/shinyreact.js (205 KB) + CSS, in both the wheel and inst/.
  6. Does it replace npm publication, or sit beside it? A third-party
    component library that wants to peer-depend on @posit/shinyreact (Ship shiny-react bindings as npm packages for popular UI frameworks #32) has
    no answer except the registry. Most likely outcome: the registry stays the
    public story and this becomes the locked-version alternative — which means
    maintaining both, and that cost belongs in the decision.

Shinylive is a non-issue

The obvious objection — "an exported app has no node, no npm, no venv layout" —
does not apply. The npm client is a build-time dependency: npm install and
vite build run on the developer's machine and produce www/ui.js.
shinylive export runs afterwards and ships that already-compiled bundle. The
toolchain and the exported runtime never meet.

If anything the npm tier is the better shinylive story, since with
include_runtime=False (#217) the entire client runtime lives inside the app's
own ui.js rather than being resolved from a server-side HTMLDependency
worth confirming as part of this work rather than assuming.

Refs

decisions/2026-08-17-js-distribution.md (Options C and E), #217, #218, #172,
#32.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions