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

php-cgi-wasm

v0.2.0

Published

php-cgi for wasm.

Readme

seanmorris/php-cgi-wasm php-cgi-wasm

find php-cgi-wasm on npm

The CGI Counterpart to php-wasm.

This package encompasses the CGI-specific build artifacts of php-wasm. The goal of this part of the project is to provide a version of PHP that more closely resembles the environment facilitated by PHP when running under web servers like Apache and nginx. This is achieved by building a binary artifact of PHP-CGI, rather than PHP-CLI to Web Assembly. The resulting WASM binary can then be run under Node.js with Express or inside a service worker.

Example Service Worker

import { PhpCgiWorker } from "php-cgi-wasm/PhpCgiWorker";

// Spawn the PHP-CGI binary
const php = new PhpCgiWorker({
	prefix: '/php-wasm'
	, docroot: '/persist/www'
	, types: {
		jpeg: 'image/jpeg'
		, jpg: 'image/jpeg'
		, gif: 'image/gif'
		, png: 'image/png'
		, svg: 'image/svg+xml'
	}
});

// Set up the event handlers
self.addEventListener('install',  event => php.handleInstallEvent(event));
self.addEventListener('activate', event => php.handleActivateEvent(event));
self.addEventListener('fetch',    event => php.handleFetchEvent(event));
self.addEventListener('message',  event => php.handleMessageEvent(event));

On current Node releases, the core PhpCgiNode entrypoint can be consumed from either ESM or CommonJS:

const { PhpCgiNode } = require('php-cgi-wasm/PhpCgiNode');

const php = new PhpCgiNode({
	prefix: '/php-wasm/cgi-bin/',
	docroot: '/persist/www',
});

Runtime-loadable extension helper JS packages remain ESM-only; pass extension assets manually when you need to manage them directly.

First messages to a service worker

Call the worker's filesystem methods from the page with quickbus 1.0.2 or newer. php.handleMessageEvent speaks its request/reply protocol:

import { Client } from 'quickbus';

await navigator.serviceWorker.register('/cgi-worker.mjs', {type: 'module'});
const registration = await navigator.serviceWorker.ready;

const bus = navigator.serviceWorker.controller
	? Client.forServiceWorker(navigator.serviceWorker)
	: Client.forServiceWorkerRegistration(registration);

const entries = await bus.readdir('/persist', {withFileTypes: true});

On first load the page is not yet controlled, so use the registration client after navigator.serviceWorker.ready. Errors raised in the worker, such as denied persistent storage, reject the call; private-mode storage support depends on the browser. The original php-cgi-wasm/msg-bus.mjs helper still ships for existing code.

Directory listings and persistence

await php.readdir(path) still returns string[]. For directory names and types in one operation, use await php.readdir(path, {withFileTypes: true}). Each entry is a serializable {name: string, isFolder: boolean} object. Both forms preserve filesystem order and include . and ... Types follow symbolic links, as analyzePath does; listing and metadata errors reject the operation.

With automatic browser transactions enabled, queued filesystem calls share a batch, waiting up to 25 ms after the queue becomes idle for more work. Storage is refreshed once. A batch containing only analyzePath, readdir, readFile, or stat does not flush; any mutation makes the batch writable. Every call waits for the shared commit, and a commit failure rejects the whole batch. Promise.all calls can share this work; sequentially awaited calls cannot. Batches are bounded to 64 operations or a 250 ms processing window, checked between operations. With autoTransaction: false, the caller owns transaction boundaries and operations are not delayed for automatic batching.

Browser CGI requests, filesystem RPCs, and runtime refreshes share one lock. PHP keeps that lock while suspended on asynchronous work and until its writes are persisted. HTTP CGI requests still flush after each successful PHP request; the 25 ms batching window applies to filesystem RPCs. Await writes before opening a URL that uses them. Persistence failures return a non-cacheable HTTP 500; onRequest receives the final response after the commit attempt, and later queued work can still run. Without Web Locks, serialization is limited to the current JavaScript realm.

Browser CGI journals PHP and filesystem API mutations, committing only changed IDBFS records. This includes metadata, renamed trees and deletions. Clean mounts need no write transaction. Existing IDBFS storage remains compatible; hydration still reconciles remote changes, with a cheaper local-node traversal. Nested mounts use ordinary reconciliation. Failed commits retain pending changes and retry them before the next hydration. Symlink hydration restores the link's metadata without following or changing its target.

Persisted cookies

The internal /config/.cookies snapshot stores absolute expiry deadlines, so restarting or refreshing a worker does not renew Max-Age cookies. Cookie deletion and replacement are persisted with the next successful request.

Legacy snapshots retain session cookies and unexpired Expires cookies. Legacy Max-Age cookies are discarded because their original expiry cannot be recovered; users with those cookies may need to sign in again. Fresh cookie headers supplied through the constructor still start their lifetime when received. Treat the snapshot format as internal rather than editing its JSON directly.

Phar applications

For a dynamic build, enable Phar and, for gzip support, zlib:

import { PhpCgiWorker } from 'php-cgi-wasm/PhpCgiWorker.mjs';
import phar from 'php-wasm-phar';
import zlib from 'php-wasm-zlib';

const php = new PhpCgiWorker({
	prefix: '/php-wasm/'
	, docroot: '/persist/www'
	, sharedLibs: [zlib, phar]
	, entrypoint: 'app.phar'
});

Place the archive at /persist/www/app.phar and use a Phar::webPhar() stub in it. Requests to /php-wasm/app.phar/index.php execute the archive with SCRIPT_NAME=/php-wasm/app.phar and PATH_INFO=/index.php. PHP handles the archive's scripts, assets and missing members. Direct archive requests redirect within the URL prefix rather than including the filesystem docroot.

The optional entrypoint also routes fallback requests such as /php-wasm/hello.txt into the archive. Existing physical files and directory index.php files take precedence. A virtual host's entrypoint overrides the runtime default. Object rewrites retain their explicit scriptName.

Archives remain read-only by default. Static builds with Phar and zlib compiled in do not need the sharedLibs entries above.