Back to blog

Mitigating runtime lock-in

When teams evaluate a PHP-to-JavaScript migration tool, one question comes up every time: are we just trading PHP lock-in for Pext lock-in?

It is a fair question. You are considering a commercial transpiler. The compiler is proprietary. Your production code will call into a runtime you did not write. If the vendor disappears or raises prices, what happens to the application you shipped?

We built Pext with this objection in mind. The short version: we get you running quickly, then help you de-risk over time. This post walks through the nine concrete mechanisms behind that claim.

The compiler is proprietary. The runtime is not a black box. (licensing)

The Pext transpiler is closed source. That is how we fund the work. We are not going to pretend otherwise.

But the runtime, the thing your production application actually depends on, is fully documented and available to customers. Enterprise license holders get access to the runtime source and can maintain their own fork if needed. Think of it as escrow-available: the code that runs in your production environment is never hidden from you. You can read it, audit it, patch it, and if necessary, maintain it independently.

Generated code you can actually read

Some transpilers produce output that is effectively obfuscated: minified identifiers, internal slot references, call signatures that mean nothing to a human. Pext does not do this. The generated JavaScript preserves your class names, method names, and variable names. PHP standard library calls remain recognizable.

Here is a real example from Sebastian Bergmann's diff library. The original PHP is on the left, Pext's JavaScript output on the right. Hover any line to see how it maps across.

Line.php
final class Line {
public const int ADDED = 1;
public const int REMOVED = 2;
public const int UNCHANGED = 3;
private int $type;
private string $content;
 
public function __construct(
int $type = self::UNCHANGED,
string $content = ''
) {
$this->type = $type;
$this->content = $content;
}
 
public function isAdded(): bool {
return $this->type === self::ADDED;
}
}
pextc
Line.js
class Line {
static ADDED = 1;
static REMOVED = 2;
static UNCHANGED = 3;
$type;
$content;
 
__construct(
type = self(this, Line).UNCHANGED,
content = ``
) {
return ctx.method(this, ($) => {
$.this.$type = $.type = asInt(type);
$.this.$content = $.content = asString(content);
});
}
 
isAdded() {
return ctx.method(this, ($) => {
return same($.this.$type, self(this, Line).ADDED);
});
}
}

A developer can set a breakpoint, read a stack trace, and reason about this code without Pext in the room. PHP function calls like array_merge, str_replace, and implode appear by name in the output. The generated code is not a binary blob. It is JavaScript you own, can inspect, and can modify.

Two-phase adoption: ship first, optimize later

A Pext migration has two natural phases.

Phase 1: transpile everything, ship on Node. The priority is behavioral compatibility. Every PHP construct, every standard library call, every quirk of type juggling gets handled by the runtime so your application works correctly on day one. This is where Pext earns its keep: you get a working Node.js application without a manual rewrite.

Phase 2: progressively reduce runtime dependency. Once you are running on Node, you can start replacing runtime-backed calls with idiomatic JavaScript. Simple arithmetic, string concatenation, basic array operations. These can be rewritten to vanilla JS at your own pace. The transpiler's roadmap is actively moving in this direction: we are optimizing the compiler to detect common patterns and emit plain JavaScript instead of runtime calls wherever the semantics are identical.

Over time, the runtime footprint shrinks. The end state is not "forever dependent on Pext". It is "as dependent as you choose to be."

The runtime is modular, not monolithic (features)

The Pext runtime is not one large package. It is split into 27+ independent npm modules, each handling a specific area of PHP's standard library:

  • pext-core: scoping, value semantics, reference counting, copy-on-write, arrays
  • pext-strings: string functions
  • pext-arrays: array manipulation
  • pext-math: mathematical functions
  • pext-pcre: regular expressions with PHP-specific modifiers
  • pext-dom: DOM and XML handling
  • pext-reflection: runtime introspection
  • pext-json, pext-date-time, pext-file-handling, pext-iterators, and more

Each module is its own npm package with its own test suite. If your application never touches DOM, you never ship pext-dom. You can audit, replace, or vendor individual modules without touching the rest. The runtime is not a monolithic black box. It is a collection of focused, replaceable parts.

Platform independent: just Node

There is no SaaS component. No cloud dependency. No license server in the hot path. No phone-home. The runtime is a set of npm packages. Your application runs anywhere Node runs: AWS, GCP, Azure, on-prem, air-gapped environments, your own hardware. You choose the platform, the hosting provider, and the deployment strategy. Pext has zero opinions about where your application runs and zero presence in your production infrastructure.

One caveat: eval. If your PHP codebase uses eval to execute dynamically generated PHP at runtime, that code needs to be transpiled on the fly. This is the one case where the running application calls the Pext API. Our REST endpoint performs the conversion and returns JavaScript. If your application relies heavily on eval, you will need an integration with our online conversion service. For most codebases, we recommend rewriting eval usage to eliminate this dependency entirely, which is also a best practice in PHP itself.

Versioning, LTS, and migration guides

The compatibility contract is straightforward: output from compiler version X.Y runs on any runtime in the X.* major line. We do not break this contract within a major version.

Major versions get long-term support branches. When we do ship a major bump, it comes with a published migration guide that documents every breaking change and the steps to address it. You upgrade on your schedule, not ours. There are no surprise deprecations and no forced upgrades.

Pinning and vendoring for enterprise

For teams that need full control over their dependency chain: pin an exact runtime version in your package.json and vendor it into your own artifact storage. Artifactory, Nexus, a private npm registry, a tarball in your repo. Whatever your organization uses.

Air-gapped builds work. Offline installs work. Combined with runtime source access, a customer with sufficient appetite can freeze a specific Pext version and never contact our servers again. The generated code keeps running. The runtime keeps working. Nothing phones home.

Contractual protections

Architecture tells you what is possible. Contracts tell you what is guaranteed. Pext offers multi-year price protection, support SLAs, LTS maintenance branches, and dedicated enterprise branches for customers that need them. Source escrow provides a legal fallback beyond the technical one. The business protections back up the architectural ones.

Not all runtime calls are equally scary

When people hear "runtime dependency," they imagine a single switch that is either on or off. In practice, different constructs carry different levels of migration risk. We provide a risk assessment so customers can make informed decisions about what to keep on the runtime and what to migrate to vanilla JS.

Low risk: already vanilla or trivially replaceable

  • Arithmetic and simple string concatenation. The compiler already emits plain JavaScript for these.
  • Basic control flow, loops, conditionals. These are native JS constructs in the output.

Low-to-medium risk: straightforward manual migration

  • array_map, array_filter, array_merge. Direct equivalents exist in JavaScript.
  • Simple string functions like str_replace, substr, trim. One-to-one mappings.
  • JSON encoding and decoding. Native JSON.parse and JSON.stringify.

Medium risk: replaceable with some effort

  • DOM and libxml. Swap for a native JS library like jsdom when convenient.
  • PCRE with PHP-specific modifiers. Most cases port cleanly to native RegExp.
  • Date and time functions. Replaceable with libraries like luxon or date-fns.

High risk: keep on the runtime

  • Reference semantics (&$var) and copy-on-write. These have no JavaScript equivalent; manual migration is dangerous and error-prone.
  • Reflection and magic methods (__get, __set, __call). The runtime provides the correct behavioral semantics.
  • Dynamic code generation and eval. The runtime is the right home indefinitely. Note that eval is the one construct that requires a call to the Pext conversion API at runtime, so we recommend rewriting it where possible.

The point is not that you must migrate everything or nothing. You choose which dependencies to shed and which to keep, based on your application's actual usage and your team's priorities.

What Pext is and what it is not

Pext is not just a way to get to Node.js. It is a fast path from PHP to the JavaScript ecosystem that prioritizes behavioral compatibility on day one while giving you the tools to progressively optimize and modernize.

The model, stated plainly:

  • Transpiler: proprietary (this is how we fund the work)
  • Runtime: escrow-available, modular, fully documented
  • Generated code: owned by the customer, readable, debuggable
  • Production: zero proprietary cloud dependencies
  • Deployment: self-host anywhere, or bring your own vendor
  • Roadmap: actively reducing runtime surface area over time

No subscriptions to run your own code. No breaking changes without LTS support. No unreadable output. No undocumented edge cases. No vendor lock-in.

Pext prioritizes compatibility first, but the system is designed so your generated application remains runnable, inspectable, and maintainable, even if you stop using Pext commercially.

Want to see how this works on your codebase? Book a demo and we will walk through it live.