Back to blog

Week in review: 15 June 2026

A recap of what shipped during the week of 15 June. Three items this week: an old PHP 7 codebase (NanoCMS) ported to PHP 8 and running under Apache through Pext, a streamlined WASM pipeline for the PHP C dependencies (starting with PCRE and LibXML), and a set of ten end-to-end REST scenarios for opensourcepos that get cross-checked against both PHP and Pext runtimes.

NanoCMS from PHP 7 to PHP 8, under Apache

NanoCMS is a small flat-file CMS written for PHP 5.x and later run against PHP 7. It is the kind of codebase every legacy shop has one of: no Composer, no framework, a handful of top-level scripts, direct require chains, and enough drift from modern PHP that upgrading in place is more work than it looks. It made a good test case for two things at once: how Pext handles a codebase written before namespaces were a habit, and how the FPM-style SAPI holds up under a real Apache setup.

The port itself was mostly the easy kind of work. A pass to raise the source to a PHP 8 baseline (typed parameters where it was safe, replacing each and the deprecated create_function, tightening a few string operations that had been sloppy about null), then the transpile, then a bring-up on the Pext runtime. The files session handler that pext-sessions ships by default was the missing piece for the admin login flow to survive a redirect; once that landed, the same NanoCMS source now serves through three different backends side by side: mod_php in Apache, mod_proxy_fcgi in Apache pointing at pext-fpm with a cluster of workers, and the embedded pext -S server.

WASM as the strategy for C dependencies

A recurring headache in the runtime modules is the PHP standard library's reliance on a handful of C libraries: PCRE for regex, LibXML for DOM and XML validation, ICU for locale-aware transliteration, and so on. Every one of these has a Node binding somewhere, and every one of those bindings ties us to a specific NODE_MODULE_VERSION and a specific platform triple. The bindings work, but they lock the runtime to whichever version of Node we happen to be on, and they exclude Bun and other JavaScriptCore-based runtimes entirely.

The alternative that landed this week is a reproducible WASM build pipeline for these libraries. Each vendored C library gets its own pext-wasm-<name> npm package with a hand-written TypeScript loader and a checked-in dist/<name>.wasm artifact produced under Emscripten inside a Docker image. The runtime module that used to depend on a native binding now depends on the WASM package and calls into it through the loader. First two on the pipeline are PCRE (previously blocked on node-oniguruma across Node versions) and LibXML (previously libxmljs2, which does not load on Bun at all). PCRE work is documented in the portable-ascii post's treatment of transliteration; the same reasoning applies here.

The trade-off is a larger install footprint (a few hundred kilobytes of WASM per library) in exchange for a runtime that behaves the same on Node, on Bun, and on any future JavaScript runtime that speaks WASM. For a transpiler whose whole selling point is that PHP semantics stay stable across the target ecosystem, that trade-off pays for itself.

Ten end-to-end scenarios for opensourcepos

Opensourcepos is the CodeIgniter 4 application the pext test matrix has been leaning on for real-world CI4 coverage. Unit tests are one thing; a set of user-facing REST scenarios that walk the app the way a cashier would is another. This week produced ten of those: login, product creation, price lookup, cart add and remove, tax calculation, receiving, sale completion, cash-drawer close, customer create and update, and reporting. Each scenario is a Postman collection driven by newman.

The interesting part is that each scenario runs against both php -S and pext -S, with the responses diffed at the JSON level. Both runtimes are now functionally on par for every scenario in the set: same responses, same HTTP status codes, same headers up to the SAPI-specific ones. That is the level of parity the wider ecosystem work has been building toward, and it is now the CI gate that catches any runtime module or transpiler regression that would silently change behavior on one runtime but not the other.