Porting npm Packages
Status: experimental. This guide — and the
port-npm-to-perryskill that ships alongside it — is a first pass at systematizing what Perry contributors have been doing ad-hoc. Results will vary by package. Feedback at issue #115.
Perry compiles a practical subset of TypeScript. Most pure TS/JS packages can be pulled into a native compile via perry.compilePackages, but some will need small patches to avoid the constructs Perry doesn’t support. This page is a field guide for doing that port — by hand, or by driving a coding agent with the prompt template below.
When porting makes sense
| Situation | Try this first |
|---|---|
Package uses native addons (.node files, binding.gyp, node-gyp) | Don’t port — no path forward. Find an alternative package or use the QuickJS fallback. |
| Package is pure TS/JS with only light use of dynamic features | Good candidate. Add to compilePackages, patch whatever trips the compiler. |
Package’s core API is built on Proxy (ORMs, validation DSLs, reactive stores) | Probably not portable. The surface Perry-users touch is the Proxy. |
Package is pure TS/JS but uses lookbehind regex, Symbol, WeakMap, etc. | Patchable. See Common gaps below. |
The workflow
1. Add it to compilePackages
In your project’s package.json:
{
"perry": {
"compilePackages": ["@noble/curves", "@noble/hashes"]
}
}
This is what tells Perry to pull the package into the native compile instead of routing it through a JavaScript runtime. See Project Configuration for the full semantics — including how first-resolved directories get cached so transitive copies dedup.
2. Try compiling
perry compile src/main.ts -o /tmp/port-test && /tmp/port-test
Most of the time this is where you find out what’s actually broken. Compile-time errors cite a file:line in the package — that’s your patch list.
3. Patch the gaps
See Common gaps for the typical fixes. Keep patches minimal and localized — the goal is a clean compile, not a refactor.
Record each patch in a file at your project root (convention: perry-patches/<package>.md) so you can reapply them after npm install blows them away. Until compilePackages grows a native patch-file convention, this is the one bit of maintenance overhead.
4. Re-check after each compile
Iterate: compile, patch the next error, compile again. Don’t try to catch everything in a single pass — some errors only surface after earlier ones are fixed.
Common gaps
Perry’s full limitations list is the canonical reference. In practice, these are the ones you hit when porting:
Lookbehind regex
Perry uses Rust’s regex crate, which doesn’t support lookbehind ((?<=…) / (?<!…)).
// Not supported
str.match(/(?<=prefix)\w+/);
// Rewrite — capture the prefix and slice
const m = str.match(/prefix(\w+)/);
const rest = m ? m[1] : null;
Symbol
Not supported as a primitive. When a package uses Symbol as a sentinel (the common case — e.g., for unique keys in a registry), swap for a string:
// Before
const REGISTRY_KEY = Symbol("registry");
// After
const REGISTRY_KEY = "__pkg_registry__";
When Symbol is used to implement Symbol.iterator/Symbol.asyncIterator, check whether the iteration is actually reached in your use case — often the class has a for-loop method alongside the iterator and you can ignore the iterator path.
Proxy, Reflect
Not supported. These are usually load-bearing for the package’s public API, so porting is often not feasible. If the Proxy is only in an optional path (e.g., dev-mode warnings), delete that branch.
WeakMap / WeakRef / FinalizationRegistry
Not implemented. Swap WeakMap for a regular Map if the GC semantics aren’t critical for correctness (most caches can tolerate this — they’ll just hold references slightly longer).
Decorators
// Not supported
@Component
class Foo {}
// Remove the decorator and inline the behavior, or use a factory function
const Foo = Component(class Foo {});
Dynamic require() / await import(…)
Perry only supports static imports. If a package branches on typeof require !== "undefined" for a Node/browser split, pick the branch that works natively and delete the other.
Prototype manipulation
// Not supported
Object.setPrototypeOf(obj, proto);
MyClass.prototype.newMethod = function() {};
Usually appears in fallback shims for older runtimes. Often dead code in the Perry path — just delete it.
Computed property keys in object literals
// Not supported
const obj = { [key]: value };
// Rewrite
const obj: Record<string, V> = {};
obj[key] = value;
Using a coding agent
A general coding agent (Claude Code, Cursor, Codex, Aider) can drive most of this workflow. If you’re using a skill-aware agent, invoke the port-npm-to-perry skill directly. Otherwise, paste this prompt:
I want to port the npm package <NAME> to run under Perry
(https://github.com/PerryTS/perry). Perry compiles a subset of TypeScript
natively; the subset's gaps are documented at
https://github.com/PerryTS/perry/blob/main/docs/src/language/limitations.md.
Please:
1. Read the package at node_modules/<NAME>/. Check package.json for
native addons (binding.gyp, gypfile, prebuilds/ — stop if present).
2. Scan for unsupported constructs: eval, new Function, dynamic require,
Symbol, Proxy, WeakMap, WeakRef, Reflect, decorators, lookbehind
regex (?<= / ?<!), Object.setPrototypeOf, computed property keys.
3. Report a triage: what rules the package out vs. what's patchable.
4. If patchable: add the package to perry.compilePackages in
package.json, apply minimal localized patches, and record each
patch in perry-patches/<NAME>.md.
5. Verify by running `perry compile` against a small file that imports
the package.
Don't patch blindly — a grep hit inside a string or comment isn't real.
Show me the triage before applying substantial patches.
This is intentionally an agent-agnostic prompt — it’ll work with any competent coding agent. The skill version bundles the same instructions with richer context and is auto-discovered by Claude Code.
Giving feedback
This whole workflow is experimental. If a port fails in a way that feels like Perry should handle it — or if the guide misses a common gap — please comment on issue #115 so we can iterate.