npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lightning-pyodide

v0.1.0

Published

Mount an @isomorphic-git/lightning-fs filesystem into Pyodide's Emscripten filesystem

Readme

lightning-pyodide

npm version CI Coverage Status

Mount an @isomorphic-git/lightning-fs filesystem into Pyodide, so Python can read and write files that live in IndexedDB — and that isomorphic-git can operate on.

pyodide.runPython(`open('/data/notes.txt', 'w').write('hello')`);
await mount.push(); // now in IndexedDB, and visible to isomorphic-git

How it works (read this first)

Emscripten's filesystem API is synchronous. LightningFS is promise-based. Python's open() cannot await an IndexedDB read, so a direct pass-through adapter is impossible.

Instead, a mount holds an in-memory working copy and reconciles it with LightningFS on demand. This is exactly the approach Pyodide itself takes for mountNativeFS, whose NATIVEFS_ASYNC bridges to the equally-async File System Access API.

What that means in practice:

  • Reads and writes inside Python are fast and synchronous.
  • Changes are not persisted until you push() (or enable autoSync).
  • Changes made to LightningFS from outside are not visible until you pull().
  • The mounted subtree is held in memory, so mount a directory, not a huge archive.

The alternative — a truly synchronous pass-through via Atomics.wait and a SharedArrayBuffer — requires cross-origin isolation (COOP/COEP headers) and forces Pyodide into a worker, since it must block the thread it runs on. That is deliberately out of scope here.

Install

npm install lightning-pyodide

pyodide and @isomorphic-git/lightning-fs are not dependencies — the adapter takes whatever you already have and only requires a compatible shape.

Usage

ESM / TypeScript

import { loadPyodide } from 'pyodide';
import LightningFS from '@isomorphic-git/lightning-fs';
import { mountLightningFS } from 'lightning-pyodide';

const pyodide = await loadPyodide();
const fs = new LightningFS('my-app');

const mount = await mountLightningFS(pyodide, { fs, path: '/data' });

// Files already in LightningFS are visible immediately.
pyodide.runPython(`
import os
print(os.listdir('/data'))

with open('/data/report.txt', 'w') as f:
    f.write('written by Python')
`);

// Persist back to IndexedDB.
await mount.push();

Browser <script> tag

<script src="https://cdn.jsdelivr.net/npm/lightning-pyodide"></script>
<script>
  const mount = await LightningPyodide.mountLightningFS(pyodide, {
      fs,
      path: '/data'
  });
</script>

Sharing a repository with isomorphic-git

The reason LightningFS exists is isomorphic-git, and both sides can use one filesystem:

import git from 'isomorphic-git';

const fs = new LightningFS('repos');
await git.clone({ fs, http, dir: '/project', url: '...' });

const mount = await mountLightningFS(pyodide, {
  fs,
  path: '/project',
  root: '/project',
});

pyodide.runPython(`open('/project/generated.py', 'w').write('X = 1')`);
await mount.push();

await git.add({ fs, dir: '/project', filepath: 'generated.py' });
await git.commit({ fs, dir: '/project', message: 'Generated from Python' });

// Pick up whatever git just wrote (index, refs, objects).
await mount.pull();

Automatic syncing

autoSync pushes on a timer, so you do not have to call push() by hand:

const mount = await mountLightningFS(pyodide, {
  fs,
  path: '/data',
  autoSync: 2000, // milliseconds; `true` means 1000
  onError: err => console.warn('sync failed', err),
});

A tick with nothing to do costs one in-memory tree walk and touches neither IndexedDB nor the mount. When autoSync is on, unmount() performs a final push automatically.

API

mountLightningFS(pyodide, options)

Returns a Promise<LightningMount>.

| Option | Type | Default | Description | | ---------- | ------------------- | --------------- | --------------------------------------------------------------------------- | | fs | LightningFS | (required) | The filesystem to mount. Any object with a compatible promises API works. | | path | string | '/lightning' | Emscripten mountpoint. Created if missing; must be empty if it exists. | | root | string | '/' | Directory inside LightningFS to expose. Created if missing. | | populate | boolean | true | Copy LightningFS into the mount before returning. | | autoSync | boolean \| number | false | Push on a timer. true uses 1000 ms; a number sets the interval. | | onError | (error) => void | console.error | Called when an autoSync tick fails — those have no caller to reject. |

The first argument only needs an Emscripten FS, so { FS: Module.FS } works for a non-Pyodide Emscripten build too.

LightningMount

| Member | Description | | ------------------- | --------------------------------------------------------------- | | path | The Emscripten mountpoint. | | root | The LightningFS directory exposed at path. | | push() | Copy Emscripten → LightningFS. | | pull() | Copy LightningFS → Emscripten. | | syncfs(populate?) | Emscripten-style alias: true pulls, false (default) pushes. | | flush() | Resolve once any in-flight or queued sync has settled. | | unmount(options?) | Detach the mount. Later sync calls reject. |

Every sync is serialised per mount, so concurrent push()/pull() calls queue rather than interleave.

unmount({ push }) overrides whether a final push happens; by default it does when autoSync is enabled and does not otherwise.

pyodide.FS.syncfs

The adapter registers itself as FS.filesystems.LIGHTNINGFS, so Emscripten's own API drives it too — useful when other mounts need syncing at the same time:

await new Promise((resolve, reject) =>
  pyodide.FS.syncfs(false, err => (err ? reject(err) : resolve())),
);

registerFilesystem(FS) is exported if you would rather call FS.mount yourself.

What is synced

Regular files, directories and symlinks, along with their permission bits. Character devices, sockets and FIFOs have no LightningFS equivalent and stay local to the mount.

A file is considered stale when its modification time is newer or its size differs. Both clocks have millisecond resolution, so size is what catches two writes landing in the same millisecond — the same quick check rsync uses. Two same-size edits within one millisecond are indistinguishable and would be missed.

Directory timestamps are ignored: Emscripten bumps a directory's mtime whenever a child is created, so they drift apart on their own and say nothing about staleness.

After a push, the local timestamp is advanced to whatever LightningFS recorded (it stamps its own and has no utimes). That keeps both sides comparable, so a repeated push() is a true no-op instead of recopying everything.

Symlink targets

LightningFS normalises every path it stores, rewriting a relative target such as target.txt to ./target.txt. A symlink round-tripped through LightningFS therefore comes back in that form. The adapter treats the two spellings as equal, so an unchanged link is not rewritten on every sync.

Caveats

  • Memory. The mounted subtree is held in memory for as long as it is mounted.
  • No read-through. A file added to LightningFS after mounting is invisible until pull().
  • Last writer wins. Reconciliation is one-directional per call; it compares timestamps and sizes and does not merge. Nothing stops a pull() from overwriting unpushed local edits.
  • One process at a time. LightningFS coordinates tabs with a mutex, but a long-lived mount holding an in-memory copy is not a substitute for that; do not mount the same subtree from two tabs and expect them to converge.

Development

npm install
npm test          # boots a real Pyodide runtime
npm run coverage
npm run lint
npm run build

Tests run in Node against the real Pyodide package, with fake-indexeddb standing in for the browser's IndexedDB.

License

Copyright (c) 2026 Jakub T. Jankiewicz

Released under the MIT License. See LICENSE for details.