Working on vlcn-io/wa-sqlite inside our (librocco) monorepo turned out to be a little adventure in dependency management. Our goal was simple: make it possible to build, load, and test the entire toolchain with minimal changes to our main app, while keeping the development cycle fast.
Why Submodules?
To keep iteration tight, we decided to pull vlcn-io/wa-sqlite into librocco as a submodule. That way, any changes could immediately be tested in our app without publishing intermediate builds.
The toolchain we needed looked like this:
- vlcn-io/crsqlite-wasm — part of the vlcn-io/js monorepo. It exposes the wa-sqlite binary and provides a wrapped JS API.
- vlcn-io/wa-sqlite — the actual
wa-sqlitepackage. - Since
crsqlite-wasmdepends onwa-sqlite, we didn’t need to explicitly addwa-sqliteto our monorepo — it comes along for free.
Pulling in vlcn-io/js
Here’s where things got tricky:
vlcn-io/crsqlite-wasmisn’t available as a standalone package. Instead, it lives inside the [vlcn-io/js] monorepo.- Our monorepo (librocco) uses Rush, while
vlcn-io/jsuses pnpm workspaces. Similar, but not the same. vlcn-io/jsalso references other packages (sometimes via relative paths outside the repo root). These dependencies, luckily, are maintained in othervlcn-io/* repos.
In the end, our relevant tree looked like this:
librocco/
├── 3rd-party/
│ └── js/ (submodule: vlcn-io/js)
│ └── deps/
│ └── wa-sqlite/ (submodule: vlcn-io/wa-sqlite)
│ └── ... (other deps: emsdk, crsql rust code)
That meant juggling three git layers:
codemyriad/wa-sqlite(our fork) — where active dev happenscodemyriad/vlcn-js(our fork) — tracks submodule hash updateslibrocco— tracks thecodemyriad/vlcn-jssubmodule
Aside from a few minor tweaks in vlcn-io/js, the integration was minimal.
Making It Work in Development
Since we didn’t want to fully integrate vlcn-io/js packages into our Rush setup, we relied on import aliasing:
- In dev, we aliased
@vlcn.io/*imports in Vite + TypeScript to their submodule sources. - This worked well locally, but failed in CI for reasong I can’t remember anymore (anyway, this was fixed afterwards), but we needed a quick solution for the time being, so instead, we built a different workflow for CI and non-submodule dev.
Building Tarballs for CI
To avoid forcing every dev (or CI job) to rebuild from scratch, we opted for building tarballs of the relevant packages (and committing to git lfs):
-
Run
pnpm packinside the relevantvlcn-io/jspackages to produce.tgzfiles. -
Collect those into
3rd-party/artefacts. -
Use Rush’s
pnpm-config.jsonglobal overrides to force installs to come from our local tarballs:"globalOverrides": { "@vlcn.io/crsqlite-wasm": "file:../../3rd-party/artefacts/vlcn.io-crsqlite-wasm-0.16.0.tgz" }
This solved a subtle problem with workspace → registry fallbacks.
For example, if @vlcn.io/ws-client depends on @vlcn.io/crsqlite-wasm via workspace:*, pnpm pack rewrites that dependency to a fixed version (e.g., "0.16.0"). Without overrides, installs would happily fetch that version from the public registry — not our local build. The override ensures every resolution points to our .tgz tarball.
Two Modes: Submodules vs. Tarballs
We introduced an environment variable to make switching easy:
USE_SUBMODULES=true→ use Vite aliases to load directly from submodules (fast iteration).USE_SUBMODULES=false→ usenode_moduleswith tarballs (slower, but simpler for people not working onwa-sqlite).
This way, core devs can iterate quickly, while everyone else gets a stable experience without rebuilding from scratch.
CI Flow
Finally, we got CI working fully with submodules:
- Checkout repo with submodules.
- Build from source (
makewhere needed +pnpm pack). - Upload built tarballs as artifacts for downstream jobs.
This ensures consistency across environments while keeping while keeping dev fast and CI reproducible.