JSPM

create-nodejs-fn

0.0.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 17
  • Score
    100M100P100Q46896F
  • License MIT

Vite plugin to enable calling Node.js-dependent functions directly from Cloudflare Workers

Package Exports

  • create-nodejs-fn
  • create-nodejs-fn/cli
  • create-nodejs-fn/package.json

Readme

Create NodeJS Fn

โšก A crazy Vite plugin that lets you transparently call Node.js native code from Cloudflare Workers

๐Ÿšจ WARNING: This project uses INSANE black magic! DO NOT use in production!! ๐Ÿšจ

๐Ÿคฏ What is this?

Cloudflare Workers are amazing, but they run on the V8 JavaScript engineโ€”not Node.js. This means native modules (binary addons compiled with node-gyp) simply don't work. Want to use @napi-rs/canvas for image generation, sharp for image processing, or pdfjs-dist with canvas rendering? You're out of luck...

...or are you? ๐Ÿ”ฅ

create-nodejs-fn bridges this gap by leveraging Cloudflare Containers (currently in beta). Here's how it works:

  1. You write functions in *.container.ts files using any Node.js native modules you want
  2. The Vite plugin analyzes your code using ts-morph (TypeScript AST manipulation)
  3. It auto-generates type-safe proxy functions that look identical to your original exports
  4. Your container code is bundled with esbuild and packaged into a Docker image
  5. At runtime, the proxy transparently routes calls via Cap'n Proto RPC to the container
  6. Cloudflare Durable Objects manage container lifecycle and connection state

The result? You import { myFunction } from "./native.container" and call it like any normal functionโ€”but it actually executes inside a Docker container running full Node.js with native module support!

alt

๐ŸŽฎ Live Demo

Try it now! This example uses @napi-rs/canvas + pdfjs-dist to render PDF pages as images:

๐Ÿ‘‰ Render Bitcoin Whitepaper (Page 1)

https://example-create-nodejs-fn.inaridiy.workers.dev/renderPdf?url=https://bitcoin.org/bitcoin.pdf&pageNum=1&scale=3

Yes, this is running on Cloudflare Workers. Yes, it's using native Node.js modules. Yes, it's black magic.

๐Ÿš€ Quick Start

Prerequisites

You need a Cloudflare Workers + Vite project. Create one with:

# Using Hono (recommended)
pnpm create hono@latest my-app --template cloudflare-workers+vite

# Then cd into it
cd my-app

1. Install dependencies

pnpm add create-nodejs-fn @cloudflare/containers capnweb@0.2.0 @napi-rs/canvas

2. Initialize config

pnpm create-nodejs-fn init

This configures:

  • Adds Containers & Durable Objects config to wrangler.jsonc
  • Generates .create-nodejs-fn/Dockerfile
  • Creates src/__generated__/ directory
  • Adds DO export to entry file

3. Configure Vite plugin

// vite.config.ts
import { cloudflare } from "@cloudflare/vite-plugin";
import { defineConfig } from "vite";
import { createNodejsFnPlugin } from "create-nodejs-fn";

export default defineConfig({
  plugins: [
    createNodejsFnPlugin({
      // Native dependencies to install in the container
      external: ["@napi-rs/canvas"],
      // Docker config with fonts for text rendering
      docker: {
        baseImage: "node:20-bookworm-slim",
        systemPackages: [
          "fontconfig",
          "fonts-noto-core",
          "fonts-noto-cjk",
          "fonts-noto-color-emoji",
        ],
      },
    }),
    cloudflare(),
  ],
});

4. Write a container function

// src/clock.container.ts
import { createCanvas } from "@napi-rs/canvas";
import { nodejsFn } from "./__generated__/create-nodejs-fn.runtime";

export const renderClock = nodejsFn(async () => {
  // ๐ŸŽจ Create an image with current time using @napi-rs/canvas!
  const canvas = createCanvas(600, 200);
  const ctx = canvas.getContext("2d");

  // Background
  ctx.fillStyle = "#1a1a2e";
  ctx.fillRect(0, 0, 600, 200);

  // Text with Noto font (installed via systemPackages)
  ctx.font = "bold 36px 'Noto Sans CJK JP', 'Noto Color Emoji', sans-serif";
  ctx.fillStyle = "#eee";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";

  const now = new Date().toISOString();
  ctx.fillText(`๐Ÿ• ${now}`, 300, 100);

  // Return as PNG data URL
  return await canvas.toDataURLAsync("image/webp");
});

5. Call it from your Worker like any normal function

// src/index.ts
import { Hono } from "hono";
import { renderClock } from "./clock.container";

const app = new Hono();

app.get("/clock", async (c) => {
  // ๐Ÿ˜ฑ Looks like a normal function call!
  // But behind the scenes, RPC flies to the container!
  const pngDataUrl = await renderClock();

  // Convert data URL to response
  return fetch(pngDataUrl);
});

// Don't forget to export the DO
export { NodejsFnContainer } from "./__generated__/create-nodejs-fn.do";
export default { fetch: app.fetch };

6. Launch!

pnpm dev

Visit http://localhost:5173/clock to see a dynamically generated image with the current timestamp! ๐ŸŽ‰

๐Ÿช„ The Black Magic Revealed

1๏ธโƒฃ Transparent Proxy Generation via AST Transformation

Uses ts-morph to statically analyze *.container.ts files. Detects exported functions and auto-generates proxy functions with identical type signatures.

// Your code (clock.container.ts)
export const renderClock = nodejsFn(async () => {
  // Node.js native processing...
  return pngDataUrl;
});

// ๐Ÿง™ The plugin auto-generates a proxy
// โ†’ Types fully preserved! IDE autocomplete works!
// โ†’ Calls are routed to the container via RPC!

2๏ธโƒฃ Container Management via Durable Objects

Uses Cloudflare Durable Objects to manage container connections. Stateful, with multi-instance routing support!

// Route to specific instances with containerKey
export const renderClock = nodejsFn(
  async () => { /* ... */ },
  containerKey(({ args }) => {
    // Route to containers based on arguments! Load balancing!
    return `instance-${Math.floor(Math.random() * 3)}`;
  })
);

3๏ธโƒฃ Fully Automated Build with esbuild + Docker

  • Bundles container server code with esbuild
  • Auto-generates Dockerfile
  • Native deps specified in external are auto-extracted to package.json

โš™๏ธ Plugin Options

createNodejsFnPlugin({
  // File patterns for container functions (default: ["src/**/*.container.ts"])
  files: ["src/**/*.container.ts"],
  
  // Output directory for generated files (default: "src/__generated__")
  generatedDir: "src/__generated__",
  
  // Durable Object binding name (default: "NODEJS_FN")
  binding: "NODEJS_FN",
  
  // Container class name (default: "NodejsFnContainer")
  className: "NodejsFnContainer",
  
  // Container port (default: 8080)
  containerPort: 8080,
  
  // External dependencies to install in container
  external: ["@napi-rs/canvas", "sharp"],
  
  // Docker image settings
  docker: {
    baseImage: "node:20-bookworm-slim",
    systemPackages: [
      "fontconfig",
      "fonts-noto-core",
      "fonts-noto-cjk",
      "fonts-noto-color-emoji",
    ],
    preInstallCommands: [],
    postInstallCommands: [],
    env: { MY_VAR: "value" },
  },
  
  // Environment variables to pass from Worker to Container
  workerEnvVars: ["API_KEY", "SECRET"],
  
  // Auto-rebuild on file changes (default: true)
  autoRebuildContainers: true,
  
  // Rebuild debounce time (default: 600ms)
  rebuildDebounceMs: 600,
});

๐Ÿ—๏ธ Internal Architecture

project/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ clock.container.ts        # Your code
โ”‚   โ”œโ”€โ”€ index.ts                  # Worker entry
โ”‚   โ””โ”€โ”€ __generated__/            # ๐Ÿง™ Auto-generated magic
โ”‚       โ”œโ”€โ”€ create-nodejs-fn.ts         # RPC client & type definitions
โ”‚       โ”œโ”€โ”€ create-nodejs-fn.do.ts      # Durable Object class
โ”‚       โ”œโ”€โ”€ create-nodejs-fn.context.ts # Container key resolution
โ”‚       โ”œโ”€โ”€ create-nodejs-fn.runtime.ts # nodejsFn / containerKey helpers
โ”‚       โ””โ”€โ”€ proxy.src__clock.container.ts # Proxy functions
โ”‚
โ””โ”€โ”€ .create-nodejs-fn/            # ๐Ÿณ Container build artifacts
    โ”œโ”€โ”€ Dockerfile                # Auto-generated
    โ”œโ”€โ”€ container.entry.ts        # Server entry (generated)
    โ”œโ”€โ”€ server.mjs                # Bundled with esbuild
    โ””โ”€โ”€ package.json              # Only external deps extracted

โš ๏ธ Limitations & Caveats

  • Not for production: This is an experimental project
  • Requires Cloudflare Containers (currently in beta)
  • Function arguments and return values must be serializable
  • Container cold starts exist (adjust with sleepAfter)
  • Debugging is hard (check your logs if something breaks)

๐Ÿ“ License

MIT