Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Porting npm Packages

Status: experimental. This guide — and the port-npm-to-perry skill 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

SituationTry 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 featuresGood 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.