Perry provides native Rust implementations of popular npm packages. When you import these packages in your TypeScript code, they are compiled directly to native code using high-performance Rust crates - no Node.js runtime required.
27 npm packages are supported with native implementations, organized by category:
| npm Package | Rust Backend | Description |
|---|---|---|
mysql2 |
sqlx | MySQL/MariaDB client with connection pooling |
pg |
sqlx | PostgreSQL client with connection pooling |
mongodb |
mongodb | MongoDB driver with full CRUD support |
better-sqlite3 |
rusqlite | Synchronous SQLite3 with prepared statements |
ioredis |
redis | Redis client with all common operations |
| npm Package | Rust Backend | Description |
|---|---|---|
bcrypt |
bcrypt | Password hashing (bcrypt algorithm) |
argon2 |
argon2 | Password hashing (Argon2id, more secure) |
jsonwebtoken |
jsonwebtoken | JWT signing and verification |
crypto |
sha2, aes, pbkdf2, scrypt | Hashing, encryption, key derivation |
| npm Package | Rust Backend | Description |
|---|---|---|
axios |
reqwest | HTTP client with full method support |
node-fetch |
reqwest | Fetch API implementation |
ws |
tokio-tungstenite | WebSocket client |
nodemailer |
lettre | SMTP email sending |
| npm Package | Rust Backend | Description |
|---|---|---|
cheerio |
scraper | HTML parsing with jQuery-like API |
sharp |
image | Image processing (resize, convert, transform) |
zlib |
flate2 | Gzip/deflate compression |
lodash |
Native Rust | Utility functions for arrays/strings |
| npm Package | Rust Backend | Description |
|---|---|---|
dayjs |
chrono | Lightweight date manipulation |
moment |
chrono | Full-featured date library |
date-fns |
chrono | Functional date utilities |
node-cron |
cron | Cron job scheduling |
| npm Package | Rust Backend | Description |
|---|---|---|
uuid |
uuid | UUID generation (v1, v4, v7) |
nanoid |
nanoid | Compact unique ID generation |
slugify |
Native Rust | URL-friendly string conversion |
validator |
validator | String validation (email, URL, etc.) |
dotenv |
Native Rust | Environment variable loading |
rate-limiter-flexible |
dashmap | In-memory rate limiting |
Using native libraries requires no special setup - just import and use:
// Your TypeScript code works exactly like Node.js
import mysql from 'mysql2/promise';
import bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
async function main() {
// Connect to MySQL
const conn = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'secret',
database: 'myapp'
});
// Hash a password
const hashedPassword = await bcrypt.hash('user-password', 10);
// Generate a UUID
const id = uuidv4();
// Insert user
await conn.execute(
'INSERT INTO users (id, password) VALUES (?, ?)',
[id, hashedPassword]
);
await conn.end();
}
main();Compile and run:
perry app.ts -o app && ./appThe resulting binary has no Node.js dependency - it's a standalone native executable.
Click any library name to jump to its documentation:
| Library | Category | Jump |
|---|---|---|
| axios | HTTP Client | docs |
| argon2 | Security | docs |
| bcrypt | Security | docs |
| better-sqlite3 | Database | docs |
| cheerio | HTML Parsing | docs |
| crypto | Security | docs |
| date-fns | Date/Time | docs |
| dayjs | Date/Time | docs |
| dotenv | Config | docs |
| ioredis | Database | docs |
| jsonwebtoken | Security | docs |
| lodash | Utilities | docs |
| moment | Date/Time | docs |
| mongodb | Database | docs |
| mysql2 | Database | docs |
| nanoid | Utilities | docs |
| node-cron | Scheduling | docs |
| node-fetch | HTTP Client | docs |
| nodemailer | docs | |
| pg | Database | docs |
| rate-limiter-flexible | Rate Limiting | docs |
| sharp | Image Processing | docs |
| slugify | Utilities | docs |
| uuid | Utilities | docs |
| validator | Validation | docs |
| ws | WebSocket | docs |
| zlib | Compression | docs |
npm package: uuid Rust backend: uuid v1.11
import * as uuid from 'uuid';
// Generate UUIDs
const id1 = uuid.v4(); // Random UUID (most common)
const id2 = uuid.v1(); // Timestamp + MAC-based UUID
const id3 = uuid.v7(); // Unix timestamp-based UUID (sortable)
// Validate and inspect
const isValid = uuid.validate(id1); // Returns true/false
const version = uuid.version(id1); // Returns version number (1, 4, 7, etc.)uuid.v1()uses a fixed node ID (01:23:45:67:89:ab) since MAC address access is not available- All UUIDs are lowercase, hyphenated format:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
npm package: mysql2 Rust backend: sqlx v0.8
import mysql from 'mysql2/promise';
// Create connection
const conn = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'mydb'
});
// Execute queries
const [rows, fields] = await conn.query('SELECT * FROM users');
const [result] = await conn.execute('INSERT INTO users (name) VALUES (?)', ['Alice']);
// Transactions
await conn.beginTransaction();
await conn.query('UPDATE accounts SET balance = balance - 100 WHERE id = 1');
await conn.commit();
// or: await conn.rollback();
// Close connection
await conn.end();const pool = await mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'mydb',
connectionLimit: 10
});
const [rows] = await pool.query('SELECT * FROM users');
await pool.end();- Only Promise-based API is supported (use
mysql2/promise) - Prepared statements use
?placeholders - Connection options:
host,port,user,password,database
npm package: pg Rust backend: sqlx v0.8
import { Client, Pool } from 'pg';
// Create client connection
const client = new Client({
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'password',
database: 'mydb'
});
await client.connect();
// Execute queries
const result = await client.query('SELECT * FROM users');
console.log(result.rows);
console.log(result.rowCount);
// Parameterized queries
const result2 = await client.query(
'SELECT * FROM users WHERE id = $1',
[123]
);
// Close connection
await client.end();const pool = new Pool({
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'password',
database: 'mydb',
max: 10 // connection limit
});
// Query via pool
const result = await pool.query('SELECT * FROM users');
// Parameterized query
const result2 = await pool.query('SELECT * FROM users WHERE id = $1', [123]);
// Close pool
await pool.end();result.rows- Array of row objectsresult.rowCount- Number of rows affected/returnedresult.fields- Field metadata arrayresult.command- SQL command executed (SELECT, INSERT, etc.)
- PostgreSQL uses
$1,$2, etc. for parameterized queries (not?) - Connection options:
host,port,user,password,database,max - Uses sqlx under the hood with async/await
npm package: bcrypt Rust backend: bcrypt v0.15
import bcrypt from 'bcrypt';
// Hash a password
const saltRounds = 10;
const hash = await bcrypt.hash('myPassword', saltRounds);
// Verify a password
const match = await bcrypt.compare('myPassword', hash);
if (match) {
console.log('Password correct!');
}
// Generate salt separately (optional)
const salt = await bcrypt.genSalt(10);
const hash2 = await bcrypt.hash('myPassword', salt);saltRounds(cost factor) typically ranges from 10-12 for production- Higher values = more secure but slower
- Hashes are 60 characters in the standard bcrypt format
npm package: ioredis Rust backend: redis v0.25
import Redis from 'ioredis';
// Connect to Redis
const redis = new Redis(); // localhost:6379
const redis2 = new Redis(6380); // localhost:6380
const redis3 = new Redis(6379, '192.168.1.1'); // custom host
const redis4 = new Redis({
host: 'localhost',
port: 6379,
password: 'secret',
db: 0
});
// String operations
await redis.set('key', 'value');
await redis.set('key', 'value', 'EX', 60); // with expiry (60 seconds)
const value = await redis.get('key');
// Delete keys
await redis.del('key');
await redis.del('key1', 'key2', 'key3');
// Check existence
const exists = await redis.exists('key');
// Expiration
await redis.expire('key', 60); // seconds
await redis.ttl('key'); // get remaining TTL
// Increment/Decrement
await redis.incr('counter');
await redis.incrby('counter', 5);
await redis.decr('counter');
await redis.decrby('counter', 5);
// Hash operations
await redis.hset('user:1', 'name', 'Alice');
await redis.hget('user:1', 'name');
await redis.hgetall('user:1');
await redis.hdel('user:1', 'name');
// List operations
await redis.lpush('list', 'value');
await redis.rpush('list', 'value');
await redis.lpop('list');
await redis.rpop('list');
await redis.lrange('list', 0, -1);
// Set operations
await redis.sadd('set', 'member');
await redis.srem('set', 'member');
await redis.smembers('set');
await redis.sismember('set', 'member');
// Disconnect
await redis.quit();- Async/Promise-based API only
- Cluster mode not yet supported
- Pub/Sub not yet supported
npm package: crypto (Node.js built-in) Rust backend: sha2, md5, hmac, aes, pbkdf2, scrypt, rand
import crypto from 'crypto';
// Hash functions
const sha256Hash = crypto.sha256('hello world'); // Returns hex string
const md5Hash = crypto.md5('hello world'); // Returns hex string
// HMAC
const hmac = crypto.hmacSha256('message', 'secret-key'); // Returns hex string
// Random data
const randomHex = crypto.randomBytes(16); // Returns hex string (32 chars for 16 bytes)
const uuid = crypto.randomUUID(); // Returns UUID v4 string// Key must be 32 bytes, IV must be 16 bytes
const key = '01234567890123456789012345678901'; // 32 bytes
const iv = '0123456789012345'; // 16 bytes
// Encrypt (returns base64 string)
const encrypted = crypto.aes256Encrypt('secret message', key, iv);
// Decrypt (expects base64 input)
const decrypted = crypto.aes256Decrypt(encrypted, key, iv);// PBKDF2 (Password-Based Key Derivation Function 2)
const key = crypto.pbkdf2('password', 'salt', 100000, 32);
// Returns: 32-byte hex string derived from password
// Parameters: password, salt, iterations, keyLength
// Scrypt (memory-hard KDF)
const key2 = crypto.scrypt('password', 'salt', 32);
// Returns: 32-byte hex string with default params (N=16384, r=8, p=1)
// Scrypt with custom parameters
const key3 = crypto.scryptCustom('password', 'salt', 32, 14, 8, 1);
// Parameters: password, salt, keyLength, logN, r, p
// logN = log2(N), so 14 means N=16384- All hash functions return lowercase hex strings
randomBytes(n)returns2*nhex characters- Uses cryptographically secure random number generator
- AES encryption uses PKCS7 padding
- Key derivation functions return hex-encoded output
npm package: zlib (Node.js built-in) Rust backend: flate2 v1.0
import zlib from 'zlib';
// Gzip compression (sync)
const compressed = zlib.gzipSync(data);
const decompressed = zlib.gunzipSync(compressed);
// Deflate compression (sync)
const deflated = zlib.deflateSync(data);
const inflated = zlib.inflateSync(deflated);
// Async versions
const compressedAsync = await zlib.gzip(data);
const decompressedAsync = await zlib.gunzip(compressedAsync);- Input/output are treated as byte buffers (strings work for text data)
- Default compression level used
- Async versions run compression in background thread
npm package: node-fetch Rust backend: reqwest v0.12
import fetch from 'node-fetch';
// Simple GET request
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const text = await response.text();
const json = await response.json();
// Check response status
if (response.ok) {
console.log('Success:', response.status);
}
// Convenience function for text
const text2 = await fetch.fetchText('https://jsonplaceholder.typicode.com/posts/1');response.status- HTTP status code (number)response.statusText- HTTP status text (string)response.ok- true if status is 200-299 (boolean)response.text()- Get body as text (Promise)response.json()- Get body as JSON (Promise)- HTTPS supported via rustls (no OpenSSL dependency)
- Currently supports GET requests; POST coming soon
- Redirects are followed automatically
npm package: ws Rust backend: tokio-tungstenite v0.24
import WebSocket from 'ws'; // Connect to WebSocket server const ws = await WebSocket.connect('wss://echo.websocket.org'); // Check connection status if (ws.isOpen()) { // Send a message ws.send('Hello, server!'); } // Receive messages const message = ws.receive(); // Get next message (null if none) const count = ws.messageCount(); // Number of pending messages // Wait for message with timeout const msg = await ws.waitForMessage(5000); // 5 second timeout, null if timeout // Close connection ws.close();
- WebSocket Secure (wss://) supported via rustls
- Messages are queued and can be polled with
receive() - Text messages only (binary messages not yet supported)
- Connection events (onopen, onclose, onerror) not yet supported - use polling pattern
npm package: dotenv Rust backend: Native implementation
import dotenv from 'dotenv'; // Load .env file (from current directory) dotenv.config(); // Load from custom path dotenv.config({ path: '.env.local' }); // Parse env content string const parsed = dotenv.parse('KEY=value\nFOO=bar'); // Returns: { KEY: 'value', FOO: 'bar' }
- Supports comments (lines starting with
#) - Supports quoted values (single and double quotes)
- Supports escape sequences (
\n,\t) in double-quoted values - Missing .env file is not an error (silent fail)
npm package: jsonwebtoken Rust backend: jsonwebtoken v9.3
import jwt from 'jsonwebtoken'; const secret = 'your-secret-key'; // Sign a token const token = jwt.sign({ userId: 123 }, secret, 3600); // 3600 seconds = 1 hour // Verify a token (returns payload or null if invalid) const payload = jwt.verify(token, secret); if (payload) { console.log('Valid token:', payload); } // Decode without verification (unsafe, for debugging) const decoded = jwt.decode(token);
- Uses HS256 algorithm
sign()third parameter is expiration time in seconds (0 = no expiration)verify()returns null for invalid/expired tokens- Payload is JSON stringified/parsed automatically
npm package: nanoid Rust backend: nanoid v0.4
import { nanoid, customAlphabet } from 'nanoid'; // Generate default ID (21 characters, URL-safe) const id = nanoid(); // Generate ID with custom length const shortId = nanoid(10); // Generate with custom alphabet const customId = customAlphabet('0123456789abcdef', 12); // 12 hex chars
- Default alphabet is URL-safe:
A-Za-z0-9_- - Default length is 21 characters
- IDs are cryptographically random
npm package: slugify Rust backend: Native implementation
import slugify from 'slugify'; // Basic usage slugify('Hello World'); // 'hello-world' slugify('Héllo Wörld'); // 'hello-world' (accents converted) slugify('Hello World!'); // 'hello-world' (special chars removed) // Strict mode (only alphanumeric) slugify.strict('Hello_World'); // 'hello-world'
- Converts to lowercase
- Replaces spaces and common separators (
_,/,\) with hyphens - Converts common accented characters to ASCII equivalents
- Removes non-alphanumeric characters (except separator)
- Trims leading/trailing separators
npm package: validator Rust backend: validator v0.18
import validator from 'validator'; // Email validation validator.isEmail('user@example.com'); // true validator.isEmail('invalid'); // false // URL validation validator.isURL('https://example.com'); // true // UUID validation validator.isUUID('550e8400-e29b-41d4-a716-446655440000'); // true // Character type checks validator.isAlpha('Hello'); // true validator.isAlphanumeric('Hello123'); // true validator.isNumeric('12345'); // true validator.isNumeric('-123'); // true (allows leading +/-) validator.isHexadecimal('0xFF'); // true validator.isHexadecimal('abc123'); // true // Number validation validator.isInt('42'); // true validator.isFloat('3.14'); // true // String checks validator.isEmpty(' '); // true (after trim) validator.isJSON('{"key": "value"}'); // true validator.isLowercase('hello'); // true validator.isUppercase('HELLO'); // true // String comparison validator.contains('hello world', 'world'); // true validator.equals('hello', 'hello'); // true // Length validation validator.isLength('hello', 1, 10); // true (min 1, max 10)
- All functions return boolean (true/false)
- Email validation uses the validator crate's implementation
- URL validation requires protocol (http://, https://)
npm package: nodemailer Rust backend: lettre v0.11
import nodemailer from 'nodemailer'; // Create transporter with SMTP config const transporter = nodemailer.createTransport({ host: 'smtp.example.com', port: 587, secure: false, // true for 465, false for other ports auth: { user: 'username', pass: 'password' } }); // Send email const info = await transporter.sendMail({ from: 'sender@example.com', to: 'recipient@example.com', subject: 'Hello', text: 'Plain text body', html: '<p>HTML body</p>' // optional, overrides text }); console.log('Message sent:', info.messageId); // Verify connection const isValid = await transporter.verify();
host- SMTP server hostnameport- SMTP port (default: 587)secure- Use TLS (true for port 465)auth.user- SMTP usernameauth.pass- SMTP password
from- Sender email addressto- Recipient email addresssubject- Email subject linetext- Plain text bodyhtml- HTML body (optional)
- Uses STARTTLS for non-secure connections
- Returns
{ messageId, response }on success verify()tests SMTP connection without sending
npm package: dayjs Rust backend: chrono v0.4
import dayjs from 'dayjs'; // Create instances const now = dayjs(); // Current time const fromTimestamp = dayjs(1609459200000); // From Unix timestamp (ms) const parsed = dayjs('2021-01-01'); // Parse ISO string // Format dates const formatted = now.format('YYYY-MM-DD HH:mm:ss'); const iso = now.toISOString(); // ISO 8601 format // Get values now.valueOf(); // Unix timestamp (ms) now.unix(); // Unix timestamp (seconds) now.year(); // Year (e.g., 2024) now.month(); // Month (0-11) now.date(); // Day of month (1-31) now.day(); // Day of week (0=Sunday) now.hour(); // Hour (0-23) now.minute(); // Minute (0-59) now.second(); // Second (0-59) now.millisecond(); // Millisecond (0-999) // Manipulate dates const tomorrow = now.add(1, 'day'); const lastWeek = now.subtract(7, 'day'); const startOfMonth = now.startOf('month'); const endOfYear = now.endOf('year'); // Compare dates const diff = now.diff(parsed, 'day'); // Difference in days now.isBefore(parsed); // Returns boolean now.isAfter(parsed); // Returns boolean now.isSame(parsed, 'day'); // Same day? now.isValid(); // Is valid date?
For
add(),subtract(),startOf(),endOf(),diff(),isSame():year,years,ymonth,months,Mday,days,dhour,hours,hminute,minutes,msecond,seconds,smillisecond,milliseconds,ms
- All dates are UTC internally
- Immutable API (methods return new instances)
- Format tokens:
YYYY,MM,DD,HH,mm,ss, etc.
npm package: date-fns Rust backend: chrono v0.4
import { format, parseISO, addDays, addMonths, addYears, differenceInDays, differenceInHours, differenceInMinutes, isAfter, isBefore, startOfDay, endOfDay } from 'date-fns'; // Format a date const formatted = format(new Date(), 'yyyy-MM-dd HH:mm:ss'); // Parse ISO string const date = parseISO('2021-01-01T00:00:00Z'); // Add time const tomorrow = addDays(new Date(), 1); const nextMonth = addMonths(new Date(), 1); const nextYear = addYears(new Date(), 1); // Calculate differences const daysDiff = differenceInDays(date1, date2); const hoursDiff = differenceInHours(date1, date2); const minutesDiff = differenceInMinutes(date1, date2); // Compare dates const after = isAfter(date1, date2); const before = isBefore(date1, date2); // Day boundaries const dayStart = startOfDay(new Date()); const dayEnd = endOfDay(new Date());
- Uses Unix timestamps (milliseconds) internally
- Format tokens differ from dayjs:
yyyy(notYYYY), etc. - Functions are pure (no mutation)
- All times are UTC
npm package: axios Rust backend: reqwest v0.12
import axios from 'axios'; // Simple requests const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1'); const postResponse = await axios.post('https://jsonplaceholder.typicode.com/posts', { title: 'hello' }); const putResponse = await axios.put('https://jsonplaceholder.typicode.com/posts/1', { title: 'updated' }); const deleteResponse = await axios.delete('https://jsonplaceholder.typicode.com/posts/1'); // Full request with config const response2 = await axios.request({ method: 'POST', url: 'https://jsonplaceholder.typicode.com/posts', headers: { 'Content-Type': 'application/json' }, data: { title: 'hello' } }); // Create instance with defaults const api = axios.create({ baseURL: 'https://jsonplaceholder.typicode.com', timeout: 5000, headers: { 'Authorization': 'Bearer token' } });
response.status- HTTP status coderesponse.statusText- HTTP status textresponse.data- Response body (JSON parsed)response.headers- Response headers
- HTTPS supported via rustls
- JSON bodies automatically serialized/parsed
- Timeouts supported via config
npm package: argon2 Rust backend: argon2 v0.5
import argon2 from 'argon2'; // Hash a password (with default options) const hash = await argon2.hash('myPassword'); // Hash with custom options const hash2 = await argon2.hash('myPassword', { type: 2, // 0=Argon2d, 1=Argon2i, 2=Argon2id (default) memoryCost: 65536, // Memory in KB (default: 65536 = 64MB) timeCost: 3, // Iterations (default: 3) parallelism: 4 // Threads (default: 4) }); // Verify a password const isValid = await argon2.verify(hash, 'myPassword'); if (isValid) { console.log('Password correct!'); }
- Argon2id is recommended (type: 2) and used by default
- Memory cost is in KB, not bytes
- Hash format is PHC string format (contains all parameters)
- More secure than bcrypt for new applications
npm package: mongodb Rust backend: mongodb v2.8
import { MongoClient } from 'mongodb'; // Connect to MongoDB const client = await MongoClient.connect('mongodb://localhost:27017'); // Get database and collection const db = client.db('mydb'); const users = db.collection('users'); // Insert documents const result = await users.insertOne({ name: 'Alice', age: 30 }); console.log('Inserted:', result.insertedId); const bulkResult = await users.insertMany([ { name: 'Bob', age: 25 }, { name: 'Charlie', age: 35 } ]); // Find documents const user = await users.findOne({ name: 'Alice' }); const allUsers = await users.find({ age: { $gte: 25 } }); // Update documents const updateResult = await users.updateOne( { name: 'Alice' }, { $set: { age: 31 } } ); await users.updateMany( { age: { $lt: 30 } }, { $inc: { age: 1 } } ); // Delete documents await users.deleteOne({ name: 'Bob' }); await users.deleteMany({ age: { $gt: 50 } }); // Count documents const count = await users.countDocuments({ age: { $gte: 25 } }); // Close connection await client.close();
- Filters and updates are passed as JSON strings internally
- Supports standard MongoDB query operators ($eq, $gt, $lt, $gte, $lte, $in, etc.)
- Connection pooling is automatic
- All operations are async/Promise-based
npm package: better-sqlite3 Rust backend: rusqlite v0.31
import Database from 'better-sqlite3'; // Open database (creates if not exists) const db = Database.open('mydb.sqlite'); // Execute raw SQL db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)'); // Prepare statements const insert = db.prepare('INSERT INTO users (name) VALUES (?)'); const result = insert.run(['Alice']); // Returns { changes, lastInsertRowid } // Query single row const select = db.prepare('SELECT * FROM users WHERE id = ?'); const user = select.get([1]); // Returns object or null // Query all rows const selectAll = db.prepare('SELECT * FROM users'); const users = selectAll.all([]); // Returns array of objects // Transactions const tx = db.transaction(); try { insert.run(['Bob']); insert.run(['Charlie']); tx.commit(); } catch (e) { tx.rollback(); } // Close database db.close();
- Synchronous API (no await needed for queries)
- Parameters passed as JSON array
- Results returned as JSON objects
- SQLite bundled (no external dependency)
npm package: sharp Rust backend: image v0.25
import sharp from 'sharp'; // Load image from file const image = sharp.fromFile('input.jpg'); // Load from buffer const buffer = /* ... */; const image2 = sharp.fromBuffer(buffer, buffer.length); // Resize const resized = image.resize(800, 600); // width, height // Transformations (chainable) const processed = image .resize(800, 600) .rotate(90) .blur(2.5) .grayscale() .flip() // vertical flip .flop() // horizontal flip .negate(); // Set output format const jpeg = image.toFormat('jpeg'); const png = image.toFormat('png'); const webp = image.toFormat('webp'); // Set quality (1-100) const highQuality = image.quality(90); // Save to file await image.toFile('output.jpg'); // Get buffer const outputBuffer = await image.toBuffer(); // Get metadata const meta = image.metadata(); // Returns: { width, height, format }
- Chainable API (all methods return new image handle)
- Supported formats: JPEG, PNG, WebP, GIF
- Blur sigma: 0.3-1000 (higher = more blur)
- Quality only affects JPEG and WebP
npm package: cheerio Rust backend: scraper v0.19
import cheerio from 'cheerio'; // Load HTML const $ = cheerio.load('<html><body><h1>Hello</h1><p class="intro">World</p></body></html>'); // Load fragment (no <html>/<body> wrapper) const $frag = cheerio.loadFragment('<div><p>Hello</p></div>'); // Select elements const selection = $.select('p.intro'); // Get text content const text = selection.text(); // 'World' // Get HTML content const html = selection.html(); // Inner HTML // Get attributes const className = selection.attr('class'); // 'intro' // Get length const count = selection.length; // 1 // Navigation const first = selection.first(); const last = selection.last(); const nth = selection.eq(0); const parent = selection.parent(); const children = selection.children(); const found = selection.find('span'); // Predicates const hasClass = selection.hasClass('intro'); // true const matches = selection.is('.intro'); // true // Iteration const textArray = selection.texts(); // Array of text contents const htmlArray = selection.toArray(); // Array of HTML strings const attrs = selection.attrs('href'); // Array of attribute values
- CSS selectors supported (class, id, tag, attribute, combinators)
- Immutable API (methods return new selections)
- No DOM manipulation (read-only parsing)
npm package: lodash Rust backend: Native Rust implementation
import _ from 'lodash'; // Array functions _.chunk([1, 2, 3, 4], 2); // [[1, 2], [3, 4]] _.compact([0, 1, false, 2, '', 3]); // [1, 2, 3] _.concat([1], [2], [3]); // [1, 2, 3] _.difference([1, 2, 3], [2, 3]); // [1] _.drop([1, 2, 3], 2); // [3] _.dropRight([1, 2, 3], 2); // [1] _.first([1, 2, 3]); // 1 (alias: head) _.last([1, 2, 3]); // 3 _.flatten([[1], [2, [3]]]); // [1, 2, [3]] _.initial([1, 2, 3]); // [1, 2] _.tail([1, 2, 3]); // [2, 3] _.take([1, 2, 3], 2); // [1, 2] _.takeRight([1, 2, 3], 2); // [2, 3] _.uniq([1, 2, 1, 3, 2]); // [1, 2, 3] _.reverse([1, 2, 3]); // [3, 2, 1] _.size([1, 2, 3]); // 3 // String functions _.camelCase('foo bar'); // 'fooBar' _.capitalize('hello'); // 'Hello' _.kebabCase('foo bar'); // 'foo-bar' _.lowerCase('FOO BAR'); // 'foo bar' _.snakeCase('foo bar'); // 'foo_bar' _.startCase('foo bar'); // 'Foo Bar' _.upperCase('foo bar'); // 'FOO BAR' _.upperFirst('hello'); // 'Hello' _.lowerFirst('Hello'); // 'hello' _.trim(' hello '); // 'hello' _.trimStart(' hello'); // 'hello' _.trimEnd('hello '); // 'hello' _.pad('hi', 6); // ' hi ' _.padStart('hi', 6); // ' hi' _.padEnd('hi', 6); // 'hi ' _.repeat('ab', 3); // 'ababab' _.truncate('hello world', 8); // 'hello...' _.startsWith('hello', 'he'); // true _.endsWith('hello', 'lo'); // true _.includes('hello', 'ell'); // true _.split('a,b,c', ','); // ['a', 'b', 'c'] _.replace('hello', 'l', 'L'); // 'heLlo' _.escape('<div>'); // '<div>' _.unescape('<div>'); // '<div>' // Number functions _.clamp(5, 0, 3); // 3 _.inRange(3, 2, 4); // true _.random(0, 10); // Random number 0-10
- Subset of full lodash API implemented
- Functions work on primitive arrays
- All functions are pure (no mutation)
npm package: moment Rust backend: chrono v0.4
import moment from 'moment'; // Create instances const now = moment(); // Current time const fromTimestamp = moment.fromTimestamp(1609459200000); const parsed = moment.parse('2021-01-01'); // Parse ISO string // Format dates const formatted = now.format('YYYY-MM-DD HH:mm:ss'); // Get values now.valueOf(); // Unix timestamp (ms) now.unix(); // Unix timestamp (seconds) now.year(); // Year now.month(); // Month (0-11) now.date(); // Day of month now.day(); // Day of week (0=Sunday) now.hour(); // Hour now.minute(); // Minute now.second(); // Second now.millisecond(); // Millisecond now.isValid(); // Is valid date? // Manipulate dates const tomorrow = now.add(1, 'day'); const lastWeek = now.subtract(7, 'day'); const startOfMonth = now.startOf('month'); const endOfYear = now.endOf('year'); // Compare dates const diff = now.diff(parsed, 'day'); // Difference in days
- Similar API to dayjs (moment-compatible)
- Immutable operations (returns new instances)
- All times are UTC internally
- Consider using dayjs for new projects (smaller footprint)
npm package: node-cron Rust backend: cron v0.12
import cron from 'node-cron'; // Validate cron expression const isValid = cron.validate('* * * * *'); // true // Schedule a job const job = cron.schedule('*/5 * * * *', () => { console.log('Running every 5 minutes'); }); // Control the job job.start(); // Start the job job.stop(); // Stop the job const running = job.isRunning(); // Check if running // Get next execution times const next = job.nextDate(); // ISO string of next run const nextFive = job.nextDates(5); // Array of next 5 run times // Get human-readable description const desc = cron.describe('0 0 * * *'); // "At second 0 minute 0 of hour *, on day * of month *, on weekday *" // Timer helpers (setTimeout/setInterval alternative) const interval = cron.setInterval(() => { console.log('Every second'); }, 1000); cron.clearInterval(interval); const timeout = cron.setTimeout(() => { console.log('After 5 seconds'); }, 5000); cron.clearTimeout(timeout);
┌────────────── second (0-59) [optional] │ ┌──────────── minute (0-59) │ │ ┌────────── hour (0-23) │ │ │ ┌──────── day of month (1-31) │ │ │ │ ┌────── month (1-12) │ │ │ │ │ ┌──── day of week (0-6, Sunday=0) │ │ │ │ │ │ * * * * * *- Both 5-field and 6-field (with seconds) formats supported
- Callback ID is used internally (actual callbacks not yet implemented)
- Job timing is based on UTC
npm package: rate-limiter-flexible Rust backend: governor v0.6
import { RateLimiterMemory } from 'rate-limiter-flexible'; // Create rate limiter const limiter = RateLimiterMemory.create({ points: 10, // Number of points duration: 1 // Per second }); // Consume points try { const result = await limiter.consume('user_123', 1); console.log('Remaining points:', result.remainingPoints); } catch (rateLimiterRes) { console.log('Rate limited! Retry after:', rateLimiterRes.msBeforeNext); } // Get current state const info = await limiter.get('user_123'); // Delete key await limiter.delete('user_123'); // Block a key for duration await limiter.block('user_123', 60); // Block for 60 seconds // Add penalty points await limiter.penalty('user_123', 2); // Consume 2 extra points // Reward points (restore consumed points) await limiter.reward('user_123', 1); // Give back 1 point
remainingPoints- Points remaining in current windowmsBeforeNext- Milliseconds until points resetconsumedPoints- Points consumed so farisFirstInDuration- Whether this is first request in window
- Memory-based storage (resets on restart)
- Uses token bucket algorithm
- Thread-safe for concurrent requests
- Consider Redis-backed limiter for distributed systems
When you import a supported npm package, perry:
- Detects the import - Recognizes
mysql2,bcrypt, etc. as native modules - Emits FFI calls - Generates calls to
js_mysql2_*,js_bcrypt_*functions - Links native code - The
libperry_stdlib.alibrary provides implementations - Runs natively - No JavaScript runtime, no Node.js, pure native code
TypeScript Perry Native Binary ───────────────────────────────────────────────────────────────────────── import mysql from 'mysql2' → HIR marks as native import → Links to stdlib await mysql.createConn() → Emits js_mysql2_connect() → Calls sqlxData crosses the FFI boundary using:
- JSValue - NaN-boxed 64-bit values for primitives
- StringHeader - Length-prefixed UTF-8 strings
- ArrayHeader - Length-prefixed arrays of JSValues
- ObjectHeader - Field count + JSValue pairs
- Handle - Opaque i64 pointers for complex objects (connections, etc.)
Async operations use a tokio runtime bridge:
- TypeScript
awaitcreates a Promise handle - Rust spawns work on the tokio runtime
- Completion resolves/rejects the Promise
- Control returns to compiled code
To add support for a new npm package:
# crates/perry-stdlib/Cargo.toml [dependencies] your-crate = "1.0"
// crates/perry-stdlib/src/your_library.rs use perry_runtime::{JSValue, StringHeader, js_string_from_bytes}; use crate::common::{get_handle, register_handle, Handle}; /// your_library.method() #[no_mangle] pub unsafe extern "C" fn js_your_library_method( arg: *const StringHeader ) -> JSValue { // Implementation using your Rust crate }
// crates/perry-stdlib/src/lib.rs pub mod your_library;
// crates/perry-codegen/src/codegen.rs // In declare_*_functions(): { let mut sig = self.module.make_signature(); sig.params.push(AbiParam::new(types::I64)); sig.returns.push(AbiParam::new(types::I64)); let func_id = self.module.declare_function( "js_your_library_method", Linkage::Import, &sig )?; self.extern_funcs.insert("js_your_library_method".to_string(), func_id); } // In NativeMethodCall handling: ("your_library", false, "method") => "js_your_library_method",
- Match the npm API - Users expect familiar interfaces
- Use proven Rust crates - Don't reinvent wheels
- Handle errors gracefully - Return null/undefined, don't panic
- Document the API - Add a section to this file
Contributions welcome! See the contributing guide for details.
Beyond npm package compatibility, perry includes a native HTTP/WebSocket server framework optimized for maximum performance.
Rust backend: hyper + tokio
// Type declarations (will be auto-generated in future) declare function js_http_server_create(port: number): number; declare function js_http_server_accept_v2(server: number): number; declare function js_http_request_method(req: number): string; declare function js_http_request_path(req: number): string; declare function js_http_request_body(req: number): string; declare function js_http_respond_text(req: number, status: number, body: string): boolean; declare function js_http_respond_json(req: number, status: number, body: string): boolean; declare function js_http_respond_not_found(req: number): boolean; // Create server const server = js_http_server_create(3000); console.log('Server started on http://localhost:3000'); // Request loop while (true) { const req = js_http_server_accept_v2(server); const method = js_http_request_method(req); const path = js_http_request_path(req); if (path === '/' && method === 'GET') { js_http_respond_text(req, 200, 'Hello World'); } else if (path === '/api/data' && method === 'GET') { js_http_respond_json(req, 200, '{"status":"ok"}'); } else { js_http_respond_not_found(req); } }
Server:
Function Description js_http_server_create(port)Create server on port, returns handle js_http_server_accept_v2(server)Block until next request, returns request handle js_http_server_close(server)Close the server Request:
Function Description js_http_request_method(req)Get HTTP method (GET, POST, etc.) js_http_request_path(req)Get URL path js_http_request_query(req)Get query string js_http_request_body(req)Get request body as string js_http_request_header(req, name)Get specific header js_http_request_query_param(req, name)Get specific query parameter Response:
Function Description js_http_respond_text(req, status, body)Send text/plain response js_http_respond_json(req, status, body)Send application/json response js_http_respond_html(req, status, body)Send text/html response js_http_respond_redirect(req, url, permanent)Send 301/302 redirect js_http_respond_not_found(req)Send 404 response js_http_respond_error(req, status, message)Send error response Rust backend: serde_json
Function Description js_json_parse(text)Parse JSON string to JSValue js_json_stringify_string(str)Convert string to JSON string js_json_stringify_number(num)Convert number to JSON string js_json_stringify_bool(bool)Convert boolean to JSON string js_json_is_valid(text)Check if string is valid JSON js_json_get_string(json, key)Get string value from JSON object js_json_get_number(json, key)Get number value from JSON object Metric Target vs Node.js Requests/sec 500k+ ~15x faster Latency p99 <1ms ~5-10x faster Memory 10MB ~5-10x less Cold start <10ms ~10-50x faster Binary size <5MB No runtime needed The target high-level API (coming soon):
import { serve, router, cors, logger } from 'perry/http'; const app = router() .use(logger()) .use(cors({ origin: '*' })) .get('/', (c) => c.text('Hello World')) .get('/api/users/:id', async (c) => { const id = c.param('id'); return c.json({ id, name: 'Alice' }); }) .post('/api/users', async (c) => { const body = await c.json(); return c.json({ created: true }, 201); }); serve(app, { port: 3000 });
See PerryTS/perry-examples for working examples.